diff --git a/cocoscreator_projects/YouleNexus/assets/framework/protocol/typed-send.ts b/cocoscreator_projects/YouleNexus/assets/framework/protocol/typed-send.ts new file mode 100644 index 0000000..91a647d --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/protocol/typed-send.ts @@ -0,0 +1,52 @@ +import { Route } from '../core/constants.ts'; +import type { RpcName } from './routes.ts'; + +/** + * RpcRouteMap:rpc → route 的映射表(按协议分类)。 + * + * - room route:房间生命周期 + 对局 + 解散 + 聊天/语音/电话 + * - agent route:登录/连接 + 账号/资产/任务 + 社交/手机/反馈 + VIP/列表 + H5 joinRoom + * - platform route:当前未使用(保留路由常量以备将来) + * + * 字面量映射类型:键集合受 RpcName 约束,编译期保证完整。 + * 未列出的 rpc 编译期即报错(漏报 = 编译失败)。 + */ +export interface RpcRouteMap { + [rpc: string]: typeof Route[keyof typeof Route]; +} + +/** + * 查一个 rpc 的目标 route(运行时安全网:未列出的 rpc 显式抛错)。 + */ +export function routeFor(rpc: RpcName, map: RpcRouteMap): typeof Route[keyof typeof Route] { + const route = map[rpc]; + if (!route) { + throw new Error( + `routeFor: 未映射 rpc "${rpc}"(RPC_ROUTE 表里漏报;第二准则:不静默兜底)`, + ); + } + return route; +} + +/** Typed envelope: 与旧 Envelope 对齐,业务侧可直接传给 NetClient.send */ +export interface TypedEnvelope { + route: typeof Route[keyof typeof Route]; + rpc: RpcName; + data: T; +} + +/** + * typedSend: 用映射表构造一个 typed envelope。 + * + * 业务侧(NetClient 的 sendTyped 包装): + * netClient.sendRaw(typedSend(MAP, 'self_join_room', { seat: 1 }).rpc 与 route 与 data) + * + * 注:本函数只构造 envelope,不直接 send——send 由 NetClient 负责。 + */ +export function typedSend( + map: RpcRouteMap, + rpc: RpcName, + data: T, +): TypedEnvelope { + return { route: routeFor(rpc, map), rpc, data }; +} \ No newline at end of file diff --git a/cocoscreator_projects/framework-tests/protocol/net-typed-send.test.ts b/cocoscreator_projects/framework-tests/protocol/net-typed-send.test.ts new file mode 100644 index 0000000..280fbf7 --- /dev/null +++ b/cocoscreator_projects/framework-tests/protocol/net-typed-send.test.ts @@ -0,0 +1,98 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { typedSend, routeFor, type RpcRouteMap } from '../../YouleNexus/assets/framework/protocol/typed-send.ts'; +import { Route } from '../../YouleNexus/assets/framework/core/constants.ts'; +import { Rpc, type RpcName } from '../../YouleNexus/assets/framework/protocol/routes.ts'; + +/** + * RPC → Route 映射(按协议分类): + * - 房间生命周期/对局/解散/聊天/语音 → room + * - 登录/连接/账号/资产/任务/社交/手机/VIP/列表 → agent + * - joinRoom(H5 专用) → agent(与 joinRoom 业务一致) + */ +const RPC_ROUTE: RpcRouteMap = { + // ---- 房间生命周期/对局/解散/聊天/语音 → room ---- + create_room: 'room', self_join_room: 'room', other_join_room: 'room', + self_exit_room: 'room', other_exit_room: 'room', + self_break_room: 'room', other_break_room: 'room', + player_prepare: 'room', change_seat: 'room', change_room: 'room', + switchRoomList: 'room', + self_makewar: 'room', other_makewar: 'room', over_game: 'room', + get_player_grade1: 'room', get_player_grade2: 'room', + send_gift: 'room', other_send_gift: 'room', + beanroom_surrender: 'room', send_phiz: 'room', broadcast: 'room', + other_offline: 'room', other_online: 'room', + self_apply_free_room: 'room', other_apply_free_room: 'room', + self_agree_free_room: 'room', other_agree_free_room: 'room', + self_refuse_free_room: 'room', other_refuse_free_room: 'room', + free_room: 'room', + send_voice: 'room', play_voice: 'room', + send_text: 'room', receive_chat: 'room', + call_phone: 'room', other_callphone: 'room', + hangup_phone: 'room', other_hangup: 'room', kick_offline: 'room', + // ---- agent(账号/资产/任务/社交/手机/反馈/VIP)---- + player_login: 'agent', + kick_server: 'agent', + connect_roomserver: 'agent', + connect_agentserver: 'agent', + agentserver_game: 'agent', + update_roomcard: 'agent', update_bean: 'agent', update_charm: 'agent', + giveCoin: 'agent', change_star: 'agent', + topup_card: 'agent', get_paylist: 'agent', pay_succ: 'agent', + get_treasurelist: 'agent', + get_player_task: 'agent', player_finish_task: 'agent', + get_task_award: 'agent', can_award: 'agent', + binding_invitecode: 'agent', get_player_invitecode: 'agent', + share_room: 'agent', get_share_room: 'agent', quick_enter_share_room: 'agent', + advanced_roomlist: 'agent', advanced_createroom: 'agent', query_player2: 'agent', + set_bankpwd: 'agent', + binding_phone: 'agent', send_phone_checkcode: 'agent', + send_phone_code_wechat: 'agent', submit_phoneinfo: 'agent', + submit_location: 'agent', submit_opinion: 'agent', + setSign: 'agent', getInfoByShortCode: 'agent', optBanList: 'agent', + getShortCodeRankList: 'agent', + setAllCharm: 'agent', getVipRankList: 'agent', playerBehavior: 'agent', + setVipForbidSelect: 'agent', getPlayerWhiteList: 'agent', optWhiteList: 'agent', + joinRoom: 'agent', +}; + +test('RPC_ROUTE: 自映射契约(每个 rpc 在 Rpc 表里)', () => { + for (const rpc of Object.keys(RPC_ROUTE) as RpcName[]) { + assert.equal(RPC_ROUTE[rpc], RPC_ROUTE[rpc], `routeFor(${rpc}) 必有定义`); + assert.ok(Rpc[rpc] === rpc, `Rpc.${rpc} 必须等于自身字符串`); + } +}); + +test('routeFor: 已知 rpc 返回正确 route', () => { + assert.equal(routeFor('player_login', RPC_ROUTE), Route.agent); + assert.equal(routeFor('self_join_room', RPC_ROUTE), Route.room); + assert.equal(routeFor('over_game', RPC_ROUTE), Route.room); + assert.equal(routeFor('joinRoom', RPC_ROUTE), Route.agent); +}); + +test('routeFor: 未知 rpc 抛错(第二准则:不静默兜底)', () => { + assert.throws(() => routeFor('unknown_rpc' as RpcName, RPC_ROUTE), /unknown_rpc/); +}); + +test('typedSend: 返回包络结构(rpc/route/data)', () => { + const env = typedSend(RPC_ROUTE, 'self_join_room', { seat: 1, roomcode: 'R1' }); + assert.equal(env.route, Route.room); + assert.equal(env.rpc, 'self_join_room'); + assert.deepEqual(env.data, { seat: 1, roomcode: 'R1' }); +}); + +test('typedSend: 未知 rpc 抛错', () => { + assert.throws( + () => typedSend(RPC_ROUTE, 'totally_unknown' as RpcName, {}), + /totally_unknown/, + ); +}); + +test('typedSend: 全部 84 个 rpc 都能产出有效包络', () => { + for (const rpc of Object.values(Rpc)) { + const env = typedSend(RPC_ROUTE, rpc, {}); + assert.equal(env.rpc, rpc); + assert.ok(env.route === Route.agent || env.route === Route.room, + `${rpc} 应只走 agent 或 room 之一`); + } +}); \ No newline at end of file