二七王:平台接线与加载段

SubGameHooks 只解包转交、不写业务:_ReceiveData 收到的是完整包,
按 rpc 分发;Free 的参数是平台已取出的 deskfree、可能为 null;
appStart 只记座位与房间选项,UI 初始化留给 C 阶段。

index.html 里 handler 排在 Dispatcher 之前——后者注册时要引用全部 handler。

另加 test_index_load_order.js:按 index.html 实际 script 顺序自动加载
codes/ 全部文件 + 3 个框架依赖,验证顺序无遗漏、无环,并断言
EQW_Dispatcher 路由表确已注册,作为长期守卫。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 20:16:45 +08:00
co-authored by Claude Opus 5
parent dac4a94b38
commit 22e43cf2e6
4 changed files with 225 additions and 1 deletions
+18 -1
View File
@@ -236,7 +236,24 @@
<script type="text/javascript" src="js/01_SubGame/codes/core/SpriteIndex.js"></script>
<!-- 5.4 布局求解 -->
<script type="text/javascript" src="js/01_SubGame/codes/ui/LayoutSolver.js"></script>
<!-- 5.5 平台接入(须在依赖之后、三文件转发壳之前) -->
<!-- 5.5 对局状态与事件 -->
<script type="text/javascript" src="js/01_SubGame/codes/state/Events.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/state/GameState.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/state/RoomOptions.js"></script>
<!-- 5.6 网络:handler 在前,Dispatcher 在后(它注册时要引用全部 handler) -->
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/FailHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/DealHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/CallHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/MainHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/BuryHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/PlayHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/QueryHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/ReadyHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/ResultHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/handlers/ResyncHandler.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/Dispatcher.js"></script>
<script type="text/javascript" src="js/01_SubGame/codes/net/Rpc.js"></script>
<!-- 5.7 平台接入(须在依赖之后、三文件转发壳之前) -->
<script type="text/javascript" src="js/01_SubGame/codes/SubGameHooks.js"></script>
<!-- 子游戏 Hooks 实现(转发壳三文件转发到此;须在依赖的 controllers/handlers 之后、三文件之前)-->
@@ -246,5 +246,50 @@ SubGameHooks.gamemydraw = function (gameid, spid, times, timelong, no2, no3, no4
// [D] gameui_stop_voice(uiIndex):停止桌面语音气泡。
// SubGameHooks.gameui_stop_voice = function (uiIndex) { /* ... */ };
//—— 网络与对局(B 阶段接线)——
//平台入口只解包转交,业务全在 net/ 的 handler 里。
// 对局推送。_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[])
SubGameHooks.StartWar = function (_msg) {
EQW_ResyncHandler.handleStartWar(_msg);
};
// 断线重连:拿到 get_deskinfo 的全量快照
SubGameHooks.Reconnect = function (_deskinfo) {
EQW_ResyncHandler.handleDeskinfo(_deskinfo);
};
// 重连但未开战:没有牌局数据,无需重画对局
SubGameHooks.ReconnectNoMakewar = function () {
console.log('[EQW] 重连(未开战)');
};
// 解散。_msg 即 Desk.deskfree,可能为 null(首局发牌前解散)
SubGameHooks.Free = function (_msg) {
EQW_ResultHandler.handleFree(_msg);
};
// 房间信息(房号 / 总局数 / roomtype 位串)
SubGameHooks.setRoomDes = function (roomcode, asetcount, roomtype) {
EQW_ResyncHandler.handleSetRoomDes(roomcode, asetcount, roomtype);
};
// 启动编排。B 阶段只记下自己的座位;UI 初始化留给 C 阶段
SubGameHooks.appStart = function () {
if (typeof C_Player !== 'undefined' && typeof C_Player.seat === 'number') {
EQW_GameState.room.mySeat = C_Player.seat;
}
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; }
+53
View File
@@ -0,0 +1,53 @@
// SubGameHooks 接线:平台入口 → 新架构
const { load } = require('./_load');
const t = require('./_assert')();
global.window = global; // 转发壳用 window.SubGameHooks 判定
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/SubGameHooks.js');
const S = EQW_GameState;
// ---- _ReceiveData:完整包 → 分发 ----
S.reset(); S.room.mySeat = 0;
SubGameHooks._ReceiveData({ app: 'youle', route: 'erqiwang', rpc: 'fapai',
data: { success: true, asetidx: 1, asetcount: 6,
cards: [1,2], seat: 0, countdown: 15 } });
t.eq('_ReceiveData 分发到 handler', S.my.cards, [1,2]);
// ---- setRoomDes ----
SubGameHooks.setRoomDes(123, 12, '00010');
t.eq('setRoomDes 解析选项', S.room.options.climb, 1);
// ---- Free:null 不崩 ----
let boom = false;
try { SubGameHooks.Free(null); } catch (e) { boom = true; }
t.eq('Free(null) 不崩', boom, false);
// ---- Reconnect ----
S.reset(); S.room.mySeat = 1;
SubGameHooks.Reconnect({ count: 6, idx: 3, PlayerInfo: [0,0,0], step: 1, MyCards: [5,6] });
t.eq('Reconnect 填状态', S.my.cards, [5,6]);
t.eq('Reconnect 填局数', S.room.asetIdx, 3);
// ---- 畸形入参一律不崩 ----
boom = false;
try {
SubGameHooks._ReceiveData(null);
SubGameHooks._ReceiveData({});
SubGameHooks.StartWar(null);
SubGameHooks.Reconnect(null);
SubGameHooks.setRoomDes();
} catch (e) { boom = true; }
t.eq('畸形入参不崩', boom, false);
process.exit(t.done('hooks') ? 0 : 1);
+109
View File
@@ -0,0 +1,109 @@
// index.html 加载顺序自检(task-13 步骤6守卫):
// 按 index.html 里 <script src> 的实际先后顺序,抽取「codes/ 全部文件 + 它们依赖的
// 3 个框架文件(EventBus/SpriteEventController/AlignmentUtils)」,依次用 vm 加载,
// 确认:1) 无异常抛出(顺序错了会因引用未定义全局而报错)
// 2) 关键全局全部就绪
// 3) EQW_Dispatcher 的路由表确已注册(hasHandler('chupai1') === true)
// 这份顺序即是 index.html 的真实加载顺序,不是我们另行假设的一份。
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const t = require('./_assert')();
const ROOT = path.resolve(__dirname, '..', '..');
const indexHtml = fs.readFileSync(path.join(ROOT, 'client', 'index.html'), 'utf8');
// 抽取全部本地 <script src="..."> 路径,按文档出现顺序
const scriptRe = /<script[^>]*\ssrc="([^"]+)"[^>]*><\/script>/g;
const allSrcs = [];
let m;
while ((m = scriptRe.exec(indexHtml)) !== null) {
allSrcs.push(m[1]);
}
t.eq('index.html 至少解析出脚本标签', allSrcs.length > 0, true);
// 只保留:codes/ 下全部文件 + 3 个框架依赖文件,其余(vendor/引擎/大厅平台代码等)
// 无法在 Node 里单独跑,本自检不覆盖,留给「浏览器人工验证」。
const NEEDED_FRAMEWORK = [
'js/gameabc-framework/system/EventBus.js',
'js/gameabc-framework/system/SpriteEventController.js',
'js/gameabc-framework/ui/AlignmentUtils.js'
];
const relevant = allSrcs.filter(function (src) {
return NEEDED_FRAMEWORK.indexOf(src) !== -1 || src.indexOf('js/01_SubGame/codes/') === 0;
});
// 期望齐全的清单:本任务新增/相关的 state/ net/ 文件,一个都不能漏
const EXPECT_PRESENT = [
'js/01_SubGame/codes/state/Events.js',
'js/01_SubGame/codes/state/GameState.js',
'js/01_SubGame/codes/state/RoomOptions.js',
'js/01_SubGame/codes/net/handlers/FailHandler.js',
'js/01_SubGame/codes/net/handlers/DealHandler.js',
'js/01_SubGame/codes/net/handlers/CallHandler.js',
'js/01_SubGame/codes/net/handlers/MainHandler.js',
'js/01_SubGame/codes/net/handlers/BuryHandler.js',
'js/01_SubGame/codes/net/handlers/PlayHandler.js',
'js/01_SubGame/codes/net/handlers/QueryHandler.js',
'js/01_SubGame/codes/net/handlers/ReadyHandler.js',
'js/01_SubGame/codes/net/handlers/ResultHandler.js',
'js/01_SubGame/codes/net/handlers/ResyncHandler.js',
'js/01_SubGame/codes/net/Dispatcher.js',
'js/01_SubGame/codes/net/Rpc.js',
'js/01_SubGame/codes/SubGameHooks.js'
];
EXPECT_PRESENT.forEach(function (src) {
t.eq('index.html 含脚本 ' + src, allSrcs.indexOf(src) !== -1, true);
});
// 相对顺序断言:handler 全部排在 Dispatcher 之前;state/ 排在 net/handlers/ 之前
const idx = {};
relevant.forEach(function (src, i) { idx[src] = i; });
var dispatcherIdx = idx['js/01_SubGame/codes/net/Dispatcher.js'];
[
'js/01_SubGame/codes/net/handlers/FailHandler.js',
'js/01_SubGame/codes/net/handlers/DealHandler.js',
'js/01_SubGame/codes/net/handlers/CallHandler.js',
'js/01_SubGame/codes/net/handlers/MainHandler.js',
'js/01_SubGame/codes/net/handlers/BuryHandler.js',
'js/01_SubGame/codes/net/handlers/PlayHandler.js',
'js/01_SubGame/codes/net/handlers/QueryHandler.js',
'js/01_SubGame/codes/net/handlers/ReadyHandler.js',
'js/01_SubGame/codes/net/handlers/ResultHandler.js',
'js/01_SubGame/codes/net/handlers/ResyncHandler.js'
].forEach(function (h) {
t.eq('handler 排在 Dispatcher 之前: ' + h, idx[h] < dispatcherIdx, true);
});
t.eq('state/ 排在 net/handlers/ 之前', idx['js/01_SubGame/codes/state/GameState.js'] < idx['js/01_SubGame/codes/net/handlers/FailHandler.js'], true);
t.eq('SubGameHooks 排在 Dispatcher 之后', idx['js/01_SubGame/codes/SubGameHooks.js'] > dispatcherIdx, true);
// 依次实际加载,确认不抛异常,且顺序自洽
global.window = global;
let loadErr = null;
try {
relevant.forEach(function (src) {
const abs = path.join(ROOT, 'client', src);
const code = fs.readFileSync(abs, 'utf8');
vm.runInThisContext(code, { filename: src });
});
} catch (e) {
loadErr = e;
}
t.eq('按 index.html 顺序加载无异常', loadErr ? (loadErr.stack || String(loadErr)) : null, null);
// 关键全局就绪
[
'EQW_Events', 'EQW_GameState', 'EQW_RoomOptions_Parse', 'EQW_Dispatcher', 'EQW_Rpc',
'EQW_DealHandler', 'EQW_CallHandler', 'EQW_MainHandler', 'EQW_BuryHandler', 'EQW_PlayHandler',
'EQW_QueryHandler', 'EQW_ReadyHandler', 'EQW_ResultHandler', 'EQW_ResyncHandler', 'EQW_FailHandler',
'SubGameHooks'
].forEach(function (name) {
t.eq('全局就绪: ' + name, typeof global[name] !== 'undefined', true);
});
// 路由表确已注册
if (typeof global.EQW_Dispatcher !== 'undefined') {
t.eq("EQW_Dispatcher.hasHandler('chupai1')", global.EQW_Dispatcher.hasHandler('chupai1'), true);
}
process.exit(t.done('index_load_order') ? 0 : 1);