Files
youle_cocos/docs/protocol/05-游戏内协议与桥接.md
T
joywayerandClaude Opus 4.8 08640ca89a docs(protocol): 交叉源码审计后重修全 8 篇协议文档
基于 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>
2026-06-28 11:35:47 +08:00

15 KiB
Raw Blame History

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.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 新前端的落地清单(桥接部分)

  1. route 分发:接收按 route 分流——platform/agent/room 查 Net[rpc] 表(带存在性守卫),否则进 Game_Modify._ReceiveData(msg);唯一权威分发点对应 12_Logic.js:258-263。
  2. 发对局包:复用统一出口 Net._SendData("youle", <game_route>, <game_rpc>, 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。