Files
erqiwang_youle/client/js/01_SubGame/codes/net/Rpc.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

124 lines
5.3 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.
///////////////////////////////////////////////////////////////
////////// EQW_Rpc: 语义化发包(一操作一方法)/////////////////
///////////////////////////////////////////////////////////////
// 前端 04 §1:业务不直接拼包,走两层封装——
// 业务 → EQW_Rpc.xxx(意图) → RpcHelper(注入平台字段)→ 引擎发送
//
// 【route 必须是 erqiwang】RpcHelper.sendGameRpc 预设 route="room"(平台房间模块),
// 用它包会被路由到平台、永远到不了子游戏 mod.js,而且不会有任何报错。
//
// 【请求包只带意图,不带结论】(前端 04 §1 / 服务端 04 §8)
// 可以带:操作类型、目标标识(牌 id / 花色 / 提示类型)、座位号(仅供一致性校验)。
// 禁止带:分数、番数、判定结果、阶段推进指令、nextSeat、handCards。
// 前端 shared/ 的计算只用于本地提示与预校验,【结果不回传】。
// 一句话:前端能自己推出来的东西,就是玩家能改的东西——这是防作弊的根本。
//
// 本文件只做【形状】校验(牌 id 范围/去重/张数),不做【规则】校验
// (这手牌能不能出、叫分够不够低由服务端裁定)。形状不合法就 fail-fast 抛错,
// 不发出残缺包——服务端会按 PARAM 拒绝,但让错误暴露在最近处更省事。
var EQW_Rpc = EQW_Rpc || {
APP: 'youle',
ROUTE: 'erqiwang',
//—— 内部:取自己的座位(fail-fast)——
//room.mySeat 由 SubGameHooks._syncMySeat 从平台 C_Player.seat 同步,初始值是 -1。
//【为什么不能透传】seat:-1 发出去,服务端 check_player 必然返回 falsy → 每个操作都回 ERR.PLAYER,
//叫分/选主/埋牌/出牌全部发不出去,而前端一声不吭。无声透传正是这类缺陷能长期潜伏的原因,
//故与本文件其余形状校验一样在最近处显式失败。
_seat: function () {
var seat = EQW_GameState.room.mySeat;
if (seat !== 0 && seat !== 1 && seat !== 2) {
throw new Error('[EQW_Rpc] 座位未同步: ' + seat +
'(须为 0/1/2;检查 SubGameHooks._syncMySeat 是否已由平台入口调用)');
}
return seat;
},
//—— 内部:发送 ——
_send: function (rpc, data) {
var pack = { seat: this._seat() };
var key;
for (key in data) {
if (data.hasOwnProperty(key)) {
pack[key] = data[key];
}
}
RpcHelper.sendRpc(this.APP, this.ROUTE, rpc, pack);
},
//—— 内部:牌 id 列表形状校验(协议 §0.2)——
//必须是非空数组,元素是 0–107 的整数,互不重复
_checkCards: function (cards, expectCount) {
if (Object.prototype.toString.call(cards) !== '[object Array]' || cards.length === 0) {
throw new Error('[EQW_Rpc] cards 必须是非空数组');
}
if (typeof expectCount === 'number' && cards.length !== expectCount) {
throw new Error('[EQW_Rpc] cards 张数应为 ' + expectCount + ',实际 ' + cards.length);
}
var seen = {};
for (var i = 0; i < cards.length; i++) {
var c = cards[i];
if (typeof c !== 'number' || c !== Math.floor(c) || c < 0 || c > 107) {
throw new Error('[EQW_Rpc] 非法牌 id: ' + c + '(须为 0–107 的整数)');
}
if (seen[c]) { throw new Error('[EQW_Rpc] 牌 id 重复: ' + c); }
seen[c] = true;
}
},
//—— 叫分(call: 5–70 的 5 的倍数;0 = 不叫)——
jiaofen: function (call) {
if (typeof call !== 'number' || call !== Math.floor(call)) {
throw new Error('[EQW_Rpc] call 必须是整数');
}
if (call !== 0 && (call < 5 || call > 70 || call % 5 !== 0)) {
throw new Error('[EQW_Rpc] 非法叫分: ' + call + '(须为 0 或 5–70 的 5 的倍数)');
}
this._send('jiaofen', { call: call });
},
//—— 投降(仅 70 分坐庄、选主阶段;合法性由服务端裁定)——
touxiang: function () {
this._send('touxiang', {});
},
//—— 选主(flower: 1方块 2梅花 3红心 4黑桃)——
xuanzhu: function (flower) {
if (typeof flower !== 'number' || flower < 1 || flower > 4) {
throw new Error('[EQW_Rpc] 非法花色: ' + flower + '(须为 1–4)');
}
this._send('xuanzhu', { flower: flower });
},
//—— 埋牌(恒 8 张)——
maipai: function (cards) {
this._checkCards(cards, 8);
this._send('maipai', { cards: cards });
},
//—— 出牌 ——
chupai: function (cards) {
this._checkCards(cards);
this._send('chupai', { cards: cards });
},
//—— 明牌(查看他家未出主牌)——
mingpai: function () {
this._send('mingpai', {});
},
//—— 出牌提示(tip: 1踩 2没分 3有分)——
tishi: function (tip) {
if (tip !== 1 && tip !== 2 && tip !== 3) {
throw new Error('[EQW_Rpc] 非法提示类型: ' + tip + '(须为 1踩 2没分 3有分)');
}
this._send('tishi', { tip: tip });
},
//—— 准备 ——
zhunbei: function () {
this._send('zhunbei', {});
}
};