Files
erqiwang_youle/client/js/01_SubGame/codes/SubGameHooks.js
T
joywayerandClaude Opus 5 af779f473a 二七王:C-1 修复 room.mySeat 永远为 -1
平台 12_Logic.js 在第 389 行调 Game_Modify.appStart(),而 C_Player = new Player(-1)
要到同函数第 480 行才执行,此刻 typeof C_Player === 'undefined',appStart 整体空转;
之后玩家登录 SetSeat 拿到真座位,appStart 再也不会被调用。于是 mySeat 永远停在 -1:
发包带 seat:-1 被服务端 check_player 全部拒绝、StartWar 差异化下发认领不到本座位而
整包丢弃、SeatMap.toDisplay(-1) 抛错——且全程无一处报错。

- 新增 SubGameHooks._syncMySeat():room.mySeat 的唯一写入处(SSOT),幂等、
  座位非法或 C_Player 缺席时保持原值不倒退
- 在 setRoomDes / StartWar / Reconnect / ReconnectNoMakewar / appStart 各调用一次,
  不赌「所有路径都经过某一个入口」
- 新增 SubGameHooks.changeSeat 并同步座位:Desk.change_seat 是唯一「座位中途变化
  且不重走 setRoomDes」的路径,漏掉会让 mySeat 静默过期
- Rpc._seat() 改 fail-fast:座位非 0/1/2 即抛错,不再无声透传 -1
- 新增 client/tests/test_appstart_timing.js:复刻平台真实初始化时序,
  全程不直接赋值 mySeat;已反向验证(退回 6 个调用点后核心断言转红)
- test_rpc_send 补座位非法抛错断言;test_consistency 删除空转的 my.mustCard EXCLUDE

client 23 脚本全绿,server 17 脚本 / 680 checks 全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 21:05:46 +08:00

334 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// SubGameHooks.js —— 二七王子游戏接入入口(实现骨架,本阶段空函数,B/F 阶段逐个填充)
// 平台契约文件(00/01/02 转发壳)会把同名平台接口转发到这里。只需实现【本游戏需要的】hook,
// 未实现的接口由转发壳走空操作(A)/平台默认返回值(B)/模板默认 UI 渲染(D)。严格 ES5。
// 接口清单与默认值见 spec §9 / 文档 06。
// ============================================================================
var SubGameHooks = window.SubGameHooks || {};
// ===== gameHallImport.*(大厅,9) =====
// [A] hallAppStart():大厅应用启动(注:与 Game_Modify.appStart 同名冲突,故加 hall 前缀区分)。
// SubGameHooks.hallAppStart = function () { /* ... */ };
// [A] jumpMenuScene():大厅跳转菜单场景。
// SubGameHooks.jumpMenuScene = function () { /* ... */ };
// [A] gameStart():大厅游戏开始入口。
// SubGameHooks.gameStart = function () { /* ... */ };
// [A] setGameList():设置游戏列表。
// SubGameHooks.setGameList = function () { /* ... */ };
// [A] clearGameinfo():清除游戏信息。
// SubGameHooks.clearGameinfo = function () { /* ... */ };
// [A] getWebdata(data):获取网络数据回调。
// SubGameHooks.getWebdata = function (data) { /* ... */ };
// [B] isInstalled(data):检查是否已安装;无 hook 时默认:return 1。
// SubGameHooks.isInstalled = function (data) { /* ... */ };
// [A] up_imgurl(recid, photo):更新图片 URL。
// SubGameHooks.up_imgurl = function (recid, photo) { /* ... */ };
// [A] getphoto(recid, photo):获取玩家头像。
// SubGameHooks.getphoto = function (recid, photo) { /* ... */ };
// ===== Game_Modify.*(事件/交互,来自 01,9) =====
// ---------------------------------------------------------------------------
// 精灵交互 6 件套:【只做转发,不写任何业务逻辑】
//
// 平台事件到达框架的完整链路:
// 引擎 → Game_Modify.xxx(01 转发壳) → SubGameHooks.xxx(本文件) → SpriteEventController.handleXxx
// → 按精灵 ID 分发到各 UI 组件注册的 handler
//
// 这一段接不上,整条链就是断的:转发壳里已注册的 13 号战绩按钮、150 号帮助按钮、
// 248 号协议勾选(绘制回调里叠绘对钩)全都收不到事件,且不报错、只是静默失效。
// 参数顺序必须与 SpriteEventController.handleXxx 的签名逐个对齐——错位会让事件【静默失效】。
// 防御风格与转发壳一致:先判断依赖存在再转发。
// ---------------------------------------------------------------------------
// [A] utlmousedown(gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6):精灵按下事件;子游戏转发到 SpriteEventController。
SubGameHooks.utlmousedown = function (gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6) {
if (typeof SpriteEventController === 'undefined') { return; }
return SpriteEventController.handleMouseDown(gameid, spid, downx, downy, no1, no2, no3, no4, no5, no6);
};
// [A] utlmousedown_nomove(gameid, spid, downx, downy, timelong, no1, no2, no3, no4, no5):精灵按下(无移动)事件。
SubGameHooks.utlmousedown_nomove = function (gameid, spid, downx, downy, timelong, no1, no2, no3, no4, no5) {
if (typeof SpriteEventController === 'undefined') { return; }
return SpriteEventController.handleMouseDownNoMove(gameid, spid, downx, downy, timelong, no1, no2, no3, no4, no5);
};
// [A] mouseup(gameid, spid_down, downx, downy, spid_up, upx, upy, timelong, no1, no2):精灵抬起事件。
SubGameHooks.mouseup = function (gameid, spid_down, downx, downy, spid_up, upx, upy, timelong, no1, no2) {
if (typeof SpriteEventController === 'undefined') { return; }
return SpriteEventController.handleMouseUp(gameid, spid_down, downx, downy, spid_up, upx, upy, timelong, no1, no2);
};
// [A] utlmousemove(gameid, spid, downx, downy, movex, movey, timelong, offmovex, offmovey, no1):精灵移动事件。
SubGameHooks.utlmousemove = function (gameid, spid, downx, downy, movex, movey, timelong, offmovex, offmovey, no1) {
if (typeof SpriteEventController === 'undefined') { return; }
return SpriteEventController.handleMouseMove(gameid, spid, downx, downy, movex, movey, timelong, offmovex, offmovey, no1);
};
// [A] utlgamemydrawbegin(gameid, spid, times, timelong, no2, no3, no4, no5, no6, no7):精灵绘制开始事件。
SubGameHooks.utlgamemydrawbegin = function (gameid, spid, times, timelong, no2, no3, no4, no5, no6, no7) {
if (typeof SpriteEventController === 'undefined') { return; }
return SpriteEventController.handleDrawBegin(gameid, spid, times, timelong, no2, no3, no4, no5, no6, no7);
};
// [A] gamemydraw(gameid, spid, times, timelong, no2, no3, no4, no5, no6, no7):精灵绘制帧事件。
SubGameHooks.gamemydraw = function (gameid, spid, times, timelong, no2, no3, no4, no5, no6, no7) {
if (typeof SpriteEventController === 'undefined') { return; }
return SpriteEventController.handleDraw(gameid, spid, times, timelong, no2, no3, no4, no5, no6, no7);
};
// [A] OpenCreateRoom():打开创建房间界面。
// SubGameHooks.OpenCreateRoom = function () { /* ... */ };
// [A] CloseCreateRoom():关闭创建房间界面。
// SubGameHooks.CloseCreateRoom = function () { /* ... */ };
// [A] setRoomDes(roomcode, asetcount, roomtype):设置房间描述文案。
// SubGameHooks.setRoomDes = function (roomcode, asetcount, roomtype) { /* ... */ };
// ===== Game_Modify.*(生命周期/回调,来自 02,45) =====
// [A] appStart():游戏应用启动(与 gameHallImport.appStart 语义不同,对应 Game_Modify 侧启动)。
// SubGameHooks.appStart = function () { /* ... */ };
// [A] StartWar(_msg):开战(开局)回调,解包开局数据并初始化游戏状态。
// SubGameHooks.StartWar = function (_msg) { /* ... */ };
// [A] _ReceiveData(_msg):接收服务端主动推送的数据包,派发给对应业务处理器。
// SubGameHooks._ReceiveData = function (_msg) { /* ... */ };
// [A] createRoom(_roomtype, _infinite):前端发起创建房间请求。
// SubGameHooks.createRoom = function (_roomtype, _infinite) { /* ... */ };
// [A] myJoinRoom(_msg):自己成功进入房间回调。
// SubGameHooks.myJoinRoom = function (_msg) { /* ... */ };
// [A] Reconnect(_deskinfo):重连回调,用服务端返回的桌面信息恢复游戏界面。
// SubGameHooks.Reconnect = function (_deskinfo) { /* ... */ };
// [A] stopAllSounds():停止所有声音(切换到后台时触发)。
// SubGameHooks.stopAllSounds = function () { /* ... */ };
// [A] Free(_msg):投票解散确认回调。
// SubGameHooks.Free = function (_msg) { /* ... */ };
// [A] DeskInfo(_msg):未开战状态下加入牌桌的数据同步。
// SubGameHooks.DeskInfo = function (_msg) { /* ... */ };
// [A] breakRoom():开局后自己退出房间触发。
// SubGameHooks.breakRoom = function () { /* ... */ };
// [A] playerJoinRoom(seat):其他玩家加入房间通知。
// SubGameHooks.playerJoinRoom = function (seat) { /* ... */ };
// [A] playerLeaveRoom(seat):其他玩家离开房间通知。
// SubGameHooks.playerLeaveRoom = function (seat) { /* ... */ };
// [A] updateScene():更新游戏界面(分辨率变化等)。
// SubGameHooks.updateScene = function () { /* ... */ };
// [A] closeGameScene():关闭游戏界面。
// SubGameHooks.closeGameScene = function () { /* ... */ };
// [A] playerOffline(seat):玩家掉线通知。
// SubGameHooks.playerOffline = function (seat) { /* ... */ };
// [A] playerOnline(seat):玩家上线通知。
// SubGameHooks.playerOnline = function (seat) { /* ... */ };
// [A] playerphonestate(seat, type):玩家电话状态变化通知。
// SubGameHooks.playerphonestate = function (seat, type) { /* ... */ };
// [A] shakeEvent():设备摇一摇事件。
// SubGameHooks.shakeEvent = function () { /* ... */ };
// [A] calResult(inputArr):计算结算结果。
// SubGameHooks.calResult = function (inputArr) { /* ... */ };
// [A] onEnterMainScene(roomtype):进入游戏主场景。
// SubGameHooks.onEnterMainScene = function (roomtype) { /* ... */ };
// [A] onExitMainScene():退出游戏主场景。
// SubGameHooks.onExitMainScene = function () { /* ... */ };
// [A] onGameConfig(_gameConfig):获取游戏配置时触发。
// SubGameHooks.onGameConfig = function (_gameConfig) { /* ... */ };
// [A] onEnterVideo():进入牌局回放模式。
// SubGameHooks.onEnterVideo = function () { /* ... */ };
// [A] onSurrender(_msg):收到投降回包。
// SubGameHooks.onSurrender = function (_msg) { /* ... */ };
// [A] onReady(seat):玩家准备触发。
// SubGameHooks.onReady = function (seat) { /* ... */ };
// [B] getRoomInfo(roomtype, type, tea):获取房间描述文案;无 hook 时默认:"房间描述" + roomtype。
// SubGameHooks.getRoomInfo = function (roomtype, type, tea) { /* ... */ };
// [B] getStarLimit(roomtype):获取星星场下限分数;无 hook 时默认:return 2001000。
// SubGameHooks.getStarLimit = function (roomtype) { /* ... */ };
// [B] getMult(roomtype, type):获取星星场倍数;无 hook 时默认:return 10000。
// SubGameHooks.getMult = function (roomtype, type) { /* ... */ };
// [B] getFullRoomInfo(roomtype):获取房间完整信息描述;无 hook 时默认:"房间描述" + roomtype。
// SubGameHooks.getFullRoomInfo = function (roomtype) { /* ... */ };
// [A] onOpenHelp(spid):打开帮助页面。
// SubGameHooks.onOpenHelp = function (spid) { /* ... */ };
// [A] onMainMenuScene():显示大厅主菜单界面。
// SubGameHooks.onMainMenuScene = function () { /* ... */ };
// [A] onCheckInput(_result):数字输入框确认回调。
// SubGameHooks.onCheckInput = function (_result) { /* ... */ };
// [A] onCreateRoom(_data):创建房间成功后触发。
// SubGameHooks.onCreateRoom = function (_data) { /* ... */ };
// [B] getLeaveLimit(roomtype):获取离场限制局数;无 hook 时默认:return 10。
// SubGameHooks.getLeaveLimit = function (roomtype) { /* ... */ };
// [A] onLocationInfo(_locationInfo):成功获取定位信息回调。
// SubGameHooks.onLocationInfo = function (_locationInfo) { /* ... */ };
// [A] onCreateDesk(roomtype):进入游戏界面创建牌桌之前触发。
// SubGameHooks.onCreateDesk = function (roomtype) { /* ... */ };
// [B] getVideoByRoomType(roomtype):获取该房型是否开启视频功能;无 hook 时默认:return 1。
// SubGameHooks.getVideoByRoomType = function (roomtype) { /* ... */ };
// [B] getRoomTopDescAry(roomtype):获取房间顶部描述文案数组;无 hook 时默认:["" + roomtype]。
// SubGameHooks.getRoomTopDescAry = function (roomtype) { /* ... */ };
// [A] getShareRoom(_msg):星星场房间列表数据。
// SubGameHooks.getShareRoom = function (_msg) { /* ... */ };
// [A] myExitRoom(seat):玩家自己退出房间。
// SubGameHooks.myExitRoom = function (seat) { /* ... */ };
// [A] changeSeat(seat1, seat2):接收到换座包触发。
// SubGameHooks.changeSeat = function (seat1, seat2) { /* ... */ };
// [A] onCloseVip():关闭 VIP 选项时调用。
// SubGameHooks.onCloseVip = function () { /* ... */ };
// [B] getRoomMode(roomtype):判断房间模式(0=非金币场);无 hook 时默认:return 0。
// SubGameHooks.getRoomMode = function (roomtype) { /* ... */ };
// [B] getMaxPlayerCount(roomtype):获取房间最大玩家数量;无 hook 时默认:return Game_Config.Max.PlayerCnt || 4。
// SubGameHooks.getMaxPlayerCount = function (roomtype) { /* ... */ };
// [D] updatePlayerInfoUI(targetSeat):更新指定座位玩家信息 UI;无 hook 时模板提供【可变人数】默认渲染
// (头像/昵称/分数随人数 2/3/4 贴合各玩家,布局见 01 的 Game_Modify.PLAYER_INFO_LAYOUT)。子游戏可用 hook 完全接管。
// SubGameHooks.updatePlayerInfoUI = function (targetSeat) { /* ... */ };
// ===== Game_Modify.*(桌面聊天/语音气泡,3) =====
// 以下接口无 hook 时,模板提供【可变人数】默认渲染(气泡随人数贴合各玩家,布局见 01 的 Game_Modify.BUBBLE_LAYOUT)。
// [D] ShowChat(seat, text):显示桌面文字聊天气泡(seat 为绝对座位)。
// SubGameHooks.ShowChat = function (seat, text) { /* ... */ };
// [D] gameui_play_voice(uiIndex):播放桌面语音气泡(uiIndex 为相对显示位序)。
// SubGameHooks.gameui_play_voice = function (uiIndex) { /* ... */ };
// [D] gameui_stop_voice(uiIndex):停止桌面语音气泡。
// SubGameHooks.gameui_stop_voice = function (uiIndex) { /* ... */ };
//—— 网络与对局(B 阶段接线)——
//平台入口只解包转交,业务全在 net/ 的 handler 里。
// ---------------------------------------------------------------------------
// 【room.mySeat 的唯一写入处】(SSOT)
//
// 为什么不能只在 appStart 里取一次:
// 平台 12_Logic.js:389 调 Game_Modify.appStart() 时,C_Player 还只是一个
// `var C_Player;`——`C_Player = new Player(-1)` 要到【同一个函数】的第 480 行才执行。
// 此刻 typeof C_Player === 'undefined',守卫为假、整个 appStart 空转;
// 之后玩家登录、07_Desk.js 里 C_Player.SetSeat() 拿到真座位,但 appStart 再也不会被调用,
// 于是 mySeat 永远停在 -1:发包带 seat:-1(服务端 check_player 必然拒绝)、
// StartWar 差异化下发找不到本座位而整包丢弃、SeatMap.toDisplay(-1) 抛错。
// ——而且【全程无报错】,这正是它能潜伏下来的原因。
//
// 因此改为:单一写入处 + 多个调用点。凡是平台已经确定座位的入口都同步一次,
// 不赌「所有路径都会经过某一个入口」。
//
// 幂等:可重复调用;C_Player 不可用或座位非法时【保持原值不写】——
// 绝不用 -1 覆盖已同步好的正确座位。
// ---------------------------------------------------------------------------
SubGameHooks._syncMySeat = function () {
if (typeof C_Player === 'undefined' || !C_Player) { return; }
var seat = C_Player.seat;
if (seat !== 0 && seat !== 1 && seat !== 2) { return; }
EQW_GameState.room.mySeat = seat;
};
// 对局推送。_msg 是完整包 {app, route, rpc, data}
// (平台 12_Logic.js 按 route 分流,只有子游戏路由 erqiwang 才到这里)
SubGameHooks._ReceiveData = function (_msg) {
if (!_msg) { return; }
EQW_Dispatcher.dispatch({ rpc: _msg.rpc, data: _msg.data });
};
// 开局。可能差异化下发(sendtype:1 + seatlist[])
// 【必须先同步座位】handleStartWar 要按 mySeat 在 seatlist[] 里认领自己那份
SubGameHooks.StartWar = function (_msg) {
SubGameHooks._syncMySeat();
EQW_ResyncHandler.handleStartWar(_msg);
};
// 断线重连:拿到 get_deskinfo 的全量快照
// (handleDeskinfo 内的 GameState.reset() 不清 room.mySeat,此处同步的值不会被冲掉)
SubGameHooks.Reconnect = function (_deskinfo) {
SubGameHooks._syncMySeat();
EQW_ResyncHandler.handleDeskinfo(_deskinfo);
};
// 重连但未开战:没有牌局数据,无需重画对局
SubGameHooks.ReconnectNoMakewar = function () {
SubGameHooks._syncMySeat();
console.log('[EQW] 重连(未开战)');
};
// 解散。_msg 即 Desk.deskfree,可能为 null(首局发牌前解散)
SubGameHooks.Free = function (_msg) {
EQW_ResultHandler.handleFree(_msg);
};
// 房间信息(房号 / 总局数 / roomtype 位串)
// 平台三条进桌路径(07_Desk.js:358 登录 / 510 建房 / 631 进房)都在 C_Player.SetSeat() 之后、
// Reconnect / StartWar / DeskInfo 之前调它——是覆盖面最广的一个同步点
SubGameHooks.setRoomDes = function (roomcode, asetcount, roomtype) {
SubGameHooks._syncMySeat();
EQW_ResyncHandler.handleSetRoomDes(roomcode, asetcount, roomtype);
};
// 换座(平台 07_Desk.js:186-191 已先把 C_Player.seat 改成新座位再回调这里)。
// 这是唯一「座位会中途变化、且不重走 setRoomDes」的路径,漏掉这里 mySeat 会静默过期
SubGameHooks.changeSeat = function (seat1, seat2) {
SubGameHooks._syncMySeat();
};
// 启动编排。B 阶段只记下自己的座位;UI 初始化留给 C 阶段
// 注意:此刻 C_Player 通常还没创建(见 _syncMySeat 上方注释),座位靠后续入口补齐
SubGameHooks.appStart = function () {
SubGameHooks._syncMySeat();
if (typeof Desk !== 'undefined' && Desk.roomtype) {
EQW_GameState.room.options = EQW_RoomOptions_Parse.parse(Desk.roomtype);
}
};
window.SubGameHooks = SubGameHooks;
if (typeof module !== 'undefined' && module.exports) { module.exports = SubGameHooks; }