Files
erqiwang_youle/client/tests/test_appstart_timing.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

151 lines
7.5 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.
// 【C-1 回归】room.mySeat 的同步必须跑在【真实的平台初始化时序】上。
//
// 平台 12_Logic.js 的 Logic.AppStart 跨 332–531 行,里面:
// line 389: Game_Modify.appStart() ← 此时 C_Player 只是 `var C_Player;`,未赋值
// line 480: C_Player = new Player(-1) ← 对象到这里才存在,seat 仍是 -1
// 之后玩家登录,07_Desk.js 的 C_Player.SetSeat(真座位),
// 但 Game_Modify.appStart() 【再也不会被调用】。
//
// 所以「只在 appStart 里读一次 C_Player.seat」必然拿不到座位,mySeat 永远停在 -1:
// 发包带 seat:-1(服务端 check_player 必拒,叫分/选主/埋牌/出牌全发不出去)、
// StartWar 差异化下发认领不到本座位而整包丢弃、SeatMap.toDisplay(-1) 抛错。
// ——全程无一处报错。本测试就是拿这条时序当尺子量。
//
// 本文件【不允许】用 `S.room.mySeat = N` 直接赋值来制造前提(其余 8 个测试文件那样做,
// 正是它们全部漏掉 C-1 的原因)。座位只能经由平台入口同步进来。
const { load, throws } = require('./_load');
const t = require('./_assert')();
global.window = global;
load('client/js/gameabc-framework/system/EventBus.js');
load('client/js/gameabc-framework/system/SpriteEventController.js');
load('client/js/01_SubGame/codes/state/Events.js');
load('client/js/01_SubGame/codes/state/GameState.js');
load('client/js/01_SubGame/codes/state/RoomOptions.js');
['DealHandler','CallHandler','MainHandler','BuryHandler','PlayHandler',
'QueryHandler','ReadyHandler','ResultHandler','ResyncHandler','FailHandler']
.forEach(h => load('client/js/01_SubGame/codes/net/handlers/' + h + '.js'));
load('client/js/01_SubGame/codes/net/Dispatcher.js');
load('client/js/01_SubGame/codes/core/SeatMap.js');
const sent = [];
global.RpcHelper = { sendRpc: (app, route, rpc, data) => sent.push({ rpc, data }) };
load('client/js/01_SubGame/codes/net/Rpc.js');
load('client/js/01_SubGame/codes/SubGameHooks.js');
const S = EQW_GameState;
// 平台 06_Player.js 的最小复刻:只有 seat 与 SetSeat 与本用例相关
function Player(seat) { this.seat = seat; }
Player.prototype.SetSeat = function (seat) { this.seat = seat; };
// ============================================================================
// 阶段 1:Logic.AppStart 第 389 行——C_Player 尚未 new
// ============================================================================
t.eq('前提:C_Player 此刻确实不存在', typeof global.C_Player, 'undefined');
t.eq('前提:mySeat 初始值是 -1', S.room.mySeat, -1);
let boom = false;
try { SubGameHooks.appStart(); } catch (e) { boom = true; }
t.eq('appStart 在 C_Player 缺席时不崩', boom, false);
t.eq('appStart 拿不到座位,mySeat 仍是 -1(不是缺陷,是时序事实)', S.room.mySeat, -1);
// 这个 -1 一旦被发包无声带出去就是 C-1 的第一重后果
t.eq('座位未同步时发包 fail-fast', throws(() => EQW_Rpc.zhunbei()), true);
t.eq('座位未同步时一个包都没发出去', sent.length, 0);
// ============================================================================
// 阶段 2:Logic.AppStart 第 480 行——C_Player = new Player(-1)
// ============================================================================
global.C_Player = new Player(-1);
SubGameHooks.setRoomDes(1234, 6, '00010');
t.eq('C_Player 存在但座位还是 -1 时,不写坏值', S.room.mySeat, -1);
t.eq('setRoomDes 该干的正事照干(解析 roomtype)', S.room.options.climb, 1);
// ============================================================================
// 阶段 3:玩家登录,07_Desk.js:320 C_Player.SetSeat(真座位)
// 紧接着 07_Desk.js:358 Game_Modify.setRoomDes(...)
// ============================================================================
C_Player.SetSeat(2);
t.eq('SetSeat 本身不写 GameState(平台不知道子游戏)', S.room.mySeat, -1);
SubGameHooks.setRoomDes(1234, 6, '00010');
t.eq('【核心】走完平台时序后 mySeat = 真座位', S.room.mySeat, 2);
// ---- 第一重后果解除:发包带真座位 ----
sent.length = 0;
EQW_Rpc.zhunbei();
t.eq('发包带上真座位', sent[0].data.seat, 2);
// ---- 第二重后果解除:StartWar 差异化下发能认领到自己那份 ----
const share = (seat, cards) => ({ seat, data: { count: 6, idx: 1, PlayerInfo: [0,0,0], step: 1, MyCards: cards } });
SubGameHooks.StartWar({ data: { deskwar: { sendtype: 1, seatlist: [
share(0, [10, 11]), share(1, [20, 21]), share(2, [30, 31])
] } } });
t.eq('StartWar 认领到本座位那份手牌', S.my.cards, [30, 31]);
// ---- 第三重后果解除:SeatMap 不再对 -1 抛错 ----
t.eq('SeatMap.toDisplay 用 mySeat 不抛错', throws(() => EQW_SeatMap.toDisplay(0, S.room.mySeat)), false);
t.eq('自己映射到 SELF', EQW_SeatMap.toDisplay(S.room.mySeat, S.room.mySeat), 'SELF');
// ============================================================================
// 阶段 4:换座(07_Desk.js:186-191 先改 C_Player.seat,再回调 changeSeat)
// 这是唯一「座位中途变化且不重走 setRoomDes」的路径
// ============================================================================
C_Player.SetSeat(0);
SubGameHooks.changeSeat(2, 0);
t.eq('换座后 mySeat 跟着变', S.room.mySeat, 0);
sent.length = 0;
EQW_Rpc.zhunbei();
t.eq('换座后发包带新座位', sent[0].data.seat, 0);
// ============================================================================
// 阶段 5:断线重连(07_Desk.js:320 SetSeat → 358 setRoomDes → 423 Reconnect)
// 这里单独校验 Reconnect 自身也同步,不依赖前面的入口是否跑过
// ============================================================================
S.room.mySeat = -1; // 人为退回未同步态,验证 Reconnect 能独立自愈
C_Player.SetSeat(1);
SubGameHooks.Reconnect({ count: 6, idx: 3, PlayerInfo: [0,0,0], step: 1, MyCards: [5,6] });
t.eq('Reconnect 自身也同步座位', S.room.mySeat, 1);
t.eq('Reconnect 的 reset() 不会把刚同步的座位清掉', S.room.mySeat, 1);
t.eq('Reconnect 正常填状态', S.my.cards, [5, 6]);
S.room.mySeat = -1;
C_Player.SetSeat(2);
SubGameHooks.ReconnectNoMakewar();
t.eq('ReconnectNoMakewar 也同步座位', S.room.mySeat, 2);
S.room.mySeat = -1;
C_Player.SetSeat(0);
SubGameHooks.StartWar({ data: { deskwar: { sendtype: 1, seatlist: [
share(0, [7, 8]), share(1, [9]), share(2, [1]) ] } } });
t.eq('StartWar 自身也同步座位', S.room.mySeat, 0);
t.eq('StartWar 同步后认领正确的那份', S.my.cards, [7, 8]);
// ============================================================================
// 幂等 / 防倒退:_syncMySeat 可重复调用,且绝不用坏值覆盖好值
// ============================================================================
SubGameHooks._syncMySeat();
SubGameHooks._syncMySeat();
t.eq('重复同步结果不变(幂等)', S.room.mySeat, 0);
C_Player.SetSeat(-1); // 平台 new Player(-1) 的初值又冒出来
SubGameHooks._syncMySeat();
t.eq('座位为 -1 时保持原值,不倒退', S.room.mySeat, 0);
C_Player.SetSeat(3);
SubGameHooks._syncMySeat();
t.eq('座位越界时保持原值,不倒退', S.room.mySeat, 0);
C_Player.SetSeat('1');
SubGameHooks._syncMySeat();
t.eq('座位是字符串时保持原值,不倒退', S.room.mySeat, 0);
delete global.C_Player;
boom = false;
try { SubGameHooks._syncMySeat(); } catch (e) { boom = true; }
t.eq('C_Player 消失时同步不崩', boom, false);
t.eq('C_Player 消失时保持原值', S.room.mySeat, 0);
process.exit(t.done('appstart_timing') ? 0 : 1);