param([string]$Action) $ErrorActionPreference = 'Stop' $interactiveMenu = [string]::IsNullOrWhiteSpace($Action) # User-requested delay after an interactive menu action finishes. $menuCloseDelaySeconds = 5 $launcherExitCode = 0 Set-Location -LiteralPath $PSScriptRoot function Merge-Settings($Base, $Override) { foreach ($property in $Override.PSObject.Properties) { $existing = $Base.PSObject.Properties[$property.Name] if ($existing -and $property.Value -is [pscustomobject] -and $existing.Value -is [pscustomobject]) { Merge-Settings $existing.Value $property.Value } else { $Base | Add-Member -MemberType NoteProperty -Name $property.Name -Value $property.Value -Force } } } function Resolve-ProjectPath([string]$Path) { if ([IO.Path]::IsPathRooted($Path)) { return [IO.Path]::GetFullPath($Path) } return [IO.Path]::GetFullPath((Join-Path $PSScriptRoot $Path)) } function Stop-ProjectServer([string]$Name) { $target = Resolve-ProjectPath ($Name + '.exe') Get-Process -Name $Name -ErrorAction SilentlyContinue | ForEach-Object { if ($_.Path -and [string]::Equals($_.Path, $target, [StringComparison]::OrdinalIgnoreCase)) { Stop-Process -Id $_.Id -ErrorAction Stop if (-not $_.WaitForExit(5000)) { throw "Server did not stop: $target" } } } } function Start-ProjectServer([string]$Name) { $path = Resolve-ProjectPath ($Name + '.exe') if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "$Name is not built. Run build first." } Stop-ProjectServer $Name $process = Start-Process -FilePath $path -WorkingDirectory $PSScriptRoot -WindowStyle Normal -PassThru if ($process.WaitForExit(250)) { throw "$Name exited with code $($process.ExitCode). Run .\$Name.exe in a terminal to inspect the startup error." } Write-Host "Started $Name (PID $($process.Id)) in its own visible console." } function Build-Servers { $go = $env:GAC_GO_COMMAND if ([string]::IsNullOrWhiteSpace($go)) { $go = $settings.launcher.go_command } if ([string]::IsNullOrWhiteSpace($go)) { throw 'Set launcher.go_command or GAC_GO_COMMAND.' } $command = Get-Command -Name $go -CommandType Application -ErrorAction Stop foreach ($name in @('gameserver', 'sdkserver')) { $next = Resolve-ProjectPath ($name + '-go.next.exe') & $command.Source build -o $next ('./cmd/' + $name) if ($LASTEXITCODE -ne 0) { throw "Build failed: $name. Existing binaries were not replaced." } } foreach ($name in @('gameserver-go', 'sdkserver-go')) { $next = Resolve-ProjectPath ($name + '.next.exe') $target = Resolve-ProjectPath ($name + '.exe') Stop-ProjectServer $name Move-Item -LiteralPath $next -Destination $target -Force } Write-Host 'Build successful. Binaries are in the project root.' } function Start-External([string]$Setting, [string]$EnvironmentName) { $path = [Environment]::GetEnvironmentVariable($EnvironmentName) if ([string]::IsNullOrWhiteSpace($path)) { $path = $settings.launcher.$Setting } if ([string]::IsNullOrWhiteSpace($path)) { Write-Host "Servers started. Set launcher.$Setting or $EnvironmentName to launch the external program." return } $path = Resolve-ProjectPath $path if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Configured program does not exist: $path" } $directory = Split-Path -Parent $path if ([IO.Path]::GetExtension($path) -in @('.bat', '.cmd')) { Start-Process -FilePath 'cmd.exe' -ArgumentList ('/d /c ""' + $path + '""') -WorkingDirectory $directory -WindowStyle Hidden } else { Start-Process -FilePath $path -WorkingDirectory $directory } } try { $configPath = $env:GAC_CONFIG if ([string]::IsNullOrWhiteSpace($configPath)) { $configPath = 'server.json' } $configPath = Resolve-ProjectPath $configPath $env:GAC_CONFIG = $configPath $settings = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json $localPath = [IO.Path]::ChangeExtension($configPath, 'local.json') if (Test-Path -LiteralPath $localPath) { Merge-Settings $settings (Get-Content -LiteralPath $localPath -Raw | ConvertFrom-Json) } $variables = @{ client_version='GAC_CLIENT_VERSION'; game_host='GAC_GAME_HOST'; game_port='GAC_GAME_PORT'; sdk_host='GAC_SDK_HOST'; sdk_port='GAC_SDK_PORT'; public_game_host='GAC_PUBLIC_GAME_HOST'; public_sdk_url='GAC_PUBLIC_SDK_URL' } foreach ($key in $variables.Keys) { $value = [Environment]::GetEnvironmentVariable($variables[$key]) if ($null -ne $value) { $settings | Add-Member -MemberType NoteProperty -Name $key -Value $value -Force } } foreach ($key in @('game_port','sdk_port')) { $port = 0 if (-not [int]::TryParse([string]$settings.$key, [ref]$port) -or $port -lt 1 -or $port -gt [uint16]::MaxValue) { throw "$key must be a port between 1 and $([uint16]::MaxValue)." } } foreach ($key in @('client_version','game_host','sdk_host','public_game_host','public_sdk_url')) { if ([string]::IsNullOrWhiteSpace($settings.$key)) { throw "Missing setting: $key" } } foreach ($key in @('game_host','sdk_host','public_game_host')) { if ([string]$settings.$key -match '[/\\\s]') { throw "Invalid host: $key" } } $uri = $null if (-not [Uri]::TryCreate($settings.public_sdk_url, [UriKind]::Absolute, [ref]$uri) -or $uri.Scheme -notin @('http','https') -or $uri.Query -or $uri.Fragment -or $uri.UserInfo) { throw 'public_sdk_url must be an HTTP(S) base URL.' } if ($Action -eq 'check') { Write-Host 'Configuration valid.'; exit 0 } if ([string]::IsNullOrWhiteSpace($Action)) { Write-Host "GaC hkrpg GO | Client $($settings.client_version)" Write-Host "Game UDP $($settings.game_host):$($settings.game_port) | SDK HTTP $($settings.sdk_host):$($settings.sdk_port)" Write-Host '[1] Build, start servers and game (default)' Write-Host '[2] Start servers | [3] SDK only | [4] Game server only' Write-Host '[5] Start servers and proxy | [6] Build only | [7] Exit | [8] Stop servers' $Action = Read-Host 'Select' if ([string]::IsNullOrWhiteSpace($Action)) { $Action = '1' } } switch ($Action.Trim().ToLowerInvariant()) { { $_ -in @('1','game') } { Build-Servers; Start-ProjectServer 'sdkserver-go'; Start-ProjectServer 'gameserver-go'; Start-External 'client_path' 'GAC_CLIENT_PATH'; break } { $_ -in @('2','servers') } { Start-ProjectServer 'sdkserver-go'; Start-ProjectServer 'gameserver-go'; break } { $_ -in @('3','sdk') } { Start-ProjectServer 'sdkserver-go'; break } { $_ -in @('4','gs') } { Start-ProjectServer 'gameserver-go'; break } { $_ -in @('5','proxy') } { Start-ProjectServer 'sdkserver-go'; Start-ProjectServer 'gameserver-go'; Start-External 'proxy_path' 'GAC_PROXY_PATH'; break } { $_ -in @('6','build') } { Build-Servers; break } '7' { break } { $_ -in @('8','stop') } { Stop-ProjectServer 'sdkserver-go'; Stop-ProjectServer 'gameserver-go'; break } default { throw "Unknown action: $Action" } } } catch { Write-Host "[ERROR] $($_.Exception.Message)" -ForegroundColor Red $launcherExitCode = 1 } if ($interactiveMenu) { Write-Host "Launcher closes in $menuCloseDelaySeconds seconds. Server windows stay open." Start-Sleep -Seconds $menuCloseDelaySeconds } exit $launcherExitCode