基于 00_Surface 平台层源码逐条交叉核对,修订 docs/protocol: - 补全前后端双向数据包(请求+响应/推送字段),覆盖框架模板全部平台层 rpc - 修正错误:重连触发条件(deskinfo 而非 isbattle)、小程序桥接(openminigamedata 而非 miniProData)、 get_player_invitecode 字段、setGameServer/GameData.Server 来源、route 分发守卫等 - 补遗漏 rpc:send_phone_code_wechat、submit_error/submit_log、refresh_task_state、 update_bean/update_charm/broadcast、connect_agentserver 字段、5 个成对推送常量 - 补 Desk 约 11 个字段、登录响应账号字段、10 个子游戏钩子 - 标注源码 bug(🐛 can_award 接收函数未定义、小程序 checkType=8 误接白名单) - 校准全篇 file:line 引用;待后端确认项标 ⚠️ - 明确范围:仅框架模板协议,不含子游戏对局包 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 KiB
05 · 框架↔子游戏桥接 与 外部(H5/小程序)桥接
本篇只讲框架侧机制:route 分发判断、发对局包 API、对局包如何进入子游戏、deskinfo 重连、开战入口、外部 deeplink(H5/小程序)进房。 不展开具体子游戏的对局 rpc(发牌/出牌/下注/结算字段等由子游戏
Game_Modify自定义实现,框架不感知)。
1. 平台层 vs 游戏内 的分界(route 分发)
WebSocket 主分发在 12_Logic.js 的 onmessage 内(约 12_Logic.js:258-263):
// 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 模式)才会执行:
// 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.* 全部是空函数桩:
// 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):
Net._SendData("youle", "<game_route>", "<game_rpc>", {
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_<game_rpc>之类的封装,子游戏直接调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):
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):
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):
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。
// 写入侧(生成 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 格式:
{ "rpc": "joinRoom", "data": { "roomcode": "<房号>" } }
h5RpcList.joinRoom = "joinRoom"(02_Const.js:112)是目前唯一支持的桥接 rpc。
10.2 小程序:本地存储 openminigamedata(不是 URL 参数)
⚠️ 实际函数段约 12_Logic.js:2406-2431。旧文档的 miniProData URL 参数在源码中根本不存在。 小程序数据来自本地存储:
// 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 后是弹确认框,真正发包在用户点确认之后:
// 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 分发里真正发包:
// 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):
gameabc_face.tcpmessage = function(tcpid, data){ ; }; // 空
gameabc_face.tcpdisconnected = function(tcpid){ }; // 空
// ……其余网络回调同样为空
对局包统一从
12_Logic.js的onmessage→_ReceiveData进入,不走 gamemain 这些钩子。新前端无需在引擎层接网络。
12. 给 CocosCreator 新前端的落地清单(桥接部分)
- route 分发:接收按
route分流——platform/agent/room查Net[rpc]表(带存在性守卫),否则进Game_Modify._ReceiveData(msg);唯一权威分发点对应12_Logic.js:258-263。 - 发对局包:复用统一出口
Net._SendData("youle", <game_route>, <game_rpc>, data);具体 rpc/字段从目标子游戏工程或抓包补全。 - 开战入口:实现 A(self_makewar) / B(other_makewar) / C(进房 deskwar 真) 三条都汇入
StartWar(整包)。 - 进房快照:区分
deskwar(开战) /deskinfo(未开战有快照→DeskInfo(deskinfo)) / 普通三分支;重连恢复看data.deskinfo存在 →Reconnect(deskinfo),勿用isbattle判断。 - 创建房间:一个响应连发
createRoom(roomtype,infinite)(无条件) 与onCreateRoom(data)(可选)。 - 外部桥接:H5 读
fGetQuery("gameData")→fromH5GameData;小程序读本地存储openminigamedata→fromMiniProData;payload 均为{rpc:"joinRoom",data:{roomcode}};解析后弹确认框,确认后才发self_join_room。实现时给小程序用独立 checkType,避开源码的 case 8 撞号 bug。