From a487c28dc18595d7e8796d5cf9366ecef6c95ab1 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 20 Oct 2026 22:53:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(local):=20=E8=BF=90=E8=A1=8C=E9=93=BE?= =?UTF-8?q?=E8=B7=AF=E9=BB=98=E8=AE=A4=E5=88=87=E5=88=B0=203DTank/src?= =?UTF-8?q?=EF=BC=8C=E5=90=AF=E5=8A=A8=E6=97=B6=E6=8C=89=E9=9C=80=E6=89=93?= =?UTF-8?q?=E5=8D=95=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serve.js 新增 --src:unpacked/module_*.js、resources.json、index.json 与 unpacked.pack.json 路由到源码目录(加载器与 hornet_bundle 仍走镜像);修复 403 白名单未包含 src 目录。Start-LocalAndRelay 默认跑 src,源码更新或单包缺失时自动打包,-Build 强制重打,-Split 回退切割产物;恢复 ps1 的 UTF-8 BOM 以免 Windows PowerShell 误读中文。 --- 3DTank/Start-LocalAndRelay.ps1 | 60 +++++++++++++++++++++++++++++----- 3DTank/docs/KtRebuild_Plan.md | 20 ++++++++++++ 3DTank/tools/serve.js | 23 +++++++++++-- 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/3DTank/Start-LocalAndRelay.ps1 b/3DTank/Start-LocalAndRelay.ps1 index 4605d6d..501ab6a 100644 --- a/3DTank/Start-LocalAndRelay.ps1 +++ b/3DTank/Start-LocalAndRelay.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS 一键启动 3DTank 本地镜像(serve.js)+ 中继(wss_server.js relay 模式) @@ -30,6 +30,14 @@ .EXAMPLE // 强制走 c2(官方默认主服) .\Start-LocalAndRelay.ps1 -SRV 2 -OpenBrowser + +.EXAMPLE + // 默认直接跑 3DTank/src 源码;改了 src 之后强制重打单包再起 + .\Start-LocalAndRelay.ps1 -Build -OpenBrowser + +.EXAMPLE + // 回到切割产物(3dtank.com/play/static/js/unpacked) + .\Start-LocalAndRelay.ps1 -Split -OpenBrowser #> # 注意:这里刻意不用 [CmdletBinding()]。 # PowerShell 原生不支持 -SRV=1 这种等号写法(会报 "parameter cannot be found"), @@ -50,7 +58,9 @@ param( [switch]$NoLog, # 中继:不写 log.txt(默认写) [switch]$NoHex, # 中继:log.txt 不写十六进制(日志体积大幅减小) [switch]$Quiet, # 中继:只写 log.txt,不刷控制台 - [switch]$NoTunnel # 中继:关闭 _reconnectFast 隧道接管(默认开启:战场连接也走中继) + [switch]$NoTunnel, # 中继:关闭 _reconnectFast 隧道接管(默认开启:战场连接也走中继) + [switch]$Build, # 强制重打单包(src 模式默认只在缺失/源码更新时打包) + [switch]$Split # 改回用切割产物 unpacked,而不是 3DTank/src 源码 ) $ErrorActionPreference = 'Stop' @@ -158,13 +168,43 @@ if ($FetchAssets) { try { & $NodeExe fetch_assets.js --concurrency 10 | Out-Host } finally { Pop-Location } } -# ---------- 3.5) 生成单包(unpacked.pack.json):126 个小文件 → 1 个包 ---------- -$PackScript = Join-Path (Split-Path -Parent $Project) 'tools\pack_unpacked.js' -if (Test-Path $PackScript) { - Write-Host '[打包] pack_unpacked.js 生成单包(unpacked.pack.json)...' -ForegroundColor Cyan - try { & $NodeExe $PackScript | Out-Host } catch { Write-Warning ('打包失败: ' + $_.Exception.Message) } +# ---------- 3.5) 模块来源与单包 ---------- +# 默认:直接跑反编译源码 3DTank/src(改完代码刷新即生效),启动时按需打单包保证加载速度 +# -Build:强制重打单包 +# -Split:改回切割产物 3dtank.com/play/static/js/unpacked +$UseSrc = -not $Split +$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 ($UseSrc) { + if (-not (Test-Path $SrcDir)) { + throw ('找不到源码目录 ' + $SrcDir + ',请先运行 3DTank/tools/extract.js 生成,或加 -Split 用切割产物') + } + $PackDir = $SrcDir + $PackFile = Join-Path $SrcDir 'unpacked.pack.json' } else { - Write-Warning ('未找到 ' + $PackScript + ',跳过单包(将逐文件加载)') + $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) 中继(可选;默认不启动 —— 页面直连真实服务器) ---------- @@ -187,6 +227,7 @@ if ($WithRelay) { # 默认加 --real-config:/config.xml 取真实站点(--config-upstream 决定 c1 / c2),游戏据此直连对应服务器 # 走中继(-WithRelay)时改用 --relay-port:serve.js 动态把 config 改写为 ws://localhost:/(不落盘) $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 @@ -201,7 +242,8 @@ $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 ("浏览器打开 : 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 diff --git a/3DTank/docs/KtRebuild_Plan.md b/3DTank/docs/KtRebuild_Plan.md index 3fa189d..9b4fc53 100644 --- a/3DTank/docs/KtRebuild_Plan.md +++ b/3DTank/docs/KtRebuild_Plan.md @@ -216,6 +216,26 @@ KT/ ## 8. 产出"可启动 JS"的端到端流程 +### 8.1 运行时模块来源(src 模式,默认) + +`Start-LocalAndRelay.ps1` 默认直接加载 **`3DTank/src`**(反编译源码),改完代码刷新即生效: + +| 行为 | 说明 | +|---|---| +| 启动时打包 | 单包缺失、或 `src/module_*.js` 比单包新 → 自动跑 `pack_unpacked.js` | +| `-Build` | 强制重打单包 | +| `-Split` | 回到切割产物 `3dtank.com/play/static/js/unpacked` | +| 路由 | `serve.js --src <目录>`:`unpacked/module_*.js`、`unpacked/resources.json`、`unpacked/index.json`、`unpacked.pack.json` → src 目录 | +| 仍取镜像 | 加载器 `main..js`、`unpacked/hornet_bundle.js`(HORNET 工具链) | + +打包命令: + +```bash +node tools/pack_unpacked.js 3DTank/src 3DTank/src/unpacked.pack.json +``` + +### 8.2 完整流程 + ```bash # 1) 基线 node tools/split.js --clean diff --git a/3DTank/tools/serve.js b/3DTank/tools/serve.js index d02874a..2eaf25a 100644 --- a/3DTank/tools/serve.js +++ b/3DTank/tools/serve.js @@ -23,6 +23,11 @@ const Arg = (Name, Def) => { const PORT = Number(Arg('port', '8080')); const WEB_DIR = path.resolve(Arg('web', path.join(PROJECT, 'web'))); const JS_DIR = path.join(ROOT, '3dtank.com', 'play', 'static', 'js'); +// --src <目录>:模块源码改从「反编译源码目录」(如 3DTank/src)加载 +// · /play/static/js/unpacked/module_*.js、resources.json、index.json → src 目录 +// · /play/static/js/unpacked.pack.json → src 目录里的单包 +// · 其它(main..js 加载器、unpacked/hornet_bundle.js 工具链)仍走 JS_DIR +const SRC_DIR = Arg('src', '') ? path.resolve(Arg('src', '')) : ''; const MISSING_LOG = path.join(PROJECT, 'names', 'missing.txt'); // 页面内探针的网络转储落盘位置:POST /__probe__?f= 追加写对应文件(不传 f 默认 railgun) const PROBE_LOGS = { @@ -94,7 +99,18 @@ function Resolve(UrlPath) { const Clean = decodeURIComponent(UrlPath.split('?')[0]); // 切割版 JS 走 TankIC 目录 if (Clean.startsWith('/play/static/js/')) { - const P = path.join(JS_DIR, Clean.slice('/play/static/js/'.length)); + const Rest = Clean.slice('/play/static/js/'.length); + // --src:模块与单包改从反编译源码目录取(加载器与 hornet 工具链仍用 JS_DIR) + if (SRC_DIR) { + if (Rest === 'unpacked.pack.json') return path.join(SRC_DIR, 'unpacked.pack.json'); + if (Rest.startsWith('unpacked/')) { + const Name = Rest.slice('unpacked/'.length); + if (/^module_\d+\.js$/.test(Name) || Name === 'resources.json' || Name === 'index.json') { + return path.join(SRC_DIR, Name); + } + } + } + const P = path.join(JS_DIR, Rest); // 主文件名的 hash 随 bundle 版本变化,而 index.html 里写的是固定名(重新拆分后会失配 → 404)。 // 这里兜底:请求 main..js 但该文件不存在时,回退到 JS 目录里最新的 main..js。 if (/[/\\]main\.[0-9a-f]{8}\.js$/.test(P) && !fs.existsSync(P)) { @@ -418,7 +434,9 @@ const Server = http.createServer((Req, Res) => { return; } const File = Resolve(UrlPath); - if (!File.startsWith(WEB_DIR) && !File.startsWith(JS_DIR)) { + // 允许的根:站点根、切割版 JS、以及 --src 指定的反编译源码目录 + const Allowed = File.startsWith(WEB_DIR) || File.startsWith(JS_DIR) || (SRC_DIR && File.startsWith(SRC_DIR)); + if (!Allowed) { Res.writeHead(403).end('Forbidden'); return; } @@ -471,6 +489,7 @@ Server.listen(PORT, () => { console.log('本地客户端: http://localhost:' + PORT + '/'); console.log(' 站点根 : ' + WEB_DIR); console.log(' 切割版 JS : ' + JS_DIR); + console.log(' 模块来源 : ' + (SRC_DIR ? ('src = ' + SRC_DIR + '(单包/模块/资源)') : 'unpacked(切割产物)')); console.log(' 404 记录 : ' + MISSING_LOG); console.log(' config.xml: ' + (RELAY_PORT > 0 ? ('中继改写 → ws://localhost:' + RELAY_PORT + '/(动态生成,不落盘)')