初始化仓库:友乐/gameabc 房卡游戏平台脚手架(client + server + docs)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 18:13:26 +08:00
co-authored by Claude Opus 5
commit 594820d393
655 changed files with 310861 additions and 0 deletions
@@ -0,0 +1,301 @@
# 06 · 子游戏接入模式与 Hooks 外置
> 权威设计来源:`docs/superpowers/specs/2026-06-30-子游戏接入Hooks外置模式-design.md`。
> 本篇面向"要接入新游戏或将老游戏迁移到 Hooks 外置模式"的开发者。
---
## 1. 两种接入模式
平台通过三个**契约文件**调用子游戏:
- `client/js/01_SubGame/00_SubGame_Config.js` —— `Game_Config.*` 配置项
- `client/js/01_SubGame/01_SubGame_modify.js` —— `Game_Modify.*` 部分接口与战绩系统
- `client/js/01_SubGame/02_SubGame_Input.js` —— `gameHallImport.*` 与 `Game_Modify.*` 大部分接口
平台运行时**只按固定全局名调用**这些接口,对实现在哪里、怎么组织毫不关心。子游戏有两种方式实现这些接口:
| 维度 | 内联模式(老) | Hooks 外置模式(新) |
|------|---------------|---------------------|
| 三文件内容 | 配置值 + 业务逻辑全部写在三文件内 | 三文件是纯转发壳,仅查 hook 并委托 |
| 业务逻辑位置 | 直接在 `01_/02_SubGame_*.js` 里 | `codes/SubGameHooks.js` + 子游戏实现层 |
| `SubGameHooks` | 不定义 | 子游戏填充,与平台接口一一对应 |
| 模板可复用性 | 低(新游戏要删改前任代码) | 高(三文件原样复用,只填 hook) |
| 平台视角 | 调全局接口直接得到实现 | 调全局接口 → 转发壳 → hook |
| 适用时机 | 已有存量代码,维持现状 | 新游戏接入、或老游戏迁移 |
**二选一,不混用**:一个游戏的代码库要么是「实心三文件」,要么是「转发壳三文件 + SubGameHooks」。同一接口不允许两处实现。这是**工程级选择**,平台运行期无需任何开关——转发壳的 `if (SubGameHooks.X)` 仅是实现存在性探测,不是模式判断。
> 现有麻将游戏使用内联模式,**本次不迁移**,三文件原样不动。
### 三文件可改性总表(硬规则)
这三个文件是**平台契约**,可改性严格受限:
| 文件 | 子游戏可否改 | 允许 | 禁止 |
|------|-------------|------|------|
| `00_SubGame_Config.js` | ✅ **仅可改值** | 给**已有** `Game_Config.*` 配置项赋本游戏需要的值 | 新增 / 删除 / 改名配置项;改变项的结构或类型;删除平台定义 |
| `01_SubGame_modify.js` | ⚠️ **仅配置区填值,转发壳不碰** | 仅在顶部「配置区」给 `Type_1/Type_2/CreateRoomData/combat/game_config/roomDes` 赋值 | 改任何转发壳;新增接口;写业务逻辑 |
| `02_SubGame_Input.js` | ❌ **完全不碰** | —— | 改任何转发壳;新增接口;写任何代码 |
子游戏的**一切业务实现**都在 `codes/SubGameHooks.js` + `codes/` 实现层,**不进这三个文件**;新增前后端交互走 `SubGameHooks` + 服务端 `mod.js` 的 rpc,**不在三文件加接口**。
### `00_SubGame_Config.js` —— 只改值,不改定义
`00` 是平台配置项的**结构定义**文件,配置项的**集合与结构由平台/框架固定**。子游戏接入只做一件事:**把已有配置项的值改成本游戏需要的**。
✅ **允许(改值)**:
```js
Game_Config.Max.PlayerCnt = 4; // 改人数
Game_Config.Share.title = "进贤麻将"; // 改分享标题
Game_Config.Info.TextContent = ["你好","谢谢","快点","不要","加油","收到","稍等"]; // 改常用语的【值】
Game_Config.Chat.ChatLoc = [[35,517],[1100,315]/* ... */]; // 改聊天气泡坐标【值】
```
❌ **禁止(改定义 / 结构)**:
```js
Game_Config.Info.MyNewField = 1; // ✗ 新增配置项(增定义)
delete Game_Config.Voice; // ✗ 删除配置项(删定义)
Game_Config.Max = [4]; // ✗ 改变项的类型/结构
// ✗ 重命名平台已定义的字段(如把 PlayerCnt 改成 playerCount)
```
> **为什么**:平台代码(`00_Surface/*`)按**固定字段名**读取 `Game_Config.*`。增 / 删 / 改名定义会让平台读到 `undefined` 或破坏约定,引发线上故障。**配置项的集合与结构是平台契约,只有「值」属于子游戏**。
>
> 对数组类配置项(如 `TextContent`、`ChatLoc`、`isLeft`),改其中元素值属「改值」(允许);但若平台对该数组**长度/索引语义有约定**(如固定 5 个聊天气泡位置、按座位索引),改变长度需以平台读取约定为准,不可随意增删。
---
## 2. 三层架构
Hooks 外置模式下,职责分为三层,单向向下:
```
平台契约层(框架维护,子游戏不碰)
00_SubGame_Config.js 平台配置项结构 + 子游戏填【配置值】
01_SubGame_modify.js 平台接口骨架,每个接口是固定【转发壳】
02_SubGame_Input.js 平台接口骨架,每个接口是固定【转发壳】
|
| 转发壳查 SubGameHooks.X,存在则委托,不存在则平台默认(B类)/空操作(A类)
v
子游戏入口层(codes/,Hooks 外置新方案核心)
SubGameHooks{} 全局对象,接口名与平台接口一一对应;子游戏只填需要的
|
| SubGameHooks.StartWar = function(_msg){ /* 委托子游戏的开局处理 */ };
v
子游戏实现层(codes/,已有)
各消息处理器 / 收发包 / 战绩等业务模块 / UIManager 扩展 ...
```
**数据流**:平台调 `Game_Modify.X(args)` → 转发壳查 `SubGameHooks.X` → 存在则委托子游戏实现并返回其结果;不存在则执行平台默认(B 类)或空操作(A 类)。
**兼容性原因**:平台只认全局接口名,不关心实现在哪。老游戏不定义 `SubGameHooks`、三文件实心实现 → 照跑;新游戏用转发壳三文件 + `SubGameHooks` → 也跑。无需任何运行期判断。
---
## 3. 新游戏:使用模板接入
模板文件位于 `gameabc-framework/templates/subgame-entry/`,共四个文件:
| 文件 | 内容 | 谁维护 |
|------|------|--------|
| `00_SubGame_Config.template.js` | `Game_Config.*` 全部配置项结构 + 占位/默认值 | 框架维护 |
| `01_SubGame_modify.template.js` | 01 段接口的转发壳 + 配置区占位 | 框架维护 |
| `02_SubGame_Input.template.js` | `gameHallImport.*` + 02 段 `Game_Modify.*` 的转发壳 | 框架维护 |
| `SubGameHooks.template.js` | 子游戏实现骨架,列出全部可填 hook,每个给空函数 + 用途注释 | 子游戏起点 |
### 接入步骤
1. **复制四个模板文件**到新游戏目录。
2. **三个转发壳直接用,不改**:`00/01/02_SubGame_*.template.js` 复制后重命名,原样放入 `01_SubGame/`。
3. **填充 SubGameHooks**:将 `SubGameHooks.template.js` 重命名为 `codes/SubGameHooks.js`,在其中实现本游戏需要的 hook(不需要的 hook 留空函数或删除,A 类无 hook 默认 no-op,B 类无 hook 返回平台默认值)。
4. **index.html 加载**:按既有顺序在原 `00_/01_/02_SubGame_*.js` 的位置加载新三文件,并在 `codes/` 相应位置加载 `SubGameHooks.js`(在其所依赖的 controllers/handlers 之后)。
### index.html 加载顺序要点
- **阶段5**:先加载 `SubGameHooks` 所依赖的控制器/管理器/网络等实现模块。
- **`SubGameHooks.js`**:在其依赖之后、三文件之前加载。
- **阶段6**:最后加载受限对接层三文件(转发壳,引用 `SubGameHooks`)。
即:**依赖模块 → `SubGameHooks` → 三文件转发壳**,顺序即依赖,不可颠倒。
---
## 4. 转发壳范式
转发壳按「无 hook 时的默认行为」分三类,必须**逐一覆盖全部 63 个接口**(见 §6)。
### A 类 — 纯子游戏行为(无 hook 即 no-op)
无 `SubGameHooks.X` 时什么都不做,语义是「模板未实现该功能」。
```js
// A 类:无 hook 则空操作
Game_Modify.StartWar = function (_msg) {
if (window.SubGameHooks && SubGameHooks.StartWar) {
return SubGameHooks.StartWar(_msg);
}
};
Game_Modify.Reconnect = function (_msg) {
if (window.SubGameHooks && SubGameHooks.Reconnect) {
return SubGameHooks.Reconnect(_msg);
}
};
```
### B 类 — 有平台默认返回值(无 hook 即返回默认)
平台某些接口需要有意义的返回值,无 hook 时应给出合理默认,而非静默返回 `undefined`。
```js
// B 类:无 hook 返回平台默认值
Game_Modify.getMaxPlayerCount = function (roomtype) {
if (window.SubGameHooks && SubGameHooks.getMaxPlayerCount) {
return SubGameHooks.getMaxPlayerCount(roomtype);
}
return (Game_Config.Max && Game_Config.Max.PlayerCnt) || 4;
};
Game_Modify.getLeaveLimit = function (roomtype) {
if (window.SubGameHooks && SubGameHooks.getLeaveLimit) {
return SubGameHooks.getLeaveLimit(roomtype);
}
return 10;
};
gameHallImport.isInstalled = function () {
if (window.SubGameHooks && SubGameHooks.isInstalled) {
return SubGameHooks.isInstalled();
}
return 1;
};
```
### C 类 — 配置数据(不是 hook,留配置文件)
`Game_Config.*`(00 全部)与房型数据(`Game_Modify.Type_1/Type_2/CreateRoomData/game_config`、`Game_Modify.combat/roomDes`)属于**配置数据**而非行为接口,不进 `SubGameHooks`。
模板在 01 文件顶部设置「配置区」,由子游戏直接填值:
```js
// 01_SubGame_modify.js 顶部:配置区(子游戏填值,与转发壳接口段物理分开)
Game_Modify.combat = null; // 战绩配置,子游戏按需赋值
Game_Modify.roomDes = ''; // 房间描述
Game_Modify.Type_1 = []; // 房型一级选项
Game_Modify.Type_2 = []; // 房型二级选项
Game_Modify.CreateRoomData = []; // 创建房间数据
Game_Modify.game_config = {}; // 游戏配置
```
---
## 5. Hook 命名约定
`SubGameHooks` 的 hook 名**默认与平台接口名严格一致**。
**跨对象同名冲突**:`gameHallImport.*` 与 `Game_Modify.*` 各有一个 `appStart` 接口,两者同名。为避免 `SubGameHooks` 上的 key 冲突,规则如下:
| 平台接口 | SubGameHooks key | 说明 |
|---------|-----------------|------|
| `Game_Modify.appStart` | `SubGameHooks.appStart` | 保持同名 |
| `gameHallImport.appStart` | `SubGameHooks.hallAppStart` | 加 `hall` 前缀驼峰 |
目前仅 `appStart` 存在此冲突。`gameHallImport` 侧所有同名接口均加 `hall` 前缀以区分。
```js
// SubGameHooks.js 中的写法
SubGameHooks.appStart = function () {
// 对应 Game_Modify.appStart:委托子游戏的启动编排
};
SubGameHooks.hallAppStart = function () {
// 对应 gameHallImport.appStart(大厅启动):委托子游戏的大厅初始化
};
```
---
## 6. 接口覆盖要求
转发壳必须**逐个覆盖全部平台接口**,遗漏会导致新游戏某平台调用落空(静默失败)。接口分三组,总计 **63 个**:
| 来源 | 数量 | 说明 |
|------|------|------|
| `gameHallImport.*` | 9 | 大厅相关;其中 `appStart` 对应 `SubGameHooks.hallAppStart`(加 `hall` 前缀) |
| `Game_Modify.*`(事件/交互,01 段) | 9 | 精灵事件类默认 no-op,改用框架 `SpriteEventController` |
| `Game_Modify.*`(生命周期/回调,02 段) | 45 | 多为 A 类 no-op,少量 B 类需给平台默认返回值 |
覆盖原则:**A 类无 hook 即 no-op;B 类无 hook 须返回有意义的平台默认值**(如玩家数、离开上限、房间文案等),而非静默返回 `undefined`。各接口的具体分类与默认值以平台接入 spec 为权威,接入时逐一核对。
---
## 7. 大块实现外置范式
> 本节为**范式说明**,供新游戏接入时参考外置思路。
内联模式下,一段大块业务实现(如战绩列表/滚动/回放,往往数百行、还自带 `utlmousedown/move/up/drawbegin` 等精灵事件方法)整体写在契约文件里,会导致契约文件膨胀。新游戏接入时,大块业务实现应按以下三步外置:
1. **移到 `codes/`**:把整块实现移到 `codes/` 下对应目录(一个或多个模块文件),声明为全局对象并在 `index.html` 按依赖顺序加载。
2. **经 `SubGameHooks` 接入平台接入点**:相关平台接口在 `SubGameHooks` 对应 hook 里委托给该模块,契约文件不出现业务实现。
3. **精灵交互改用 `SpriteEventController`**:界面的精灵交互改用框架 `SpriteEventController` 按精灵 ID 注册回调,而非散落的 `utl*` 方法。
这样契约文件中只剩干净的配置区与转发壳,该块业务完全在 `codes/` 内自治。
---
## 8. 老游戏迁移步骤与验证
> 迁移是**单独立项**的改动,不与其他功能混在一次提交里。
### 迁移步骤
1. **替换接口段为转发壳**:用模板 `01/02_SubGame_*.template.js` 的转发壳替换三文件的接口实现段;保留 `Game_Config.*` 配置值和 `Game_Modify.combat/roomDes/Type_*` 配置数据不动。
2. **逐接口搬到 SubGameHooks**:为每一个原有接口实现在 `codes/SubGameHooks.js` 里创建对应 hook,将内联实现原样移入,逐接口**核对行为等价**。
3. **大块实现按 §7 外置**:战绩等大块逻辑移到 `codes/` 对应目录,通过 hook 接入平台接入点。
4. **验证**:
- 全局接口名与签名不变(平台调用路径不变)。
- 逐接口**黑盒对比**迁移前后行为,不能靠"看起来一样",须可观测地验证(页面操作/日志/单测)。
- 连跑多次无异常。
### 验证要点
| 验证项 | 方法 |
|--------|------|
| 接口不遗漏 | grep 对比三文件原定义与转发壳一一对应(63 个) |
| 签名不变 | 检查平台调用处参数与转发壳参数列表一致 |
| 行为等价 | 逐接口黑盒测试(开局/重连/战绩/离开等主流程) |
| 配置值完整 | `Game_Config.*` 与配置区数据均已保留 |
---
## 9. 红线
使用 Hooks 外置模式时,必须严格遵守以下约束:
| 红线 | 说明 |
|------|------|
| **转发壳框架维护,子游戏不碰** | `01/02_SubGame_*.js` 转发壳由框架统一维护,子游戏不得直接修改转发壳内容 |
| **00 只改值,不改定义** | `00_SubGame_Config.js` 只允许给**已有** `Game_Config.*` 配置项改值;**禁止**新增/删除/改名/改结构定义(平台按固定字段名读取,改定义会引发线上故障)。三文件中仅此文件子游戏可碰,且仅限改值 |
| **01 仅配置区填值** | `01_SubGame_modify.js` 只允许在顶部「配置区」给 `Type_*/CreateRoomData/combat/game_config/roomDes` 赋值;转发壳一律不碰 |
| **配置值留配置文件** | `Game_Config.*` 和配置区数据(`Type_1/2/CreateRoomData` 等)不得塞进 `SubGameHooks`,保留在对应配置位置 |
| **hook 签名必须与平台接口一致** | 平台按位置传参,转发壳以相同参数透传给 hook,hook 签名不得偏移 |
| **二选一,不混用** | 同一接口不允许在三文件内联实现与 `SubGameHooks` 中同时存在 |
| **转发壳必须全覆盖** | 转发壳须覆盖全部 63 个接口,遗漏会导致平台调用落空(静默失败) |
| **严格 ES5** | `SubGameHooks.js` 与所有 codes 文件一律 ES5,禁 `let`/`const`/箭头函数等 |
| **加载顺序正确** | `SubGameHooks.js` 须在其依赖的 controllers/handlers 之后、三文件之前加载 |
---
## 10. 小结
- Hooks 外置模式将子游戏逻辑从平台契约文件中**彻底剥离**,三文件退化为纯转发壳,子游戏只需填充 `SubGameHooks`。
- 平台天然兼容两种模式——全局接口名不变,平台无感知。
- 新游戏接入:复制四个模板文件,三转发壳不改,只填 `SubGameHooks.js`,按顺序加入 `index.html`。
- 老游戏迁移:替换接口段为转发壳,逐接口搬实现到 `SubGameHooks`,黑盒验证行为等价。
- 转发壳由框架维护,子游戏不碰;配置值留配置文件;hook 命名默认同接口名,`gameHallImport.appStart` 例外用 `hallAppStart`。
上一篇 [05-开发规范与红线](./05-开发规范与红线.md),回到 [README](./README.md) 查看导航。