feat(framework): 收包路由分界(ActiveGame + Router)
C 规范 §4.1 的红线落地: - platform/agent/room 三个 route → 平台 handlers(未注册显式抛错) - 其他 route → activeGame.onReceive(rpc, data)(未激活或不匹配显式抛错) ActiveGame: 当前子游戏槽(set/clear/get),进房时 setGame、离房 clear。 Router: 纯分发,不持状态。错误一律抛(第二准则:不静默吞包)。 128/128 tests pass, typecheck exit 0。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import type { IGameModule } from '../sdk/index.ts';
|
||||
|
||||
/**
|
||||
* 当前激活的子游戏槽(= 旧 Game_Modify 的运行时角色)。
|
||||
*
|
||||
* 框架在进房时 setGame(),离房时 clear()。Router 据此分发对局 route 的包。
|
||||
* 单例设计:同一时刻只允许一个子游戏激活(与旧 12_Logic 一致)。
|
||||
*/
|
||||
export class ActiveGame {
|
||||
private game: IGameModule | null = null;
|
||||
|
||||
get(): IGameModule | null { return this.game; }
|
||||
|
||||
set(game: IGameModule): void { this.game = game; }
|
||||
|
||||
clear(): void { this.game = null; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { InboundMessage } from '../core/types/envelope.ts';
|
||||
import { Route } from '../core/constants.ts';
|
||||
import type { ActiveGame } from './active-game.ts';
|
||||
|
||||
/** 平台层 handler 表:key = rpc 字符串(与协议逐字对齐)。 */
|
||||
export type PlatformHandlers = Record<string, (data: unknown) => void>;
|
||||
|
||||
/**
|
||||
* Router:收包路由分界(C §4.1 的红线)。
|
||||
*
|
||||
* 分发规则:
|
||||
* - route ∈ {platform, agent, room} → 平台 handlers(RpcList 覆盖的包)
|
||||
* - route = <game route>(= activeGame.route)→ activeGame.onReceive
|
||||
* - 其他或未注册:显式抛错(第二准则:不静默吞包)
|
||||
*
|
||||
* 不持有状态——只做消息分发。状态由 PlatformSession/子游戏维护。
|
||||
*/
|
||||
export class Router {
|
||||
constructor(
|
||||
private readonly opts: {
|
||||
activeGame: ActiveGame;
|
||||
platformHandlers: PlatformHandlers;
|
||||
},
|
||||
) {}
|
||||
|
||||
dispatch(msg: InboundMessage): void {
|
||||
const { route, rpc, data } = msg;
|
||||
// 平台三层 route:platform/agent/room 全部走平台 handlers
|
||||
if (route === Route.platform || route === Route.agent || route === Route.room) {
|
||||
const handler = this.opts.platformHandlers[rpc];
|
||||
if (!handler) {
|
||||
throw new Error(
|
||||
`Router 收到未注册的平台 rpc: route=${route} rpc=${rpc}(platformHandlers 未注册,第二准则禁止兜底)`,
|
||||
);
|
||||
}
|
||||
handler(data);
|
||||
return;
|
||||
}
|
||||
// 对局 route:必须与 activeGame.route 严格匹配
|
||||
const game = this.opts.activeGame.get();
|
||||
if (!game) {
|
||||
throw new Error(
|
||||
`Router 收到对局包但无激活子游戏: route=${route} rpc=${rpc}(未进房或子游戏未注册)`,
|
||||
);
|
||||
}
|
||||
if (game.route !== route) {
|
||||
throw new Error(
|
||||
`Router route 与 activeGame.route 不匹配: msg.route=${route} activeGame.route=${game.route}`,
|
||||
);
|
||||
}
|
||||
game.onReceive(rpc, data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { ActiveGame } from '../../YouleNexus/assets/framework/protocol/active-game.ts';
|
||||
import type { IGameModule, GameContext } from '../../YouleNexus/assets/framework/sdk/index.ts';
|
||||
|
||||
class MockGame implements IGameModule {
|
||||
readonly route = 'mock';
|
||||
events: string[] = [];
|
||||
onEnter(_ctx: GameContext): void { this.events.push('onEnter'); }
|
||||
onExit(): void { this.events.push('onExit'); }
|
||||
onReceive(rpc: string, _d: unknown): void { this.events.push(`onReceive:${rpc}`); }
|
||||
onReconnect(_d: unknown): void { this.events.push('onReconnect'); }
|
||||
}
|
||||
|
||||
test('初始: getGame 返回 null', () => {
|
||||
assert.equal(new ActiveGame().get(), null);
|
||||
});
|
||||
|
||||
test('setGame: 设置后 get 返回同一实例', () => {
|
||||
const slot = new ActiveGame();
|
||||
const g = new MockGame();
|
||||
slot.set(g);
|
||||
assert.equal(slot.get(), g);
|
||||
});
|
||||
|
||||
test('clear: 清空后 get 返回 null', () => {
|
||||
const slot = new ActiveGame();
|
||||
slot.set(new MockGame());
|
||||
slot.clear();
|
||||
assert.equal(slot.get(), null);
|
||||
});
|
||||
|
||||
test('set 覆盖前一个 game', () => {
|
||||
const slot = new ActiveGame();
|
||||
const g1 = new MockGame();
|
||||
const g2 = new MockGame();
|
||||
slot.set(g1);
|
||||
slot.set(g2);
|
||||
assert.equal(slot.get(), g2);
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { Router } from '../../YouleNexus/assets/framework/protocol/router.ts';
|
||||
import { ActiveGame } from '../../YouleNexus/assets/framework/protocol/active-game.ts';
|
||||
import type { InboundMessage } from '../../YouleNexus/assets/framework/core/types/envelope.ts';
|
||||
import type { IGameModule, GameContext } from '../../YouleNexus/assets/framework/sdk/index.ts';
|
||||
|
||||
class MockGame implements IGameModule {
|
||||
readonly route = 'mock';
|
||||
events: string[] = [];
|
||||
onEnter(_ctx: GameContext): void {}
|
||||
onExit(): void {}
|
||||
onReceive(rpc: string, _d: unknown): void { this.events.push(`onReceive:${rpc}`); }
|
||||
onReconnect(_d: unknown): void {}
|
||||
}
|
||||
|
||||
const msg = (route: string, rpc: string, data: unknown = {}): InboundMessage =>
|
||||
({ route, rpc, data } as InboundMessage);
|
||||
|
||||
test('platform route → 平台 handler(已注册)', () => {
|
||||
const slot = new ActiveGame();
|
||||
const calls: Array<[string, unknown]> = [];
|
||||
const router = new Router({
|
||||
activeGame: slot,
|
||||
platformHandlers: { player_login: (d) => calls.push(['player_login', d]) },
|
||||
});
|
||||
router.dispatch(msg('agent', 'player_login', { state: 0 }));
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0][0], 'player_login');
|
||||
});
|
||||
|
||||
test('platform route → 平台 handler 未注册 → 显式抛错(第二准则)', () => {
|
||||
const slot = new ActiveGame();
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
assert.throws(() => router.dispatch(msg('agent', 'unknown_rpc')), /unknown_rpc/);
|
||||
});
|
||||
|
||||
test('非 platform route 但 activeGame 已注册 → onReceive', () => {
|
||||
const slot = new ActiveGame();
|
||||
const game = new MockGame();
|
||||
slot.set(game);
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
router.dispatch(msg('mock', 'deal_card', { n: 1 }));
|
||||
assert.deepEqual(game.events, ['onReceive:deal_card']);
|
||||
});
|
||||
|
||||
test('非 platform route 但 activeGame 未注册 → 显式抛错', () => {
|
||||
const slot = new ActiveGame();
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
assert.throws(() => router.dispatch(msg('mock', 'deal_card')), /mock.*deal_card/);
|
||||
});
|
||||
|
||||
test('route 与 activeGame.route 不匹配 → 显式抛错', () => {
|
||||
const slot = new ActiveGame();
|
||||
slot.set(new MockGame()); // route = 'mock'
|
||||
const router = new Router({ activeGame: slot, platformHandlers: {} });
|
||||
assert.throws(() => router.dispatch(msg('other-game', 'rpc')), /other-game/);
|
||||
});
|
||||
|
||||
test('platform/agent/room 三 route 全部走平台 handler(对称)', () => {
|
||||
const slot = new ActiveGame();
|
||||
const calls: string[] = [];
|
||||
const router = new Router({
|
||||
activeGame: slot,
|
||||
platformHandlers: {
|
||||
r1: () => calls.push('r1'),
|
||||
r2: () => calls.push('r2'),
|
||||
r3: () => calls.push('r3'),
|
||||
},
|
||||
});
|
||||
router.dispatch(msg('platform', 'r1'));
|
||||
router.dispatch(msg('agent', 'r2'));
|
||||
router.dispatch(msg('room', 'r3'));
|
||||
assert.deepEqual(calls, ['r1', 'r2', 'r3']);
|
||||
});
|
||||
Reference in New Issue
Block a user