diff --git a/docs/protocol/00-框架架构设计.md b/docs/protocol/00-框架架构设计.md new file mode 100644 index 0000000..3316f49 --- /dev/null +++ b/docs/protocol/00-框架架构设计.md @@ -0,0 +1,234 @@ +# 00 · 框架架构设计(子游戏框架总览) + +> 本章说明 `Game_Surface_3` 的**整体架构与设计思想**。它不是某一款游戏,而是一套 +> **「子游戏框架」**:平台壳(Surface)把登录、大厅、房间、网络、UI、社交等**通用能力**全部做好, +> 一款具体游戏(SubGame)只需在固定的接入点里实现**自己的对局逻辑**。 +> +> 理解这套分层,是用 CocosCreator 重写前端的前提——新前端应复刻"平台通用层 + 子游戏对局层"的边界, +> 这样才能与服务器无缝对接(服务器只认协议,不关心前端用什么引擎)。 + +--- + +## 1. 框架定位 + +``` +┌───────────────────────────────────────────────────────────────┐ +│ 一个可发布的游戏 │ +│ │ +│ ┌─────────────────────────┐ ┌───────────────────────────┐ │ +│ │ Surface 平台壳 (00_) │ │ SubGame 子游戏 (01_) │ │ +│ │ 登录/大厅/房间/网络/UI │◄─►│ 仅实现「对局逻辑」 │ │ +│ │ 社交/支付/战绩/排行... │钩子│ 发牌/出牌/结算/界面 │ │ +│ └─────────────────────────┘ └───────────────────────────┘ │ +│ ▲ │ +│ │ 引擎回调分发 (gamemain.js) │ +│ ┌──────────────┴──────────────────────────────────────────┐ │ +│ │ gameabc 自研精灵引擎 (gameabc.min.js) + Spine 骨骼动画 │ │ +│ │ Canvas 渲染 / 触摸 / 定时器 / 资源加载 / WebSocket │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└───────────────────────────────────────────────────────────────┘ +``` + +- **平台壳**和**子游戏**用同一套代码骨架;换游戏时只替换 `01_SubGame/` 与美术布局数据。 +- 平台壳通过**钩子(Hook)**反向调用子游戏;子游戏通过调用平台 API(`Net.Send_*`、`GameUI.*`、`Desk`、`C_Player`、`set_self`)使用平台能力。 + +--- + +## 2. 四层结构 + +| 层 | 载体 | 职责 | +|----|------|------| +| **引擎层** | `gameabc.min.js`、`spine-canvas.js`、`SpineMgr.js` | Canvas 精灵渲染、触摸/绘制/定时/资源/网络底层回调、Spine 骨骼动画 | +| **桥接层** | `gamemain.js`(`gameabc_face`) | 把引擎回调统一分发给上层三个消费者(GameUI / Game_Modify / gameCombat) | +| **平台层** | `js/00_Surface/*`(12 个文件) | 登录、大厅、房间、解散、网络协议、玩家数据、UI、聊天、支付、战绩、排行、敏感词等 | +| **子游戏层** | `js/01_SubGame/*`(3 个文件) | 子游戏配置、对局逻辑、自定义输入处理(接入点) | + +--- + +## 3. 引擎层:自研精灵(Sprite)系统 + +渲染不是 DOM,也不是常规游戏引擎,而是一套基于**精灵编号(spid) + 标签(tag) + 组(group)**的自研系统。 +美术界面在编辑器里排版后导出为 `output/gameabc_data.min.js`(精灵布局数据),运行时引擎据此渲染。 + +### 3.1 核心 API(子游戏与平台都用它操作界面) + +| API | 作用 | +|-----|------| +| `set_self(spid, attr, value, ...)` | 设置某精灵的属性 | +| `get_self(spid, attr, ...)` | 读取某精灵属性 | +| `set_group(groupid, attr, value, ...)` | 对一组精灵批量操作(常用于整页显隐) | +| `play_ani(...)` | 播放属性动画(位移/缩放/帧动画) | +| `set_clip(...)` | 设置裁剪区域(滚动列表用) | +| `ifast_addtospritefromspritecopy(fSpid, srcSpid, x, y, tag)` | 以模板精灵为原型,动态复制出带 tag 的子精灵(实现动态列表,如战绩/排行行项) | +| `ifast_dllpritefromspritecopy(fSpid, tag)` | 删除动态复制的子精灵 | +| `ifast_check_add(spid, x, y)` | 命中检测,返回被点中的子精灵 tag(动态列表点击) | +| `ifast_mydrawbmp(...)` | 在 `gamemydraw` 回调里自绘位图(如勾选框的对勾) | + +### 3.2 常见 attr 属性码(由源码用法归纳) + +| attr | 含义 | +|------|------| +| 7 | 文本内容(`set_self(spid,7,"文字")`) | +| 18 / 19 | x 坐标 / y 坐标 | +| 20 / 21 | 宽 / 高 | +| 37 | 显示/隐藏(1 显示 0 隐藏) | +| 43 | 按钮状态/帧索引(多态按钮切换外观) | +| 1 | 资源/图片绑定(`set_self(268,1,资源号)`,预加载/换图) | + +> 这是平台与子游戏共享的"界面操作语言"。CocosCreator 重写时,这一层用 Cocos 的 Node/Sprite/Label/Widget 取代, +> **不需要复刻 spid 体系**——只要最终把同样的协议数据展示出来即可。 + +--- + +## 4. 桥接层:`gamemain.js`(引擎 → 上层 的总分发) + +引擎对象 `gameabc_face` 的所有回调都在此**广播**给三个消费者,顺序固定为 `GameUI → Game_Modify → gameCombat`: + +| 引擎回调 | 时机 | 分发去向 | +|----------|------|----------| +| `gamestart(gameid)` | 引擎就绪 | **`Logic.AppStart()`**(应用总入口) | +| `mousedown / mousedown_nomove / mouseup / mousemove` | 触摸 | `GameUI.utl*` + `Game_Modify.utl*`/`mouseup` + `gameCombat.utl*` | +| `gamemydrawbegin / gamemydraw` | 每精灵绘制前/绘制 | 同上三方(子游戏在此自绘) | +| `gamebegindraw / gameenddraw` | 每帧开始/结束 | `GameUI` | +| `ontimer` | 定时器 | `GameUI.utlontimer` | +| `ani_doend` | 动画结束 | `GameUI` + `gameCombat` | +| `onloadurl` | 图片/资源加载完成 | `GameUI.onloadurl` | +| `onresize` | 屏幕尺寸变化 | (预留) | +| `tcpconnected/tcpmessage/tcpdisconnected/tcperror` | 引擎自带 TCP | **空**(实际网络走 `00_minhttp.js` 的 WebSocket 封装,不用引擎 TCP) | + +> 关键:**子游戏不直接向引擎注册回调**,而是由 `gamemain.js` 转发。新前端可保留这种"统一事件总线 → 平台/子游戏"的模式。 + +--- + +## 5. 平台层模块清单(`js/00_Surface/`) + +| 文件 | 模块 | 职责 | +|------|------|------| +| `02_Const.js` | `ConstVal` / `AppList` / `RouteList` / `RpcList` | 全局常量、协议名、UI 布局常量 | +| `04_Data.js` | `GameData` | 全局运行时状态(服务器地址、连接状态、各种缓存) | +| `08_Utl_Output.js` | `Utl` | 工具函数、本地存储、退出房间、部分发包 | +| `07_Desk.js` | `Desk` | **牌桌/房间状态机** + 几乎所有房间类接收处理 | +| `05_Func.js` | `Func` | 通用功能(HTTP、语音录制、截图分享、创建房间渲染等) | +| `10_Game.js` | `Game` | 定位等少量游戏级杂项 | +| `11_GameUI.js` | `GameUI` | **平台所有界面**(大厅/房间/聊天/战绩入口/弹窗/列表),最大文件 | +| `12_Logic.js` | `Logic` | **应用生命周期 + 连接管理 + 消息分发 + 重连 + 桥接** | +| `00_minhttp.js` | `min_tcp` / `min_http` | WebSocket 与 HTTP 底层封装 | +| `09_Net.js` | `Net` | **协议收发层**(`Send_*` 发包 / `Net.` 收包路由到 Desk/Player) | +| `06_Player.js` | `Player` / `C_Player` | 玩家数据结构与玩家相关接收处理 | +| `03_Banwords.js` | `banwords` | 敏感词库(聊天过滤) | + +> 网络协议、数据结构的字段细节见 **01–04 章**。 + +--- + +## 6. 子游戏层(`js/01_SubGame/`)—— 这是开发一款游戏唯一要写的部分 + +| 文件 | 模块 | 职责 | +|------|------|------| +| `00_SubGame_Config.js` | `Game_Config` | 子游戏配置:房间人数、聊天/语音气泡位置、分享、客服、声音、调试开关等 | +| `01_SubGame_modify.js` | `Game_Modify` / `gameCombat` | **子游戏实现**:输入处理、创建房间界面、战绩界面、对局渲染 | +| `02_SubGame_Input.js` | `Game_Modify`(接口桩) / `gameHallImport` | 平台会回调、子游戏需实现的**钩子接口默认空实现** | + +### 6.1 两类接入点 + +**A. 业务回调钩子**(平台 → 子游戏;定义在 `02_SubGame_Input.js`,子游戏按需重写): + +| 钩子 | 调用时机(平台侧) | +|------|-------------------| +| `Game_Modify._ReceiveData(msg)` | 收到**游戏内协议包**(route 非 platform/agent/room)| +| `Game_Modify.StartWar(msg)` | 开局(收到 makewar)| +| `Game_Modify.Reconnect(deskinfo)` | 登录/进房响应**含 `deskinfo`** 时触发(还原牌局;模板为空实现,由子游戏自定义)| +| `Game_Modify.DeskInfo(deskinfo)` | 进房响应含 `deskinfo` 但未自动开战(`deskwar` 假)时,传入 `deskinfo` | +| `Game_Modify.createRoom / onCreateRoom` | 创建房间成功 | +| `Game_Modify.myJoinRoom / playerJoinRoom(seat) / playerLeaveRoom(seat)` | 进/离房 | +| `Game_Modify.playerOffline/Online(seat)` / `playerphonestate` | 在线/电话状态 | +| `Game_Modify.onReady(seat)` / `changeSeat(s1,s2)` / `onSurrender(msg)` / `Free(msg)` | 准备/换座/投降/解散 | +| `Game_Modify.updateScene / closeGameScene / onEnterMainScene / onExitMainScene / stopAllSounds` | 场景/声音管理 | +| `Game_Modify.getRoomInfo/getFullRoomInfo/getStarLimit/getMult/getVideoByRoomType/getRoomMode...` | 平台**向子游戏取**房间展示信息(返回值)| + +**B. 引擎事件钩子**(引擎 → 子游戏;定义在 `01_SubGame_modify.js`): + +| 钩子 | 用途 | +|------|------| +| `Game_Modify.utlmousedown / mouseup / utlmousemove / utlmousedown_nomove` | 子游戏自己的触摸交互 | +| `Game_Modify.gamemydraw / utlgamemydrawbegin` | 子游戏自绘 | + +### 6.2 子游戏能调用的平台能力 + +- **发包**:`Net.Send_*(data)`(平台层 RPC,见 02/03 章);游戏内自定义包用 `Net._SendData(app, route, rpc, data)` +- **房间/玩家状态**:`Desk.*`(roomcode、PlayerList、stage…)、`C_Player.*`、`Desk.GetPlayerBySeat(seat)` +- **界面**:`set_self/get_self/set_group/play_ani` + `GameUI.*`(弹窗、提示、聊天气泡等) +- **座位换算**:`Logic.ChangeToStatus(myseat, targetseat)`(把服务器绝对座位转成"以我为视角"的相对位置) +- **配置**:`Game_Config.*` + +--- + +## 7. 应用生命周期 + +``` +引擎就绪 → gameabc_face.gamestart() (gamemain.js) + → Logic.AppStart() (12_Logic.js:313) + ├─ 读取 URL/app 参数:渠道(channelid)、代理(agentid)、市场(marketid)、启动模式(LaunchMode) + ├─ Logic.setGameServer() → 仅解析配置(Game_Config.Debugger.gameserver);配置服 update_json 回调 ServerUrl_Succ → GameData.Server = urlserver + ├─ Desk.Create() → 建空牌桌(PlayerList) + ├─ C_Player = new Player(-1) + ├─ 加载敏感词、音频、定位(cityjson IP) + └─ 连接:Logic.firstConnect/Connect → new WebSocket(ws://Server) + → onopen → Net.Send_login(...)(有条件:isLogin 重连,或读到 wxinfo cookie;见 01 章) + → Desk.login(资产+房间恢复) (见 04 章) + ├─ 有 roomcode:恢复房间;响应含 deskinfo → Game_Modify.Reconnect(deskinfo) + └─ 无 roomcode:进大厅 GameUI.JumpMenuScene() +大厅 → 创建/加入房间 → 房间(准备/开局) → 对局(游戏内协议) → 结算(over_game) → 回房间/大厅 + 期间断线 → onclose → Logic.TryConnect()(轮换服务器)→ 重连后重发 login 恢复状态 +``` + +--- + +## 8. 游戏内对局协议的通道(子游戏自定义) + +平台层协议(01–04 章)是固定的;**对局协议由子游戏定义**,复用同一条 WebSocket 与同一套信封: + +- **下行**:服务器发 `route` 非 platform/agent/room 的包 → `Game_Modify._ReceiveData(msg)` → 子游戏按 `msg.rpc` 自行分发 +- **上行**:`Net._SendData("youle", "<游戏route>", "<游戏rpc>", {agentid,gameid,playerid,roomcode,seat, ...对局字段})` +- **结算**:`over_game`;**重连快照**:登录/进房响应里的 `deskinfo`(结构由子游戏定义) + +> `Game_Surface_3` 是**模板工程**:`02_SubGame_Input.js` 里 `_ReceiveData/StartWar/Reconnect` 等为**空实现** +> (即未含具体玩法),所以本工程取不到某款游戏的对局字段。需从真实子游戏工程或抓包补全(见 05 章)。 + +### 真实数据结构样例(战绩,平台层 `get_player_grade1` 响应) + +`01_SubGame_modify.js` 的 `gameCombat` 揭示了一个真实嵌套结构,可作为对局数据风格参考: +``` +data = { + asetcount: 总局数, + gradeinfo: [ // 每场对局 + { + overtime: "结束时间", + roomcode: "房号", + idx: 翻页索引, + gameinfo1: { // 可能是 JSON 字符串,需二次 parse + roundsum: 小局数, + playerlist: [ [昵称, 总分], ... ], + round: [ [ [座位, 该局分], ... ], ... ] // 每小局每人得分 + } + }, ... + ] +} +``` + +--- + +## 9. 给 CocosCreator 重写的架构映射 + +| 原框架 | CocosCreator 对应 | 说明 | +|--------|-------------------|------| +| gameabc 精灵引擎 + spid/tag/group | Cocos 场景/Node/Prefab/Label/Sprite | **不复刻 spid 体系**,只还原界面与协议数据展示 | +| `gamemain.js` 事件总线 | Cocos 输入事件 + 自建 EventBus | 统一把交互/帧更新派发到 UI 与对局模块 | +| `00_Surface/*` 平台层 | 一个"平台 SDK"模块(登录/大厅/房间/网络) | **按 01–04 章协议 1:1 实现**,是与服务器对接的关键 | +| `Net._SendData` + 双层解包 | Cocos 的 WebSocket 封装 | 严格保持信封 `{app,route,rpc,data}`、双层包装、心跳识别(01 章)| +| `Game_Modify` 钩子 | 对局模块对外接口 | 平台模块在相应时机回调对局模块 | +| `Desk` / `C_Player` | 房间状态/玩家数据模型 | 按 04 章字段建模 | +| `01_SubGame/*` | 具体游戏对局场景 | 自由用 Cocos 实现,只需吃平台给的数据、按对局协议收发 | + +**核心结论**:与服务器"完美适配"只取决于**平台层协议(01–04 章)+ 对局协议(05 章,需补全)**的字节级一致; +渲染引擎、UI 实现方式可以完全替换。把"平台 SDK"和"对局逻辑"在新前端里分清边界,就复刻了这套子游戏框架的精髓。 diff --git a/docs/protocol/01-传输层与架构.md b/docs/protocol/01-传输层与架构.md new file mode 100644 index 0000000..99c34a2 --- /dev/null +++ b/docs/protocol/01-传输层与架构.md @@ -0,0 +1,257 @@ +# 01 · 传输层与架构 + +> 本篇是「框架模板(00_Surface 平台层)↔ 服务器」协议规范的**传输层**部分:连接建立、握手、心跳、断线重连、服务器切换、收发包封装与过滤。 +> 双向完整(C→S 与 S→C)。不含子游戏对局包(见 05 章)。 +> 所有 `文件:行` 引用均按当前源码核对。源码 bug 用 🐛 标注;需后端抓包确认的用 ⚠️待服务器确认 标注。 + +--- + +## 1. 整体架构 + +``` +┌─────────────┐ 配置请求(HTTP) ┌──────────────────┐ +│ 新前端 │ ─────────────────► │ 配置服务器 │ 返回 update_json (含 urlserver) +│ (Cocos) │ ◄───────────────── │ (gameserver 地址) │ +└─────────────┘ └──────────────────┘ + │ ws://ip:port (拿到 urlserver 后) + ▼ +┌─────────────────────────────────────────────────────┐ +│ 大厅服务器(agent) ←切换→ 房间服务器(room) │ +│ 登录/房间列表/排行/任务/支付 开局/聊天/解散/对局 │ +└─────────────────────────────────────────────────────┘ +``` + +- 同一时刻只保持**一条** WebSocket 连接(`Net.ws_tcp`)。 +- 大厅与房间是**不同的服务器地址**,通过 `connect_roomserver` / `connect_agentserver` 指令切换: + 服务器下发新地址 → 客户端把 `GameData.Server` 替换为新地址 → **关闭当前连接**(`Net.ws_tcp.close()`)→ `onclose` 触发后用新地址重连 → 重新登录/握手。 +- 路由空间 `platform / agent / room`(`02_Const.js:8-10`,字面值已确认与变量名一致)。 + +--- + +## 2. 传输方式 + +源码支持两种传输,由 `ConstVal.netType` 决定(`02_Const.js:654`,默认值 `0`): + +| netType | 方式 | 说明 | +|---------|------|------| +| `0` | **WebSocket**(默认,正式使用) | `Net.ws_tcp.send(JSON.stringify(msg))` | +| `1` | HTTP Ajax(备用/降级) | `Func.AjaxHttp2(...)`,POST JSON | + +> 大厅模式(`ConstVal.isGameHall==true`)启动时会被强制改成 `ConstVal.netType=1`(`12_Logic.js:383`)。子游戏(本框架模板的目标场景)走 `netType=0 / WebSocket`,这是线上实际通道,**新前端按 WebSocket 实现即可**。 +> 收包路径在两种传输下**不同**,详见 3.3 节对比。 + +### WebSocket 连接地址 + +WebSocket 在 `min_tcp(config)`(`00_minhttp.js:256`)内创建: +```js +var ws = new WebSocket("ws://" + config.ipport); // 00_minhttp.js:258,例: ws://127.0.0.1:5414 +``` +- `config.ipport` 来自 `GameData.Server`(`12_Logic.js:856 / 900 / 941` 等连接函数把 `server` 赋给 `config.ipport`)。 +- `GameData.Server` 的来源(netType==0 分支):配置服务器回包 `ServerUrl_Succ` 内 `GameData.Server=_msg.data.urlserver`(`12_Logic.js:549`)。 + - ⚠️ 注意:`Logic.setGameServer()`(`12_Logic.js:1270`)只设置 `Game_Config.Debugger.gameserver`(配置文件 URL),**不**设置 `GameData.Server`;两者是两步、不可混淆。 +- 切换房间/大厅服时,`GameData.Server` 被替换为 `_msg.data.roomserver` / `_msg.data.agentserver`(`09_Net.js:509 / 526`)。 +- `GameData.Server` 可为**单个字符串,也可为数组**(多候选服务器)。为数组时按 `GameData.serverIndex` 轮询,详见第 7 节。 + +--- + +## 3. 消息信封(Envelope) + +### 3.1 客户端 → 服务器(发送) + +统一出口 `Net._SendData(_app, _route, _rpc, _data)`(`09_Net.js:12`)。组装的是**单层**信封: + +```json +{ + "app": "youle", + "route": "agent", // platform | agent | room + "rpc": "player_login", // RPC 名称 + "data": { ... } // 业务数据 +} +``` + +- `app` 字段固定取 `AppList.app`,值为 `"youle"`(`02_Const.js:5`,已确认恒为 `"youle"`)。 +- `route` 取 `RouteList.platform/agent/room`(`02_Const.js:8-10`)。 +- netType==0:`Net.ws_tcp.send(JSON.stringify(_msg))`(`09_Net.js:20`)。**单层**,直接发信封。 +- netType==1:走 `Func.AjaxHttp2(GameData.Server, _msg, ...)`(`09_Net.js:24`),POST JSON。 + +### 3.2 服务器 → 客户端(接收)—— WebSocket 路径:双层包装 + +WebSocket 收包处理在 `12_Logic.js` 的 `this.onmessage`(定义于 **`12_Logic.js:119`**,路由分发在 **`12_Logic.js:258`**)。**收到的是双层结构**: + +``` +原始帧(字符串) = JSON.stringify({ + "data": <内层> // 外层只有 data 字段 +}) + +<内层> 可能是字符串或对象,再解析后 = { + "route": "...", + "rpc": "...", + "data": { ... } // 真正的业务数据 +} +``` + +完整解包顺序(含三道前置过滤,务必照此实现,否则会误处理旧连接包/错误回包/握手心跳): + +1. **旧连接去重**:`if(GameData.TcpID != this.id) return;`(`12_Logic.js:122`)——每次重连 `GameData.TcpID++`,每个 WebSocket 实例闭包持有自己的 `this.id`,只有最新连接的包才被处理,旧连接残留包直接丢弃。 +2. 若网络状态关闭 `if(!GameData.netWorkSate) return;`(`12_Logic.js:127`)。 +3. `_msg = JSON.parse(原始帧)`(仅当是字符串,`12_Logic.js:133-136`)→ 取外层 `data = _msg.data`(`12_Logic.js:137`)。 +4. **特殊错误包**:若 `data === "webserve-服务器未工作"` → 进入重发登录分支后 `return`(详见 4 节)。 +5. 若 `data` 是字符串: + - 若 `data.substr(0,9) === "@toconcon"` → **握手包,直接 `return` 忽略**(`12_Logic.js:176-179`)。 + - 否则 `data = JSON.parse(data)`(`12_Logic.js:180`)。 +6. **重置收包超时定时器**(仅 `!ConstVal.isGameHall` 时创建,见第 4 节)(`12_Logic.js:182-217`)。 +7. 若 `data.com === "@serverheartbeat"` → **心跳包,直接 `return` 忽略,不回包**(`12_Logic.js:218-223`)。 +8. 令 `_msg = _msg.data`(取内层),必要时再 `JSON.parse`(`12_Logic.js:224-229`)→ 得到 `{route, rpc, data}`。 +9. **错误回包忽略**:`if(_msg.rpc == "submit_error") return;`(`12_Logic.js:235`)。 +10. **登录态门控**(`isSendLoginState`):若 `GameData.isSendLoginState==true`(已发登录、等待 `player_login` 响应期间),则除 `player_login`(清门控)与 `kick_server` 外,**其它收包一律 `return` 丢弃**(`12_Logic.js:238-257`)。 +11. 按 `_msg.route` 分发(见第 5 节,`12_Logic.js:258`)。 + +> 注:源码外层与内层都做了 `typeof == "string"` 判断后再 `JSON.parse`,是为兼容服务器有时发对象、有时发字符串。新前端实现时对每层都「是字符串就 parse」最稳妥。 + +### 3.3 服务器 → 客户端 —— HTTP(netType==1) 路径:单层、无信封过滤 + +netType==1 收包走 `Func.AjaxHttp2` 的成功回调(`09_Net.js:24-44`),与 WebSocket 路径**完全不同**: + +- 回调直接拿到 `_msg`(字符串则 `JSON.parse`),**单层**:直接读 `_msg.route` / `_msg.rpc` 分发,**没有外层 `data` 信封、没有 `@toconcon` 握手、没有 `@serverheartbeat` 心跳、没有 30s 超时、没有 TcpID/isSendLoginState 三道过滤**。 +- 分发逻辑同 5 节:`route ∈ {platform,agent,room}` → `Net[rpc](_msg)`,否则 → `Game_Modify._ReceiveData(_msg)`。 +- 失败回调里若是登录请求(`input_msg=="playerLogin"`)→ `GameUI.OpenTips("网络状况不好")`(`09_Net.js:38-43`)。 + +> 新前端只需实现 WebSocket(netType==0) 路径;此节列出仅供对照,说明降级通道的差异。 + +--- + +## 4. 握手与心跳 + +| 包 | 方向 | 触发判定 | 频率 | 客户端动作 | +|----|------|---------|------|-----------| +| 握手包 | 服务器→客户端 | 外层 `data` 为字符串且 `substr(0,9)=="@toconcon"` | 连接成功后首包 | 忽略 `return` | +| 心跳包 | 服务器→客户端 | 解包后 `data.com === "@serverheartbeat"` | ~20s 一次 ⚠️待服务器确认 | 忽略,**不回包** | +| 收包超时 | 客户端本地 | —— | 每次收包重置定时器 | 仅 `!isGameHall` 启用;超时见下方降级逻辑 | + +关键常量:`ConstVal.Max.heartbeat = 30000`(`02_Const.js:144`),即 30 秒收包超时阈值。 + +> ⚠️待服务器确认:心跳"约 20s 一次"仅源码注释佐证(`12_Logic.js:219` 注释「20秒一次」);客户端侧唯一硬常量是收包超时 30000ms。实际心跳间隔需抓包确认。 + +### 4.1 收包超时定时器(仅子游戏模式) + +- 仅当 `!ConstVal.isGameHall` 时才 `setTimeout(..., ConstVal.Max.heartbeat)` 创建(`12_Logic.js:185-217`);**大厅模式不启用**此定时器。 +- 每次收到任意包都先 `clearTimeout` 再重建(滑动窗口)。 +- 30s 内未再收包 → `GameUI.OpenTips("网络较慢!")` + `GameData.heartBeatStage=true`,并按当前连接 `readyState` 分两条路径(仅非 Debugger 模式): + - `readyState == CLOSED`:`disType=true; ConstVal? NetType=1; Logic.TryConnect();`(直接进重连)(`12_Logic.js:190-195`)。 + - `readyState == OPEN`:`NetType=1; GameUI.StartLoad(); isClose=true; Net.ws_tcp.close();` 并启 `setInterval(..., 5000)` 反复 `close()` 直到真正断开(`12_Logic.js:196-214`)。 +- 下次正常收包时若 `heartBeatStage==true`,会 `GameUI.CloseTips()` 并复位(`12_Logic.js:129-132`)。 + +> **结论**:心跳是服务器单向 push,新前端**无需**主动发送任何心跳包,只需:① 收到 `@serverheartbeat` 时不当作业务包丢弃;② 子游戏模式下维护一个「收到任意包就重置」的 30s 超时定时器用于断线判断与降级。 + +### 4.2 特殊错误包 `webserve-服务器未工作` + +外层 `data === "webserve-服务器未工作"` 时(`12_Logic.js:138`): +- **前置条件** `if(get_self(225,37,0,0,0) == 0)`(`12_Logic.js:139`,某 UI 状态判定)成立时:`setTimeout(..., 10000)` 即 10s 后用 `C_Player` 现有信息重新组装 `data` 并 `Net.Send_login(data)`(`12_Logic.js:140-159`)。 +- 然后 `return`,不继续后续解包(`12_Logic.js:161`)。 + +--- + +## 5. 路由分发逻辑 + +解包得到内层 `{route, rpc, data}` 后(WS 路径 `12_Logic.js:258`;HTTP 路径 `09_Net.js:31`;`_SendData` 内 netType==1 回调亦同): + +```js +if (route === "platform" || route === "agent" || route === "room") { + if (typeof Net[rpc] === "function") Net[rpc](msg); // 平台层:调用 Net.(内层msg) +} else { + Game_Modify._ReceiveData(msg); // 其它路由:交给子游戏 +} +``` + +- **平台层**:`route ∈ {platform, agent, room}` → 调用 `Net[rpc](内层msg)`,`Net.` 再转 `Desk.` / `C_Player.` 处理。 +- **游戏内**:其它 `route` → `Game_Modify._ReceiveData(内层msg)`,由子游戏自行按 `rpc` 分发(见 05 章)。 + +> 在新前端中,等价于实现一个 `dispatch(inner)`:先判断 route 是否平台层,是则查表调用对应处理器,否则进入游戏内协议处理器。 + +--- + +## 6. 登录前的完整握手流程(netType==0) + +``` +1. 启动 AppStart → HTTP 请求配置服务器(gameserver=Game_Config.Debugger.gameserver) + → 回包 ServerUrl_Succ → GameData.Server = _msg.data.urlserver (12_Logic.js:549) +2. (netType==0 时) AppStart 启 GameData.TcpTimer = setInterval(..., 3*GameData.timer) = 30s 首连看门狗 + (12_Logic.js:485-496):超时则 GameUI.OpenTips("网络状况不好...") 并 Net.ws_tcp.close() + 首连成功(onopen)或拿到配置后会 clearTimeout 它。 +3. Logic.firstConnect(GameData.Server) → new game_websocket(TcpID) → min_tcp() → new WebSocket("ws://"+server) +4. onopen 触发 (12_Logic.js:3)。是否发 player_login 取决于状态(见 6.1)。 +5. 服务器先回握手包 @toconcon (忽略)。 +6. 服务器回 player_login 响应 (route=agent, rpc=player_login) → Net.player_login → Desk.login 处理。 + (登录响应字段见 02 章) +7. 进入大厅或恢复房间/对局。 +``` + +### 6.1 onopen 发送 player_login 是**有条件**的(修正:非无条件) + +`onopen`(`12_Logic.js:3-118`)按 `GameData.ConnectType` 与登录状态分支: + +- 若 `!GameData.ConnectType`(普通连接,非服务器切换)且 `NetType==0`: + - **重连场景** `GameData.isLogin == true`:用 `C_Player` 现有信息(agentid/openid/gameid/nickname/avatar/sex/province/unionid/city/version/channelid/marketid,含 deviceLogin 时附 telphone)组装 `data` 并 `Net.Send_login(data)`(`12_Logic.js:34-52`)。 + - **首登场景** `GameData.isLogin == false`:读 `Utl.getCookie(Utl.Config.wxinfo)`;**cookie 为 null 时不发** `player_login`(`12_Logic.js:53-79`);非 null 时 `SetWxInfo` 后组装并 `Net.Send_login(data)`。 +- 若 `GameData.ConnectType`(服务器切换重连):按 `GameData.ConnectRpc` 重发 `connect_roomserver`/`connect_agentserver`,或在 `disType` 下补发登录(`12_Logic.js:82-117`)。 + +> 修正要点:文档不可写「onopen 立即/无条件发 player_login」。首登必须先有 wxinfo cookie。 + +### 6.2 Send_login 的 4s 守护超时(子游戏模式) + +`Net.Send_login(_data)`(`09_Net.js:122-238`)在 `!ConstVal.isGameHall` 且 `sendLoginTimer==null` 时,设 `setTimeout(..., 4000)`(`09_Net.js:155-172`):4s 内未收到 `player_login` 响应则 +`ConnectType=false; disType=true; NetType=1; isClose=true; Net.ws_tcp.close();` 并启 `setInterval(..., 5000)` 反复 `close()` 直到断开(再由 onclose 走重连/降级)。 +发出登录时设 `GameData.isSendLoginState=true`(`09_Net.js:152`),由 3.2 第 10 步门控收包。 + +--- + +## 7. 断线重连与服务器切换 + +### 7.1 onclose(`12_Logic.js:278`) + +`onclose` 按 `GameData.ConnectType` 与 `GameData.firstConnect` 分流: + +- `GameData.urlFail` 为真直接 `return`(配置都没拿到,不重连)(`12_Logic.js:281`)。 +- **非切换** `!ConstVal? !GameData.ConnectType`: + - 若 `!GameData.firstConnect`(已过首连):`NetType=1; Logic.TryConnect();`,按 `disType` 显示断线 UI(`12_Logic.js:297-306`)。 + - 若 `GameData.firstConnect`(仍在首连阶段):`GameData.tryTimes++`;候选数 `ttimes`(数组时取 `Server.length`,否则 3);`tryTimes % ttimes == 0` 时提示「网络状况不好...」;`Logic.Connect(GameData.Server)`(`12_Logic.js:307-317`)。 +- **切换** `GameData.ConnectType`:`!disType` → `Logic.Connect(GameData.Server)`,否则 `GameUI.StartLoad()`(`12_Logic.js:319-326`)。 + +### 7.2 重连间隔与重连函数 + +- `GameData.timer = 10000`(`04_Data.js:42`)= 重连定时器间隔(`Logic.Connect`/`Logic.TryConnect` 内 `setTimeout(..., GameData.timer)`,`12_Logic.js:910 / 1021`)。 +- `Logic.firstConnect(server)`(`12_Logic.js:826`):首连,不延时。 +- `Logic.Connect(server)`(`12_Logic.js:869`):`setTimeout timer` 后连接。 +- `Logic.TryConnect()`(`12_Logic.js:915`):`netType==1` 时直接 `return`(不重连);否则立即连一次 + 再挂 `setTimeout timer` 一次。 + +### 7.3 多候选服务器轮询(`GameData.Server` 为数组) + +两套独立计数路径: + +- **首连阶段(onclose, firstConnect)**:`GameData.tryTimes++`;候选数取 `Server.length`;提示节流用 `tryTimes % ttimes`(`12_Logic.js:308-316`)。`firstConnect`/`Connect` 内若 `isArray(server)` 则 `serverIndex` 取 0 或 `(serverIndex+1)%length` 切换(`12_Logic.js:833-836 / 876-880`)。 +- **已登录后断线(TryConnect)**:`GameData.tryReconnectTimes++`;`tryReconnectTimes % 3 == 0` 时归零并 `serverIndex = (serverIndex+1)%Server.length` 切下一个(`12_Logic.js:952-962 / 1001-1011`)。即**每失败约 3 次轮换一个候选地址**。 +- 数组模式下每次连接都把 `GameData.sendLoginTimes = -1` 复位(`12_Logic.js:834 / 877 / 938 / 989`)。 + +### 7.4 服务器切换指令(S→C 推送) + +> connect_roomserver · route=room · S→C 推送;服务器要求客户端切到房间服;推送字段(S→C) `roomserver`(string)—新房间服 ip:port;客户端动作:`GameData.ConnectType=true; GameData.ConnectRpc=connect_roomserver; GameData.ConnectPack=data; GameData.Server=data.roomserver; Net.ws_tcp.close()`(onclose 后用新地址重连,重连 onopen 会重发 `Send_connect_roomserver(ConnectPack)`);接收 `09_Net.js:503-512`;备注 — + +> connect_agentserver · route=agent · S→C 推送;服务器要求客户端切回大厅服;推送字段(S→C) `agentserver`(string)—新大厅服 ip:port、`opt`(string)—切换原因;客户端动作:若 `opt == other_break_room || opt == free_room` 则先 `GameUI.StartLoad()`;随后 `ConnectType=true; ConnectRpc=connect_agentserver; ConnectPack=data; GameData.Server=data.agentserver; Net.ws_tcp.close()`;接收 `09_Net.js:518-529`;备注 ⚠️待服务器确认 `data.opt` 完整取值域(源码仅见其与 `other_break_room` / `free_room` 比较)。 + +> 这两个指令客户端也可主动发起:`Net.Send_connect_roomserver` / `Net.Send_connect_agentserver`(`09_Net.js:500 / 515`,route 分别为 room/agent,rpc 同名)。 + +--- + +## 8. 错误上报(可选实现,C→S) + +> submit_error · route=agent · C→S 单向上报(无响应);上报客户端异常;请求字段(C→S) `packet`(string)—出错的包、`msg`(string)—错误堆栈、`playerid`/`agentid`/`gameid`;发送 `09_Net.js:55-80`(`Net.submit_error`,同 `msg` 去重,仅当 `errorMsg` 变化才发);备注:catch 中由 `12_Logic.js:265-274` 在 `isSubmitError` 开启时调用。 + +> submit_log · route=agent · C→S 单向上报;与 submit_error 同结构;发送 `09_Net.js:81-101`(`Net.submit_log`);备注 ⚠️待服务器确认:其内部 `rpc` 字段写死为 `"submit_error"`(`09_Net.js:85`),疑似复用——是否存在独立 `submit_log` 协议待后端确认。🐛 若服务端需区分两类上报,此处 rpc 名错误。 + +> 注意:客户端对收到的 `rpc=="submit_error"` 回包**直接 `return` 忽略**(`12_Logic.js:235`)。新前端对适配非必需,可选实现。 + +```json +{ "app":"youle", "route":"agent", "rpc":"submit_error", + "data": { "packet":"<出错的包>", "msg":"<错误堆栈>", + "playerid":..., "agentid":..., "gameid":... } } +``` diff --git a/docs/protocol/02-协议-agent路由.md b/docs/protocol/02-协议-agent路由.md new file mode 100644 index 0000000..c077913 --- /dev/null +++ b/docs/protocol/02-协议-agent路由.md @@ -0,0 +1,409 @@ +# 02 · 平台层协议 — agent 路由(大厅服务器) + +> 信封:`{ app:"youle", route:"agent", rpc:"<下列名称>", data:{...} }` +> 本篇覆盖 **route=agent 的全部平台层数据包**(双向)。每个 rpc 给出 C→S 请求字段与 S→C 响应/推送字段。子游戏对局包(route=room)不在此篇。 +> 通用身份字段:`agentid`(代理ID)、`gameid`(游戏ID)、`playerid`(玩家ID)。多数请求由各调用点手工拼装,未必三者齐全,下文逐条标注实际字段。 +> 统一条目格式: +> > `rpc名` · route=agent · <方向> +> > 场景 / 请求字段(C→S) / 响应或推送字段(S→C) / 源码 发送·接收 / 备注(⚠️待后端确认,🐛源码 bug) + +--- + +## 登录 / 账号 + +### `player_login` · route=agent · C→S 请求 + S→C 响应 +- 场景:连接建立后 `onopen` 自动发送;断线重连、切服后重发(12_Logic.js:35、:61 构造,09_Net.js:122 `Send_login` 注入)。 +- 请求字段(C→S): + | 字段 | 类型 | 说明 | + |------|------|------| + | agentid | string/int | 代理ID | + | gameid | string/int | 游戏ID | + | openid | string | 微信 openid(游客可为空/特殊值) | + | nickname | string | 昵称 | + | avatar | string | 头像 URL | + | sex | int | 性别 0未知/1男/2女 | + | province | string | 省份 | + | city | string | 城市 | + | unionid | string/int | 微信开放平台 unionid | + | version | string | 客户端版本号(GameData.versionCode) | + | channelid | string/int | 渠道ID | + | marketid | string/int | 市场ID | + | ip | string | 客户端IP,`Send_login` 注入 `returnCitySN.ip`(09_Net.js:125,仅 returnCitySN 存在时) | + | location | object | 定位对象,`Send_login` 注入 `C_Player.addr`(09_Net.js:132,可为 null) | + | machineid | string | 机器标识 `Logic.getMachineId()`(09_Net.js:139) | + | machineroom | string | 机房标识 `Utl.getRoomcode()`(09_Net.js:140) | + | telphone | string | 绑定手机号,仅 `GameData.sysConfig.deviceLogin` 开启时随 `telphoneAuto:true` 一起携带(12_Logic.js:48-50) | + | telphoneAuto | bool | 设备号自动登录标记,同上条件下=true | + | playerid | int | 可选,本地缓存 playerid(`GameData.loginPlayerid` 开启时由 `Logic.readPlayerId()` 注入,09_Net.js:143-148,用于复用账号) | +- 响应字段(S→C):`state`(int 0成功/非0失败) + 大量字段,分两组: + - **A 组 账号资产**:`roomcard`、`bean`、`bank`(仓库星星)、`bankpower`、`bankpwd`、`charm`、`sign`、`tel`、`invitecode` 等(06_Player.js:88 `SetMyInfo` 读取,详见 [04-数据结构.md → 登录响应](./04-数据结构.md#登录响应-deskloginplayer_login))。 + - **B 组 房间恢复**:在房时附带房间快照(roomcode/seat/players/deskinfo 等),由 `Desk.login` 处理,详见 doc04。 +- 源码:发送 `09_Net.js:236`(`Net.Send_login`→`Net._SendData`);接收 `09_Net.js:240`(`Net.player_login`→`Desk.login`)。 +- 备注:— (请求侧字段以本表为准;响应数据结构引用 doc04) + +### `query_player2` · route=agent · C→S 请求 + S→C 响应 +- 场景:仓库转账前查询目标玩家信息。 +- 请求字段(C→S):`agentid`、`playerid`(目标玩家ID)。 +- 响应字段(S→C):`avatar`(string)、`nickname`(string)、`playerid`(int);昵称/头像均空时提示"未找到对应玩家"。 +- 源码:发送 `09_Net.js:870`;接收 `09_Net.js:873`→`Desk.query_player2`(`07_Desk.js:1368`)。 +- 备注:— + +### `binding_phone` · route=agent · C→S 请求 + S→C 响应 +- 场景:绑定手机号。 +- 请求字段(C→S):`agentid`、`playerid`、`phonenum`、`smmcode`(短信验证码)。 +- 响应字段(S→C):`phonenum`(string,回写 `C_Player.tel`)。 +- 源码:发送 `09_Net.js:830`;接收 `09_Net.js:834`→`Desk.binding_phone`(`07_Desk.js:1337`)。 +- 备注:— + +### `send_phone_checkcode` · route=agent · C→S 请求 + S→C 响应 +- 场景:发送手机短信验证码。 +- 请求字段(C→S):`agentid`、`phonenum`(构造点未集中定位,至少含手机号)。 +- 响应字段(S→C):无业务字段(`Desk.send_phone_checkcode` 为空实现,07_Desk.js:1342)。 +- 源码:发送 `09_Net.js:838`;接收 `09_Net.js:842`。 +- 备注:⚠️ 请求字段以后端实现为准。 + +### `send_phone_code_wechat` · route=agent · C→S 请求 + S→C 响应 +- 场景:微信渠道发送手机验证码(绑定手机流程的另一入口)。 +- 请求字段(C→S):`agentid`、`phonenum`(唯一调用点 11_GameUI.js:2620-2623 当前被注释,按注释代码为 agentid+phonenum)。 +- 响应字段(S→C):无业务字段(`Desk.send_phone_code_wechat` 为空实现,07_Desk.js:1345)。 +- 源码:RpcList 定义 `02_Const.js:81`;发送 `09_Net.js:846`(`Send_send_phone_code_wechat`);接收 `09_Net.js:850`→`Desk.send_phone_code_wechat`(`07_Desk.js:1345`)。 +- 备注:⚠️ 框架完整定义并接线,但**当前前端唯一调用点(11_GameUI.js:2623)被注释,实际不发送**;字段以后端实现为准。 + +### `setSign` · route=agent · C→S 请求 + S→C 响应 +- 场景:设置个性签名。 +- 请求字段(C→S):`agentid`、`playerid`、`sign`。 +- 响应字段(S→C):`sign`(string,回显写入 `C_Player.sign`)。 +- 源码:发送 `09_Net.js:736`;接收 `09_Net.js:740`→`C_Player.setSign`(`06_Player.js:525`)。 +- 备注:— + +--- + +## 房间创建 / 进入(大厅侧) + +### `create_room` · route=agent · C→S 请求 + S→C 响应 +- 场景:玩家创建房间。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`roomtype`(房间类型配置数组,见 doc04);`Send_create_room` 自动注入 `ip`=`C_Player.ip`、`location`=`C_Player.addr`(09_Net.js:106-107)。 +- 响应字段(S→C):`state`(0成功)、`roomcode`、`seat`、`roomtype`、`makewar`、`asetcount`、`shortcode`、`infinite`;失败 `showerror`/`error`。详见 [04 → 房间响应公共字段](./04-数据结构.md#房间创建--进入响应公共字段)。 +- 源码:发送 `09_Net.js:104`;接收 `09_Net.js:111`→`Desk.create_room` + `Game_Modify.createRoom`。 +- 备注:— + +### `self_join_room` · route=agent · C→S 请求 + S→C 响应 +- 场景:输入房号 / 快速加入 / H5 唤起 / 进 VIP 配置房。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`roomcode`(房号);`Send_self_join_room` 自动注入 `location`=`C_Player.addr`、`ip`=`C_Player.ip`(09_Net.js:254-255);进 VIP 配置房入口额外带 `vipMatch:1`(12_Logic.js:2171);比赛进房入口可带 `match_id`。 +- 响应字段(S→C):`state`、`roomcode`、`seat`、`isowner`、`players[]`、`roomtype`、`makewar`、`asetcount`、`deskwar`、`deskinfo`(重连快照)。详见 [04 → 房间响应公共字段](./04-数据结构.md#房间创建--进入响应公共字段)。 +- 源码:发送 `09_Net.js:251`;接收 `09_Net.js:259`→`Desk.self_join_room`。 +- 备注:— + +### `quick_enter_share_room` · route=agent · C→S 请求 + S→C 响应 +- 场景:快速进入分享/星星场房间。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`type`(房间类型)、`roomtype`(可选)。 +- 响应字段(S→C):走 self_join_room 进房流程(无独立 `Net.quick_enter_share_room` 接收函数,结果通过 self_join_room/show_message 等回包)。 +- 源码:发送 `09_Net.js:652`。 +- 备注:⚠️ 无对应接收处理函数,进房结果依赖其它推送。 + +### `advanced_roomlist` · route=agent · C→S 请求 + S→C 响应 +- 场景:拉取 VIP/高级房间列表。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 +- 响应字段(S→C):房间列表对象,整包写入 `GameData.snrRoomList` 并渲染(07_Desk.js:1176)。 +- 源码:发送 `09_Net.js:660`;接收 `09_Net.js:665`→`Desk.advanced_roomlist`。 +- 备注:— + +### `advanced_createroom` · route=agent · C→S 请求 + S→C 响应 +- 场景:创建 VIP/高级房间。 +- 请求字段(C→S):`agentid`、`gameid`、`playerid`、`tea`(茶水费)、`infinite`(0/1无限局)、`roomtype`、`videoConfig`(可选)、`rebateLimit`(可选)、`rebateType`(可选)。 +- 响应字段(S→C):`tea`、`rebateLimit` 及房间配置(整包写入 `GameData.snrRoomList`,并回拉 advanced_roomlist,07_Desk.js:1180)。 +- 源码:发送 `09_Net.js:669`;接收 `09_Net.js:674`→`Desk.advanced_createroom`。 +- 备注:— + +### `get_share_room` · route=agent · C→S 请求 + S→C 响应 +- 场景:获取分享/星星场房间列表(仅非大厅环境发送)。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 +- 响应字段(S→C):房间数组(`Desk.get_share_room` / `Game_Modify.getShareRoom` 处理)。 +- 源码:发送 `09_Net.js:641`(`ConstVal.isGameHall` 为 true 时不发);接收 `09_Net.js:648`。 +- 备注:— + +### `getInfoByShortCode` · route=agent · C→S 请求 + S→C 响应 +- 场景:按短码批量查询房间信息(VIP 房列表)。 +- 请求字段(C→S):`agentid`、`gameid`、`shortcodeList`(短码列表)。 +- 响应字段(S→C):`roomInfo`(短号房间信息) → `GameUI.setVipRoomListData`(07_Desk.js:1284)。 +- 源码:发送 `09_Net.js:754`;接收 `09_Net.js:758`→`Desk.getInfoByShortCode`。 +- 备注:— + +### `switchRoomList` · route=agent · C→S 请求 + S→C 响应 +- 场景:开关房间在列表中的可见/可进入状态。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`isClose`(0开/1关)。 +- 响应字段(S→C):`state`(0成功)、`isClose`;失败 `error`(07_Desk.js:1267)。 +- 源码:发送 `09_Net.js:744`;接收 `09_Net.js:748`→`Desk.switchRoomList`。 +- 备注:— + +--- + +## 战绩 / 排行 / 财富 + +### `get_player_grade1` · route=agent · C→S 请求 + S→C 响应 +- 场景:拉取战绩(类型1)。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`type`(可选)、`direction`(可选,翻页)、`gradeidx`(可选,分页索引)。 +- 响应字段(S→C):战绩数据(`gameCombat.get_player_grade1` 渲染)。 +- 源码:发送 `09_Net.js:359`;接收 `09_Net.js:364`。 +- 备注:— + +### `get_player_grade2` · route=agent · C→S 请求 + S→C 响应 +- 场景:拉取战绩(类型2)。 +- 请求字段(C→S):透传调用方 `_data`,构造点未集中定位;至少含 `agentid`/`playerid`/`gameid`。 +- 响应字段(S→C):战绩数据(`gameCombat.get_player_grade2` 渲染)。 +- 源码:发送 `09_Net.js:372`;接收 `09_Net.js:377`。 +- 备注:⚠️ 请求字段构造点未定位,待核对。 + +### `get_treasurelist` · route=agent · C→S 请求 + S→C 响应 +- 场景:财富榜。 +- 请求字段(C→S):`agentid`、`gameid`。 +- 响应字段(S→C):`list`(排行数组) → `Desk.get_treasurelist`。 +- 源码:发送 `09_Net.js:686`;接收 `09_Net.js:691`。 +- 备注:— + +### `getShortCodeRankList` · route=agent · C→S 请求 + S→C 响应 +- 场景:短号场排行榜。 +- 请求字段(C→S):`agentid`、`playerid`、`shortcode`。 +- 响应字段(S→C):成功为排行数据(整包写入 `GameData.vipRank.data`);失败 `error:true` + `message`(07_Desk.js:1311)。 +- 源码:发送 `09_Net.js:770`;接收 `09_Net.js:774`→`Desk.getShortCodeRankList`。 +- 备注:— + +### `getVipRankList` · route=agent · C→S 请求 + S→C 响应 +- 场景:VIP 排行榜。 +- 请求字段(C→S):`agentid`、`limit`(条数)。 +- 响应字段(S→C):`list`(VIP排行数组) → `GameData.rankList`(07_Desk.js:1328)。 +- 源码:发送 `09_Net.js:788`;接收 `09_Net.js:792`→`Desk.getVipRankList`。 +- 备注:— + +--- + +## 任务系统 + +### `get_player_task` · route=agent · C→S 请求 + S→C 响应 +- 场景:获取任务列表。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 +- 响应字段(S→C):`tasks`(任务数组) → `Desk.get_player_task`。 +- 源码:发送 `09_Net.js:409`;接收 `09_Net.js:414`。 +- 备注:— + +### `player_finish_task` · route=agent · C→S 请求 + S→C 响应 +- 场景:上报任务完成(如分享成功触发,06_Player.js:414)。 +- 请求字段(C→S):`agentid`、`playerid`、`taskid`。 +- 响应字段(S→C):`state`(int)——`state==1` 且当前 `taskstate==0` 时把 `C_Player.taskstate` 置 1(06_Player.js:476)。 +- 源码:发送 `09_Net.js:421`;接收 `09_Net.js:425`→`C_Player.player_finish_task`(`06_Player.js:474`)。 +- 备注:原文档"响应含 `taskstate`"无源码依据,已删除——接收函数仅读 `_msg.data.state`。 + +### `get_task_award` · route=agent · C→S 请求 + S→C 响应 +- 场景:领取任务奖励。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`taskid`。 +- 响应字段(S→C):`taskid`(对应任务 state 置 2=已领取)、`taskstate`(写入 `C_Player.taskstate`)(06_Player.js:482)。 +- 源码:发送 `09_Net.js:429`;接收 `09_Net.js:435`→`C_Player.get_task_award`。 +- 备注:— + +### `refresh_task_state` · route=agent · C→S 请求(发送侧 `Send_can_award`) +- 场景:刷新任务可领取状态。 +- 请求字段(C→S):透传 `_data`(构造点未集中定位,至少含 `agentid`/`playerid`)。 +- 响应字段(S→C):服务器推送 `can_award`(见下条)。 +- 源码:发送 `09_Net.js:440`(`Net.Send_can_award`)。 +- 备注:⚠️ 发送函数名为 `Send_can_award`,但**实际发出的 rpc 是 `"refresh_task_state"`**(硬编码字面量,**不在 RpcList**)。文档以实际 rpc 名为准。 + +### `can_award` · route=agent · S→C 推送 +- 场景:服务器通知有任务可领取。 +- 请求字段:无(纯推送)。 +- 推送字段(S→C):任务可领取标记(载荷字段待后端确认)。 +- 源码:接收 `09_Net.js:444`(`Net.can_award`)→ 调 `C_Player.can_award(_msg)`(`09_Net.js:445`)。 +- 备注:🐛 `06_Player.js` **未定义** `Player.prototype.can_award`(全文无此方法)。推送一旦到达,`C_Player.can_award` 为 undefined,调用即抛 TypeError。当前为 bug,不可当正常协议使用。 + +--- + +## 支付 / 充值 / 资产 + +### `get_paylist` · route=agent · C→S 请求 + S→C 响应 +- 场景:拉取支付项列表。 +- 请求字段(C→S):`agentid`。 +- 响应字段(S→C):`paylist`(支付项数组) → `GameData.payList`,并打开支付界面(09_Net.js:567)。 +- 源码:发送 `09_Net.js:562`;接收 `09_Net.js:567`。 +- 备注:— + +### `pay_succ` · route=agent · C→S 请求(经 HTTP) +- 场景:支付成功后通知服务器入账。 +- 请求字段(C→S):`agentid`、`playerid`、`channelid`、`productid`、`payid`、`amount`、`money`、`paytype`(05_Func.js:2240-2254 构造)。 +- 响应字段(S→C):无显式 WS 回包;资产变化通过 `update_bean`/`update_roomcard` 推送(充房卡场景前端还会本地 `UpdateRoomcard`,05_Func.js:2263)。 +- 源码:WS 发送函数 `Net.Send_pay_succ`(`09_Net.js:573`) **被注释**(05_Func.js:2249、:3199);实际改用 `Func.AjaxHttp` 以同样的 `{app,route:agent,rpc:pay_succ,data}` 信封走 **HTTP** 提交(05_Func.js:2250-2255、:3200-3205)。 +- 备注:⚠️ WebSocket 通道当前不发;该包以 HTTP POST 形式上行,rpc 名仍为 `pay_succ`,待后端确认接收端一致。 + +### `topup_card` · route=agent · C→S 请求 + S→C 响应 +- 场景:充值卡兑换。 +- 请求字段(C→S):`agentid`、`playerid`、`cardno`(卡号)。 +- 响应字段(S→C):`Desk.topup_card` 为空实现(07_Desk.js:1365),资产变化经 update_bean/update_roomcard 推送。 +- 源码:发送 `09_Net.js:863`;接收 `09_Net.js:867`。 +- 备注:— + +### `giveCoin` · route=agent · C→S 请求 + S→C 响应 +- 场景:仓库面板向他人转账豆豆/金币。 +- 请求字段(C→S):`agentid`、`playerid`(转出)、`toPlayerid`(转入目标ID)、`gameid`、`count`(数量)、`password`(仓库密码)(11_GameUI.js:2132-2139)。 +- 响应字段(S→C):`state`(0成功)、`star2`(转出后**仓库星星**数 → `setWareHouseStarCOunt`,**非豆豆**);失败 `showerror`/`error`(07_Desk.js:1381)。 +- 源码:发送 `09_Net.js:876`;接收 `09_Net.js:879`→`Desk.giveCoin`。 +- 备注:响应 `star2` 为仓库星星数,注意区别于豆豆余额。 + +--- + +## 仓库 / 星星 / 魅力 + +### `set_bankpwd` · route=agent · C→S 请求 + S→C 响应 +- 场景:设置仓库密码。 +- 请求字段(C→S):`agentid`、`playerid`、`unionid`、`password`。 +- 响应字段(S→C):`state`(0成功)、`password`;失败 `showerror`/`error`(07_Desk.js:1228)。 +- 源码:发送 `09_Net.js:697`;接收 `09_Net.js:701`→`Desk.set_bankpwd`。 +- 备注:— + +### `change_star` · route=agent · C→S 请求 + S→C 响应 +- 场景:仓库存/取(豆豆 ↔ 仓库星星)。 +- 请求字段(C→S):`agentid`、`playerid`、`mode`(0存入/1取出,源码 `safeInputType-1`)、`password`(仓库密码,字符串)、`count`(数量)(11_GameUI.js:2065-2069 调用点齐全携带此 5 字段)。 +- 响应字段(S→C):`state`(0成功)、`star1`(更新后豆豆余额 → `update_bean2`)、`star2`(更新后仓库星星数 → `setWareHouseStarCOunt`)、`msg`(可选提示)、`count`(可选,回填安全输入);失败 `showerror`/`error`(07_Desk.js:1238)。 +- 源码:发送 `09_Net.js:707`;接收 `09_Net.js:711`→`Desk.change_star`。 +- 备注:原审计疑虑"`mode`/`password` 未见"——经核对仓库存/取调用点(11_GameUI.js:2068-2069)**确含** `mode` 与 `password`,文档正确,疑虑解除。审计提到的"agentid/playerid/toPlayerid/gameid/count"实为相邻的 `giveCoin` 转账包(11_GameUI.js:2132-2139),并非本 rpc。 + +### `update_charm` · route=agent · S→C 推送 +- 场景:座位魅力值更新。 +- 请求字段:`Net.Send_update_charm`(`09_Net.js:727`) 已定义但**无任何调用点**;实际只作服务器→客户端推送。 +- 推送字段(S→C):`seatlist`:[ {`seat`(int), `charm`(number)} ](07_Desk.js:1260 遍历 setCharm)。 +- 源码:接收 `09_Net.js:731`→`Desk.update_charm`(`07_Desk.js:1260`)。 +- 备注:— + +### `setAllCharm` · route=agent · C→S 请求 + S→C 响应 +- 场景:批量设置总魅力。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`value`(11_GameUI.js:1269-1275)。 +- 响应字段(S→C):无显式业务字段,`Desk.setAllCharm` 仅本地存储并提示成功(07_Desk.js:1321)。 +- 源码:发送 `09_Net.js:779`;接收 `09_Net.js:783`→`Desk.setAllCharm`。 +- 备注:— + +--- + +## 邀请码 / 绑定 + +### `binding_invitecode` · route=agent · C→S 请求 + S→C 响应 +- 场景:绑定邀请码。 +- 请求字段(C→S):`agentid`、`playerid`、`invitecode`(11_GameUI.js:1367-1369)。 +- 响应字段(S→C):`state`(0成功时写入 `invitecode`)、`invitecode`、`error`(提示文案)(06_Player.js:251)。 +- 源码:发送 `09_Net.js:587`;接收 `09_Net.js:592`→`C_Player.binding_invitecode`。 +- 备注:— + +### `get_player_invitecode` · route=agent · C→S 请求 + S→C 响应 +- 场景:获取自己的邀请码(打开绑定界面)。 +- 请求字段(C→S):`agentid`、`playerid`、`unionid`、`openid`(11_GameUI.js:1378-1382)。 +- 响应字段(S→C):`invitecode`(string) → `C_Player.setInvitecod` 并打开绑定界面(09_Net.js:607)。 +- 源码:发送 `09_Net.js:603`;接收 `09_Net.js:607`→`Net.get_player_invitecode`。 +- 备注:修正原文档——请求字段为 `agentid/playerid/unionid/openid`,**无 `gameid`**,新增 `unionid`/`openid`(11_GameUI.js:1378)。 + +--- + +## VIP 管理 / 黑白名单 + +### `optBanList` · route=agent · C→S 请求 + S→C 响应 +- 场景:黑名单查看/添加/移除(多入口)。 +- 请求字段(C→S):随入口不同,公共字段 `agentid`、`playerid`、`type`: + - `type=1` 查看黑名单列表(agentid/playerid/type,11_GameUI.js:2401-2405) + - `type=3` 按 ID 添加(agentid/playerid/optId/type,11_GameUI.js:2376-2381) + - `type=4` 按 ID 移除(agentid/playerid/optId/type,11_GameUI.js:2389-2394 及列表项删除 2453-2458) + - `type=6` 一键全部添加(agentid/playerid/type,11_GameUI.js:1262-1267) + - 另有带 `breakRoom`(0/1) 与 `gameid` 的 `type=3` 添加入口(agentid/gameid/playerid/optId/breakRoom/type,11_GameUI.js:2417-2428;breakRoom 由 `GameData.blackList.breakRoom` 决定,开关在 case 3260) + - 服务器响应中亦见 `type=5`(另一种列表返回,07_Desk.js:1299) +- 响应字段(S→C):`type`(1/3/4/5/6)、`banList`(黑名单数组)、`message`(可选提示)(07_Desk.js:1289)。 +- 源码:发送 `09_Net.js:762`;接收 `09_Net.js:766`→`Desk.optBanList`。 +- 备注:— + +### `getPlayerWhiteList` · route=agent · C→S 请求 + S→C 响应 +- 场景:获取白名单。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`。 +- 响应字段(S→C):`whiteList`(数组) → `GameData.whiteList.data`(07_Desk.js:1412)。 +- 源码:发送 `09_Net.js:882`;接收 `09_Net.js:886`→`Desk.getPlayerWhiteList`。 +- 备注:— + +### `optWhiteList` · route=agent · C→S 请求 + S→C 响应 +- 场景:白名单添加/修改/删除。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`shortcode`、`mode`、`userid`(目标ID): + - `mode=1` 添加/修改,并带 `value`(设置的魅力值) + - `mode=2` 删除(仅 `userid`) +- 响应字段(S→C):`whiteList`(更新后数组)、`mode`(可选)、`message`(可选提示)(07_Desk.js:1398)。 +- 源码:发送 `09_Net.js:890`;接收 `09_Net.js:894`→`Desk.optWhiteList`。 +- 备注:— + +### `setVipForbidSelect` · route=agent · C→S 请求 + S→C 响应 +- 场景:VIP 房禁止玩家选桌开关。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`forbidSelect`(1开/0关,11_GameUI.js:2631-2639)。 +- 响应字段(S→C):`state`(0成功)、`forbidSelect`(1开/0关);失败 `error`(07_Desk.js:1348)。 +- 源码:发送 `09_Net.js:855`;接收 `09_Net.js:859`→`Desk.setVipForbidSelect`。 +- 备注:— + +--- + +## 其它 agent 协议 + +### `submit_opinion` · route=agent · C→S 请求 + S→C 响应 +- 场景:提交反馈/意见。 +- 请求字段(C→S):`agentid`、`playerid`、`gameid`、`content`。 +- 响应字段(S→C):`state`(0成功)(07_Desk.js:1100)。 +- 源码:发送 `09_Net.js:547`;接收 `09_Net.js:551`→`Desk.submit_opinion`。 +- 备注:— + +### `submit_location` · route=agent · C→S 请求 + S→C 响应 +- 场景:提交定位信息(仅非大厅环境发送)。 +- 请求字段(C→S):`agentid`、`playerid`、`info`(定位对象)。 +- 响应字段(S→C):经 `Game.submit_location` 处理(09_Net.js:583)。 +- 源码:发送 `09_Net.js:577`(`ConstVal.isGameHall` 为 true 时不发);接收 `09_Net.js:583`。 +- 备注:— + +### `submit_phoneinfo` · route=agent · C→S 请求(无响应处理) +- 场景:提交手机/通讯录信息(仅非大厅环境发送)。 +- 请求字段(C→S):`agentid`、`playerid`、`info`{ `phoneInfo`(手机信息), `addrBook`(通讯录) }。 +- 响应字段(S→C):`Net.submit_phoneinfo` 接收函数为空实现(09_Net.js:722)。 +- 源码:发送 `09_Net.js:717`;接收 `09_Net.js:722`。 +- 备注:— + +### `submit_error` / `submit_log` · route=agent · C→S 上行(硬编码包) +- 场景:上报前端异常/日志。异常捕获时(12_Logic.js:270)、获取 HTML 失败(12_Logic.js:1990/2009/2014)、收到踢下线包(07_Desk.js:963)等处调用。 +- 请求字段(C→S):`packet`(出错数据包字符串)、`msg`(错误/堆栈信息)、`playerid`、`agentid`、`gameid`(09_Net.js:66-72)。 +- 响应字段(S→C):服务器若回 rpc `"submit_error"`,前端在分发处直接 `return` 忽略(12_Logic.js:235)。 +- 源码:发送 `Net.submit_error`(`09_Net.js:55`) / `Net.submit_log`(`09_Net.js:81`)。 +- 备注:均为**硬编码** `route:"agent", rpc:"submit_error"`,不经 `Send_*`/RpcList。注意 `Net.submit_log`(`09_Net.js:81`) 实际发出的 rpc 同样是 `"submit_error"`(与 submit_error 同包结构)。两者带去重:`submit_error` 对相同 `msg` 只发一次(09_Net.js:57-61),`submit_log` 不去重。 + +### `kick_server` · route=agent · C→S 请求 + S→C 推送 +- 场景:管理端踢出玩家 / 被踢下线弹窗。 +- 请求字段(C→S):`agentid` 等(构造点未集中定位)。 +- 推送字段(S→C):`msg`(踢出提示) → `GameUI.OpenKick`(07_Desk.js:1108)。 +- 源码:发送 `09_Net.js:555`;接收 `09_Net.js:558`→`Desk.kick_server`。 +- 备注:⚠️ 请求字段待补。 + +### `broadcast` · route=agent · C→S 请求 + S→C 推送 +- 场景:广播消息/滚动公告(主要为服务器推送)。 +- 请求字段(C→S):`agentid` 等(`Send_broadcast` 存在,09_Net.js:530)。 +- 推送字段(S→C):`msgtype`(0消息框/1滚动公告,可选,缺省 0)、`msgcontent`(内容)(07_Desk.js:1112)。 +- 源码:发送 `09_Net.js:530`;接收 `09_Net.js:534`→`Desk.broadcast`。 +- 备注:— + +### `connect_agentserver` · route=agent · 双向(切服) +- 场景:切换到大厅服务器。 +- 请求字段(C→S):`Send_connect_agentserver`(`09_Net.js:515`) 透传 `_data`(切服时使用)。 +- 推送字段(S→C):`agentserver`(新大厅服地址)、`opt`(切换原因,如 `other_break_room`/`free_room`)(09_Net.js:518)。 +- 源码:发送 `09_Net.js:515`;接收 `09_Net.js:518`→`Net.connect_agentserver`(关闭当前连接并重连新地址)。 +- 备注:— + +### `playerBehavior` · route=agent(实际走 HTTP GET) +- 场景:玩家行为埋点。 +- 请求字段:原 WS 路径(agentid/gameid/playerid/tag)**被注释**(09_Net.js:799-806),实际改走独立 HTTP GET 上报 `http://test3.1888day.com/api/gamedo/gamedo?agentid=&gameid=&playerid=&tag=`(09_Net.js:807-815)。 +- 响应字段(S→C):HTTP 回调 `playerBehavior_Succ`/`_Fail`(09_Net.js:818/822);WS 接收函数 `Net.playerBehavior`(`09_Net.js:826`) 当前无触发。 +- 源码:发送 `09_Net.js:797`(`Send_playerBehavior`)。 +- 备注:**非 WebSocket 协议**,RpcList 中虽有定义,实际不走 agent 路由。 + +--- + +## 仅接收的 agent 推送(无对应主动请求) + +| rpc | 推送 data 字段 | 说明 | 接收源码 | +|-----|---------------|------|---------| +| `update_roomcard` | `roomcard`、`text`(可选) | 房卡变化(仅 `change` 未定义时更新,09_Net.js:383→06_Player.js:141) | 09_Net.js:383 | +| `update_bean` | `bean`、`change`(可选)、`seat`(可选)、`type`(可选)、`text` | 豆豆变化(09_Net.js:598→Desk.update_bean,07_Desk.js:1201) | 09_Net.js:598 | +| `can_award` | 任务可领取标记 | 🐛 接收即抛异常,见上文 任务系统 章 | 09_Net.js:444 | +| `kick_offline` | `fromOther`(可选)、`gameid`(可选) | 被踢下线,弹 OpenKick;同时本地 `Net.submit_error` 上报"收到踢下线包"(07_Desk.js:945-963) | 09_Net.js:448 | +| `show_message` | `msg`、`time` | 通用消息提示 → `GameUI.OpenTips`(07_Desk.js:1173) | 09_Net.js:656 | diff --git a/docs/protocol/03-协议-room路由.md b/docs/protocol/03-协议-room路由.md new file mode 100644 index 0000000..067568d --- /dev/null +++ b/docs/protocol/03-协议-room路由.md @@ -0,0 +1,306 @@ +# 03 · 平台层协议 — room 路由(房间服务器) + +> 信封:`{ app:"youle", route:"room", rpc:"<下列名称>", data:{...} }` +> 本篇覆盖 **route=room 的全部平台层数据包**(房间生命周期 / 解散投票 / 开局 / 房内社交 / 服务器切换)。不含子游戏对局内协议(见 05 章)。 +> 房间内操作通用请求字段(除特别说明外,C→S 请求恒含这四项):`agentid`、`gameid`、`playerid`、`roomcode`。 +> 「响应/推送」均指服务器返回内层 `data` 字段。座位号 `seat` 通常为 0 起整数;`Logic.ChangeToStatus(C_Player.seat, seat)` 把绝对座位转为以自己为基准的相对视角。 +> +> **图例**:🐛 源码 bug;⚠️ 待后端确认;— 无特别说明。 +> +> **条目格式**: +> `rpc名` · route=room · <方向> — 场景;请求字段(C→S);响应/推送字段(S→C);源码 发送/接收;备注。 +> +> **路由说明(交叉引用)**:发送(C→S)走 `RouteList.room`,但少数推送虽在 room 场景内消费,其上行请求实际走 `agent` 路由——下文逐条标注;这类项归档于 [02 · agent 路由](./02-协议-agent路由.md),此处仅记录其在房内的接收语义。 + +--- + +## 房间生命周期 + +### self_break_room · route=room · C→S 请求 + S→C 响应 +- 场景:房主在**未开局**前主动解散房间。 +- 请求字段(C→S):仅通用四字段。 +- 响应字段(S→C):`roomcode`(可选,用于从本地"我的房间"列表移除)。处理:清空牌桌、回大厅。 +- 源码:发送 `09_Net.js:266`(`Send_self_break_room`)/接收 `09_Net.js:271` → `07_Desk.js:707`(`Desk.self_break_room`)。 +- 备注:— + +### other_break_room · route=room · S→C 推送 +- 场景:他人(房主)解散房间,推送给房内其余玩家。 +- 请求字段(C→S):无(纯推送)。 +- 响应字段(S→C):无业务字段;触发 `Func.exitRoom()`、清桌、回大厅、提示"房主已解散房间!"。 +- 源码:接收 `09_Net.js:277` → `07_Desk.js:721`(`Desk.other_break_room`)。 +- 备注:常伴随 **connect_agentserver** 切服包(`data.opt == other_break_room`,见下文)。 + +### self_exit_room · route=room · C→S 请求 + S→C 响应 +- 场景:自己在**未开局**前退出房间。 +- 请求字段(C→S):仅通用四字段(发送处见 `08_Utl_Output.js:993`)。 +- 响应字段(S→C):`isowner`(可选,==1 且非无限局时把房间加回本地列表), `seat`(可选,传给 `Game_Modify.myExitRoom`), `roomcode`(可选)。 +- 源码:发送 `09_Net.js:289`(`Send_self_exit_room`)/接收 `09_Net.js:294` → `07_Desk.js:753`(`Desk.self_exit_room`)。 +- 备注:— + +### other_exit_room · route=room · S→C 推送 +- 场景:其他玩家(未开局)退出房间。 +- 请求字段(C→S):无(纯推送)。 +- 响应字段(S→C):`seat`(离开者座位)。处理:清该座位、`playercnt--`;若 `seat==0` 且非无限局,提示房主已离开。 +- 源码:接收 `09_Net.js:301` → `07_Desk.js:785`(`Desk.other_exit_room`)。 +- 备注:— + +### player_prepare · route=room · C→S 请求 + S→C 推送 +- 场景:玩家点击准备(`needprepare==1` 的房间)。 +- 请求字段(C→S):仅通用四字段。 +- 响应/推送字段(S→C):`seat`(准备者座位), `deskwar`(可选;为真表示满足开战条件 → `HideStartScene` + `Game_Modify.StartWar`)。推送给房内所有人。 +- 源码:发送 `09_Net.js:629`(`Send_player_prepare`)/接收 `09_Net.js:632` → `07_Desk.js:1126`(`Desk.player_prepare`)。 +- 备注:— + +### change_room · route=room · C→S 请求 → S→C 响应(change_seat) · 跨座换桌 +- 场景:玩家请求换座 / 换桌。 +- 请求字段(C→S):仅通用四字段(**不带目标座位**,由服务器决定换到哪个空位)。发送处 `11_GameUI.js:1945`、`08_Utl_Output.js:1006`。 +- 响应字段(S→C):rpc 名改为 **`change_seat`**,`data`:`seat1`, `seat2`(两个互换的座位号)。处理:交换两座 Desk 信息,若自己在其中则 `C_Player.SetSeat` 更新。 +- 源码:发送 `09_Net.js:678`(`Send_change_room`)/接收 `09_Net.js:682`(`Net.change_seat`) → `07_Desk.js:178`(`Desk.change_seat`)。 +- 备注:上行 rpc=`change_room`,下行 rpc=`change_seat`,二者成对。 + +### share_room · route=room · C→S 请求 + S→C 响应 +- 场景:把房间分享到世界房列表。 +- 请求字段(C→S):通用四字段;可选 `roomlist`、`roomtype`、`shareType`(高级房分享时携带,见 `11_GameUI.js:1734`/`11_GameUI.js:1891`)。 +- 响应字段(S→C):无业务字段;提示"已成功分享至平台!"。 +- 源码:发送 `09_Net.js:635`(`Send_share_room`)/接收 `09_Net.js:638` → `07_Desk.js:1152`(`Desk.share_room`)。 +- 备注:— + +--- + +## 进房推送(他人视角) + +### other_join_room · route=room · S→C 推送 +- 场景:其他玩家加入当前房间。 +- 请求字段(C→S):无(纯推送;自己进房用 `self_join_room`,走 agent 路由,见 02 章)。 +- 响应/推送字段(S→C):`seat`(新玩家座位) + 该玩家完整对象(直接传给 `Player.SetDeskInfo`,见 [04 · Player 座位对象](./04-数据结构.md#player-座位对象)): + `playerid`, `nickname`, `avatar`, `sex`, `ip`, `onstate`, `bean`, `charm`, `sign` 等;另含 `needprepare`(可选), `deskwar`(可选;为真直接走 `Desk.makewar` 开战)。 +- 源码:接收 `09_Net.js:281` → `07_Desk.js:729`(`Desk.other_join_room`);字段写入见 `06_Player.js:263`(`Player.SetDeskInfo`)。 +- 备注:⚠️ `charm`/`sign` 是否每次必含、其余字段是否完整以 `Player.SetDeskInfo` 实际读取为准,详见 [04 章](./04-数据结构.md#player-座位对象)。 + +### other_offline · route=room · S→C 推送 +- 场景:其他玩家离线。 +- 请求字段(C→S):无(纯推送)。 +- 响应字段(S→C):`seat`。处理:该座 `onstate=1`,刷新 UI。 +- 源码:接收 `09_Net.js:390` → `07_Desk.js:887`(`Desk.other_offline`)。 +- 备注:— + +### other_online · route=room · S→C 推送 +- 场景:其他玩家重新上线。 +- 请求字段(C→S):无(纯推送)。 +- 响应字段(S→C):`seat`, `ip`。处理:该座 `onstate=0`、更新 ip。 +- 源码:接收 `09_Net.js:395` → `07_Desk.js:893`(`Desk.other_online`)。 +- 备注:— + +--- + +## 解散投票(开局后) + +> 流程:某玩家 apply → 全员收到 `other_apply_free_room`(带各座状态与倒计时)→ +> 各玩家 agree/refuse → 最终 `free_room` 广播结果。 + +### self_apply_free_room · route=room · C→S 请求 + S→C 响应 +- 场景:自己申请解散房间。 +- 请求字段(C→S):仅通用四字段。 +- 响应字段(S→C):`agreefree` { `state`:[各座同意状态数组], `countdown`:倒计时 }。 +- 源码:发送 `09_Net.js:308`/接收 `09_Net.js:313` → `07_Desk.js:804`(`Desk.self_apply_free_room`)。 +- 备注:— + +### other_apply_free_room · route=room · S→C 推送 +- 场景:他人申请解散。 +- 请求字段(C→S):无(纯推送)。 +- 响应字段(S→C):`seat`(申请者), `agreefree` { `state`[], `countdown` }。 +- 源码:接收 `09_Net.js:319` → `07_Desk.js:817`(`Desk.other_apply_free_room`)。 +- 备注:— + +### self_agree_free_room · route=room · C→S 请求 + S→C 响应 +- 场景:自己同意解散。 +- 请求字段(C→S):仅通用四字段。 +- 响应字段(S→C):无业务字段;本地把自己加入同意列表、刷新投票 UI。 +- 源码:发送 `09_Net.js:323`/接收 `09_Net.js:328` → `07_Desk.js:829`(`Desk.self_agree_free_room`)。 +- 备注:— + +### other_agree_free_room · route=room · S→C 推送 +- 场景:他人同意解散。 +- 请求字段(C→S):无(纯推送)。 +- 响应字段(S→C):`seat`(同意者)。 +- 源码:接收 `09_Net.js:333` → `07_Desk.js:836`(`Desk.other_agree_free_room`)。 +- 备注:— + +### self_refuse_free_room · route=room · C→S 请求 + S→C 响应 +- 场景:自己拒绝解散。 +- 请求字段(C→S):仅通用四字段。 +- 响应字段(S→C):无业务字段;清空同意列表、投票结果置不通过、弹"已拒绝"结果。 +- 源码:发送 `09_Net.js:338`/接收 `09_Net.js:343` → `07_Desk.js:842`(`Desk.self_refuse_free_room`)。 +- 备注:— + +### other_refuse_free_room · route=room · S→C 推送 +- 场景:他人拒绝解散(任一人拒绝即否决本轮)。 +- 请求字段(C→S):无(纯推送)。 +- 响应字段(S→C):`seat`(拒绝者)。 +- 源码:接收 `09_Net.js:348` → `07_Desk.js:853`(`Desk.other_refuse_free_room`)。 +- 备注:— + +### free_room · route=room · S→C 推送 +- 场景:解散投票最终结果。 +- 请求字段(C→S):无(纯推送)。 +- 响应字段(S→C): + + | 字段 | 说明 | + |------|------| + | `freeNow` | true=立即解散;false=投票通过待确认 | + | `deskfree` | 解散结算信息对象(可选) | + | `roomcard` | 解散后房卡数(可选,写回 `C_Player.setRoomcard`) | + | `tips` | 提示文本(`freeNow==true` 时) | + | `time` | 提示显示时长(`freeNow==true` 时) | + | `seats` | 投票通过的座位数组(`freeNow==false` 时传给 `OpenApplyResult`) | + +- 源码:接收 `09_Net.js:353` → `07_Desk.js:864`(`Desk.free_room`)。 +- 备注:可伴随 **connect_agentserver** 切服包(`data.opt == free_room`,见下文)。 + +### beanroom_surrender · route=room · C→S 请求 + S→C 响应 +- 场景:豆豆房(金币房)投降。 +- 请求字段(C→S):通用四字段 + `count`(投降数量 = `GameData.surrendCount`,发送处 `11_GameUI.js:1245`)。 +- 响应字段(S→C):`state`(0=成功 → `Game_Modify.onSurrender(_msg)`); 失败时 `showerror`(==1 则弹) / `error`(错误文本)。 +- 源码:发送 `09_Net.js:613`(`Send_beanroom_surrender`)/接收 `09_Net.js:617`(`Net.beanroom_surrender`,逻辑直接在 Net 内处理)。 +- 备注:⚠️ 未见成对的 `other_xxx` 推送,是否向房内其他玩家广播他人投降待后端确认。 + +--- + +## 开局 + +### self_makewar · route=room · C→S 请求 → S→C 响应(self_makewar) +- 场景:房主主动开局。 +- 请求字段(C→S):仅通用四字段。 +- 响应字段(S→C):rpc=`self_makewar`,无业务字段;触发 `Desk.self_makewar` → `Game_Modify.StartWar(_msg)`。 +- 源码:发送 `09_Net.js:470`(`Send_self_makewar`)/接收 `09_Net.js:475` → `07_Desk.js:987`(`Desk.self_makewar`)。 +- 备注:— + +### other_makewar · route=room · S→C 推送 +- 场景:开局广播(房主开局或满员自动开局)给房内其他玩家。 +- 请求字段(C→S):无(纯推送)。 +- 响应字段(S→C):开局信息对象 → `Desk.makewar` → `Game_Modify.StartWar(msg)`。 +- 源码:接收 `09_Net.js:480`(`Net.other_makewar`) → `07_Desk.js:1020`(`Desk.makewar`)。 +- 备注:实际发牌等对局数据在此之后通过**子游戏内协议**下发(见 05 章)。 + +--- + +## 房间内社交 + +### send_text · route=room · C→S 请求 + S→C 推送 · 文字聊天 +- 场景:文字聊天 / 全服公告 / 预定义常用语。 +- 请求字段(C→S):通用四字段 + `text`(内容;点常用语时为 `Game_Config.Info.TextContent[spid-206]` 文本) + `type`(0=普通 / 1=全服公告;按是否勾选公告设 0/1) + `info`(可选;**有 info 时 `type` 改为 2**)。发送处 `11_GameUI.js:1170`(输入框)、`11_GameUI.js:2717`(常用语)。 +- 推送字段(S→C):`type`(0=普通 / 1=全服公告 / 2=机器人 / 3=预定义文字), `text`(内容;**type==3 时为 `Game_Config.Info.TextContent` 的 1 基索引**,客户端按 `(idx-1)%len` 取文本), `seat`(发送者座位,type≠1 时使用), `info`(type==2 时附加)。 +- 源码:发送 `09_Net.js:400`(`Send_send_text`)/接收 `09_Net.js:404` → `07_Desk.js:900`(`Desk.send_text`)。 +- 备注:百人场(`vipInfinite`)只处理 type==1 公告分支。 + +### receive_chat · route=room · S→C 推送(⚠️) +- 场景:聊天推送的另一可能 rpc 名(与 `send_text` 成对)。 +- 请求字段(C→S):无。 +- 推送字段(S→C):未知(疑似同 `send_text` 推送结构)。 +- 源码:常量 `02_Const.js:38`(`RpcList.receive_chat`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。 +- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器聊天推送究竟用 `send_text` 还是 `receive_chat` 待后端确认。 + +### send_voice · route=room · C→S 请求 + S→C 推送 · 语音 +- 场景:发送语音消息。 +- 请求字段(C→S):通用四字段 + `voiceurl`(已上传音频地址) + `time`(时长) + `info`(可选) + `type`(可选,有 info 时为 2)。发送处 `05_Func.js:1714`、`05_Func.js:2984`。 + > 语音需先上传得到 `voiceurl` 再随包发送,客户端不直接传音频二进制。 +- 推送字段(S→C):`type`(0=普通 / 2=机器人), `seat`(发送座位), `voiceurl`, `time`, `info`(type==2 时)。 +- 源码:发送 `09_Net.js:492`(`Send_send_voice`)/接收 `09_Net.js:495` → `07_Desk.js:1073`(`Desk.send_voice`)。 +- 备注:— + +### play_voice · route=room · S→C 推送(⚠️) +- 场景:与 `send_voice` 成对,疑为语音**播放**推送。 +- 请求字段(C→S):无(`send_voice` 上行,`play_voice` 疑为下行播放推送)。 +- 推送字段(S→C):未知。 +- 源码:常量 `02_Const.js:36` 与 `02_Const.js:54`(重复定义 `RpcList.play_voice`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**(注:`GameUI.play_voice` 是本地播放方法,非网络处理器)。 +- 备注:⚠️ `02_Const.js` 已定义但客户端无网络处理函数;服务器是否下发 `play_voice` 待后端确认。 + +### send_gift · route=room · C→S 请求 + S→C 推送 · 互动/送礼 +- 场景:向指定座位送互动礼物。 +- 请求字段(C→S):通用四字段 + `giftid`(=`spid_up - 255`,按钮精灵号算出,发送处 `11_GameUI.js:2725`) + `receiveseat`(=`GameData.InteractPlayer`) + `info`(可选) + `type`(有 info 时为 2)。 +- 推送字段(S→C):`type`(0=普通 / 2=机器人), `giftid`(客户端做 `(giftid-1)%4+1` 归一为 1~4 动画), `sendseat`(发送座位), `receiveseat`(接收座位), `info`(type==2 时)。 +- 源码:发送 `09_Net.js:484`(`Send_send_gift`)/接收 `09_Net.js:487` → `07_Desk.js:1060`(`Desk.send_gift`)。 +- 备注:— + +### other_send_gift · route=room · S→C 推送(⚠️) +- 场景:与 `send_gift` 成对,疑为他人送礼推送。 +- 请求字段(C→S):无。 +- 推送字段(S→C):未知(疑似同 `send_gift` 推送结构)。 +- 源码:常量 `02_Const.js:52`(`RpcList.other_send_gift`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。 +- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器送礼推送用 `send_gift` 还是 `other_send_gift` 待后端确认。 + +### send_phiz · route=room · C→S 请求 + S→C 推送 · 表情 +- 场景:发送表情动画。 +- 请求字段(C→S):通用四字段 + `text`(=`up_id`,表情按钮序号 1~`ConstVal.Emotion.count`,发送处 `11_GameUI.js:815`) + `info`(可选) + `type`(有 info 时为 2)。 +- 推送字段(S→C):`type`(0=普通 / 2=机器人), `text`(表情 ID,客户端按 `(text-1)%ConstVal.Emotion.src_list.length+1` 归一), `seat`(发送座位), `info`(type==2 时)。 +- 源码:发送 `09_Net.js:539`(`Send_send_phiz`)/接收 `09_Net.js:543` → `07_Desk.js:1086`(`Desk.send_phiz`)。 +- 备注:— + +### call_phone · route=room · C→S 请求 + S→C 推送 · 拨打电话 +- 场景:拨打/接听电话,置玩家 `onstate=2`(通话中)。 +- 请求字段(C→S):仅通用四字段(发送处 `06_Player.js:380` / `:388` / `:396`,对应接起/电话进来/去电三种触发)。 +- 推送字段(S→C):rpc=`call_phone`,`seat`(拨打者座位 → 该玩家 `onstate=2`)。 +- 源码:发送 `09_Net.js:452`(`Send_call_phone`)/接收 `09_Net.js:456` → `07_Desk.js:967`(`Desk.call_phone`)。 +- 备注:— + +### other_callphone · route=room · S→C 推送(⚠️) +- 场景:与 `call_phone` 成对,疑为他人拨打电话推送。 +- 请求字段(C→S):无。 +- 推送字段(S→C):未知(疑似含 `seat`)。 +- 源码:常量 `02_Const.js:45`(`RpcList.other_callphone`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。 +- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器是否用 `other_callphone` 单独推送他人拨号待后端确认(当前客户端用 `call_phone` 推送统一处理本人与他人)。 + +### hangup_phone · route=room · C→S 请求 + S→C 推送 · 挂断电话 +- 场景:挂断电话,置玩家 `onstate=0`。 +- 请求字段(C→S):仅通用四字段(发送处 `06_Player.js:372`)。 +- 推送字段(S→C):rpc=`hangup_phone`,`seat`(挂断者座位 → 该玩家 `onstate=0`)。 +- 源码:发送 `09_Net.js:461`(`Send_hangup_phone`)/接收 `09_Net.js:465` → `07_Desk.js:977`(`Desk.hangup_phone`)。 +- 备注:— + +### other_hangup · route=room · S→C 推送(⚠️) +- 场景:与 `hangup_phone` 成对,疑为他人挂断电话推送。 +- 请求字段(C→S):无。 +- 推送字段(S→C):未知(疑似含 `seat`)。 +- 源码:常量 `02_Const.js:47`(`RpcList.other_hangup`) 已定义;**客户端 `Net` / `Desk` 未注册同名处理函数**。 +- 备注:⚠️ `02_Const.js` 已定义但客户端无处理函数;服务器是否用 `other_hangup` 单独推送待后端确认。 + +--- + +## 房内可收的其它推送(接收语义在 room 场景,发送走 agent 路由) + +> 以下推送在房间内被消费,但其上行请求实际走 `agent` 路由,详细发送定义见 [02 · agent 路由](./02-协议-agent路由.md);此处仅记录房内接收语义,避免遗漏。 + +### update_bean · S→C 推送(房内他人充值场景) +- 场景:房间内**他人**充值豆豆/金币,刷新该座余额。 +- 推送字段(S→C):`seat`(充值者座位;无 `seat` 则为自己充值,走 `C_Player.update_bean`), `bean`(新余额), `type`(==6 时播放金币音效 `Logic.playCoinMp3()`);另含 `change`(存在时为大厅购买分支,不在房内座位场景)。 +- 源码:接收 `09_Net.js:598`(`Net.update_bean`) → `07_Desk.js:1201`(`Desk.update_bean`)。 +- 备注:与 [02 章的 update_bean](./02-协议-agent路由.md) 为**同名推送**,此处侧重"房间内座位刷新"分支(`seat` 已定义时)。 + +### update_charm · S→C 推送(房内座位魅力更新) +- 场景:批量更新房内座位魅力值。 +- 推送字段(S→C):`seatlist`:[ { `seat`, `charm` }, ... ],逐项写入对应座位 `Player.setCharm`。 +- 源码:接收 `09_Net.js:731`(`Net.update_charm`) → `07_Desk.js:1260`(`Desk.update_charm`)。 +- 备注:⚠️ 交叉引用——**发送走 agent 路由**(`09_Net.js:727` `Send_update_charm` → `RouteList.agent`),归档于 02 章;本篇仅记录房内接收。 + +### broadcast · S→C 推送(房内跑马灯 / 弹窗) +- 场景:房内可收到的全局广播(跑马灯或顶部弹窗)。 +- 推送字段(S→C):`msgtype`(0=顶部即时弹窗 `ShowiMessage` / 1=跑马灯 `addBroadcast`), `msgcontent`(内容文本)。 +- 源码:接收 `09_Net.js:534`(`Net.broadcast`) → `07_Desk.js:1112`(`Desk.broadcast`)。 +- 备注:交叉引用——**发送走 agent 路由**(`09_Net.js:530` `Send_broadcast` → `RouteList.agent`),归档于 02 章;本篇仅记录房内接收。 + +--- + +## 服务器切换(room 侧) + +### connect_roomserver · route=room · C→S 请求 + S→C 推送 · 切到房间服 +- 场景:从大厅服切换到房间服务器。 +- 请求字段(C→S):由 `Send_connect_roomserver` 发起(走 room 路由)。 +- 推送字段(S→C):`data.roomserver`(新房间服地址)。处理:置 `GameData.ConnectType=true`、`GameData.ConnectRpc=connect_roomserver`、`GameData.Server=data.roomserver`,关闭当前连接并用新地址重连。 +- 源码:发送 `09_Net.js:500`(`Send_connect_roomserver`)/接收 `09_Net.js:503`(`Net.connect_roomserver`)。 +- 备注:— + +### connect_agentserver · S→C 推送(切回大厅服;上行走 agent 路由) +- 场景:解散/退房后从房间服切回大厅服(agent)。 +- 推送字段(S→C):`data.opt`(切服原因,值为 `other_break_room` 或 `free_room`;命中其一时 `GameUI.StartLoad()`), `data.agentserver`(新大厅服地址)。处理:置 `GameData.ConnectRpc=connect_agentserver`、`GameData.Server=data.agentserver`,关连接重连。 +- 源码:接收 `09_Net.js:518`(`Net.connect_agentserver`);上行 `09_Net.js:515`(`Send_connect_agentserver` → `RouteList.agent`)。 +- 备注:交叉引用——**上行请求走 agent 路由**,归档于 02 章;本篇记录其作为 `other_break_room`/`free_room` 后续切服推送的语义(原文档"服务器切换"仅写了 `connect_roomserver`,此处补全 `connect_agentserver`)。 diff --git a/docs/protocol/04-数据结构.md b/docs/protocol/04-数据结构.md new file mode 100644 index 0000000..06b6e17 --- /dev/null +++ b/docs/protocol/04-数据结构.md @@ -0,0 +1,313 @@ +# 04 · 核心数据结构 + +> 这些结构是协议 `data` 的承载体,也是双向数据包共用的数据模型。新前端可据此定义自己的数据模型,字段名须与协议一致。 +> +> 本篇负责**壳框架核心数据结构**:`C_Player`、`Player(seat)`、`Desk`、`player_login` 响应、`roomtype` 配置、`GameData` 相关字段。 +> **不含子游戏对局态结构**:`deskinfo` 内部结构由各子游戏定义,壳框架只把它当作"子游戏对局快照"原样透传给 `Game_Modify.Reconnect / DeskInfo`,用于断线重连(详见 05 章)。 + +--- + +## C_Player(本地玩家对象) + +来源:`06_Player.js` `function Player(seat)`(`06_Player.js:1`)。 +全局唯一的本地玩家实例 `C_Player = new Player(-1)`(`12_Logic.js:480`),即初始 `seat = -1`。登录与各推送会写入这些字段。 + +逐字段与构造函数初值核对(`06_Player.js:2`–`33`): + +| 字段 | 初值 | 类型 | 说明 | +|------|------|------|------| +| openid | "" | string | 微信 openid | +| playerid | -1 | int | 玩家ID | +| nickname | "" | string | 昵称 | +| avatar | "" | string | 头像URL | +| sex | 0 | int | 性别 0未知/1男/2女 | +| ip | "" | string | IP 地址 | +| province | "" | string | 省(微信) | +| city | "" | string | 市(微信) | +| roomcard | -1 | int | 房卡数量 | +| taskstate | 0 | int | 任务状态 | +| unionid | 0 | string \| number | 开放平台唯一标识。⚠️ 构造初值为 `0`(number),但 `SetWxInfo` 用服务器下发字符串赋值(`06_Player.js:81`),运行期实际为 string | +| seat | seat(参数) | int | 座位号(C_Player 为 -1=大厅,≥0=房间内) | +| score | 0 | int | 积分 | +| state | -1 | int | 解散投票状态,见下方"state 语义重载"说明 | +| status | 0 | int | 身份 0默认/1房主/2非房主 | +| canexit | 1 | int | 是否可直接退出 1是/0否,见下方说明 | +| onstate | 0 | int | 在线状态 0在线/1离线/2通话中 | +| addr | null | object \| null | 定位信息(含 province/city/errorCode) | +| invitecode | "" | string | 邀请码 | +| isStart | false | bool | 能否点击按钮开始游戏 | +| bean | 0 | int | 豆豆(游戏币 / 星星) | +| initialBean | 0 | int | 进房时豆豆初始值 | +| isprepare | 0 | int | 准备状态 0未/1已 | +| advanced | 0 | int | 是否有高级选项 0无/1有 | +| paycode | "" | string | 吱口令 | +| wareHouseStarCount | 0 | int | 仓库库存星星数 | +| bankpower | 0 | int | 是否有仓库权限 | +| bankpwd | 0 | int | 是否已设仓库密码 | +| charm | undefined | int \| undefined | 魅力值(构造时显式 `undefined`,`06_Player.js:31`) | +| sign | "" | string | 签名 | +| tel | "" | string | 绑定手机号 | + +> **注意**:构造函数中 `offline` 字段被注释掉(`06_Player.js:17`),因此 `C_Player`(及 `new Player()` 出来的座位对象)初始**不带 `offline` 字段**;`offline` 仅在调用 `Init()` 后才被赋值(见下文 Player 节)。 + +### state 语义重载(重点订正) + +`state` 默认值为 **-1**(非 0)。各取值含义以方法实际赋值为准(`06_Player.js`): + +| 取值 | 含义 | 赋值来源 | +|------|------|----------| +| -1 | 默认 / 无投票状态 | 构造函数 `06_Player.js:15`、`BreakRoom()` `06_Player.js:312` | +| 0 | 申请解散(发起方)/ 未同意 | `ApplyBreakRoom()` `06_Player.js:321` | +| 1 | 同意解散 | `AgreeBreakRoom()` `06_Player.js:327` | +| 2 | 拒绝解散 | `RefuseBreakRoom()` `06_Player.js:333` | + +⚠️ **`0` 是语义重载值**,不能绝对化为"申请": +- `ApplyBreakRoom()` 把 state 设为 `0`(申请解散); +- 但 `self_refuse_free_room` / `other_refuse_free_room` 在拒绝后会把**所有玩家** `state` **重置为 0**(`07_Desk.js:849`、`07_Desk.js:860`),此处 `0` 是"重置/默认"语义; +- `login` 重连恢复投票时,`agreefree[i]==0` 也把对应玩家 state 写成 `0`(`07_Desk.js:409`),表示"未同意"。 + +> 源码 `06_Player.js:15` 的行内注释把 `-1` 写成"申请解散",是**错误注释**;以上述四个方法的实际赋值为准。 + +### canexit 推断规则 + +`ChangeExit(v)` 设置(`06_Player.js:338`)。在 `login` / 进房 / 开战流程中: +- `isbet == 0` → `ChangeExit(1)`(可退出);`isbet != 0` → `ChangeExit(0)`(不可退出)(`07_Desk.js:381`); +- `infinite == 1`(无限局)→ 强制 `ChangeExit(1)`(始终可退出)(`07_Desk.js:386`)。 + +--- + +## Player(座位对象 / 房内其他玩家) + +来源:`06_Player.js` `function Player(seat)`。`Desk.PlayerList[]` 中每个元素都是一个 `Player`,由 `SetDeskInfo(data, bTemp)` 从服务器数据填充。其字段表与 C_Player 完全相同(同一构造函数),差异仅在于哪些字段来自服务器、哪些本地维护。 + +### ① `SetDeskInfo(_data, bTemp)` 实际读取的服务器字段(权威,`06_Player.js:262`) + +| 字段 | 类型 | 说明 | +|------|------|------| +| playerid | int | 玩家ID | +| nickname | string | 昵称 | +| avatar | string | 头像URL | +| sex | int | 性别 | +| ip | string | IP | +| onstate | int | 在线状态 0在线/1离线/2通话中 | +| bean | int | 豆豆数 | +| isprepare | int | 准备状态 0/1 | +| paycode | string | 吱口令(`_data.paycode` 有才读,否则置 "",`06_Player.js:277`) | +| charm | int | 魅力值 | +| sign | string | 签名 | + +**initialBean 的赋值依赖 `bTemp`(重点订正)**(`06_Player.js:271`): + +```js +if(!bTemp){ this.initialBean = _data.bean; } // 正常入座:用本局豆豆做初始值 +else { this.initialBean = _data.initialBean; } // 临时填充:保留原始 initialBean +``` + +- `bTemp` 缺省(`undefined`/`false`):`initialBean = _data.bean`(如 `login`/`self_join_room`/`other_join_room` 填充,调用时**不传** bTemp)。 +- `bTemp == true`:`initialBean = _data.initialBean`,即**不**用 bean 覆盖。典型场景是 **换座** `change_seat`(`07_Desk.js:183`),两座玩家通过 `getDeskInfo()` 互换数据时以 `bTemp=true` 调用,保留各自原始初值。 +- 因此原文档"无条件把 bean 复制为 initialBean"的描述是**错误**的,须按上面分支理解。 + +> `other_join_room` 推送中,座位号在外层 `data.seat`,其余玩家字段与上表同级**平铺**在 `data` 上(`07_Desk.js:730`)。 + +### ② 客户端本地维护、不来自 `SetDeskInfo` 的字段 + +由其它包写入或本地推断,新前端按本地状态处理即可: + +- `seat`:进房时分配(`SetSeat`)。 +- `status`:0默认/1房主/2非房主,由 `isowner` 或 `seat==0` 推断(`07_Desk.js:325`)。 +- `state`:解散投票 -1/0/1/2,由 apply/agree/refuse 包写入。 +- `canexit`:由 isbet/infinite 推断。 +- `offline`:0否/1离线。⚠️ **不在构造函数中**,仅 `Init(bTemp)` 会写入 `offline = 0`(`06_Player.js:53`);房内离线/上线则改 `onstate`(`07_Desk.js:889`、`07_Desk.js:895`),实际在线状态以 `onstate` 为准。 +- `tel`、`initialBean`(见上)、`isStart`、`roomcard`、`unionid`、`taskstate`、`advanced`、`bankpower`、`bankpwd`、`wareHouseStarCount`、`addr`、`invitecode`:座位对象一般用不到,仅 C_Player 维护。 + +--- + +## Desk(牌桌/房间对象) + +来源:`07_Desk.js:3`–`29` 顶部定义。表示当前房间整体状态,全部字段如下(逐字段核对,共 25 项): + +| 字段 | 初值 | 类型 | 说明 | +|------|------|------|------| +| PlayerList | [] | array\ | 座位玩家数组(`07_Desk.js:5`) | +| roomcode | "" | string | 房号(`07_Desk.js:6`) | +| stage | 0 | int | 牌桌阶段 0未开局/1已开局(`07_Desk.js:7`) | +| state | 0 | int | 解散状态 0正常/1申请解散中(`07_Desk.js:8`) | +| applyresult | -1 | int | 投票结果 -1无/0不通过/1通过(`07_Desk.js:9`) | +| AgreeList | [] | array\ | 同意解散的座位号列表(`07_Desk.js:10`) | +| agreefree | [] | array\ | 解散投票各座位 state 数组(来自 `agreefree.state`,`07_Desk.js:11`/`394`) | +| roomtype | [] | array | 房间类型配置(透传,见下) | +| warcnt | 0 | int | 开战条件(满足人数等,来自 `makewar`,`07_Desk.js:13`) | +| playercnt | 0 | int | 当前玩家总数(`07_Desk.js:14`) | +| count | 0 | int | 总局数(来自 `asetcount`,`07_Desk.js:15`) | +| deskfree | null | object \| null | 解散结算快照(来自 `free_room` 的 `deskfree`,`07_Desk.js:16`/`868`) | +| starCount | 0 | int | 投降扣除星星数(来自 `beanlimit`,见下方 ⚠️,`07_Desk.js:17`/`66`) | +| roomMode | 0 | int | 0普通场/1星星场(`07_Desk.js:18`) | +| needprepare | 0 | int | 是否需要准备(`07_Desk.js:19`) | +| myInfo | null | Player \| null | 自己的信息(`07_Desk.js:20`) | +| infinite | 0 | int | 是否无限局 0否/1是(`07_Desk.js:21`) | +| isSystem | 1 | int | 是否系统房间,初值 1(`07_Desk.js:22`) | +| shortcode | null | string \| null | 房间短号,初值 **null**(`07_Desk.js:23`/`93`) | +| videoConfig | null | object \| null | 视频房选项(`07_Desk.js:24`) | +| ownerNotice | null | string \| null | 短号房房主留言(`07_Desk.js:25`/`45`) | +| videoDes | "" | string | 视频房描述(由 `setVideoConfig` 拼接,`07_Desk.js:26`/`108`) | +| rebateNumber | 0 | int | 房间抽成数量(`07_Desk.js:27`) | +| rebateMode | 0 | int | 抽成类型/方式(`07_Desk.js:28`) | +| rebateType | 0 | int | 抽成对象——抽金币还是魅力值,⚠️ 数字取值见下(`07_Desk.js:29`) | + +> 另有方法 `Desk.setMyInfo / setRoom / Create / Init / login / create_room / self_join_room / other_join_room` 等填充逻辑,详见对应推送章节。 + +--- + +## 登录响应(Desk.login / player_login) + +`route=agent, rpc=player_login` 的内层 `data`。`Desk.login(_msg)` 解析(`07_Desk.js:212`)。 +`state==0` 为成功;字段分两组:A. 账号与资产(始终下发)、B. 房间恢复(在房 / 重连才下发)。 + +### A. 账号与资产(始终下发) + +| 字段 | 类型 | 说明 | 出处 | +|------|------|------|------| +| state | int | 0成功,非0失败 | `07_Desk.js:214` | +| playerid | int | 玩家ID(`SetMyInfo` 读取) | `07_Desk.js:268` | +| score | int | 积分(缺省补 0,`07_Desk.js:243`) | | +| bean | int | 豆豆 | `SetMyInfo` | +| roomcard | int | 房卡 | `SetMyInfo` | +| taskstate | int | 任务状态 | `SetMyInfo` | +| ip | string | 玩家IP | `SetMyInfo` | +| bankpower | int | 仓库权限 | `SetMyInfo` | +| bank | int | 仓库库存星星数(→ wareHouseStarCount) | `06_Player.js:97` | +| bankpwd | int | 是否已设仓库密码 | `06_Player.js:102` | +| charm | int | 魅力值 | `SetMyInfo` | +| sign | string | 签名 | `SetMyInfo` | +| tel | string | 绑定手机号 | `06_Player.js:109`、`07_Desk.js:228` | +| agentid | string | 代理ID(→ GameData.AgentId) | `07_Desk.js:276` | +| channelid | string | 渠道ID(→ GameData.ChannelId) | `07_Desk.js:277` | +| invitecode | string | 邀请码(可选,有才设) | `07_Desk.js:246` | +| initCard | int | 初始房卡(可选,→ GameData.initCard) | `07_Desk.js:257` | +| initBean | int | 初始豆豆(可选,→ GameData.initBean) | `07_Desk.js:263` | +| advanced | int | 是否有高级选项(可选,缺省 0) | `07_Desk.js:271` | +| **openid** | string | 微信 openid(仅 `deviceLogin` 分支读取) | `07_Desk.js:220` | +| **nickname** | string | 昵称(deviceLogin 分支) | `07_Desk.js:222` | +| **avatar** | string | 头像URL(deviceLogin 分支,作 `headimgurl`) | `07_Desk.js:221` | +| **sex** | int | 性别(deviceLogin 分支) | `07_Desk.js:223` | +| **city** | string | 城市(deviceLogin 分支) | `07_Desk.js:224` | +| **province** | string | 省份(deviceLogin 分支) | `07_Desk.js:225` | +| **unionid** | string | 开放平台ID(deviceLogin 分支) | `07_Desk.js:226` | + +> `openid/nickname/avatar/sex/city/province/unionid/tel` 仅在 `GameData.sysConfig.deviceLogin` 为真时被读取并通过 `SetWxInfo` 写入 C_Player(`07_Desk.js:215`–`230`)。非设备登录时这些信息由微信授权链路另行获取。 + +### B. 房间恢复(在房 / 断线重连时才下发) + +仅当 `_msg.data.roomcode` 存在时进入此分支(`07_Desk.js:282`),代表玩家原本就在房间内。 + +| 字段 | 类型 | 说明 | 出处 | +|------|------|------|------| +| roomcode | string | 房号 | `07_Desk.js:282` | +| roomtype | array | 房间类型配置(透传) | `07_Desk.js:284` | +| asetcount | int | 总局数(→ Desk.count) | `07_Desk.js:323` | +| isbattle | int | 0未开局/1已开局(→ Desk.stage) | `07_Desk.js:324` | +| makewar | int | 开战条件(→ Desk.warcnt) | `07_Desk.js:321` | +| seat | int | 自己的座位 | `07_Desk.js:320` | +| isowner | int | 1房主/0非房主(→ C_Player.status) | `07_Desk.js:325` | +| players | array | 房内玩家数组(按座位下标,元素为 SetDeskInfo 字段集,可含 null) | `07_Desk.js:338` | +| roommode | int | 0普通/1星星场(→ Desk.roomMode) | `07_Desk.js:295` | +| beanlimit | int | ⚠️ → `setStarCount`(投降扣除星星数),见下方说明 | `07_Desk.js:298` | +| needprepare | int | 是否需准备 | `07_Desk.js:301` | +| infinite | int | 0普通/1无限局 | `07_Desk.js:304` | +| rebateNumber | int | 抽成数量 | `07_Desk.js:307` | +| rebateMode | int | 抽成方式/类型 | `07_Desk.js:308` | +| rebateType | int | 抽成对象(⚠️ 取值见下) | `07_Desk.js:309` | +| sign | string | 签名(→ C_Player.setSign) | `07_Desk.js:310` | +| ownerNotice | string | 短号房房主留言 | `07_Desk.js:311` | +| videoConfig | object | 视频房配置 | `07_Desk.js:312` | +| shortcode | string | VIP 房短号 | `07_Desk.js:313` | +| match | object | 比赛信息(→ GameData.matchInfo) | `07_Desk.js:285` | +| matchid | string | 比赛ID(→ GameData.matchId) | `07_Desk.js:290` | +| agreefree | object | 解散投票信息 `{ state:int[], countdown }`,存在即表示恢复进行中的投票 | `07_Desk.js:389` | +| isbet | int | 0可退出/1不可退出(→ ChangeExit) | `07_Desk.js:381` | +| deskinfo | object | 子游戏对局快照,存在即触发重连(见下方 ⚠️) | `07_Desk.js:419` | + +#### ⚠️ B 组存疑/订正点 + +- **deskinfo 与 isbattle 的关系**:`login()` 仅判断 `if(_msg.data.deskinfo)`(`07_Desk.js:419`)是否存在来决定是否调用 `Game_Modify.Reconnect`,**并不**判断 `isbattle`。原文档"isbattle=1 时用于重连"是推断;准确表述应为 **"当响应含 `deskinfo` 时触发子游戏重连(服务器通常仅在对局进行中才下发该字段)"**。 +- **beanlimit 语义**:源码为 `if(_msg.data.beanlimit){ Desk.setStarCount(_msg.data.beanlimit); }`(`07_Desk.js:298`),`setStarCount` 写入 `Desk.starCount`,而 `starCount` 注释为"投降扣除星星数量"(`07_Desk.js:17`)。因此 `beanlimit` 倾向于 **"星星场投降扣除数"** 而非泛指"豆豆限制",⚠️ 确切业务含义待后端确认。 +- **rebateType 取值**:`07_Desk.js:29` 仅注释"抽金币还是魅力值",源码**无明确数字定义**。原文档标注的"0金币/1魅力值"⚠️ **待后端确认**。 +- **deskwar 不在 login 中**:`login()` 函数体内**未读取** `deskwar`。`deskwar`(是否自动开战)由 `self_join_room`(`07_Desk.js:634`)、`other_join_room`(`07_Desk.js:747`)、`player_prepare`(`07_Desk.js:1137`)读取。因此 deskwar **不属于登录响应字段**,已移至下方"创建/进入响应"表(见 ⚠️ 标注)。 + +--- + +## 房间创建 / 进入响应公共字段 + +`create_room`(`07_Desk.js:454`)/ `self_join_room`(`07_Desk.js:529`)响应内层 `data`,字段集与登录 B 组高度一致: + +| 字段 | 说明 | 备注 | +|------|------|------| +| state | 0成功,非0失败 | self_join_room 另有 99=房间不存在 | +| roomcode | 房号 | | +| seat | 自己座位 | | +| isowner | 是否房主 0/1 | | +| roomtype | 房间类型配置数组(透传) | | +| makewar | 开战条件 | | +| asetcount | 总局数 | | +| players | 房内玩家数组(join 时有) | | +| roommode / beanlimit / needprepare / infinite | 房间模式相关 | beanlimit 语义同上 ⚠️ | +| rebateNumber / rebateMode / rebateType | 抽成相关 | rebateType 取值待确认 ⚠️ | +| shortcode | 短号 | | +| match / matchid | 比赛信息 | | +| videoConfig | 视频房配置 | | +| ownerNotice / ownerNote | 房主留言 / 备注(ownerNote 仅 self_join_room,`07_Desk.js:575`) | | +| paycode | 吱口令(join 时可选,`07_Desk.js:584`) | | +| **deskwar** | 是否自动开战(join 时可选,`07_Desk.js:634`)⚠️ 此字段属"进入"而非登录响应 | | +| deskinfo | 子游戏对局快照(join 时可选,重连用,`07_Desk.js:647`) | | +| showerror / error | 失败时:是否显示错误 / 错误文案 | | + +--- + +## roomtype(房间类型配置数组)⚠️ 子游戏自定义 + +`Desk.setRoom` 接收并直接保存到 `Desk.roomtype`(`07_Desk.js:166`),创建/进入时原样透传给 `Game_Modify.onCreateDesk` / `Game_Modify.setRoomDes`(`07_Desk.js:358`、`508`、`629`)。**壳框架本身不解析该数组的各位含义**。 + +示例(来自 `01_SubGame_modify.js` 创建房间,仅供参考): +```js +roomtype: [1, 4, 1, 2, 2, [1,1,[1,2000,10],null,null,1], [1,0,5]] +``` +- 这是一个**嵌套数组**,各位含义(局数、人数、玩法选项、星星场配置、抽成配置等)由**具体子游戏**约定。 +- **服务器按这套数组解析房间规则**,新前端创建房间时必须发送与原子游戏**完全一致的 roomtype 结构**。 +- 上述示例数组各位精确含义属**子游戏范畴**,需对照目标子游戏的创建房间界面逐项核对,本壳框架未给出通用定义。 + +--- + +## GameData(全局数据,相关字段) + +来源:`04_Data.js`(`var GameData = GameData || {}`,`04_Data.js:24`)。login / 进房流程写入的相关键: + +| 字段 | 来源 | 说明 | +|------|------|------| +| AgentId | login `agentid`(`07_Desk.js:276`) | 代理ID | +| ChannelId | login `channelid`(`07_Desk.js:277`) | 渠道ID | +| initCard | login `initCard`(`07_Desk.js:258`) | 初始房卡 | +| initBean | login `initBean`(`07_Desk.js:264`) | 初始豆豆 | +| matchInfo | login/进房 `match`(`07_Desk.js:286`) | 比赛场信息 | +| matchId | login/进房 `matchid`(`07_Desk.js:291`) | 比赛ID | +| starName | 配置(`04_Data.js:189`,默认"星星") | 货币显示名 | +| infoSeat | `04_Data.js:225`(默认 -1) | 信息面板焦点座位 | +| sysConfig | `04_Data.js:440` | 子游戏系统配置(含 `deviceLogin` 等开关) | +| isLogin / hallLogin / isReconnect / vipRoomJump | login 流程置位(`07_Desk.js:241` 等) | 登录/重连状态标记 | + +> GameData 字段众多,此处仅列与本篇数据流(登录、房间恢复)直接相关者。完整 GameData 配置项见配置文档。 + +--- + +## 其它推送数据结构 + +| rpc | data 结构 | 出处 | +|-----|-----------|------| +| update_bean | `{ bean, change?, seat?, type?, text }` | `07_Desk.js:1201` | +| update_roomcard | `{ roomcard, text, change? }`(无 change 时刷新本地) | `06_Player.js:142` | +| update_charm | `{ seatlist:[ {seat, charm} ] }` | `07_Desk.js:1260` | +| change_star | `{ state, star1, star2, msg?, count?, error? }` | `07_Desk.js:1238` | +| broadcast | `{ msgtype?:0框/1滚动, msgcontent }` | `07_Desk.js:1112` | +| show_message | `{ msg, time }` | `07_Desk.js:1173` | +| kick_server | `{ msg }` | `07_Desk.js:1108` | +| kick_offline | `{ fromOther?, gameid? }` | `07_Desk.js:945` | +| free_room | `{ freeNow?, deskfree?, roomcard?, seats?, tips?, time? }` | `07_Desk.js:864` | diff --git a/docs/protocol/05-游戏内协议与桥接.md b/docs/protocol/05-游戏内协议与桥接.md new file mode 100644 index 0000000..418cc35 --- /dev/null +++ b/docs/protocol/05-游戏内协议与桥接.md @@ -0,0 +1,294 @@ +# 05 · 框架↔子游戏桥接 与 外部(H5/小程序)桥接 + +> 本篇只讲**框架侧机制**:route 分发判断、发对局包 API、对局包如何进入子游戏、deskinfo 重连、开战入口、外部 deeplink(H5/小程序)进房。 +> **不展开**具体子游戏的对局 rpc(发牌/出牌/下注/结算字段等由子游戏 `Game_Modify` 自定义实现,框架不感知)。 + +## 1. 平台层 vs 游戏内 的分界(route 分发) + +WebSocket 主分发在 `12_Logic.js` 的 `onmessage` 内(约 `12_Logic.js:258-263`): + +```js +// 12_Logic.js onmessage 内(约 258-263) +if (_msg.route == RouteList.platform || _msg.route == RouteList.agent || _msg.route == RouteList.room) { + if (min_ExitsFunction(Net[_msg.rpc])) { // 存在性守卫:表里有该处理函数才调用 + Net[_msg.rpc](_msg); // 平台层(02/03 章覆盖) + } +} else { + Game_Modify._ReceiveData(_msg); // 游戏内对局协议 → 交子游戏 +} +``` + +- `RouteList.platform = "platform"`、`RouteList.agent = "agent"`、`RouteList.room = "room"`(`02_Const.js:8-10`)。 +- `AppList.app = "youle"`(`02_Const.js:5`)。 +- **存在性守卫**:平台分支用 `min_ExitsFunction(Net[_msg.rpc])` 包裹,表里没有对应 `Net[rpc]` 处理函数时**静默丢弃**,不报错。新前端实现分发表时须复现这一点(未知 rpc 不应崩溃)。 + +> **关键**:游戏内对局包使用一个**非** `platform/agent/room` 的 `route`,落到 `else` 分支, +> 由子游戏的 `Game_Modify._ReceiveData(_msg)` 接管,自行按 `_msg.rpc` 分发。框架对其字段一无所知。 + +### ⚠️ 另有一处同形分发,勿混淆 + +`09_Net.js:31-37` 里有一段**结构相同**的 route 分发,但它位于 `Net._SendData` 的 **HTTP/Ajax 成功回调内**,且**仅当 `ConstVal.netType != 0`(HTTP 模式)才会执行**: + +```js +// 09_Net.js Net._SendData 内(约 19-45) +if (ConstVal.netType == 0) { + Net.ws_tcp.send(JSON.stringify(_msg)); // 默认:WebSocket,走第 1 节的 onmessage 主分发 +} else { + Func.AjaxHttp2(GameData.Server, _msg, function(_msg, state, input_msg){ + // —— HTTP 模式下的响应回调,内部才有那段 route 分发(约 31-37)—— + }); +} +``` + +默认 `netType == 0`(WebSocket),**不进**该分支。它是 HTTP 兜底模式的同步响应处理,**不是**与主分发并列的第二条主链路。**唯一权威的运行时分发是第 1 节的 `12_Logic.js:258-263`。** + +## 2. 本工程状态:子游戏逻辑为空模板 + +`Game_Surface_3` 是**平台壳/模板工程**。`01_SubGame/02_SubGame_Input.js` 中 `Game_Modify.*` 全部是**空函数桩**: + +```js +// 01_SubGame/02_SubGame_Input.js(约 32-57) +Game_Modify._ReceiveData = function(_msg){ } // 接收游戏内数据包(空) +Game_Modify.StartWar = function(_msg){ } // 开战(空) +Game_Modify.Reconnect = function(_deskinfo){ } // 重连恢复对局(空) +Game_Modify.DeskInfo = function(_msg){ } // 未开战自己加入时的牌桌数据(空) +``` + +**因此本工程内不存在某款具体游戏的对局字段定义。** 要拿到「发牌/出牌/结算」等精确字段,需从**目标子游戏工程**(其 `01_SubGame_modify.js` 有真实实现)提取,或对线上服务器**抓包**。本篇只描述上面这些钩子的**调用时机与实参**(框架契约)。 + +## 3. 游戏内发送通道(客户端 → 服务器) + +游戏内操作同样走统一出口 `Net._SendData(_app, _route, _rpc, _data)`(`09_Net.js:12`): + +```js +Net._SendData("youle", "", "", { + agentid: GameData.AgentId, + gameid: GameData.GameId, + playerid: C_Player.playerid, + roomcode: Desk.roomcode, + seat: C_Player.seat, + // ... 该操作的业务字段(出牌/下注内容等,子游戏自定义) +}); +``` + +- `_route` 用游戏约定值(非 agent/room/platform),服务器据此把包路由给对局逻辑。 +- 身份字段 `agentid/gameid/playerid/roomcode/seat` 是对局操作的通用前缀(约定俗成,非框架强制)。 +- 发送/接收的双层包装、心跳、握手规则与平台层**完全相同**(见 01 章)。 +- 框架不提供 `Net.Send_` 之类的封装,子游戏直接调 `Net._SendData` 上行;**具体 rpc 名与字段不在框架职责内**。 + +## 4. 开战入口(框架侧) + +框架共有三条进入 `Game_Modify.StartWar(_msg)` 的路径,**全部把整包 `_msg` 透传给子游戏**: + +``` +入口 A · 房主主动开局 + 房主点开始 → Net.Send_self_makewar(data) [route=room](09_Net.js:470-473) + → 服务器回 self_makewar → Net.self_makewar → Desk.self_makewar(_msg) + → Game_Modify.StartWar(_msg)(07_Desk.js:1018) + +入口 B · 他人/自动开战广播 + 服务器广播 other_makewar [route=room](09_Net.js:480-482) + → Desk.makewar(_msg) → Game_Modify.StartWar(_msg)(07_Desk.js:1057) + +入口 C · 进房即已开战(断线/中途进房) + Desk.self_join_room 中若 _msg.data.deskwar 为真: + → Desk.stage = 1 → Game_Modify.StartWar(_msg)(07_Desk.js:634-642) +``` + +> 入口 C 是「进房自动开战」:玩家加入时这局已在打(`deskwar` 真),直接以整包 `_msg` 调 `StartWar`,无需再等 makewar 广播。 + +## 5. 进房分支与对局快照(self_join_room) + +`Desk.self_join_room(_msg)`(`07_Desk.js:529` 起)是自己进房的总处理。`isbattle` 仅用于设置 `this.stage`(`07_Desk.js:324`),**不**决定重连。三种分支(`07_Desk.js:634-659`): + +```js +Game_Modify.myJoinRoom(_msg); // 总是先回调(07_Desk.js:632) + +if (_msg.data.deskwar) { // ① 进房即已开战 + Desk.stage = 1; + Game_Modify.StartWar(_msg); // 入口 C(07_Desk.js:642) +} else { + if (_msg.data.deskinfo) { // ② 未开战但有牌桌快照 + Desk.stage = 1; + Game_Modify.DeskInfo(_msg.data.deskinfo); // 实参是 deskinfo,非整包(07_Desk.js:653) + } else { // ③ 普通未开战 + Desk.stage = 0; + GameUI.ShowStartScene(); + C_Player.ChangeExit(1); + } +} +``` + +- `Game_Modify.DeskInfo` 收到的是 **`_msg.data.deskinfo`**(牌桌快照对象),**不是**整包 `_msg`。 +- `deskinfo` 内部结构由子游戏定义(应含当前轮次、各家手牌/明牌、出牌历史、分数等足以还原牌局的字段)。框架只负责把它原样递进去。 + +## 6. 重连恢复(deskinfo) + +断线重连进房时的另一条恢复路径,在 `Desk.self_join_room` 进入主场景分支(`07_Desk.js:419-423`): + +```js +GameUI.mainSceneLoaded(); +if (_msg.data.deskinfo) { // 触发条件是 deskinfo 存在,不是 isbattle==1 + if (get_self(149,37,0,0,0) == 0 && !GameData.iscloseVideo) { + Func.createRoom(); // 开视频相关 + } + Game_Modify.Reconnect(_msg.data.deskinfo); // 实参是 deskinfo(07_Desk.js:423) +} +``` + +- **触发条件是 `_msg.data.deskinfo` 存在**,而非 `isbattle == 1`(旧文档此处有误)。 +- `Game_Modify.Reconnect` 的实参同样是 **`_msg.data.deskinfo`**,子游戏据此重建对局界面。 + +## 7. 结算与相关保留 rpc + +- `RpcList.over_game = "over_game"`(`02_Const.js:29`):结算广播的保留 rpc 名。具体结算字段由子游戏在 `_ReceiveData` 中按自己的 route/rpc 处理,框架不解析。 +- `RpcList.agentserver_game = "agentserver_game"`(`02_Const.js:50`):对局数据在大厅(agent)服中转的保留 rpc 名。 +- 结算后资产更新走平台层既有推送(`update_bean` / `update_roomcard` 等,见 02/03 章),与对局协议分属两条链路。 + +## 8. 创建房间的双回调(create_room) + +服务器一次 `create_room` 响应会**连续触发两个子游戏钩子**(`09_Net.js:111-119`): + +```js +Net.create_room = function(_msg){ + Desk.create_room(_msg); + GameUI.EndLoad(); + Game_Modify.createRoom(_msg.data.roomtype, _msg.data.infinite); // 无条件调用 + if (Game_Modify.onCreateRoom) { // 带存在性守卫 + Game_Modify.onCreateRoom(_msg.data); + } +} +``` + +- `createRoom(roomtype, infinite)`:**无条件**调用,传两个标量。 +- `onCreateRoom(data)`:**带 `if` 守卫**(子游戏可不实现),传整包 `data`。 + +## 9. 子游戏向框架暴露的钩子(接口清单) + +新前端虽不用 JS 类,但**必须实现等价逻辑**——这些是框架流程的回调点(`01_SubGame/02_SubGame_Input.js`)。注意实参形态: + +| 钩子 | 触发时机 | 实参 | +|------|---------|------| +| `_ReceiveData(msg)` | 收到游戏内(非平台层 route)数据包 | 整包 `_msg` | +| `StartWar(msg)` | 开战(入口 A/B/C,见第 4 节) | 整包 `_msg` | +| `Reconnect(deskinfo)` | 进房发现有 deskinfo,需恢复对局 | **`_msg.data.deskinfo`** | +| `DeskInfo(deskinfo)` | 未开战自己加入但有牌桌快照 | **`_msg.data.deskinfo`** | +| `createRoom(roomtype, infinite)` | 创建房间成功(无条件) | 两个标量 | +| `onCreateRoom(data)` | 创建房间成功(带守卫,可选实现) | 整包 `data` | +| `myJoinRoom(msg)` / `playerJoinRoom(seat)` / `playerLeaveRoom(seat)` | 进/离房 | — | +| `myExitRoom(seat)` / `breakRoom()` | 自己退房 / 已开局退房 | — | +| `playerOffline(seat)` / `playerOnline(seat)` | 玩家上下线 | — | +| `playerphonestate(seat,type)` | 玩家电话状态 | — | +| `onReady(seat)` | 玩家准备 | — | +| `changeSeat(seat1,seat2)` | 换座 | — | +| `onSurrender(msg)` | 投降回包 | — | +| `Free(msg)` | 解散确认 | — | +| `updateScene()` / `closeGameScene()` | 刷新/关闭游戏界面 | — | +| `onEnterMainScene(roomtype)` / `onExitMainScene()` | 进/出主场景 | — | +| `stopAllSounds()` | 关闭所有游戏声音 | — | + +部分钩子需**返回**房间展示信息(创建/列表界面用): + +| 钩子 | 返回 | +|------|------| +| `getRoomInfo(roomtype,type,tea)` | 房间描述(每行≤18字符) | +| `getFullRoomInfo(roomtype)` | 完整房间描述(每行≤26字符) | +| `getRoomTopDescAry(roomtype)` | 房间标题描述字符串数组 | +| `getStarLimit(roomtype)` | 星星场准入下限 | +| `getMult(roomtype,type)` | 星星场倍数 | +| `getLeaveLimit(roomtype)` | 离场限制 | +| `getVideoByRoomType(roomtype)` | 是否开视频 0/1 | +| `getRoomMode(roomtype)` | 是否金币场 0/1 | + +## 10. 外部桥接:H5 / 小程序唤起进房(deeplink) + +**两套数据来源不同,勿混为一谈。** + +### 10.1 H5:URL 参数 `gameData` + +⚠️ 实际函数段约 `12_Logic.js:1900-1940`。 + +```js +// 写入侧(生成 deeplink):Logic.setGameData(12_Logic.js:1900-1905) +return encodeURI(JSON.stringify({ rpc: _rpc, data: _data })); + +// 读取侧:Logic.getGameDataFromH5(12_Logic.js:1907-1918) +var _data = fGetQuery("gameData"); +if (_data) { + _data = decodeURI(_data); + GameData.fromH5GameData = JSON.parse(_data); // payload 落到 fromH5GameData +} +``` + +payload 格式: + +```json +{ "rpc": "joinRoom", "data": { "roomcode": "<房号>" } } +``` + +`h5RpcList.joinRoom = "joinRoom"`(`02_Const.js:112`)是目前唯一支持的桥接 rpc。 + +### 10.2 小程序:本地存储 `openminigamedata`(不是 URL 参数) + +⚠️ 实际函数段约 `12_Logic.js:2406-2431`。**旧文档的 `miniProData` URL 参数在源码中根本不存在。** 小程序数据来自本地存储: + +```js +// Logic.getGameDataFromMiniPro(12_Logic.js:2406-2417) +var _data = Utl.ReadData("openminigamedata"); // 读本地存储键 "openminigamedata" +if (_data) { + _data = decodeURI(_data); + GameData.fromMiniProData = JSON.parse(_data); // payload 落到 fromMiniProData +} +``` + +(`openminigamedata` 会在进房等时机被清空,见 `07_Desk.js:283/456/531`。) + +### 10.3 解析后并不直接发包,而是弹确认框 + +旧文档「解析后等价触发 `self_join_room`」是错的,且**与源码相反**——解析到 `joinRoom` 后是**弹确认框**,真正发包在用户点确认之后: + +```js +// H5:Logic.joinRoomFromH5(12_Logic.js:1919-1940) +case h5RpcList.joinRoom: + GameData.checkType = 5; + GameUI.openCheck("是否进入房间?"); + //Net.Send_self_join_room(data); // ← 此处发包被注释,不在这里发 + +// 小程序:Logic.joinRoomFromMiniPro(12_Logic.js:2418-2431) +case h5RpcList.joinRoom: + GameData.checkType = 8; + GameUI.openCheck("是否进入房间?"); +``` + +确认框点「确定」后在 `11_GameUI.js` 的 `checkType` 分发里真正发包: + +```js +// 11_GameUI.js openCheck 确认处理(约 1219 起的 switch(GameData.checkType)) +case 5: // H5 跳转进房确认(11_GameUI.js:1253-1261) + data.roomcode = GameData.fromH5GameData.data.roomcode; + Net.Send_self_join_room(data); // ← H5 真正发包点 + break; +``` + +🐛 **小程序桥接存在 checkType 撞号 bug**:`joinRoomFromMiniPro` 设 `GameData.checkType = 8` 后弹确认框,但同一确认处理 switch 的 `case 8` 是「删除白名单玩家」`Net.Send_optWhiteList`(`11_GameUI.js:1278-1288`),**并非进房**。即小程序 deeplink 走确认框「确定」时不会发 `self_join_room`,反而触发白名单删除逻辑。(H5 的 `case 5` 正确。)新前端实现小程序唤起时应改用独立的进房 checkType,勿沿用 8。 + +## 11. gamemain.js:网络回调统一留空 + +`gamemain.js` 把引擎回调转发到框架;但 `tcpconnected` / `tcpmessage` / `tcpdisconnected` / `tcperror` / `httpmessage` **均为空体**(`gamemain.js:125-151`): + +```js +gameabc_face.tcpmessage = function(tcpid, data){ ; }; // 空 +gameabc_face.tcpdisconnected = function(tcpid){ }; // 空 +// ……其余网络回调同样为空 +``` + +> 对局包**统一从 `12_Logic.js` 的 `onmessage` → `_ReceiveData` 进入**,不走 gamemain 这些钩子。新前端无需在引擎层接网络。 + +## 12. 给 CocosCreator 新前端的落地清单(桥接部分) + +1. **route 分发**:接收按 `route` 分流——`platform/agent/room` 查 `Net[rpc]` 表(带存在性守卫),否则进 `Game_Modify._ReceiveData(msg)`;唯一权威分发点对应 `12_Logic.js:258-263`。 +2. **发对局包**:复用统一出口 `Net._SendData("youle", , , data)`;具体 rpc/字段从目标子游戏工程或抓包补全。 +3. **开战入口**:实现 A(self_makewar) / B(other_makewar) / C(进房 deskwar 真) 三条都汇入 `StartWar(整包)`。 +4. **进房快照**:区分 `deskwar`(开战) / `deskinfo`(未开战有快照→`DeskInfo(deskinfo)`) / 普通三分支;重连恢复看 `data.deskinfo` 存在 → `Reconnect(deskinfo)`,**勿**用 `isbattle` 判断。 +5. **创建房间**:一个响应连发 `createRoom(roomtype,infinite)`(无条件) 与 `onCreateRoom(data)`(可选)。 +6. **外部桥接**:H5 读 `fGetQuery("gameData")` → `fromH5GameData`;小程序读本地存储 `openminigamedata` → `fromMiniProData`;payload 均为 `{rpc:"joinRoom",data:{roomcode}}`;解析后**弹确认框**,确认后才发 `self_join_room`。实现时给小程序用独立 checkType,避开源码的 case 8 撞号 bug。 diff --git a/docs/protocol/06-子游戏开发模式与Cocos方案.md b/docs/protocol/06-子游戏开发模式与Cocos方案.md new file mode 100644 index 0000000..17cfa41 --- /dev/null +++ b/docs/protocol/06-子游戏开发模式与Cocos方案.md @@ -0,0 +1,237 @@ +# 06 · 子游戏开发模式(基于框架)与 CocosCreator 方案 + +> 本章讲**框架模板提供给子游戏的钩子契约 + 状态归属 + 子游戏开发模式**,并给出 CocosCreator 重写的两种设计方案。 +> **不涉及任何一款具体子游戏的对局算法/字段/专属模块**——凡涉及处统一标注"由各子游戏自定义"。 +> 模板工程以 `Game_Surface_3` 为基准,源码事实均以该工程为准。 + +--- + +## 1. 一个子游戏工程 = 模板壳 + 三处定制 + +对比模板 `Game_Surface_3`,真实子游戏的改动集中在固定几处,**平台层(`00_Surface/*`)几乎原样保留**: + +| 部分 | 模板 | 子游戏 | 是否改动 | +|------|------|--------|----------| +| `js/00_Surface/*`(平台层) | 完整 | **几乎不动**(与服务器对接的协议层,保持兼容) | ❌ 基本不改 | +| `js/01_SubGame/00_SubGame_Config.js` | 默认配置 | 改:人数/聊天气泡位/分享/声音/系统开关等 | ✅ 配置 | +| `js/01_SubGame/01_SubGame_modify.js` | 含创建房间界面 + 战绩页,对局留空 | **填**:创建房间界面、战绩定制、对局界面相关 | ✅ 重写 | +| `js/01_SubGame/02_SubGame_Input.js` | 全是空桩 | **填**:所有 `Game_Modify.*` 钩子的真实实现(含 `_ReceiveData`) | ✅ 重写 | +| `js/gamemain.js` | 纯转发引擎回调,`tcpmessage` 等为空体 | **扩展**:对局精灵初始化、引擎定时器/动画回调驱动对局 | ✅ 定制 | +| `js/*.js`(子游戏专属模块) | 无 | **新增**:玩法相关模块(牌型/出牌/理牌/回放等,**由各子游戏自定义**) | ✅ 新增 | +| `js/class/*`(OOP 类库,可选) | 无 | **可选新增**:牌型/牌局/算法类库(**由各子游戏自定义**) | ✅ 新增 | +| `output/*.min.js` | 模板界面 | **替换**:本游戏的精灵布局数据(编辑器导出) | ✅ 美术 | + +> 一句话:**改服务器对接以上的那一层(`01_SubGame` 三件套 + `gamemain` + 专属模块 + 美术数据)就得到一款新游戏,平台层和协议不动**。这正是"子游戏框架"的价值,也是"服务器零改动"的根因。 + +### 加载顺序 + +``` +引擎(gameabc) + Spine +→ 00_Surface/00..12 平台层(与模板一致,含 Desk/Net/GameUI/Logic 等) +→ 01_SubGame/00,01,02 子游戏三件套 +→ gamemain.js 子游戏定制的引擎回调 +→ 专属模块 玩法相关模块(由各子游戏自定义) +→ class/* 类库(可选,由各子游戏自定义) +→ output/*.min.js 美术布局数据 +``` +后加载的 `01_SubGame/02_SubGame_Input.js` 用**真实实现覆盖**了平台默认的 `Game_Modify` 空桩。 + +--- + +## 2. 子游戏 ↔ 框架的契约(最重要) + +### 2.1 框架 → 子游戏:回调钩子(平台在关键时机调用) + +子游戏在 `02_SubGame_Input.js` 实现这些 `Game_Modify.*` 钩子,模板里它们全是空桩(或仅 `console.log`);平台层在固定时机调用它们,子游戏据此驱动对局界面与状态。下表的"平台调用点"以模板工程 `Game_Surface_3` 的源码为准(`文件:行号`),子游戏内部的具体实现"**由各子游戏自定义**"。 + +> 方向均为 **平台 → 子游戏调用**(框架在内部回调子游戏实现的钩子)。 + +| 钩子(签名) | 触发时机 / 平台调用点(文件:行) | 说明 | +|------|------|------| +| `_ReceiveData(_msg)` | 收到对局自定义包时(`12_Logic.js:263`、`09_Net.js:36`) | 对局包入口。子游戏内 `switch(_msg.rpc)` 自分发并刷新对局,rpc 集合由各子游戏自定义 | +| `StartWar(_msg)` | 开战(`07_Desk.js:642 / 1018 / 1057 / 1141`) | 开局:初始化牌桌、起手发牌等 | +| `Reconnect(_deskinfo)` | 进房响应**含 `deskinfo`** 时(`07_Desk.js:423`) | 断线重连还原对局快照。**触发条件是响应含 `deskinfo`**(见 §2.4);模板为空实现,还原逻辑由各子游戏自定义 | +| `DeskInfo(_msg)` | 未开战状态下自己加入、携带牌桌数据时(`07_Desk.js:653`) | 同步未开战阶段的牌桌信息 | +| `createRoom(_roomtype,_infinite)` | 收到创建房间回包(`09_Net.js:115`) | 重置对局变量、铺座位等 | +| `onCreateRoom(_data)` | 创建房间成功后(`09_Net.js:117`,可选钩子) | 创建房间成功后的子游戏处理 | +| `myJoinRoom(_msg)` | 自己进入房间(`07_Desk.js:632`) | 自己入座后的初始化 | +| `playerJoinRoom(seat)` | 其他玩家加入(`07_Desk.js:736`) | 同步该座位玩家显示 | +| `playerLeaveRoom(seat)` | 其他玩家离开(`07_Desk.js:801`) | 清理该座位显示 | +| `myExitRoom(seat)` | 自己退出房间(`07_Desk.js:766`,可选钩子) | 自己离场处理 | +| `breakRoom()` | 已开局情况下自己退出(`07_Desk.js:756`) | 开局中退出的清理 | +| `playerOffline(seat)` | 玩家离线(`07_Desk.js:891`) | 标记离线态 | +| `playerOnline(seat)` | 玩家上线(`07_Desk.js:898`) | 标记在线态 | +| `playerphonestate(seat,type)` | 玩家电话状态(`07_Desk.js:974`/`984`,`type` 1=挂断、0=通话/来电) | 电话状态显示 | +| `onReady(seat)` | 玩家准备(`07_Desk.js:1136`) | 同步准备态 | +| `changeSeat(seat1,seat2)` | 收到换座包(`07_Desk.js:191`,可选钩子) | 同步换座后界面 | +| `onSurrender(_msg)` | 收到投降回包(`09_Net.js:621`) | 投降结果处理 | +| `Free(_msg)` | 投票解散同意后确认时(`07_Desk.js:873`、`11_GameUI.js:5026`) | 解散结算分支 | +| `updateScene()` | 按本地状态重绘界面(`05_Func.js:1642 / 2922`,可选钩子) | 断线恢复 / 切前台时重绘整个对局界面 | +| `closeGameScene()` | 关闭游戏界面(`07_Desk.js:427`) | 退出对局界面 | +| `onEnterMainScene(roomtype)` | 进入游戏主场景(`11_GameUI.js:4795`) | 进入主场景 | +| `onExitMainScene()` | 退出游戏主场景(`11_GameUI.js:3901`) | 退出主场景 | +| `onMainMenuScene()` | 显示大厅界面(`11_GameUI.js:3904`,可选钩子) | 回到大厅 | +| `onCreateDesk(roomtype)` | 进入游戏界面创建牌桌之前(`12_Logic.js:2125`) | 创建牌桌前的子游戏准备 | +| `onGameConfig(_gameConfig)` | 获取到游戏配置时(`12_Logic.js:1825`,仅 game_config 有数据时调用) | 接收服务器下发的游戏配置 | +| `onEnterVideo()` | 进入牌局回放时(`08_Utl_Output.js:593`) | 回放入口 | +| `calResult(inputArr)` | 倍率结算面板确认(`11_GameUI.js:6556`,参数为倍率数组) | 结算计算回调 | +| `onOpenHelp(spid)` | 打开帮助页面(`11_GameUI.js:4720`) | 帮助页处理 | +| `onCheckInput(_result)` | 数字输入框确认(`11_GameUI.js:7986`/`7988`) | 数字输入回调 | +| `onLocationInfo(_locationInfo)` | 成功获取定位信息(`05_Func.js:2063 / 3087`,可选钩子) | 定位信息回调 | +| `onCloseVip()` | 关闭 vip 选项时(`11_GameUI.js:7436`,可选钩子) | 关闭 vip 处理 | +| `getShareRoom(_msg)` | 收到星星场(分享房)信息时(`07_Desk.js:1160`,可选钩子) | 星星场房间处理 | +| `stopAllSounds()` | 需要静音对局声音时(`05_Func.js:1658 / 2934`、`07_Desk.js:718 / 725 / 772`) | 关闭子游戏声音 | +| `shakeEvent()` | 摇一摇事件(`05_Func.js:1958 / 3049`) | 模板示例里据 `GameData.shakeID` 走 `Net.Send_self_makewar()` 开战 | + +**返回类钩子**(平台据返回值渲染大厅 / 房间列表,返回内容**由各子游戏自定义**): + +| 钩子(签名) | 平台调用点(文件:行) | 返回值含义 | +|------|------|------| +| `getRoomInfo(roomtype,type,tea)` | `11_GameUI.js:6942` | 房间描述文本(`type` 1=系统房间、2=非系统房间) | +| `getFullRoomInfo(roomtype)` | `11_GameUI.js:7479 / 9284 / 9337` | 房间全部信息描述文本 | +| `getRoomTopDescAry(roomtype)` | `11_GameUI.js:8951` | 房间顶部一组描述(字符数组) | +| `getStarLimit(roomtype)` | `11_GameUI.js:6874 / 7167 …` | 星星场准入下限 | +| `getMult(roomtype,type)` | `11_GameUI.js:6823 / 6921 / 7168` | 星星场倍数 | +| `getLeaveLimit(roomtype)` | `11_GameUI.js:6948` | 离场限制 | +| `getVideoByRoomType(roomtype)` | 据 roomtype 决定是否开视频(模板内当前为注释状态) | 0=不开、1=开 | +| `getRoomMode(roomtype)` | `11_GameUI.js:1886 / 6721 / 7183 …` | 是否金币场(1/0) | + +> `roomtype` 的具体取值、房间描述文案、各返回值的格式均"**由各子游戏自定义**",本框架文档不展开。 + +### 2.2 子游戏 → 框架:可调用的 API(契约清单) + +这是 CocosCreator 重写时**必须在"平台 SDK"侧提供的接口**。子游戏通过这些接口读取框架态、发包、复用平台 UI: + +| 类别 | API | 作用 | +|------|-----|------| +| **座位/身份** | `Utl.getMySeat()` | 我的绝对座位 | +| | `Logic.ChangeToStatus(mySeat, targetSeat)` | 绝对座位 → 以我为视角的相对位(UI 摆位关键) | +| | `C_Player.playerid` / `GameData.AgentId` / `GameData.GameId` | 身份 | +| **房间状态** | `Desk.roomcode` / `Desk.roomtype` / `Desk.GetPlayerBySeat(seat)` / `Desk.PlayerList` | 房间/座位玩家数据 | +| | `Utl.getIsInfinite()` / `Utl.getIsDebugger()` | 房间属性/调试 | +| | `Utl.setDeskStage(...)` | 设置牌桌阶段 | +| **座位展示** | `Utl.setGrade(seat, value)` | 设置某座积分显示 | +| | `Utl.setPlayerPrepare(seat, ...)` / `Utl.getPlayerReadyState(seat)` | 准备态读写 | +| **发包(平台 RPC)** | `Net.Send_*(data)` | 平台 RPC(开局/聊天/解散/创建房间…见 02/03 章) | +| | `Net.Send_self_makewar(data)` | 主动开战(摇一摇等触发) | +| **发包(对局自定义)** | `Net._SendData(_app, _route, _rpc, _data)` | **对局自定义包**(出牌等);`_app/_route/_rpc` 取值由各子游戏自定义 | +| **平台 UI** | `GameUI.*`(弹窗/按钮/提示等) | 复用平台通用界面 | +| **渲染引擎** | `set_self/get_self/set_group/play_ani/set_clip/ifast_*` | 直接操作精灵(见 00 章) | +| **配置/全局** | `Game_Config.*` / `GameData.*` | 配置与全局态 | + +> 平台 RPC 的发包入口与字段见 02/03 章;对局自定义包统一走 `Net._SendData(_app,_route,_rpc,_data)`,其 `_app/_route/_rpc` 与 `_data` 结构"**由各子游戏自定义**"。 + +### 2.3 状态归属:框架态 vs 子游戏态 + +| | 框架维护 | 子游戏维护 | +|---|---------|-----------| +| 对象 | `Desk`、`C_Player`、`GameData` | 自建命名空间(对局状态对象,**由各子游戏自定义**) | +| 内容 | 房间/座位/玩家公共信息、连接、资产 | 纯对局数据(牌、轮次、结算明细等) | +| 来源 | 平台协议(01–04 章) | 对局协议(`_ReceiveData` 收到的 `_msg.data`) + `deskinfo`(重连快照) | + +- **框架态**:由 `00_Surface/*` 平台层统一维护,跟随平台协议(登录/大厅/房间/解散/资产)更新,子游戏**只读不写**。 +- **子游戏态**:子游戏自建命名空间存放纯对局数据,来源是对局协议包与重连快照 `deskinfo`,结构由各子游戏自定义。 + +### 2.4 deskinfo 与断线重连机制(与 05 章一致) + +- **触发条件**:进房响应里**携带 `deskinfo` 字段**时,平台在 `07_Desk.js:423` 调用 `Game_Modify.Reconnect(_msg.data.deskinfo)`。判定依据是"响应含 `deskinfo`"(`07_Desk.js:419` 的 `if(_msg.data.deskinfo)`),**不是** `isbattle==1` 之类的标志位。 +- **deskinfo 含义**:`deskinfo` 是子游戏对局态的**完整快照**,由服务器在开战时记录、在玩家重连时回发;其内部结构属对局协议,"**由各子游戏自定义**",本框架文档不展开。 +- **模板实现**:模板里 `Game_Modify.Reconnect` 是**空桩**(`02_SubGame_Input.js`)。"用 `deskinfo` 还原对局状态"的具体逻辑**由各子游戏实现**,模板为空实现。 +- **新前端约束**:CocosCreator 重写时,对局态必须能从同一份 `deskinfo` 完整还原,才能与旧服务器的重连流程兼容。 + +--- + +## 3. 对局驱动机制(子游戏的"心跳") + +子游戏不轮询,而是靠**引擎回调**推进,全部经 `gamemain.js`(`gameabc_face.*`)转发到子游戏模块: + +- **`ontimer(...)`**:子游戏用精灵定时器(`set_self(spid, 57, 间隔ms)`)开启,在此驱动动画 / 回合节奏。具体定时器 spid 与节奏"**由各子游戏自定义**"。 +- **`ani_doend(id,sx,count,allend)`**:动画结束回调,用于串联动画链。 +- **`gamemydraw / gamemydrawbegin`**:精灵自绘与裁剪(手牌滚动、勾选标记等)。 +- **`mousedown / mouseup / mousemove`**:对局交互(选牌/滑动/出牌);模板 `gamemain.js` 已把这些引擎回调转发给 `GameUI`、`Game_Modify`、`gameCombat` 三方。 +- **`gamestart`**:`Logic.AppStart()` 后,子游戏在此把对局精灵组初始化(通常先隐藏)。 + +> 网络回调(`tcpmessage / tcpconnected / tcpdisconnected` 等)在 `gamemain.js` 里**保持为空体**——对局包统一从平台的 `_ReceiveData` 进入,不走引擎 TCP。模板 `gamemain.js` 的桥接职责就是"把引擎回调转发给平台/子游戏模块",本身不含业务逻辑。 + +--- + +## 4. 子游戏开发的通用模式(小结) + +| 维度 | 框架提供 | 子游戏负责 | +|------|----------|------------| +| 平台协议层 `00_Surface/*` | 完整、稳定,不动 | 只读消费 | +| 三件套 `01_SubGame/*` | 空桩契约(钩子签名固定) | 填实现(接入点统一) | +| `gamemain.js` | 引擎回调桥接(转发,`tcp*` 空体) | 扩展对局精灵初始化与驱动 | +| 对局 rpc(`_ReceiveData`) | 提供入口 | `switch(_msg.rpc)` 自分发,rpc 集合自定义 | +| 重连 | 提供 `deskinfo` 快照与 `Reconnect` 钩子 | 实现"快照 → 对局态"的还原 | +| 结算 | 提供 `Free` / 倍率面板等通用 UI | 据玩法走结算分支 | +| 专属模块 / 类库 / 美术 | 无 | 全部自定义 | + +**结论**:换一款游戏 = 换 `01_SubGame` 三件套 + `gamemain` 定制 + 专属模块 + 美术数据;**框架与协议是稳定底座**。具体玩法(牌型、出牌规则、roomtype 取值、deskinfo 结构、专属模块拆分)均"**由各子游戏自定义**",不在本框架文档范围内。 + +--- + +## 5. CocosCreator 重写方案(设计建议) + +目标不变:**服务器零改动**。因此无论哪种方案,"平台 SDK + 对局协议"的字节级一致是硬约束(01–05 章),可自由替换的是渲染与工程组织。本节为设计建议,须与前述源码事实(钩子契约、状态归属、deskinfo 重连机制)保持一致。 + +### 方案 A:忠实复刻(迁移成本低、风险小) + +把原框架的分层直接映射到 Cocos: + +``` +NetworkLayer WebSocket 封装:信封{app,route,rpc,data}+双层解包+心跳(01章) +PlatformSDK 复刻 00_Surface:登录/大厅/房间/解散/聊天/资产(02–04章协议 1:1) + ├─ Net Send_*/收包分发,含 _SendData(app,route,rpc,data) 对局自定义包通道 + ├─ Desk 房间/座位状态(04章) + ├─ Player C_Player(04章) + └─ GameUI 大厅/房间通用界面(Cocos 场景/Prefab 重画,不复刻 spid) +SubGameModule 对局模块,暴露与 Game_Modify 等价的钩子接口: + _ReceiveData(msg) / StartWar / Reconnect(deskinfo) / DeskInfo / + myJoinRoom / playerJoinRoom / onReady / Free / getRoomInfo... (§2.1 全套) +EventBus 取代 gamemain.js 的统一分发(输入/帧/定时 → UI 与对局) +``` +- 优点:与原游戏行为高度一致,便于逐个对照验证、对接旧 `deskinfo`。 +- 适合:先快速上线、再逐步现代化。 + +### 方案 B:现代化重构(推荐,更高效) + +保留协议层不变,对局层用 Cocos 的能力重做: + +1. **协议层 TypeScript 化**:把 RpcList / 字段定义成 TS `interface` 与枚举,收发用泛型包裹,编译期校验字段。 + ```ts + interface Envelope { app: string; route: string; rpc: string; data: T } + ``` +2. **平台 SDK 做成独立模块/npm 包**:`NetClient`(连接/心跳/解包) + `PlatformApi`(login/room/...) + `RoomStore`(响应式房间态)。多款游戏复用,等价于原 `00_Surface`。 +3. **对局用 Cocos 组件化**:场景=Scene,玩家位/手牌/牌桌=Prefab+组件;动画用 Cocos Animation/tween 取代引擎定时器与 `play_ani`;骨骼继续用 Spine(原框架也用 Spine)。 +4. **对局接入用接口而非全局函数**:定义 `IGameModule { onReceive(rpc,data); onReconnect(deskinfo); render(state) }`,平台 SDK 通过它回调对局;对局通过注入的 `sdk` 调 `sendAction()/getMySeat()/seatToView()`。这与原框架 `Game_Modify.*` 钩子 + 子游戏调 `Net/Desk/Utl` 的边界一一对应。 +5. **状态驱动渲染**:对局态集中在一个 store(对应原框架的子游戏命名空间),`render(state)` 纯函数刷新。**重连**:响应含 `deskinfo` 时 `onReconnect(deskinfo)` 把快照灌入 store——与原框架"响应含 deskinfo 即触发 Reconnect"的判定一致(§2.4)。 +6. **座位视角换算保留**:实现 `seatToView(mySeat,target)` 等价 `Logic.ChangeToStatus`,对局只关心"相对位"。 + +建议工程结构: +``` +assets/ + scripts/ + net/ NetClient, Envelope, heartbeat, reconnect + platform/ PlatformApi(02/03章), RoomStore/PlayerStore(04章), 通用UI + game/<玩法>/ GameModule(实现 IGameModule), state, view 组件, prefab + core/ EventBus, seatUtil, types(协议 TS 定义) +``` + +### 两方案对比 + +| | 方案 A 复刻 | 方案 B 现代化 | +|---|-----------|--------------| +| 协议兼容 | ✅ | ✅(不动协议) | +| 上手速度 | 快 | 中 | +| 多游戏复用 | 一般 | 强(SDK 独立) | +| 可维护性/类型安全 | 一般 | 强 | +| 适合 | 首款快速验证 | 长期多游戏平台 | + +> **共同底线**:把"平台 SDK"和"对局模块"边界划清(即 §2.1 钩子契约 + §2.2 API 清单 + §2.4 重连机制),协议字段严格对齐 01–05 章。这样新前端对服务器而言与旧前端**不可区分**,即达成"完美适配、服务器零改动"。 + +--- + +## 6. 待补:具体对局字段 + +本章聚焦框架使用模式与钩子契约,**未列任何对局包字段**。某款子游戏的对局 rpc 详细字段(`StartWar/Free/deskinfo` 等结构)、roomtype 取值、专属模块划分等,均属该子游戏范畴;可在确定首款游戏后,对其 `_ReceiveData` 各 case 与发包点做一次专项提取,补入 05 章。 diff --git a/docs/protocol/README.md b/docs/protocol/README.md new file mode 100644 index 0000000..c2650c1 --- /dev/null +++ b/docs/protocol/README.md @@ -0,0 +1,114 @@ +# 友乐游戏前端通信协议文档(Game_Surface_3 框架逆向) + +> 本文档基于现有 H5 前端框架 `projects/Game_Surface_3` 逆向整理,目标是为 +> **使用 CocosCreator 重写一款全新前端**提供完整、精确的协议参考,做到 +> **服务器不改动一行代码、完美适配**。 + +## 适配核心原则 + +新前端只要严格满足以下三点,即可被现有服务器无差别接受: + +1. **传输层一致**:WebSocket(`ws://ip:port`)传输,纯文本 JSON;消息双层包装、心跳/握手包识别方式与本文档一致。 +2. **消息信封一致**:发送/接收的信封字段 `{app, route, rpc, data}` 完全一致,`app` 恒为 `"youle"`。 +3. **字段名/语义一致**:每个 RPC 的 `data` 字段名、类型、取值含义与本文档一致(服务器按字段名读取,多字段不影响,缺字段会报错)。 + +## 文档结构 + +| 文件 | 内容 | +|------|------| +| [00-框架架构设计.md](./00-框架架构设计.md) | **先读**:子游戏框架的整体架构——四层结构、自研精灵引擎、桥接层、平台/子游戏分层、生命周期、子游戏接入机制、CocosCreator 映射 | +| [01-传输层与架构.md](./01-传输层与架构.md) | 网络架构、消息信封、双层包装、心跳、握手、连接/重连、服务器切换、登录握手流程 | +| [02-协议-agent路由.md](./02-协议-agent路由.md) | 大厅服务器(route=agent)全部 RPC 收发字段 | +| [03-协议-room路由.md](./03-协议-room路由.md) | 房间服务器(route=room)全部 RPC 收发字段 | +| [04-数据结构.md](./04-数据结构.md) | C_Player、Desk(牌桌)、Player(座位)、登录响应、房间配置等核心数据结构 | +| [05-游戏内协议与桥接.md](./05-游戏内协议与桥接.md) | 子游戏(SubGame)游戏内协议机制、开局/结算流程、H5/小程序桥接 | +| [06-子游戏开发模式与Cocos方案.md](./06-子游戏开发模式与Cocos方案.md) | **重写参考**:框架提供给子游戏的钩子契约、状态归属、子游戏开发模式;含 CocosCreator 两套重写方案(具体某款子游戏的对局模块/roomtype/deskinfo 由各子游戏自定义,本框架文档不展开)| + +## 关键事实速览 + +- **信封格式**:`{ "app":"youle", "route":"agent|room|platform", "rpc":"", "data":{...} }` +- **应用名**:`app = "youle"`(`AppList.app`) +- **路由**:`platform` / `agent`(大厅服)/ `room`(房间服);其它路由 → 走子游戏 `Game_Modify._ReceiveData` +- **服务器→客户端**为**双层包装**:外层 `{data: <内层>}`,内层才是 `{route,rpc,data}`(详见 01) +- **心跳单向**:服务器约 ~20s 推 `@serverheartbeat`(周期仅源码注释佐证,⚠️待抓包确认),**客户端不回包**;客户端仅做 30s 收包超时检测(`ConstVal.Max.heartbeat=30000`,且仅 `!isGameHall` 时启用) +- **握手包**:连接后服务器首包为 `@toconcon...`,客户端忽略 +- **身份字段**:几乎所有请求都带 `agentid`、`gameid`、`playerid`(房间相关再加 `roomcode`) + +## 提取来源文件 + +| 模块 | 文件 | +|------|------| +| 网络发送/接收封装 | `js/00_Surface/09_Net.js` | +| 协议常量(AppList/RouteList/RpcList) | `js/00_Surface/02_Const.js` | +| 连接/分发/心跳/重连 | `js/00_Surface/12_Logic.js` | +| WebSocket/Ajax 底层封装 | `js/00_Surface/00_minhttp.js` | +| 牌桌/房间接收处理 | `js/00_Surface/07_Desk.js` | +| 玩家数据结构 | `js/00_Surface/06_Player.js` | +| 全局数据 | `js/00_Surface/04_Data.js` | +| 子游戏钩子(空模板) | `js/01_SubGame/02_SubGame_Input.js` | +| 子游戏配置 | `js/01_SubGame/00_SubGame_Config.js` | + +> ⚠️ **重要说明**:`Game_Surface_3` 是「平台壳框架」。平台层协议(登录/房间/大厅/聊天/排行等)完整可用, +> 已在本文档中精确给出。**游戏内对局协议**(发牌/出牌/结算等)在本工程中 `Game_Modify._ReceiveData` 为 +> **空模板**,由各具体子游戏自定义。第 05 章给出其传输通道与格式约定;若需某款具体游戏的对局协议字段, +> 须从对应子游戏工程或抓包补充。 + +--- + +## 附录 · 完整 RPC 清单与覆盖状态(对照 `02_Const.js` RpcList) + +> ✓=已在文档详述并核对源码;预留=`RpcList` 中定义但框架内**无处理函数/无调用**(历史遗留,新前端可不实现)。 + +| rpc | 路由 | 方向 | 状态 / 所在章节 | +|-----|------|------|----------------| +| player_login | agent | 收发 | ✓ 02 / 04(响应) | +| create_room | agent | 收发 | ✓ 02 / 04 | +| self_join_room | agent | 收发 | ✓ 02 / 04 | +| quick_enter_share_room | agent | 收发 | ✓ 02 | +| get_share_room | agent | 收发 | ✓ 02 | +| advanced_roomlist / advanced_createroom | agent | 收发 | ✓ 02 | +| getInfoByShortCode | agent | 收发 | ✓ 02 | +| switchRoomList | agent | 收发 | ✓ 02 | +| get_player_grade1 / get_player_grade2 | agent | 收发 | ✓ 02 | +| get_treasurelist / getShortCodeRankList / getVipRankList | agent | 收发 | ✓ 02 | +| get_player_task / player_finish_task / get_task_award | agent | 收发 | ✓ 02 | +| can_award | agent | 推送 | 🐛 02(接收函数 `C_Player.can_award` 未定义,到达即异常)| +| send_phone_code_wechat | agent | 收发 | ✓ 02(微信发手机验证码;调用点被注释,当前不发)| +| submit_error / submit_log | agent | 发送 | ✓ 02(错误上报,二者 rpc 均为 `submit_error`)| +| refresh_task_state | agent | 发送 | ✓ 02(`Send_can_award` 实发此 rpc,硬编码、不在 RpcList)| +| get_paylist / pay_succ / topup_card | agent | 收发 | ✓ 02(pay_succ 经 HTTP 上行,WS 发送处被注释)| +| giveCoin / set_bankpwd / change_star | agent | 收发 | ✓ 02 | +| update_charm / setAllCharm | agent | 推送/发 | ✓ 02 | +| binding_invitecode / get_player_invitecode | agent | 收发 | ✓ 02 | +| optBanList / getPlayerWhiteList / optWhiteList / setVipForbidSelect | agent | 收发 | ✓ 02 | +| binding_phone / send_phone_checkcode / setSign / query_player2 | agent | 收发 | ✓ 02 | +| submit_opinion / submit_location / submit_phoneinfo | agent | 发送 | ✓ 02 | +| kick_server / broadcast / playerBehavior | agent | 收/特殊 | ✓ 02 | +| connect_agentserver | agent | 推送 | ✓ 01 / 02 | +| update_roomcard / update_bean / kick_offline / show_message | agent | 推送 | ✓ 02(末表) | +| self_break_room / other_break_room | room | 收发 | ✓ 03 | +| self_exit_room / other_exit_room | room | 收发 | ✓ 03 | +| other_join_room | room | 推送 | ✓ 03 | +| other_offline / other_online | room | 推送 | ✓ 03 | +| self_apply_free_room / other_apply_free_room | room | 收发 | ✓ 03 | +| self_agree_free_room / other_agree_free_room | room | 收发 | ✓ 03 | +| self_refuse_free_room / other_refuse_free_room | room | 收发 | ✓ 03 | +| free_room | room | 推送 | ✓ 03 | +| self_makewar / other_makewar | room | 收发 | ✓ 03 / 05 | +| player_prepare | room | 收发 | ✓ 03 | +| change_room → change_seat | room | 发/推送 | ✓ 03 | +| send_text / send_voice / send_gift / send_phiz | room | 收发 | ✓ 03 | +| call_phone / hangup_phone | room | 收发 | ✓ 03 | +| beanroom_surrender | room | 收发 | ✓ 03 | +| share_room | room | 收发 | ✓ 03 | +| connect_roomserver | room | 推送 | ✓ 01 / 03 | +| over_game | (游戏内) | 推送 | ✓ 05(结算) | +| agentserver_game | (游戏内) | — | ✓ 05(对局中转保留名) | +| joinRoom (h5RpcList) | (桥接) | — | ✓ 05(H5/小程序唤起) | +| **play_voice** | room | — | **预留**:未实现,语音回包复用 `send_voice` | +| **receive_chat** | room | — | **预留**:未实现,聊天回包复用 `send_text` | +| **other_callphone** | room | — | **预留**:未实现,复用 `call_phone`(带 seat) | +| **other_hangup** | room | — | **预留**:未实现,复用 `hangup_phone`(带 seat) | +| **other_send_gift** | room | — | **预留**:未实现,复用 `send_gift`(带 sendseat) | + +> 上述为 `RpcList` 全量(与 `02_Const.js` 1:1 对照)。除 5 个预留名外,全部已在 01–05 章给出收发字段并经源码核对。