[CmdletBinding()] param( [string]$GameDir, [switch]$SkipTests, [switch]$SchedulesOnly ) $ErrorActionPreference = 'Stop' $root = $PSScriptRoot $goRoot = Join-Path $root 'go' $buildRoot = Join-Path $root '.build' $packageParent = Join-Path $buildRoot 'package' $serverPackage = Join-Path $packageParent 'bd2server' $clientPackage = Join-Path $packageParent 'bd2client' $serverGoDir = Join-Path $serverPackage 'go' $serverStateDir = Join-Path $serverPackage 'data\state' $clientPluginDir = Join-Path $clientPackage 'plugins' $versionConfig = Join-Path $root 'versions.json' $scheduleDirectory = Join-Path $root 'schedules' try { $releaseVersions = Get-Content -LiteralPath $versionConfig -Raw | ConvertFrom-Json -ErrorAction Stop } catch { throw "Could not read release versions at ${versionConfig}: $($_.Exception.Message)" } $serverArchive = Join-Path $buildRoot ("bd2server-{0}-windows-x64.zip" -f $releaseVersions.server_version) $clientArchive = Join-Path $buildRoot ("bd2client-{0}-windows-x64.zip" -f $releaseVersions.client_version) # Hash both filenames and contents so schedule releases evolve independently of # the server executable and distinguish additions, removals and renamed files. $scheduleFiles = @(Get-ChildItem -LiteralPath $scheduleDirectory -File -Filter '*.bd2schedule' | Sort-Object Name) if ($scheduleFiles.Count -eq 0) { throw "No project schedule files found at $scheduleDirectory" } $scheduleManifest = ($scheduleFiles | ForEach-Object { "{0}:{1}" -f $_.Name, (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash }) -join "`n" $scheduleHasher = [Security.Cryptography.SHA256]::Create() try { $scheduleHash = ([BitConverter]::ToString($scheduleHasher.ComputeHash([Text.Encoding]::UTF8.GetBytes($scheduleManifest)))).Replace('-', '').ToLowerInvariant().Substring(0, 12) } finally { $scheduleHasher.Dispose() } $scheduleRelease = Join-Path $buildRoot ("bd2schedules-{0}-{1}" -f $releaseVersions.game_version, $scheduleHash) function Write-ScheduleRelease { New-Item -ItemType Directory -Force -Path $scheduleRelease | Out-Null foreach ($scheduleFile in $scheduleFiles) { Copy-Item -LiteralPath $scheduleFile.FullName -Destination $scheduleRelease -Force } Write-Host "Built schedule release files: $scheduleRelease" } if ($SchedulesOnly) { Write-ScheduleRelease return } if ([string]::IsNullOrWhiteSpace($GameDir)) { $developmentConfigPath = Join-Path $goRoot 'config.json' if (-not [IO.File]::Exists($developmentConfigPath)) { throw "GameDir was not provided and $developmentConfigPath does not exist. Copy go/config.example.json to go/config.json and set game_directory, or pass -GameDir explicitly." } try { $developmentConfig = Get-Content -LiteralPath $developmentConfigPath -Raw | ConvertFrom-Json -ErrorAction Stop } catch { throw "Could not read development config at ${developmentConfigPath}: $($_.Exception.Message)" } $propertyNames = @($developmentConfig.PSObject.Properties.Name | Sort-Object) if (($propertyNames -join ',') -ne 'game_directory,schema_version' -or $developmentConfig.schema_version -ne 1 -or -not ($developmentConfig.game_directory -is [string]) -or [string]::IsNullOrWhiteSpace($developmentConfig.game_directory)) { throw "Development config must contain only schema_version 1 and a non-empty game_directory: $developmentConfigPath" } $GameDir = $developmentConfig.game_directory Write-Host "Using development game directory: $GameDir" } $GameDir = [IO.Path]::GetFullPath($GameDir) $gameExecutable = Join-Path $GameDir 'BrownDust II.exe' if (-not [IO.File]::Exists($gameExecutable)) { throw "GameDir does not contain Brown Dust II.exe: $GameDir" } $env:GOCACHE = Join-Path $goRoot '.cache\go-build' New-Item -ItemType Directory -Force -Path $env:GOCACHE, $buildRoot | Out-Null foreach ($target in @($packageParent, $serverArchive, $clientArchive)) { if (Test-Path -LiteralPath $target) { $resolved = [IO.Path]::GetFullPath($target) $expectedRoot = [IO.Path]::GetFullPath($buildRoot) + [IO.Path]::DirectorySeparatorChar if (-not $resolved.StartsWith($expectedRoot, [StringComparison]::OrdinalIgnoreCase)) { throw "Refusing to remove unexpected build path: $resolved" } Remove-Item -LiteralPath $resolved -Recurse -Force } } New-Item -ItemType Directory -Force -Path $serverPackage, $serverGoDir, $serverStateDir, $clientPackage, $clientPluginDir | Out-Null Push-Location $goRoot try { if (-not $SkipTests) { go test ./... if ($LASTEXITCODE -ne 0) { throw "go test failed with exit code $LASTEXITCODE" } go vet ./... if ($LASTEXITCODE -ne 0) { throw "go vet failed with exit code $LASTEXITCODE" } go test -tags 'release,production' ./... if ($LASTEXITCODE -ne 0) { throw "release-tag go test failed with exit code $LASTEXITCODE" } go vet -tags 'release,production' ./... if ($LASTEXITCODE -ne 0) { throw "release-tag go vet failed with exit code $LASTEXITCODE" } } go build -tags release -trimpath -ldflags '-s -w' -o (Join-Path $serverPackage 'bd2server.exe') .\cmd\bd2server if ($LASTEXITCODE -ne 0) { throw "bd2server build failed with exit code $LASTEXITCODE" } go build -tags 'release,production' -trimpath -ldflags '-H windowsgui -s -w' -o (Join-Path $clientPackage 'bd2client.exe') .\cmd\bd2client if ($LASTEXITCODE -ne 0) { throw "bd2client build failed with exit code $LASTEXITCODE" } } finally { Pop-Location } $clientPlugins = @( @{ Name = 'LocalIdentity'; Project = Join-Path $root 'plugins\LocalIdentity\LocalIdentity.csproj'; Output = Join-Path $root 'plugins\LocalIdentity\bin\Release\netstandard2.1\BD2LocalIdentity.dll'; FileName = 'BD2LocalIdentity.dll' }, @{ Name = 'LoginUI'; Project = Join-Path $root 'plugins\LoginUI\LoginUI.csproj'; Output = Join-Path $root 'plugins\LoginUI\bin\Release\netstandard2.1\BD2LoginUI.dll'; FileName = 'BD2LoginUI.dll' }, @{ Name = 'CashShop'; Project = Join-Path $root 'plugins\CashShop\CashShop.csproj'; Output = Join-Path $root 'plugins\CashShop\bin\Release\netstandard2.1\BD2CashShop.dll'; FileName = 'BD2CashShop.dll' } ) foreach ($plugin in $clientPlugins) { $pluginBuildArgs = @('build', $plugin.Project, '-c', 'Release', "-p:GameDir=$GameDir", '--nologo') dotnet @pluginBuildArgs if ($LASTEXITCODE -ne 0) { throw "$($plugin.Name) build failed with exit code $LASTEXITCODE" } if (-not (Test-Path -LiteralPath $plugin.Output -PathType Leaf)) { throw "$($plugin.Name) build output is missing" } Copy-Item -LiteralPath $plugin.Output -Destination (Join-Path $clientPluginDir $plugin.FileName) -Force $sharedRuntime = Join-Path (Split-Path -Parent $plugin.Output) 'BD2.GameNames.dll' if (-not (Test-Path -LiteralPath $sharedRuntime -PathType Leaf)) { throw "Shared game names runtime is missing: $sharedRuntime" } $packagedRuntime = Join-Path $clientPluginDir 'BD2.GameNames.dll' if ((Test-Path -LiteralPath $packagedRuntime) -and (Get-FileHash -LiteralPath $packagedRuntime).Hash -ne (Get-FileHash -LiteralPath $sharedRuntime).Hash) { throw 'Client plugins were built with different BD2.GameNames libraries.' } Copy-Item -LiteralPath $sharedRuntime -Destination $packagedRuntime -Force } Copy-Item -LiteralPath (Join-Path $goRoot 'seed') -Destination $serverGoDir -Recurse -Force $serverScheduleDirectory = Join-Path $serverPackage 'schedules' New-Item -ItemType Directory -Force -Path $serverScheduleDirectory | Out-Null foreach ($scheduleFile in $scheduleFiles) { Copy-Item -LiteralPath $scheduleFile.FullName -Destination $serverScheduleDirectory -Force } Copy-Item -LiteralPath $versionConfig -Destination (Join-Path $serverPackage 'versions.json') -Force Copy-Item -LiteralPath $versionConfig -Destination (Join-Path $clientPackage 'versions.json') -Force Copy-Item -LiteralPath (Join-Path $root 'RELEASE.md') -Destination (Join-Path $serverPackage 'README.md') -Force Copy-Item -LiteralPath (Join-Path $root 'AUTHENTICATION.md') -Destination (Join-Path $serverPackage 'AUTHENTICATION.md') -Force Copy-Item -LiteralPath (Join-Path $root 'GAME_CONFIGURATION.md') -Destination (Join-Path $serverPackage 'GAME_CONFIGURATION.md') -Force Copy-Item -LiteralPath (Join-Path $root 'docs\CLIENT.md') -Destination (Join-Path $clientPackage 'README.md') -Force Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination (Join-Path $serverPackage 'LICENSE') -Force Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination (Join-Path $clientPackage 'LICENSE') -Force Compress-Archive -LiteralPath $serverPackage -DestinationPath $serverArchive -CompressionLevel Optimal Compress-Archive -LiteralPath $clientPackage -DestinationPath $clientArchive -CompressionLevel Optimal Write-Host "Built pure server directory: $serverPackage" Write-Host "Built pure server archive: $serverArchive" Write-Host "Built client directory: $clientPackage" Write-Host "Built client archive: $clientArchive"