# Shared build helpers for S³ (dot-sourced by build-local.ps1 and build-release.ps1). # Not run directly. Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $script:RepoRoot = Split-Path $PSScriptRoot -Parent $script:ModId = "S3" function Get-ModVersion { # Single source of truth: the Version field in Info.json. $info = Get-Content (Join-Path $RepoRoot "Info.json") -Raw | ConvertFrom-Json return $info.Version } function Invoke-ManagedBuild { param([string]$Configuration = "Release", [string]$GameDir = "") Write-Host "=== Building managed (S3.dll, $Configuration) ===" -ForegroundColor Cyan $csproj = Join-Path $RepoRoot "src\S3.csproj" $args = @($csproj, "-c", $Configuration, "--nologo", "-v", "minimal") if ($GameDir) { $args += "/p:GameDir=$GameDir" } dotnet build @args | Out-Host if ($LASTEXITCODE -ne 0) { throw "Managed build failed." } $dll = Join-Path $RepoRoot "src\bin\$Configuration\S3.dll" if (-not (Test-Path $dll)) { throw "S3.dll not found at $dll" } return $dll } function Invoke-McpToolsBuild { param([string]$Configuration = "Release", [string]$GameDir = "") Write-Host "=== Building MCP tools (S3.Mcp.Tools.dll, $Configuration) ===" -ForegroundColor Cyan $csproj = Join-Path $RepoRoot "mcp-tools\S3.Mcp.Tools.csproj" $args = @($csproj, "-c", $Configuration, "--nologo", "-v", "minimal") if ($GameDir) { $args += "/p:GameDir=$GameDir" } dotnet build @args | Out-Host if ($LASTEXITCODE -ne 0) { throw "MCP tools build failed." } } function Invoke-NativeBuild { param([string]$Configuration = "Release") $cmakeLists = Join-Path $RepoRoot "native\CMakeLists.txt" if (-not (Test-Path $cmakeLists)) { Write-Host "=== No native/CMakeLists.txt — skipping native build ===" -ForegroundColor DarkYellow return $null } Write-Host "=== Building native (S3Native.dll, $Configuration) ===" -ForegroundColor Cyan $buildDir = Join-Path $RepoRoot "native\build" if (-not (Test-Path $buildDir)) { cmake -B $buildDir -A x64 (Join-Path $RepoRoot "native") | Out-Host } cmake --build $buildDir --config $Configuration | Out-Host if ($LASTEXITCODE -ne 0) { throw "Native build failed." } # MSVC multi-config generators place the DLL in a per-config subfolder. $dll = Join-Path $buildDir "bin\$Configuration\S3Native.dll" if (-not (Test-Path $dll)) { $dll = Join-Path $buildDir "bin\S3Native.dll" } # single-config fallback if (-not (Test-Path $dll)) { throw "S3Native.dll not found under $buildDir\bin" } return $dll } function Test-ModDllLocked { param([Parameter(Mandatory)][string]$Path) if (-not (Test-Path $Path)) { return $false } try { [System.IO.File]::Open($Path, 'Open', 'ReadWrite', 'None').Close() return $false } catch { return $true } } function Get-LockedModDlls { param([Parameter(Mandatory)][string]$DestModDir) $locked = @() foreach ($name in @("S3.dll", "S3Native.dll")) { if (Test-ModDllLocked (Join-Path $DestModDir $name)) { $locked += $name } } return $locked } # Poll until Mods\S3 DLLs are writable. Railroader.exe is the usual locker; # we wait on the file itself so a hung Unity process is still detected. function Wait-ModUnlocked { param( [Parameter(Mandatory)][string]$DestModDir, [int]$PollSeconds = 2 ) $locked = @(Get-LockedModDlls $DestModDir) if ($locked.Count -eq 0) { return } Write-Host "=== Waiting for Railroader to close ($($locked -join ', ') locked) ===" -ForegroundColor Yellow Write-Host " Close the game; install will run automatically. Ctrl+C to abort." -ForegroundColor DarkYellow while ($true) { Start-Sleep -Seconds $PollSeconds $still = @(Get-LockedModDlls $DestModDir) if ($still.Count -eq 0) { Write-Host "=== Unlocked, installing ===" -ForegroundColor Green return } $rr = Get-Process -Name "Railroader" -ErrorAction SilentlyContinue $hint = if ($rr) { "Railroader.exe still running" } else { "$($still -join ', ') still locked" } Write-Host " ... $hint" -ForegroundColor DarkGray } } # Assembles the Mods\S3 layout into $DestModDir (the S3 folder itself). # # Overwrites in place rather than wiping the folder. A full wipe is dangerous: if # the game is running, a loaded DLL is locked, so the wipe deletes Info.json and # the managed DLL, then aborts on the locked native DLL — leaving the mod with no # manifest, so it silently vanishes from the loader. Overwriting in place keeps # the last-good install if a copy fails, and preserves the user's settings files. function New-ModLayout { param([Parameter(Mandatory)][string]$DestModDir, [Parameter(Mandatory)][string]$ManagedDll, [string]$NativeDll = $null, [switch]$Wait) New-Item -ItemType Directory -Force -Path $DestModDir | Out-Null if ($Wait) { Wait-ModUnlocked -DestModDir $DestModDir } # Fail fast with a clear message if a DLL we're about to overwrite is locked # (game still running) — before touching anything. $locked = @(Get-LockedModDlls $DestModDir) if ($locked.Count -gt 0) { throw "$($locked -join ', ') is locked - close Railroader before installing (or rerun with -Wait)." } # Drop stale artifacts from older builds (the pre-rename native DLL, ngen # caches) so they don't linger, but leave Info.json and settings intact. $stale = Join-Path $DestModDir "RRPopout.dll" if (Test-Path $stale) { Remove-Item $stale -Force } Get-ChildItem $DestModDir -Filter "*.cache" -ErrorAction SilentlyContinue | Remove-Item -Force Copy-Item (Join-Path $RepoRoot "Info.json") (Join-Path $DestModDir "Info.json") -Force Copy-Item $ManagedDll (Join-Path $DestModDir "S3.dll") -Force $icon = Join-Path $RepoRoot "src\Modules\QuickActions\Icons\consist.png" if (Test-Path $icon) { Copy-Item $icon (Join-Path $DestModDir "consist.png") -Force } if ($NativeDll) { Copy-Item $NativeDll (Join-Path $DestModDir "S3Native.dll") -Force # Ship ProggyClean.ttf alongside the native DLL so IMGUI_DISABLE_DEFAULT_FONT # can load it at runtime without the base85 blob in the binary. $fontSrc = Join-Path $RepoRoot "native\fonts\ProggyClean.ttf" if (Test-Path $fontSrc) { Copy-Item $fontSrc (Join-Path $DestModDir "ProggyClean.ttf") -Force } } }