Files
AIC-Project/docs/superpowers/specs/2026-07-07-minigame-publish-pipeline-design.md
2026-07-10 10:24:29 +08:00

238 lines
14 KiB
Markdown
Raw Permalink 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.
# 小游戏打包流程:Unity 菜单「XWorld/生成小游戏更新」设计
日期:2026-07-07
状态:已与用户逐段确认(§1-§4
## 0. 目标与范围
一条菜单驱动的发布流水线,对选定的小游戏:
- 客户端:打**代码热更 AB**DLL 以 .bytes/TextAsset 进 AB)与**资源热更 AB**,落到
`CDN/minigame/<gameId>/<version>/<platform>/`,供多端下载更新(本地 `Cdn.Runner` 或外网 CDN 同一布局)。
- 服务器:`Server/games-src/<对应目录>` 编译出普通 DLL,落到
`deploy/minigame/<gameId>/<version>/`,整目录上传外网服务器即可作为网关 `--gamesRoot` 的 games 根热更运行。
- `Client/Assets/MiniGames` 下有多个小游戏目录时,先弹窗选择其一(单个也弹窗,用于确认版本/平台)。
本设计同时落地 2026-06-22 的「统一 AB」决策(设计 §10.1):客户端发布产物全部为 `.unity3d`
弃用裸 `.bytes` 直下双管线,并同步改造客户端运行时消费端。
已确认的决策:
- 代码热更形态:**落地统一 AB**(DLL .bytes 进代码 AB,运行时同步改造)。
- 平台维度:**加 `<platform>/` 子目录,一次菜单可勾选多平台构建**。
- 版本号:**自动递增(扫 CDN 已有版本取 max+1)+ 弹窗可改**,两端共用同一 version。
- 元数据:**每游一份 `publish.json`**(解决 RockPaperScissors ↔ RPS ↔ rps 名称映射)。
- 签名:**可选**——约定私钥路径存在则签,否则空 sig + 警告。
- 实现架构:**方案 BEditor 编排 + 复用 PublishTool,新增瘦 CLI 壳)**。
## 1. 产物布局与格式
### 1.1 publish.json(每个小游戏一份,驱动全流程)
`Client/Assets/MiniGames/<Game>/publish.json`
```json
{
"gameId": "rps",
"serverSrcDir": "RPS",
"serverProject": "RPS.Server/RPS.Server.csproj",
"serverAssembly": "RPS.Server.dll",
"serverEntryType": "RPS.Server.RpsServerRoom",
"clientProject": "Client/HotUpdateGames/RPS.Client/RPS.Client.csproj",
"coreDll": "RPS.Core.dll",
"clientDll": "RPS.Client.dll",
"clientEntryType": "RPS.Client.RpsGameClient",
"minFrameworkVersion": 1,
"playerCount": 2,
"tickRateHz": 10
}
```
- `serverSrcDir` 相对 `Server/games-src/``serverProject` 相对 `serverSrcDir`
`clientProject` 相对仓库根。
- 菜单只把「含 publish.json 的目录」视为可发布小游戏;新游戏拷模板改字段即接入。
### 1.2 客户端产物(每平台一份):`CDN/minigame/<gameId>/<version>/<platform>/`
```
game.json gameId/version/clientEntryType/coreDll/clientDll/minFrameworkVersion
+ 新字段 "codeAb": "code.unity3d" + "assets": ["res.unity3d"]
files.txt {'code.unity3d','<md5>',size}, {'res.unity3d','<md5>',size},
files.txt.sig 有私钥则 RSA 签名,否则空文件(沿用两端"未配置则跳过"语义)
code_<md5>.unity3d 代码 AB<coreDll>.bytes + <clientDll>.bytes 两个 TextAsset
res_<md5>.unity3d 资源 AB:整个 res/ 目录打一个包(assets 数组支持以后拆多包)
```
- 平台名沿用底包惯例:`pc / android / ios / webgl`(与 `LoadDll.sPlatform`
`CDN/XWorld/<platform>/` 一致)。
- 线上文件 md5 命名(`name_<md5>.ext`),files.txt 登记逻辑名——与现有
`MiniGameDownloader.Md5Name` 推导一致。
- 边界:`res/` 不存在或为空时跳过资源 AB`assets` 为空数组(仅发代码 AB)。
- **不做 AES 加密**minigame 链路现状如此,与 XWorld 底包 AB 加密体系无关)。
### 1.3 服务器产物:`deploy/minigame/<gameId>/<version>/`
```
<serverAssembly> / <coreDll> 普通 DLL(不含 Framework.Shared.dllPrivate=false 保证)
game.json server 视图(serverAssembly/serverEntryType/playerCount/tickRateHz/minFrameworkVersion
files.txt + files.txt.sig 供 GameModuleLoader 配公钥后加载前验签
```
结构与网关 `--gamesRoot` 消费的 `<id>/<ver>/` 完全一致。
### 1.4 配套修正:Core 单源
现状问题:`RPS.Client.csproj``<Compile Include="scripts~\**\*.cs">` 把 Core+Client
全编进 RPS.Client.dll(内嵌 Core 类型副本,下载的 RPS.Core.dll 实为冗余);而服务器
`RPS.Core.csproj` 编译的是 `Server/games-src/RPS/RPS.Core/` 下另一份已发生格式漂移的拷贝。
两端 Core 一旦真漂移即确定性不同步。
修正:
- `Server/games-src/RPS/RPS.Core/` 删除重复 .cscsproj 改
`<Compile Include>` 链接 `Client/Assets/MiniGames/RockPaperScissors/scripts~/Core/**`
(恢复「Core 唯一源码位在客户端 scripts~/Core」的原设计)。
- `RPS.Client.csproj` 改为只编 `scripts~/Client/**` + `ProjectReference` RPS.Core
Private=false),Client.dll 不再内嵌 Core 副本;发布用的客户端 Core.dll 取自
RPS.Core 构建产物(与服务器同一 IL)。
## 2. 构建编排(Editor 菜单 + PublishTool.Cli
### 2.1 Editor 侧
重写 `Client/Assets/Editor/MiniGamePublishMenu.cs`(替换 stub 菜单「XWorld/生成小游戏」),
新增 `MiniGamePublishWindow`EditorWindow):
- **游戏选择**:扫 `Assets/MiniGames/*/publish.json`;多个则列表选择,仅一个预选。
- **版本号**:默认 = 扫 `CDN/minigame/<gameId>/` 下数字目录取 max+1(无则 1),可手改;
覆盖已有版本前弹确认。
- **平台勾选**PC/Android/iOS/WebGL,默认勾选当前激活 BuildTarget 对应项。
- **生成按钮**:执行 §2.2 流水线,每步失败 `DisplayDialog` 报错中断。
### 2.2 流水线
```
1. dotnet build Server/games-src/<serverSrcDir>/<serverProject> -c Release
→ <serverAssembly> + <coreDll>
2. dotnet build <clientProject> -c Release
→ <clientDll>ProjectReference 引用步骤 1 的 Core 工程产物)
3. 拷 Core.dll/Client.dll → Assets/MiniGameStaging/code/*.dll.bytes
AssetDatabase.Refresh 导入为 TextAssetstaging 必须在 Assets 下且**不能带 `~` 后缀**——
`~` 目录 Unity 不导入、资产进不了 AB;用完即删含 .meta)
4. 对每个勾选平台:
BuildPipeline.BuildAssetBundles(平台临时目录,
[ code.unity3d ← 两个 .bytes 资产,
res.unity3d ← AssetDatabase 枚举 Assets/MiniGames/<Game>/res/ 下全部资产 ],
ChunkBasedCompression, 对应 BuildTarget) ← 不切激活平台,直接传 target
5. 写 resolved-spec.jsonpublish.json 字段 + version + AB 文件名列表)到临时目录
6. Process 调 PublishTool.Cli(惯例同 XWorldUtil 起 dotnet 的方式):
server:调一次 → 临时 out → 成功后原子 Directory.Move 入 deploy/minigame/<id>/<ver>/
client:每平台一次 → 临时 out → 成功后移入 CDN/minigame/<id>/<ver>/<platform>/
7. 清理 staging;汇总日志 + 完成对话框(列出全部落位路径、是否已签名)
```
先落临时目录、成功后整体 Move 的原因:`CDN/` 可能正被 `Cdn.Runner` 对外提供下载,
避免半成品被客户端拉走。
### 2.3 PublishTool 侧(.NET,本环境可 dotnet test 回归)
- **`PublishLayout` 改造**:拆 `PublishServer(srcDir, outDir, spec, key?)`
`PublishClient(srcDir, outDir, spec, key?)`;客户端侧删除 `CopyBytes` 双 .bytes 逻辑,
改为登记 `codeAb + assets` 各 ABmd5 命名落盘 + files.txt 登记逻辑名)。
- **`GameSpec` / `GameJsonWriter`**:加 `CodeAb` 字段;`WriteClient` 输出 `codeAb`
- **新增 `Server/PublishTool.Cli`**(瘦控制台,入 Server.sln):
`--mode client|server --src <dir> --out <dir> --spec <resolved-spec.json> [--key <pem>]`
- **签名**:约定私钥 `Tools/keys/minigame-private.pem`gitignore);Editor 检测到则传
`--key`,否则 CLI 写空 sig 并在输出中警告「未签名」。
- **`PublishTool.Tests` 同步更新**:新布局断言(codeAb 进清单、无 .bytes 散文件、
双端目录形状、无 key 时空 sig)。
## 3. 客户端运行时改造(XWorld.Link
- **`MiniGameManifest`**:解析新字段 `codeAb`(校验非空);`assets` 语义变为
「资源 AB 文件名列表」。
- **`MiniGameDownloader`**`RemoteBase` 加平台段 →
`<cdnBase><gameId>/<version>/<platform>/`(平台名以编译宏判定,热更层内新增小工具函数,
不引用底包 LoadDll)。下载逻辑零改动(按 files.txt 通用增量下载)。
`LocalDir` 不加平台段(设备平台固定)。
- **`MiniGameAssemblyLoader`**:从 `LocalDir + codeAb`(本地为明文件名)
`AssetBundle.LoadFromFile``LoadAsset<TextAsset>("<coreDll>.bytes"/"<clientDll>.bytes")`
`Assembly.Load`Core 先于 Client)→ `AB.Unload(false)`
找不到 TextAsset 时报清晰错误(列出 AB 内实际资产名)。
- **资源 AB 加载**:新增 `MiniGameAssetLoader : IAssetLoader`——按需
`AssetBundle.LoadFromFile(LocalDir + 资源AB)`,缓存已开包,按资产路径 `LoadAsset`
小游戏退出统一 `Unload(true)``MiniGameHost` 注入 `GameClientCtx` 的 loader 由
`UnityAssetLoader`(走 XResLoader 大厅管线,不认识 minigame AB)换成它,并在
`ExitGame` 补上此前 TODO 的 AB 卸载(MiniGameHost.cs:111)。
## 4. 错误处理、测试与配套
### 4.1 错误处理
- Editor 流水线每步检查退出码/产物存在性,失败即 `DisplayDialog` + 保留日志,
临时目录不移入 CDN/deploy(无半成品)。
- 覆盖已有版本目录:确认弹窗后先删旧目录再 Move。
- 运行时:沿用 Fail/Error 路径;新增错误信息注明「需要 codeAb 格式,旧 .bytes 包不再支持」。
### 4.2 测试与验证
- **本环境可实证**PublishTool 新布局 + Cli 的 xUnit`dotnet test Server/Server.sln`
全量回归);两端 csproj 改造后 `dotnet build` 0 错;客户端改动照惯例 csc 编译验证。
- **须 Unity 人工核对**(本环境无 Unity):EditorWindow 交互、BuildAssetBundles
跨平台产出、Editor Play 端到端(菜单生成 → Cdn.Runner 供 CDN → 网关
`--gamesRoot deploy/minigame` → 进 RPS 对局)。
### 4.3 配套调整
- **删除 `Tools/PcMiniGameSmoke/Build-PcMiniGameSmoke.ps1`**:其裸 .bytes 产物格式
运行时不再支持,且 ps1 打不了 AB;README 改指菜单。
- **本地开发网关改指 `deploy/minigame`**`XWorldUtil.MINI_GAME_SERVER_GAMES_ROOT`
`../../Build/PcMiniGameSmoke/server/games` 改为指向仓库 `deploy/minigame`——
菜单产物直接被 Play 自动起的本地网关消费,本地/外网同一份服务器产物布局;
目录缺失提示改为「先跑 XWorld/生成小游戏更新」。
- `.gitignore``Assets/MiniGameStaging/``Tools/keys/`CDN/deploy 产物默认不 ignore
当前 `.git` 不可用、仓库无 .gitignore,此条待启用 git 后生效)。
- WebGL 注意:`AssetBundle.LoadFromFile` 在 WebGL 不可用,运行时按平台走
`LoadFromMemory(File.ReadAllBytes(...))` 回退(微信 WASM 实测仍是已知开放风险)。
### 4.3.1 修订(2026-07-07,用户定):本地网关无条件启动,发布即生效
原 §4.3 给本地网关加的「`deploy/minigame` 下须有 game.json 才启动」检测与热更架构矛盾——
服务器本就支持运行中热更(`ServerLoop.BuildGameList` 每次请求重扫目录;
`GameModuleRegistry.Acquire` 建房时按需加载,新版本自动淘汰旧版本),不应做启动期存在性检测。
- `XWorldUtil` 删除 `HasMiniGameServerPackage()` 及其两处调用:`CanOpenLocalServer`
只看网关 Runner DLL 存在;`StartMiniGameGatewayRunnerProcess` 删除
「PC minigame package not found」弹框分支。
- 启动前 `Directory.CreateDirectory(GetMiniGameServerGamesRoot())`(幂等)保证
`--gamesRoot` 有效;目录下暂无任何 `*/*/game.json` 时仅 `Debug.Log` 提示
「可随时用菜单发布,运行中即生效」,不打断。
- ~~已知边界:同版本覆盖重发会因 `LoadFromAssemblyPath` 文件锁失败~~(已被 §4.3.2 消除)。
- 大厅列表为实时扫描结果,客户端重新拉列表即见新游戏,不做推送(YAGNI)。
### 4.3.2 修订(2026-07-07,用户定方案 B):字节加载去锁 + 内容指纹自动失效
消除"同版本覆盖重发被文件锁挡住"的问题,使覆盖重发也**发布即生效**(新房间用新代码,
已入房玩家打完旧代码):
- **GameModuleAlc 字节加载**`LoadEntry` 与私有依赖解析两处 `LoadFromAssemblyPath`
改为 `File.ReadAllBytes` + `LoadFromStream`——加载后不持有任何文件句柄,覆盖/删除
目录永远成功;`Unload()` 仍惰性异步(无锁故无需强制 GC)。已知约束:字节加载的
Assembly `Location` 为空,小游戏 DLL 不得依赖它。验签照旧不受影响。
- **GameModuleRegistry 内容指纹失效**`ComputeFingerprint(moduleDir)` = 目录下全部
`*.dll`+`game.json` 按名 Ordinal 排序拼 `name=md5;` 再整体 md5。结构改为
`_current`key=`gameId@version`Entry 加 Fingerprint+ `_retired` 列表。
`Acquire` 命中时重算磁盘指纹:相同 → 计数++返回;目录读不到(覆盖交换瞬间窗口)→
视为未变化用缓存;不同 → 先加载新模块(失败旧条目原样保留),成功后旧条目标
Superseded(引用 0 即卸载,否则挪 `_retired`),新条目入 `_current`
`Release` 改按模块实例识别(先 `_current` 同实例,否则 `_retired` ReferenceEquals)。
`RefCount/IsLoaded` 语义指 `_current`。生产环境版本不可变、指纹恒命中,行为零变化。
- 测试(TDD):字节加载后磁盘 DLL 可删/可覆盖;同 key 内容变化 → 新实例 + 旧条目归零
后弱引用 GC 断言卸载;新旧并存期各自 Release 正确;指纹未变命中缓存;现有
Registry/AlcUnload/RoomHost 测试零回归。
- 配套:PcMiniGameSmoke README 的文件锁注意事项改为"覆盖重发即生效"语义说明。
### 4.4 兼容性说明
- 旧格式(裸 .bytes、无平台段)的既有 CDN 产物在运行时改造后不再可用;当前仅 RPS 一款、
由本流水线重新生成即可,无迁移负担。
- 服务器端 game.json/DLL 布局不变,网关/GameModuleLoader 零改动。