<# .SYNOPSIS Collects every mod currently enabled in tModLoader (Windows) into one folder, ready to copy into /opt/terraria//data/ on the Linux server. .DESCRIPTION Output layout (mirrors the server's data/ folder): \Mods\*.tmod \Mods\enabled.json (only lists mods that were actually found) \ModConfigs\*.json .tmod files are taken from Steam's Workshop folder (all Steam libraries are searched), falling back to Documents\My Games\Terraria\tModLoader\Mods for mods you installed manually or built yourself. .PARAMETER Out Where to put the export. Default: Desktop\tmod-export .PARAMETER TmlDir tModLoader's save folder. Default: Documents\My Games\Terraria\tModLoader For the preview branch use ...\My Games\Terraria\tModLoader-preview .PARAMETER MaxVersion Optional. Your tModLoader version as year.month (e.g. 2025.3), shown at the bottom-left of the tModLoader main menu. Workshop builds made for a newer tModLoader than this are skipped. .EXAMPLE powershell -ExecutionPolicy Bypass -File .\export-tmod-mods.ps1 powershell -ExecutionPolicy Bypass -File .\export-tmod-mods.ps1 -MaxVersion 2025.3 #> param( [string]$Out = (Join-Path ([Environment]::GetFolderPath('Desktop')) 'tmod-export'), [string]$TmlDir = (Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'My Games\Terraria\tModLoader'), [version]$MaxVersion ) $ErrorActionPreference = 'Stop' # --- Which mods are enabled? ------------------------------------------------ $enabledJson = Join-Path $TmlDir 'Mods\enabled.json' if (-not (Test-Path $enabledJson)) { throw "Can't find $enabledJson. Has tModLoader been run? For the preview branch pass -TmlDir ...\tModLoader-preview" } $enabled = @(Get-Content $enabledJson -Raw | ConvertFrom-Json) if ($enabled.Count -eq 0) { throw "No mods are enabled in $enabledJson" } # --- Find Steam Workshop folders for tModLoader (app 1281930) ---------------- $steamPath = (Get-ItemProperty 'HKCU:\Software\Valve\Steam' -ErrorAction SilentlyContinue).SteamPath if (-not $steamPath) { $steamPath = 'C:\Program Files (x86)\Steam' } $libraries = @($steamPath) $vdf = Join-Path $steamPath 'steamapps\libraryfolders.vdf' if (Test-Path $vdf) { $libraries += Select-String -Path $vdf -Pattern '"path"\s+"([^"]+)"' | ForEach-Object { $_.Matches[0].Groups[1].Value -replace '\\\\', '\' } } $workshopDirs = @($libraries | ForEach-Object { Join-Path $_ 'steamapps\workshop\content\1281930' } | Where-Object { Test-Path $_ } | ForEach-Object { (Resolve-Path $_).Path } | Sort-Object -Unique) if ($workshopDirs.Count -eq 0) { Write-Warning "No Steam Workshop folder for tModLoader found; only local mods will be exported." } function Get-FolderVersion([string]$dir) { # Workshop items keep builds in folders named after the tModLoader version, e.g. 2024.3 $v = $null if ([version]::TryParse((Split-Path $dir -Leaf), [ref]$v)) { return $v } return $null } # --- Collect ------------------------------------------------------------------ $modsOut = Join-Path $Out 'Mods' New-Item -ItemType Directory -Force -Path $modsOut | Out-Null $exported = @() $missing = @() foreach ($mod in $enabled) { $candidates = @() foreach ($w in $workshopDirs) { $candidates += @(Get-ChildItem -Path $w -Recurse -File -Filter "$mod.tmod" -ErrorAction SilentlyContinue) } $pick = $null if ($candidates.Count -gt 0) { $withVer = $candidates | ForEach-Object { [pscustomobject]@{ File = $_; Version = (Get-FolderVersion $_.DirectoryName) } } # Newest version folder that isn't newer than -MaxVersion $best = @($withVer | Where-Object { $_.Version -and (-not $MaxVersion -or $_.Version -le $MaxVersion) } | Sort-Object Version -Descending) if ($best.Count -gt 0) { $pick = $best[0].File } else { # Build stored at the item's top level (no version folder) $root = @($withVer | Where-Object { -not $_.Version } | Sort-Object { $_.File.LastWriteTime } -Descending) if ($root.Count -gt 0) { $pick = $root[0].File } } } if (-not $pick) { $local = Join-Path $TmlDir "Mods\$mod.tmod" if (Test-Path $local) { $pick = Get-Item $local } } if ($pick) { Copy-Item $pick.FullName $modsOut -Force $exported += $mod Write-Host (" {0,-28} {1}" -f $mod, $pick.FullName) } else { $missing += $mod Write-Warning "$mod is enabled but no .tmod file was found; skipped." } } # enabled.json listing only what we exported (a missing mod would stop the server loading) $json = ConvertTo-Json -InputObject @($exported) [IO.File]::WriteAllText((Join-Path $modsOut 'enabled.json'), $json, (New-Object Text.UTF8Encoding $false)) # Mod settings (server-side configs matter; client-side ones are harmless) $cfgSrc = Join-Path $TmlDir 'ModConfigs' if (Test-Path $cfgSrc) { $cfgOut = Join-Path $Out 'ModConfigs' New-Item -ItemType Directory -Force -Path $cfgOut | Out-Null Copy-Item (Join-Path $cfgSrc '*') $cfgOut -Recurse -Force } Write-Host "" Write-Host "Exported $($exported.Count) mod(s) to $Out" if ($missing.Count -gt 0) { Write-Warning "Missing: $($missing -join ', ')" }