2026-10-16 22:19:52 +07:00
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
2026-10-21 20:40:13 +07:00
. ( Join-Path $PSScriptRoot 'tools/go-toolchain.ps1' )
2026-10-16 22:19:52 +07:00
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 ))
}
2026-10-28 18:54:43 +07:00
function Stop-ProjectServerAt([string]$Name , [ string ] $Target ) {
2026-10-16 22:19:52 +07:00
Get-Process -Name $Name -ErrorAction SilentlyContinue | ForEach-Object {
2026-10-28 18:54:43 +07:00
if ( $_ . Path -and [ string ]:: Equals ( $_ . Path , $Target , [ StringComparison ]:: OrdinalIgnoreCase )) {
2026-10-16 22:19:52 +07:00
Stop-Process -Id $_ . Id -ErrorAction Stop
2026-10-28 18:54:43 +07:00
if ( -not $_ . WaitForExit ( 5000 )) { throw "Server did not stop: $Target " }
2026-10-16 22:19:52 +07:00
}
}
}
2026-10-28 18:54:43 +07:00
function Stop-ProjectServer([string]$Name ) {
Stop-ProjectServerAt $Name ( Resolve-ProjectPath ( $Name + '.exe' ))
}
2026-10-16 22:19:52 +07:00
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 {
2026-10-21 20:40:13 +07:00
$go = Resolve-GoToolchain $settings . launcher
2026-10-28 18:54:43 +07:00
$shipDirectory = Resolve-ProjectPath 'for ship'
New-Item -ItemType Directory -Path $shipDirectory -Force | Out-Null
2026-10-16 22:19:52 +07:00
foreach ( $name in @ ( 'gameserver' , 'sdkserver' )) {
$next = Resolve-ProjectPath ( $name + '-go.next.exe' )
2026-10-21 20:40:13 +07:00
& $go build -o $next ( './cmd/' + $name )
2026-10-16 22:19:52 +07:00
if ( $LASTEXITCODE -ne 0 ) { throw "Build failed: $name . Existing binaries were not replaced." }
2026-10-28 18:54:43 +07:00
$shipName = $name + '-go'
$shipNext = Join-Path $shipDirectory ( $shipName + '.next.exe' )
& $go build -tags ship -o $shipNext ( './cmd/' + $name )
if ( $LASTEXITCODE -ne 0 ) { throw "Shipping build failed: $name . Existing binaries were not replaced." }
2026-10-16 22:19:52 +07:00
}
foreach ( $name in @ ( 'gameserver-go' , 'sdkserver-go' )) {
$next = Resolve-ProjectPath ( $name + '.next.exe' )
$target = Resolve-ProjectPath ( $name + '.exe' )
2026-10-28 18:54:43 +07:00
$shipNext = Join-Path $shipDirectory ( $name + '.next.exe' )
$shipTarget = Join-Path $shipDirectory ( $name + '.exe' )
2026-10-16 22:19:52 +07:00
Stop-ProjectServer $name
2026-10-28 18:54:43 +07:00
Stop-ProjectServerAt $name $shipTarget
2026-10-16 22:19:52 +07:00
Move-Item -LiteralPath $next -Destination $target -Force
2026-10-28 18:54:43 +07:00
Move-Item -LiteralPath $shipNext -Destination $shipTarget -Force
2026-10-16 22:19:52 +07:00
}
2026-10-28 18:54:43 +07:00
Write-Host "Build successful. Standard binaries are in the project root; self-contained binaries are in ' $shipDirectory '."
2026-10-16 22:19:52 +07:00
}
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 = @ {
2026-10-21 20:40:13 +07:00
account_name = 'GAC_ACCOUNT_NAME' ;
2026-10-17 23:44:36 +07:00
player_uid = 'GAC_PLAYER_UID' ; client_version = 'GAC_CLIENT_VERSION' ; game_host = 'GAC_GAME_HOST' ; game_port = 'GAC_GAME_PORT' ;
2026-10-16 22:19:52 +07:00
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 }
}
2026-10-21 20:40:13 +07:00
foreach ( $pair in @ ( @ ( 'begin_time' , 'GAC_ACTIVITY_BEGIN_TIME' ), @ ( 'end_time' , 'GAC_ACTIVITY_END_TIME' ))) {
$value = [ Environment ]:: GetEnvironmentVariable ( $pair [ 1 ])
if ( $null -ne $value ) {
if ( -not $settings . activity_schedule ) { $settings | Add-Member -MemberType NoteProperty -Name activity_schedule -Value ([ pscustomobject ] @ {}) }
$settings . activity_schedule | Add-Member -MemberType NoteProperty -Name $pair [ 0 ] -Value $value -Force
}
}
$liveStaleSecondsText = [ string ] $settings . battle_diagnostics . live_report_stale_seconds
$liveStaleSecondsOverride = [ Environment ]:: GetEnvironmentVariable ( 'GAC_LIVE_BATTLE_STALE_SECONDS' )
if ( $null -ne $liveStaleSecondsOverride ) {
if ( -not $settings . battle_diagnostics ) {
$settings | Add-Member -MemberType NoteProperty -Name battle_diagnostics -Value ([ pscustomobject ] @ {})
}
$settings . battle_diagnostics | Add-Member -MemberType NoteProperty -Name live_report_stale_seconds -Value $liveStaleSecondsOverride -Force
$liveStaleSecondsText = $liveStaleSecondsOverride
}
$liveStaleSeconds = [ uint16 ] 0
if ( -not [ uint16 ]:: TryParse ( $liveStaleSecondsText , [ ref ] $liveStaleSeconds ) -or $liveStaleSeconds -eq 0 ) {
throw 'battle_diagnostics.live_report_stale_seconds must be an integer between 1 and 65535.'
}
$activityBegin = [ long ] 0
$activityEnd = [ long ] 0
if ( -not $settings . activity_schedule -or
$null -eq $settings . activity_schedule . begin_time -or $null -eq $settings . activity_schedule . end_time -or
-not [ long ]:: TryParse ([ string ] $settings . activity_schedule . begin_time ,[ ref ] $activityBegin ) -or
-not [ long ]:: TryParse ([ string ] $settings . activity_schedule . end_time ,[ ref ] $activityEnd ) -or
$activityBegin -lt 0 -or $activityEnd -le $activityBegin ) {
throw 'activity_schedule requires nonnegative begin_time and end_time greater than begin_time.'
}
2026-10-17 23:44:36 +07:00
$configuredUID = [ uint32 ] 0
if ( -not [ uint32 ]:: TryParse ([ string ] $settings . player_uid , [ ref ] $configuredUID ) -or $configuredUID -eq 0 ) {
throw 'player_uid must be an integer between 1 and 4294967295.'
}
2026-10-16 22:19:52 +07:00
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 ) ."
}
}
2026-10-21 20:40:13 +07:00
foreach ( $key in @ ( 'account_name' , 'client_version' , 'game_host' , 'sdk_host' , 'public_game_host' , 'public_sdk_url' )) {
if ( $settings . $key -isnot [ string ] -or [ string ]:: IsNullOrWhiteSpace ( $settings . $key )) { throw "Missing or invalid string setting: $key " }
2026-10-16 22:19:52 +07:00
}
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.'
}
2026-10-21 20:40:13 +07:00
$leaseSeconds = [ uint32 ] 0
if ( $settings . srtool . lease_file -isnot [ string ] -or [ string ]:: IsNullOrWhiteSpace ( $settings . srtool . lease_file ) -or -not [ uint32 ]:: TryParse ([ string ] $settings . srtool . lease_seconds ,[ ref ] $leaseSeconds ) -or $leaseSeconds -eq 0 ) { throw 'Set srtool.lease_file and positive uint32 srtool.lease_seconds.' }
$install = $settings . launcher . go_install
if ( $install -and $install . enabled ) {
if ([ string ]:: IsNullOrWhiteSpace ( $install . directory )) { throw 'Set launcher.go_install.directory.' }
foreach ( $key in @ ( 'manifest_url' , 'download_url' )) {
$uri = $null
if ( -not [ Uri ]:: TryCreate ( $install . $key , [ UriKind ]:: Absolute , [ ref ] $uri ) -or $uri . Scheme -ne 'https' -or $uri . UserInfo -or $uri . Fragment ) { throw "launcher.go_install. $key must be an HTTPS URL." }
}
}
if ( $Action -eq 'check' ) { Write-Host "Configuration valid. Activity schedule: $activityBegin -> $activityEnd " ; exit 0 }
2026-10-16 22:19:52 +07:00
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'
2026-10-28 18:54:43 +07:00
Write-Host '[9] Check/install Go build requirement'
2026-10-16 22:19:52 +07:00
$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 }
2026-10-28 18:54:43 +07:00
{ $_ -in @ ( '9' , 'requirements' ) } { $go = Resolve-GoToolchain $settings . launcher ; Write-Host "Go ready: $go " ; break }
2026-10-16 22:19:52 +07:00
'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