Files
TankIC/3DTank/Start-LocalAndRelay.ps1
T
TermiNexus 78c02345c2 revert(local): 运行链路默认改回 unpacked,src 改为 -UseSrc 可选
保持原有行为:默认加载 3dtank.com/play/static/js/unpacked(含 4 个模块的人工补丁),启动时按需打单包,-Build 强制重打;-UseSrc 才切到 3DTank/src,-Split 作为兼容旧参数等同默认。serve.js 的 --src 能力保留但不启用。
2026-10-20 23:06:09 +08:00

270 lines
14 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
.SYNOPSIS
一键启动 3DTank 本地镜像(serve.js)+ 中继(wss_server.js relay 模式)
.DESCRIPTION
- 本地镜像:把官方 web 客户端在 http://localhost:<MirrorPort>/ 跑起来
· web/ 提供首页与镜像资源;/play/static/js/** 直接挂 3dtank.com 的切割版 JS
· /config.xml:-WithRelay 时由 serve.js 动态改写为 ws://localhost:<RelayPort>/(不落盘);否则代理真实 config
- 中继:客户端 → ws://localhost:<RelayPort>/ → 真实服务器(抓包落盘 + 实时解密/解析)
· 产物目录:3DTank/packets/<label>/session_00N/
- 依赖:中继需要 ws(Node 自带的是客户端 WebSocket,服务端不够用),脚本会自动装
.EXAMPLE
// 最省事:装好依赖、起两个服务、打开浏览器
powershell -ExecutionPolicy Bypass -File .\3DTank\Start-LocalAndRelay.ps1 -OpenBrowser
.EXAMPLE
// 资源缺失(页面 404 很多)时先镜像资源再起(首次约 70MB)
.\Start-LocalAndRelay.ps1 -FetchAssets -OpenBrowser
.EXAMPLE
// 端口被上次残留占用时,先杀掉再起
.\Start-LocalAndRelay.ps1 -KillExisting -OpenBrowser
.EXAMPLE
// 强制整个链路(前端 config 来源 + 中继上游)都走 c1
// 注意:PowerShell 不认 -SRV=1 这种等号写法,要写 -SRV 1 或 -SRV:1
.\Start-LocalAndRelay.ps1 -SRV 1 -WithRelay -OpenBrowser
.EXAMPLE
// 强制走 c2(官方默认主服)
.\Start-LocalAndRelay.ps1 -SRV 2 -OpenBrowser
.EXAMPLE
// 默认用切割产物 unpacked;启动时按需打单包(-Build 强制重打)
.\Start-LocalAndRelay.ps1 -OpenBrowser
.EXAMPLE
// 改用反编译源码 3DTank/src(改完刷新即生效)
.\Start-LocalAndRelay.ps1 -UseSrc -OpenBrowser
#>
# 注意:这里刻意不用 [CmdletBinding()]。
# PowerShell 原生不支持 -SRV=1 这种等号写法(会报 "parameter cannot be found"),
# 去掉 CmdletBinding 后,未绑定的 -SRV=1 会落进 $args,由下面手动解析兼容。
param(
[int]$MirrorPort = 8080,
[int]$RelayPort = 9091,
[string]$Upstream = 'wss://c2.3dtank.com:9090/', # 主上游(大厅:首帧 = Hash Request)
[string]$UpstreamAlt = 'wss://c1.3dtank.com:9090/', # 备用上游(首帧直接 EnterSpace 的「二次进空间」连接)
[ValidateSet(0, 1, 2)][int]$SRV = 0, # 服务器选择:0=保持默认(c2);1=前后端都走 c1.3dtank.com;2=前后端都走 c2.3dtank.com
[string]$Label = (Get-Date -Format 'yyyyMMdd-HHmmss'),
[switch]$FetchAssets, # 先跑 fetch_assets.js 镜像资源(首次或资源缺失时用)
[switch]$KillExisting, # 端口被占用时,先结束占用进程
[switch]$OpenBrowser, # 启动后打开 http://localhost:<MirrorPort>/
[switch]$WithRelay, # 启动本地中继(默认**不启动**:页面直连真实服务器 wss://c1.3dtank.com:9090/)
[switch]$Hook, # 本地镜像:注入页面内 ws 抓包(不用改 URL;游戏用 hash 做路由,加 hash 会进不去战场)
[int]$DumpEvery = 1, # 中继:每 N 帧写一个 .bin(默认 1 = 每帧都写;0 = 不写抓包文件)
[switch]$NoLog, # 中继:不写 log.txt(默认写)
[switch]$NoHex, # 中继:log.txt 不写十六进制(日志体积大幅减小)
[switch]$Quiet, # 中继:只写 log.txt,不刷控制台
[switch]$NoTunnel, # 中继:关闭 _reconnectFast 隧道接管(默认开启:战场连接也走中继)
[switch]$Build, # 强制重打单包(默认只在缺失/模块更新时打包)
[switch]$UseSrc, # 可选:改用反编译源码 3DTank/src;默认仍用切割产物 unpacked
[switch]$Split # 兼容旧参数:默认就是 unpacked,此开关等同默认
)
$ErrorActionPreference = 'Stop'
# ---------- 兼容 -SRV=1 / --SRV=1 等号写法 ----------
# PowerShell 只认 -SRV 1 或 -SRV:1;等号形式会被当作未绑定参数落进 $args,这里手动识别
foreach ($A in $args) {
if ($A -is [string] -and $A -match '^--?SRV=(.+)$') {
$SRV = [int]$Matches[1]
}
}
if ($SRV -notin 0, 1, 2) { throw "-SRV 只能是 0 / 1 / 2(收到: $SRV)" }
$Project = $PSScriptRoot # 3DTank/
$Tools = Join-Path $Project 'tools'
$PacketDir = Join-Path $Project ("packets\{0}" -f $Label)
# ---------- 服务器选择(-SRV 1/2)----------
# 同时决定:① 前端 /config.xml 的回源(真实 config 里的 server address)
# ② 中继 --upstream(后端实际连哪台)
$ConfigUpstream = 'https://c2.3dtank.com'
if ($SRV -eq 1 -or $SRV -eq 2) {
$ServerHost = "c$SRV.3dtank.com"
$Upstream = "wss://${ServerHost}:9090/"
$UpstreamAlt = $Upstream
$ConfigUpstream = "https://$ServerHost"
Write-Host ("[服务器] -SRV={0} → 前端 config 与中继上游统一走 {1}" -f $SRV, $ServerHost) -ForegroundColor Yellow
}
# ---------- 小工具 ----------
function Get-CmdPath([string]$Name) {
$c = Get-Command $Name -ErrorAction SilentlyContinue
if (-not $c) { throw "找不到命令 '$Name',请先安装 Node.js(自带 npm)并加入 PATH" }
return $c.Source
}
function Test-PortFree([int]$Port) {
$client = New-Object System.Net.Sockets.TcpClient
try {
$client.Connect('127.0.0.1', $Port)
return $false # 连上了 = 已被占用
} catch {
return $true # 连不上 = 空闲
} finally {
try { $client.Close() } catch {}
}
}
function Get-PortPid([int]$Port) {
try {
return (Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction Stop |
Select-Object -First 1 -ExpandProperty OwningProcess)
} catch { return $null }
}
function Assert-PortReady([int]$Port, [string]$What) {
if (Test-PortFree $Port) { return }
if ($KillExisting) {
# 注意:$pid 是 PowerShell 只读自动变量(当前进程),不能用来存占用者 PID
$OwnerPid = Get-PortPid $Port
if ($OwnerPid) {
Write-Host ("[端口] {0} 被 PID={1} 占用,按 -KillExisting 结束它" -f $Port, $OwnerPid) -ForegroundColor Yellow
Stop-Process -Id $OwnerPid -Force -ErrorAction SilentlyContinue
Start-Sleep -Milliseconds 500
}
}
if (-not (Test-PortFree $Port)) { throw "端口 $Port 已被占用($What)。用 -KillExisting 或换端口" }
}
function Test-TcpOpen([string]$HostName, [int]$Port, [int]$TimeoutMs = 2500) {
$client = New-Object System.Net.Sockets.TcpClient
try {
$task = $client.ConnectAsync($HostName, $Port)
if (-not $task.Wait($TimeoutMs)) { return $false }
return $client.Connected
} catch {
return $false
} finally {
try { $client.Close() } catch {}
}
}
# ---------- 0) 环境 ----------
$NodeExe = Get-CmdPath 'node'
$NpmExe = Get-CmdPath 'npm'
Write-Host ("[环境] node={0} npm={1}" -f (& $NodeExe -v), (& $NpmExe -v)) -ForegroundColor DarkGray
# ---------- 1) 依赖:中继需要 ws(服务端) ----------
$WsPackage = Join-Path $Tools 'node_modules\ws\package.json'
if (-not (Test-Path $WsPackage)) {
Write-Host '[依赖] 安装 ws 到 3DTank/tools ...' -ForegroundColor Cyan
Push-Location $Tools
try { & $NpmExe install --no-save ws | Out-Host } finally { Pop-Location }
}
if (-not (Test-Path $WsPackage)) { throw 'ws 安装失败(检查网络/代理)' }
# ---------- 2) 端口 ----------
Assert-PortReady $RelayPort '中继 wss_server.js'
Assert-PortReady $MirrorPort '本地镜像 serve.js'
# ---------- 3) 可选:镜像资源 ----------
if ($FetchAssets) {
Write-Host '[资源] fetch_assets.js 镜像资源(首次约 70MB,请耐心等)...' -ForegroundColor Cyan
Push-Location $Tools
try { & $NodeExe fetch_assets.js --concurrency 10 | Out-Host } finally { Pop-Location }
}
# ---------- 3.5) 模块来源与单包 ----------
# 默认:用切割产物 3dtank.com/play/static/js/unpacked(保持原有行为)
# -UseSrc:改用反编译源码 3DTank/src(改完代码刷新即生效)
# -Build :强制重打单包
$SrcDir = Join-Path $Project 'src'
$RootDir = Split-Path -Parent $Project
$PackScript = Join-Path $RootDir 'tools\pack_unpacked.js'
if (-not (Test-Path $PackScript)) { throw ('找不到打包脚本: ' + $PackScript) }
if ($Split) { $UseSrc = $false }
if ($UseSrc) {
if (-not (Test-Path $SrcDir)) {
throw ('找不到源码目录 ' + $SrcDir + ',请先运行 3DTank/tools/extract.js 生成;或去掉 -UseSrc 用切割产物')
}
$PackDir = $SrcDir
$PackFile = Join-Path $SrcDir 'unpacked.pack.json'
} else {
$PackDir = Join-Path $RootDir '3dtank.com\play\static\js\unpacked'
$PackFile = Join-Path $RootDir '3dtank.com\play\static\js\unpacked.pack.json'
}
$NeedPack = $Build -or (-not (Test-Path $PackFile))
if (-not $NeedPack) {
$PackTime = (Get-Item $PackFile).LastWriteTimeUtc
$Newest = Get-ChildItem $PackDir -Filter 'module_*.js' -ErrorAction SilentlyContinue |
Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1
if ($Newest -and $Newest.LastWriteTimeUtc -gt $PackTime) {
$NeedPack = $true
Write-Host ('[打包] 检测到源码有更新(' + $Newest.Name + '),重新打包') -ForegroundColor Yellow
}
}
if ($NeedPack) {
Write-Host ('[打包] ' + $PackDir + ' → ' + $PackFile) -ForegroundColor Cyan
& $NodeExe $PackScript $PackDir $PackFile | Out-Host
if ($LASTEXITCODE -ne 0) { throw '单包打包失败' }
} else {
Write-Host '[打包] 单包已是最新,跳过(要强制重打请加 -Build)' -ForegroundColor DarkGray
}
# ---------- 4) 中继(可选;默认不启动 —— 页面直连真实服务器) ----------
$RelayProc = $null
if ($WithRelay) {
Write-Host ("[中继] wss_server.js relay ws://localhost:{0}/ -> {1}" -f $RelayPort, $Upstream) -ForegroundColor Green
$RelayArgs = @('wss_server.js', '--port', "$RelayPort", '--upstream', $Upstream, '--upstream-alt', $UpstreamAlt, '--label', $Label, '--dump-every', "$DumpEvery", '--tunnel-reconnect', $(if ($NoTunnel) { '0' } else { '1' }))
if (-not $NoLog) { $RelayArgs += @('--log', '1') }
if ($NoHex) { $RelayArgs += @('--no-hex', '1') }
if ($Quiet) { $RelayArgs += @('--quiet', '1') }
# 用 cmd /k 启动:进程退出(含崩溃)后窗口不关闭,堆栈能直接看到("崩溃后 pause")
$RelayProc = Start-Process -FilePath 'cmd.exe' `
-ArgumentList (@('/k', 'node') + $RelayArgs) `
-WorkingDirectory $Tools -PassThru
} else {
Write-Host '[中继] 已跳过(默认直连真实服务器;要中继/抓包请加 -WithRelay)' -ForegroundColor DarkGray
}
# ---------- 5) 启动本地镜像 ----------
# 默认加 --real-config:/config.xml 取真实站点(--config-upstream 决定 c1 / c2),游戏据此直连对应服务器
# 走中继(-WithRelay)时改用 --relay-port:serve.js 动态把 config 改写为 ws://localhost:<RelayPort>/(不落盘)
$MirrorArgs = @('serve.js', '--port', "$MirrorPort", '--config-upstream', $ConfigUpstream)
if ($UseSrc) { $MirrorArgs += @('--src', $SrcDir) }
if ($WithRelay) { $MirrorArgs += @('--relay-port', "$RelayPort") } else { $MirrorArgs += '--real-config' }
if ($Hook) { $MirrorArgs += '--ws-hook' }
Write-Host ("[镜像] serve.js http://localhost:{0}/ {1}{2}" -f $MirrorPort, ($(if ($WithRelay) {'(config → 本地中继)'} else {'(config → 真实服务器,直连)'})), ($(if ($Hook) {' + ws 抓包'} else {''}))) -ForegroundColor Green
$MirrorProc = Start-Process -FilePath 'cmd.exe' `
-ArgumentList (@('/k', 'node') + $MirrorArgs) `
-WorkingDirectory $Tools -PassThru
# ---------- 6) 健康检查 ----------
Start-Sleep -Milliseconds 1200
$RelayOk = if ($WithRelay) { Test-TcpOpen '127.0.0.1' $RelayPort } else { $null }
$MirrorOk = Test-TcpOpen '127.0.0.1' $MirrorPort
Write-Host ''
Write-Host '================= 已启动 =================' -ForegroundColor Yellow
Write-Host ("浏览器打开 : http://localhost:{0}/" -f $MirrorPort) -ForegroundColor White
Write-Host ("模块来源 : {0}" -f ($(if ($UseSrc) {'3DTank/src(反编译源码,改完刷新即生效)'} else {'unpacked(切割产物)'}))) -ForegroundColor White
Write-Host ("工具链 : 默认已注入(纯净页 http://localhost:{0}/#nohornet);切勿加查询参数 ?xxx —— 会被游戏当成 config 参数,导致握手失败 1005" -f $MirrorPort) -ForegroundColor White
if ($WithRelay) {
Write-Host ("连接路径 : 页面 → ws://localhost:{0}/ (中继)→ {1}" -f $RelayPort, $Upstream) -ForegroundColor White
Write-Host ("隧道接管 : {0}" -f ($(if ($NoTunnel) {'关闭(客户端直连战场服)'} else {'开启(_reconnectFast 改写为本地,战场连接继续走中继)'}))) -ForegroundColor White
$DiskInfo = if ($DumpEvery -gt 0 -or -not $NoLog) { "dump-every=$DumpEvery log=$(if ($NoLog) {'off'} else {'on'}) hex=$(if ($NoHex) {'off'} else {'on'})" } else { '不落盘(只转发 + 实时解析)' }
Write-Host ("抓包目录 : {0} (仅落盘时使用)" -f $PacketDir) -ForegroundColor White
Write-Host ("落盘策略 : {0} (默认落盘保存;不落盘加 -DumpEvery 0 -NoLog)" -f $DiskInfo) -ForegroundColor White
Write-Host ("进程 : 中继 PID={0} 镜像 PID={1}" -f $RelayProc.Id, $MirrorProc.Id) -ForegroundColor White
Write-Host ("自检 : 中继={0} 镜像={1}" -f ($(if ($RelayOk) {'OK'} else {'未监听'})), ($(if ($MirrorOk) {'OK'} else {'未监听'}))) -ForegroundColor White
} else {
Write-Host ("连接路径 : 页面 → config 取自 {0} → 直连 {1}(未启动中继)" -f $ConfigUpstream, $Upstream) -ForegroundColor White
Write-Host ("服务器 : {0}" -f ($(if ($SRV -eq 0) {'-SRV 未指定(默认 c2)'} else {"-SRV=$SRV"}))) -ForegroundColor White
Write-Host ("进程 : 镜像 PID={0}" -f $MirrorProc.Id) -ForegroundColor White
Write-Host ("自检 : 镜像={0}" -f ($(if ($MirrorOk) {'OK'} else {'未监听'}))) -ForegroundColor White
}
Write-Host '停止 : 关掉 node 窗口,或 Stop-Process -Id <PID>' -ForegroundColor DarkGray
Write-Host '=========================================' -ForegroundColor Yellow
if ($WithRelay -and -not $RelayOk) { Write-Warning '中继未监听,查看它的 node 窗口报错(常见:ws 未装上/端口被占)' }
if (-not $MirrorOk) { Write-Warning '本地镜像未监听,查看它的 node 窗口报错' }
if ($OpenBrowser -and $MirrorOk) { Start-Process ("http://localhost:{0}/" -f $MirrorPort) }