381 lines
17 KiB
PowerShell
381 lines
17 KiB
PowerShell
$ErrorActionPreference = 'Stop'
|
|
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$ConfigPath = Join-Path $Root 'config\private-server.json'
|
|
$ProxyRoot = Join-Path $Root 'Ananta.Proxy'
|
|
$ProxyDir = Join-Path $ProxyRoot 'proxy'
|
|
$ServerProject = Join-Path $Root 'Ananta.Server\Ananta.App\Ananta.App.csproj'
|
|
$HostsPath = Join-Path $env:WINDIR 'System32\drivers\etc\hosts'
|
|
|
|
$script:ConsoleLogLatest = $null
|
|
$script:ConsoleLogArchive = $null
|
|
|
|
function Append-RunLog([string]$Text) {
|
|
if (-not $script:ConsoleLogLatest) { return }
|
|
$line = $Text + [Environment]::NewLine
|
|
try { [System.IO.File]::AppendAllText($script:ConsoleLogLatest, $line, [System.Text.UTF8Encoding]::new($false)) } catch { }
|
|
try { [System.IO.File]::AppendAllText($script:ConsoleLogArchive, $line, [System.Text.UTF8Encoding]::new($false)) } catch { }
|
|
}
|
|
|
|
function Status([string]$Text, [System.ConsoleColor]$Color = [System.ConsoleColor]::Gray) {
|
|
Microsoft.PowerShell.Utility\Write-Host $Text -ForegroundColor $Color
|
|
Append-RunLog $Text
|
|
}
|
|
|
|
function Fail([string]$Text) {
|
|
Status "[ERROR] $Text" Red
|
|
if ($script:ConsoleLogLatest) { Status "[LOG] $script:ConsoleLogLatest" DarkGray }
|
|
exit 1
|
|
}
|
|
|
|
function Test-AnantaAdministrator {
|
|
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
|
$principal = New-Object System.Security.Principal.WindowsPrincipal($id)
|
|
return $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
}
|
|
|
|
function Find-Exe([string]$EnvName, [string]$CommandName, [string[]]$Fallbacks = @()) {
|
|
$fromEnv = [Environment]::GetEnvironmentVariable($EnvName)
|
|
if ($fromEnv -and (Test-Path -LiteralPath $fromEnv)) { return $fromEnv }
|
|
$cmd = Get-Command $CommandName -ErrorAction SilentlyContinue
|
|
if ($cmd) { return $cmd.Source }
|
|
foreach ($candidate in $Fallbacks) {
|
|
if ($candidate -and (Test-Path -LiteralPath $candidate)) { return $candidate }
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Quote-WindowsProcessArgument([string]$Value) {
|
|
if ($null -eq $Value) { return '""' }
|
|
if ($Value -notmatch '[\s"]') { return $Value }
|
|
$escaped = [System.Text.RegularExpressions.Regex]::Replace($Value, '(\\*)"', '$1$1\\"')
|
|
$escaped = [System.Text.RegularExpressions.Regex]::Replace($escaped, '(\\+)$', '$1$1')
|
|
return '"' + $escaped + '"'
|
|
}
|
|
|
|
function Invoke-SilentNative {
|
|
param([string]$FilePath, [string[]]$Arguments, [string]$WorkingDirectory)
|
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
$psi.FileName = $FilePath
|
|
$psi.WorkingDirectory = if ($WorkingDirectory) { $WorkingDirectory } else { $Root }
|
|
$psi.UseShellExecute = $false
|
|
$psi.CreateNoWindow = $true
|
|
$psi.RedirectStandardOutput = $true
|
|
$psi.RedirectStandardError = $true
|
|
$utf8 = New-Object System.Text.UTF8Encoding($false)
|
|
try { $psi.StandardOutputEncoding = $utf8 } catch { }
|
|
try { $psi.StandardErrorEncoding = $utf8 } catch { }
|
|
$psi.Arguments = (($Arguments | ForEach-Object { Quote-WindowsProcessArgument ([string]$_) }) -join ' ')
|
|
|
|
$process = New-Object System.Diagnostics.Process
|
|
$process.StartInfo = $psi
|
|
if (-not $process.Start()) { return 1 }
|
|
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
|
|
$stderrTask = $process.StandardError.ReadToEndAsync()
|
|
$process.WaitForExit()
|
|
foreach ($text in @($stdoutTask.Result, $stderrTask.Result)) {
|
|
if ([string]::IsNullOrWhiteSpace($text)) { continue }
|
|
foreach ($line in ($text -split "`r?`n")) {
|
|
if ($line.Length -gt 0) { Append-RunLog $line }
|
|
}
|
|
}
|
|
return $process.ExitCode
|
|
}
|
|
|
|
function Stop-StaleAnantaProcesses {
|
|
foreach ($proc in @(Get-Process -Name 'Ananta.App' -ErrorAction SilentlyContinue)) {
|
|
try { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue } catch { }
|
|
}
|
|
try {
|
|
$proxyServerJs = [System.IO.Path]::GetFullPath((Join-Path $ProxyDir 'server.js'))
|
|
foreach ($proc in @(Get-CimInstance Win32_Process -Filter "Name='node.exe'" -ErrorAction SilentlyContinue)) {
|
|
$cmd = [string]$proc.CommandLine
|
|
if ($cmd -and ($cmd.IndexOf($proxyServerJs, [System.StringComparison]::OrdinalIgnoreCase) -ge 0)) {
|
|
try { Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue } catch { }
|
|
}
|
|
}
|
|
} catch { }
|
|
Start-Sleep -Milliseconds 200
|
|
}
|
|
|
|
# START.cmd remains the only launcher. Elevate once for certificate/hosts setup and ports 80/443.
|
|
if (-not (Test-AnantaAdministrator)) {
|
|
$psExe = Join-Path $PSHOME 'powershell.exe'
|
|
if (-not (Test-Path -LiteralPath $psExe)) { $psExe = 'powershell.exe' }
|
|
try {
|
|
$elevated = Start-Process -FilePath $psExe -Verb RunAs -ArgumentList ('-NoLogo -NoProfile -ExecutionPolicy Bypass -File "' + $PSCommandPath + '"') -WorkingDirectory $Root -Wait -PassThru
|
|
exit $elevated.ExitCode
|
|
} catch {
|
|
Microsoft.PowerShell.Utility\Write-Host "[ERROR] administrator launch failed: $($_.Exception.Message)" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
$ExampleConfigPath = Join-Path $Root 'config\private-server.example.json'
|
|
foreach ($required in @(
|
|
$ServerProject,
|
|
(Join-Path $ProxyDir 'server.js'),
|
|
(Join-Path $ProxyDir 'package.json'),
|
|
(Join-Path $ProxyDir 'tools\generate_client_config_patch.js'),
|
|
$ExampleConfigPath,
|
|
(Join-Path $Root 'Ananta.Server\ClientData\4229938\MethodIds.json'),
|
|
(Join-Path $Root 'Ananta.Server\ClientData\4229938\RpcSurface.json')
|
|
)) {
|
|
if (-not (Test-Path -LiteralPath $required)) { Fail "server package is incomplete: $required" }
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath $ConfigPath)) {
|
|
Copy-Item -LiteralPath $ExampleConfigPath -Destination $ConfigPath
|
|
}
|
|
try { $Config = Get-Content -Raw -Encoding UTF8 -LiteralPath $ConfigPath | ConvertFrom-Json }
|
|
catch { Fail "invalid config/private-server.json: $($_.Exception.Message)" }
|
|
|
|
$RunId = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$LogDir = [string]$Config.logging.directory
|
|
if ([string]::IsNullOrWhiteSpace($LogDir)) { $LogDir = 'logs' }
|
|
if (-not [System.IO.Path]::IsPathRooted($LogDir)) { $LogDir = Join-Path $Root $LogDir }
|
|
$LogDir = [System.IO.Path]::GetFullPath($LogDir)
|
|
[System.IO.Directory]::CreateDirectory($LogDir) | Out-Null
|
|
$ConsoleLoggingEnabled = [bool]$Config.logging.console.enabled
|
|
$PacketLoggingEnabled = [bool]$Config.logging.packets.enabled
|
|
$script:ConsoleLogLatest = $null
|
|
$script:ConsoleLogArchive = $null
|
|
|
|
if ($ConsoleLoggingEnabled) {
|
|
$script:ConsoleLogLatest = Join-Path $LogDir 'console-latest.log'
|
|
$script:ConsoleLogArchive = Join-Path $LogDir ("console-{0}.log" -f $RunId)
|
|
foreach ($file in @($script:ConsoleLogLatest, $script:ConsoleLogArchive)) {
|
|
[System.IO.File]::WriteAllText($file, '', [System.Text.UTF8Encoding]::new($false))
|
|
}
|
|
$env:Ananta_CONSOLE_LOG_LATEST = $script:ConsoleLogLatest
|
|
$env:Ananta_CONSOLE_LOG_ARCHIVE = $script:ConsoleLogArchive
|
|
} else {
|
|
Remove-Item Env:Ananta_CONSOLE_LOG_LATEST -ErrorAction SilentlyContinue
|
|
Remove-Item Env:Ananta_CONSOLE_LOG_ARCHIVE -ErrorAction SilentlyContinue
|
|
}
|
|
|
|
$PacketLogLatest = $null
|
|
$PacketLogArchive = $null
|
|
if ($PacketLoggingEnabled) {
|
|
$PacketLogLatest = Join-Path $LogDir 'packets-latest.log'
|
|
$PacketLogArchive = Join-Path $LogDir ("packets-{0}.log" -f $RunId)
|
|
foreach ($file in @($PacketLogLatest, $PacketLogArchive)) {
|
|
[System.IO.File]::WriteAllText($file, '', [System.Text.UTF8Encoding]::new($false))
|
|
}
|
|
$env:Ananta_PACKET_LOG_LATEST = $PacketLogLatest
|
|
$env:Ananta_PACKET_LOG_ARCHIVE = $PacketLogArchive
|
|
} else {
|
|
Remove-Item Env:Ananta_PACKET_LOG_LATEST -ErrorAction SilentlyContinue
|
|
Remove-Item Env:Ananta_PACKET_LOG_ARCHIVE -ErrorAction SilentlyContinue
|
|
}
|
|
$env:Ananta_RUN_ID = $RunId
|
|
$env:Ananta_LOG_DIR = $LogDir
|
|
$env:Ananta_LOGS_PREPARED = '1'
|
|
|
|
# Preserve the useful bootstrap diagnostics in the normal log files without printing them.
|
|
$consoleDisplay = if ($ConsoleLoggingEnabled) { $script:ConsoleLogLatest } else { 'disabled' }
|
|
$packetDisplay = if ($PacketLoggingEnabled) { $PacketLogLatest } else { 'disabled' }
|
|
Append-RunLog '[START] launching standalone bootstrap...'
|
|
Append-RunLog ("[LOGS] console={0} | packets={1}" -f $consoleDisplay, $packetDisplay)
|
|
Append-RunLog ("[BUILD] Ananta Private Server | client {0} | world-entry + switching + buffs/combat" -f $Config.client.version)
|
|
Append-RunLog "[CONFIG] $ConfigPath"
|
|
Append-RunLog "[CONFIG] pid=$($Config.player.pid) spirit=$($Config.player.initialSpiritTemplateId) raid=$($Config.world.raidId)"
|
|
|
|
$env:Ananta_CONFIG = $ConfigPath
|
|
$env:Ananta_ROOT = $Root
|
|
$env:Ananta_COMPACT_LOG = '1'
|
|
$env:Ananta_HIDE_BOOT_LOG = '1'
|
|
Remove-Item Env:Ananta_QUIET_CONSOLE -ErrorAction SilentlyContinue
|
|
$env:DOTNET_CLI_UI_LANGUAGE = 'en-US'
|
|
$env:DOTNET_NOLOGO = '1'
|
|
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
|
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
|
|
|
|
|
|
$NodeExe = Find-Exe 'Ananta_NODE_EXE' 'node' @('C:\Program Files\nodejs\node.exe')
|
|
if (-not $NodeExe) { Fail 'Node.js was not found. Install Node.js LTS.' }
|
|
$DotnetExe = Find-Exe 'Ananta_DOTNET_EXE' 'dotnet'
|
|
if (-not $DotnetExe) { Fail '.NET 8 SDK was not found.' }
|
|
|
|
$FengariPackage = Join-Path $ProxyDir 'node_modules\fengari\package.json'
|
|
if (-not (Test-Path -LiteralPath $FengariPackage)) {
|
|
$NpmExe = Find-Exe 'Ananta_NPM_EXE' 'npm.cmd' @((Join-Path (Split-Path -Parent $NodeExe) 'npm.cmd'))
|
|
if (-not $NpmExe) { Fail 'Node dependencies are missing and npm.cmd was not found.' }
|
|
if ((Invoke-SilentNative $NpmExe @('ci', '--ignore-scripts', '--no-audit', '--no-fund') $ProxyDir) -ne 0) {
|
|
Fail 'npm dependency restore failed'
|
|
}
|
|
}
|
|
if (-not (Test-Path -LiteralPath $FengariPackage)) { Fail 'fengari dependency is missing.' }
|
|
Append-RunLog '[NODE] exact Lua serializer dependency OK (fengari)'
|
|
|
|
Stop-StaleAnantaProcesses
|
|
|
|
$DnsName = [string]$Config.network.proxy.updateHost
|
|
$PfxPath = Join-Path $ProxyDir ("certs\{0}.pfx" -f $DnsName)
|
|
$PfxPassphrase = [string]$Config.network.proxy.certificatePassphrase
|
|
|
|
# The login screen depends on more than the update CDN. In particular,
|
|
# mgbsdk.matrix.netease.com must resolve to the local HTTPS proxy; otherwise
|
|
# UniSDK's /sdk/uni_sauth request reaches the real service and the client stops
|
|
# at the menu with login code 220. Validate the whole runtime redirect set.
|
|
$RequiredProxyDomains = @(
|
|
$DnsName,
|
|
'serverlist-test.l50.leihuo.netease.com',
|
|
'l50.gdl.netease.com',
|
|
'l50.gph.netease.com',
|
|
'l50.gsgph.netease.com',
|
|
'service.mkey.163.com',
|
|
'qatest.g.mkey.163.com',
|
|
'qatest-1.g.mkey.163.com',
|
|
'qatest-2.g.mkey.163.com',
|
|
'qatest-3.g.mkey.163.com',
|
|
'qatest-4.g.mkey.163.com',
|
|
'qatest-5.g.mkey.163.com',
|
|
'qatest-6.g.mkey.163.com',
|
|
'qatest-7.g.mkey.163.com',
|
|
'qatest-8.g.mkey.163.com',
|
|
'bind-mobile.g.mkey.163.com',
|
|
'bind-mobile-test.g.mkey.163.com',
|
|
'mpay-common-server.g.mkey.163.com',
|
|
'mpay-common-server-test.g.mkey.163.com',
|
|
'mpay-personal-privacy-protection.g.mkey.163.com',
|
|
'mpay-personal-privacy-protection-dev.g.mkey.163.com',
|
|
'whoami.nie.netease.com',
|
|
'whoami.nie.easebar.com',
|
|
'protocol.unisdk.netease.com',
|
|
'tpsl.nie.netease.com',
|
|
'g0.gsf.netease.com',
|
|
'g0.gsf.easebar.com',
|
|
'mcount.easebar.com',
|
|
'analytics.mpay.netease.com',
|
|
'applog.matrix.netease.com',
|
|
'applog.matrix.easebar.com',
|
|
'mgbsdktest.matrix.netease.com',
|
|
'mgbsdk.matrix.netease.com',
|
|
'mgbsdk.matrix.easebar.com',
|
|
'dns.update.netease.com',
|
|
'dns.update.easebar.com',
|
|
'openapi.music.163.com'
|
|
) | Select-Object -Unique
|
|
|
|
$hostsOk = Test-Path -LiteralPath $HostsPath
|
|
if ($hostsOk) {
|
|
$hostLines = @(Get-Content -LiteralPath $HostsPath -ErrorAction SilentlyContinue)
|
|
foreach ($domain in $RequiredProxyDomains) {
|
|
$pattern = '^\s*127\.0\.0\.1\s+' + [regex]::Escape($domain) + '(?:\s|$)'
|
|
if (-not ($hostLines | Where-Object { $_ -match $pattern })) {
|
|
$hostsOk = $false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
$certOk = $false
|
|
if (Test-Path -LiteralPath $PfxPath) {
|
|
try {
|
|
$pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($PfxPath, $PfxPassphrase)
|
|
$trustedRoot = Get-ChildItem Cert:\LocalMachine\Root -ErrorAction SilentlyContinue | Where-Object { $_.Subject -eq 'CN=Ananta Local Proxy Root' }
|
|
# Require the new normal root->leaf chain. Old packages used a self-signed
|
|
# CA=TRUE server certificate, which a later client TLS stack can reject.
|
|
$certOk = (
|
|
$pfx.NotAfter -gt (Get-Date).AddHours(1) -and
|
|
$pfx.Subject -eq ("CN={0}" -f $DnsName) -and
|
|
$pfx.Issuer -eq 'CN=Ananta Local Proxy Root' -and
|
|
$null -ne $trustedRoot
|
|
)
|
|
$pfx.Dispose()
|
|
} catch { $certOk = $false }
|
|
}
|
|
if (-not $certOk -or -not $hostsOk) {
|
|
Append-RunLog '[PROXY] repairing certificate/hosts redirects for login services'
|
|
$setupScript = Join-Path $ProxyRoot 'SETUP_PROXY_AS_ADMIN.ps1'
|
|
if (-not (Test-Path -LiteralPath $setupScript)) { Fail 'proxy setup script is missing' }
|
|
if ((Invoke-SilentNative 'powershell.exe' @('-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $setupScript) $Root) -ne 0) {
|
|
Fail 'certificate/hosts setup failed'
|
|
}
|
|
}
|
|
|
|
# Fail early instead of opening a client that can only reach the menu.
|
|
$hostLines = @(Get-Content -LiteralPath $HostsPath -ErrorAction SilentlyContinue)
|
|
foreach ($domain in @('serverlist-test.l50.leihuo.netease.com','l50.gdl.netease.com','service.mkey.163.com','mgbsdk.matrix.netease.com')) {
|
|
$pattern = '^\s*127\.0\.0\.1\s+' + [regex]::Escape($domain) + '(?:\s|$)'
|
|
if (-not ($hostLines | Where-Object { $_ -match $pattern })) {
|
|
Fail "local proxy redirect is missing for $domain"
|
|
}
|
|
}
|
|
|
|
# Generate the only client Lua override: UID/watermark branding.
|
|
Append-RunLog '[1/3] fastpatch + startup checks'
|
|
if ((Invoke-SilentNative $NodeExe @('.\tools\generate_client_config_patch.js') $ProxyDir) -ne 0) {
|
|
Fail 'fastpatch generation failed'
|
|
}
|
|
$FastPatchPath = Join-Path $ProxyDir ("public\fastpatch_{0}.zip" -f $Config.client.version)
|
|
$RuntimeFastPatchRoot = [string]$Config.paths.runtimeFastpatch
|
|
if (-not [System.IO.Path]::IsPathRooted($RuntimeFastPatchRoot)) { $RuntimeFastPatchRoot = Join-Path $Root $RuntimeFastPatchRoot }
|
|
$FastPatchSource = Join-Path $RuntimeFastPatchRoot ([string]$Config.client.version)
|
|
try {
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
|
|
$temp = "$FastPatchPath.tmp"
|
|
if (Test-Path -LiteralPath $temp) { Remove-Item -LiteralPath $temp -Force }
|
|
[System.IO.Compression.ZipFile]::CreateFromDirectory($FastPatchSource, $temp, [System.IO.Compression.CompressionLevel]::Optimal, $false)
|
|
if (Test-Path -LiteralPath $FastPatchPath) { Remove-Item -LiteralPath $FastPatchPath -Force }
|
|
Move-Item -LiteralPath $temp -Destination $FastPatchPath
|
|
} catch { Fail "cannot build fastpatch: $($_.Exception.Message)" }
|
|
|
|
# Clean rebuild; output goes to logs instead of filling the console.
|
|
Append-RunLog '[2/3] build (clean)'
|
|
$cleanDirs = @(Get-ChildItem -LiteralPath (Join-Path $Root 'Ananta.Server') -Directory -Recurse -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Name -in @('bin','obj') } | Select-Object -ExpandProperty FullName)
|
|
$cleanDirs += @((Join-Path $Root 'Ananta.SDK\bin'), (Join-Path $Root 'Ananta.SDK\obj'))
|
|
foreach ($dir in $cleanDirs | Select-Object -Unique) {
|
|
if (Test-Path -LiteralPath $dir) { Remove-Item -LiteralPath $dir -Recurse -Force }
|
|
}
|
|
if ((Invoke-SilentNative $DotnetExe @('build', $ServerProject, '--nologo', '-v:q') $Root) -ne 0) {
|
|
Fail 'server build failed'
|
|
}
|
|
|
|
$wantedPorts = @(
|
|
[int]$Config.network.proxy.httpPort,
|
|
[int]$Config.network.proxy.httpsPort,
|
|
[int]$Config.network.proxy.loginListPort,
|
|
[int]$Config.network.proxy.loginTcpPort,
|
|
[int]$Config.network.proxy.gameTcpPort,
|
|
[int]$Config.network.proxy.sceneSubPort,
|
|
[int]$Config.network.loginPorts[0],
|
|
[int]$Config.network.loginPorts[1],
|
|
[int]$Config.network.gamePort
|
|
) | Select-Object -Unique
|
|
try {
|
|
$busy = @(Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $wantedPorts -contains $_.LocalPort })
|
|
if ($busy.Count -gt 0) {
|
|
$desc = ($busy | Sort-Object LocalPort -Unique | ForEach-Object { ":$($_.LocalPort) pid=$($_.OwningProcess)" }) -join ', '
|
|
Fail "ports already in use: $desc"
|
|
}
|
|
} catch { }
|
|
|
|
|
|
Append-RunLog '[3/3] start'
|
|
Append-RunLog '[OK] one console | Ctrl+C stops the stack'
|
|
|
|
# All bootstrap diagnostics were already written to the normal console log files.
|
|
# Keep the interactive window clean and let only runtime traffic appear from this point on.
|
|
try { Clear-Host } catch { }
|
|
try { $Host.UI.RawUI.WindowTitle = 'Ananta 4229938 Private Server' } catch { }
|
|
|
|
$proxy = $null
|
|
$serverExit = 0
|
|
try {
|
|
$proxy = Start-Process -FilePath $NodeExe -ArgumentList @('server.js') -WorkingDirectory $ProxyDir -NoNewWindow -PassThru
|
|
Start-Sleep -Milliseconds 500
|
|
if ($proxy.HasExited) { Fail "proxy exited with code $($proxy.ExitCode)" }
|
|
|
|
& $DotnetExe run --no-build --project $ServerProject
|
|
$serverExit = $LASTEXITCODE
|
|
} finally {
|
|
if ($proxy -and -not $proxy.HasExited) {
|
|
try { Stop-Process -Id $proxy.Id -Force -ErrorAction SilentlyContinue } catch { }
|
|
}
|
|
Stop-StaleAnantaProcesses
|
|
Append-RunLog '[STOP] Ananta stack stopped'
|
|
}
|
|
if ($serverExit -ne 0) { exit $serverExit }
|