保持原有行为:默认加载 3dtank.com/play/static/js/unpacked(含 4 个模块的人工补丁),启动时按需打单包,-Build 强制重打;-UseSrc 才切到 3DTank/src,-Split 作为兼容旧参数等同默认。serve.js 的 --src 能力保留但不启用。
270 lines
14 KiB
PowerShell
270 lines
14 KiB
PowerShell
<#
|
||
.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) }
|