feat: integrate platform room entry, UI migration and erqiwang documentation

This commit is contained in:
2026-09-07 21:01:08 +08:00
parent 9c4510697c
commit 30a1b4114c
259 changed files with 84995 additions and 30201 deletions
+67 -59
View File
@@ -1,61 +1,69 @@
# AGENTS.md
# AGENTS.md
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
## 第一准则:服务器零改动
**任何前端开发都必须完全遵循前后端数据包的协议与数据结构,做到服务器零改动。** 这是本仓库不可逾越的最高准则,优先于一切其它考量:
- 协议信封、`route`/`rpc` 命名、字段名与类型、`roomtype` 配置编码、`deskinfo` 快照结构等,**必须与现有协议逐字节对齐**(依据 `docs/protocol/`),不得为前端方便而改变格式或新增/重命名字段。
- 遇到协议层有疑问时,**先读 `docs/protocol/` 对应章节核对,不要臆测**;宁可前端多做适配,也绝不要求服务器配合改动。
- 新的 Cocos 前端(`YouleNexus`)的目标,就是在不改服务器的前提下复刻该协议契约、实现联调兼容。
## 第二准则:数据源权威、唯一,下游不兜底
**每一类数据只能有一个权威且唯一的来源(single source of truth);下游消费方不得猜测、修补、兜底。**
- **唯一来源**:同一份配置/数据(如服务器地址、渠道身份、协议常量)只在一个地方定义,其它地方一律引用,不得各处复制或并存第二份。
- **下游零兜底**:消费方拿到的数据缺失或非法时,**直接显式报错/抛出,把问题暴露到来源**,不得用默认值「猜一个」、不得静默 `?? 兜底值`、不得 try/catch 后塞个 fallback 蒙混。错误要早暴露、可定位,而不是被下游悄悄掩盖。
- **配置即契约**:来源(如 `profiles.ts` 的服务器地址)必须把该提供的字段显式提供齐全;缺了就是配置错误,应在来源处修正,而不是让下游补。
- 例外只有一种:来源本身明确定义了「可选 + 缺省语义」,且该缺省**写在来源处**(而非散落在各下游)。
> 这条与「服务器零改动」并列:前者保边界契约不变,后者保内部数据流可信、单源、可追溯。新增/修改任何带默认值、回退、容错分支的代码前,先问:这是不是在替上游兜底?若是,改为在来源处修正 + 在下游暴露。
## 仓库总览
这是友乐(Youle)棋牌游戏平台的前端工作区,核心是「**一套前端模板 + 多个子游戏**」的架构,并正在迁移到 Cocos Creator。主要目录:
- **`projects/`** —— 旧版 HTML5 前端。`projects/Game_Surface_3` 是**前端模板(平台外壳)**,也是新前端 UI 迁移的**原工程(迁移源)**;其余目录都是**基于该模板扩展出来的子游戏**,平台层几乎不动,只改子游戏层 + 美术数据。分层结构与关键文件见 `projects/AGENTS.md`。
- **`docs/protocol/`** —— 前端模板框架与服务器收发包的**协议规范**(权威文档,8 篇)。改动任何网络层代码前**必须先读对应章节**,服务器不可改,前端必须严格对齐协议。
- **`cocoscreator_projects/YouleNexus/`** —— 新的 Cocos Creator 3.8+ 前端工程,目标是按 `docs/protocol` 复刻协议契约,做到服务器零改动即可联调。
### 服务器文档与二七王服务端
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
- **`server/development-guide/`**:服务器通用开发文档。查服务端框架、子游戏接入、通信协议和开发约束时,从 [README](server/development-guide/README.md) 开始,按任务阅读对应章节。
- **`server/games/erqiwang/`**:二七王当前改动后的服务器代码。核对实际行为时读取这里的实现,不以 `projects/erqiwang` 的旧前端推断当前服务器规则。
- 二七王规则设计位于 [design.md](server/games/erqiwang/docs/design/design.md),玩法协议位于 [packet_protocol.md](server/games/erqiwang/docs/protocol/packet_protocol.md)。前端接入时同时核对设计、协议与服务端实现;发现差异应明确记录,不把设计要求冒充已实现行为。
- `roomtype` 由目标子游戏定义,可能是数组或字符串;平台通用流程不可擅自转换类型。二七王实际解析以 `class.config.js` 为入口,局数与扣卡检查 `class.export.js`;位索引只在协议文档中维护,不在本文件重复定义。
- 查阅服务器源码用于前端适配,不代表授权修改服务器代码;前端开发继续遵守“服务器零改动”。
## 第一准则:服务器零改动
**任何前端开发都必须完全遵循前后端数据包的协议与数据结构,做到服务器零改动。** 这是本仓库不可逾越的最高准则,优先于一切其它考量:
- 协议信封、`route`/`rpc` 命名、字段名与类型、`roomtype` 配置数组、`deskinfo` 快照结构等,**必须与现有协议逐字节对齐**(依据 `docs/protocol/`),不得为前端方便而改变格式或新增/重命名字段。
- 遇到协议层有疑问时,**先读 `docs/protocol/` 对应章节核对,不要臆测**;宁可前端多做适配,也绝不要求服务器配合改动。
- 新的 Cocos 前端(`YouleNexus`)的目标,就是在不改服务器的前提下复刻该协议契约、实现联调兼容。
## 第二准则:数据源权威、唯一,下游不兜底
**每一类数据只能有一个权威且唯一的来源(single source of truth);下游消费方不得猜测、修补、兜底。**
- **唯一来源**:同一份配置/数据(如服务器地址、渠道身份、协议常量)只在一个地方定义,其它地方一律引用,不得各处复制或并存第二份。
- **下游零兜底**:消费方拿到的数据缺失或非法时,**直接显式报错/抛出,把问题暴露到来源**,不得用默认值「猜一个」、不得静默 `?? 兜底值`、不得 try/catch 后塞个 fallback 蒙混。错误要早暴露、可定位,而不是被下游悄悄掩盖。
- **配置即契约**:来源(如 `profiles.ts` 的服务器地址)必须把该提供的字段显式提供齐全;缺了就是配置错误,应在来源处修正,而不是让下游补。
- 例外只有一种:来源本身明确定义了「可选 + 缺省语义」,且该缺省**写在来源处**(而非散落在各下游)。
> 这条与「服务器零改动」并列:前者保边界契约不变,后者保内部数据流可信、单源、可追溯。新增/修改任何带默认值、回退、容错分支的代码前,先问:这是不是在替上游兜底?若是,改为在来源处修正 + 在下游暴露。
## 仓库总览
这是友乐(Youle)棋牌游戏平台的前端工作区,核心是「**一套前端模板 + 多个子游戏**」的架构,并正在迁移到 Cocos Creator。三个顶层目录:
- **`projects/`** —— 旧版 HTML5 前端。`projects/Game_Surface_3` 是**前端模板(平台外壳)**,也是新前端 UI 迁移的**原工程(迁移源)**;其余目录都是**基于该模板扩展出来的子游戏**,平台层几乎不动,只改子游戏层 + 美术数据。分层结构与关键文件见 `projects/AGENTS.md`。
- **`docs/protocol/`** —— 前端模板框架与服务器收发包的**协议规范**(权威文档,8 篇)。改动任何网络层代码前**必须先读对应章节**,服务器不可改,前端必须严格对齐协议。
- **`cocoscreator_projects/YouleNexus/`** —— 新的 Cocos Creator 3.8+ 前端工程,目标是按 `docs/protocol` 复刻协议契约,做到服务器零改动即可联调。
### 原工程 `Game_Surface_3` → `YouleNexus` 的 UI 迁移
`YouleNexus` 的 UI 资源(`assets/framework/ui/prefabs/`)从**原工程 `projects/Game_Surface_3`** 逐层复刻。数据契约与流程规范:
- 数据模型(权威):`docs/superpowers/specs/原工程数据模型.md`;迁移流程:`docs/superpowers/specs/UI-手工迁移规范.md`(9 步 funplay MCP + 8 项验证清单)。
- 已完成的参照真相源:`assets/framework/ui/prefabs/Login_Layer.prefab`(Layer 2)。
> ⚠️ **索引与类型(易踩坑,实测修正)**:源数据索引**直接按 ID**——`gameabc_Object.json` 的 `ObjectList[ObjectID]`、`gameabc_Image.json` 的 `ImageFileList[ImageFileID]`(**不是** `ID - 1`,旧文档的 off-by-one 是错的);组件类型看 `ObjectType`(`2`=Sprite、`4`=Text),**不是** XML 的 `SpiritType`(那是另一套枚举)。
## 网络协议(`docs/protocol/`,新旧前端共同契约)
`docs/protocol/` 是权威文档,改任何网络层代码前先读对应章节,**不要凭本文件的转述臆测**。
**两个不可妥协的约束:** ① `roomtype` 房间配置数组结构必须与目标子游戏**逐字节一致**(服务器据此解析规则);② `deskinfo` 对局快照须可序列化/反序列化以支持 `isbattle==1` 时 `Game_Modify.Reconnect(deskinfo)` 断线重连。
## 客户端侧适配约束:远程配置 / 原生数据接口 / 原生↔H5 桥接
继「服务器零改动」之后的**第二类不可妥协约束**:除 WebSocket 协议外,前端还通过「远程配置文件」(`gameserver` → `urlserver`)与「原生接口」(`window.settings` 同步取值、WVJB 异步桥)获取大量数据。新 Cocos 前端必须与原项目**逐字一致**地复刻这些读取/互调/注册方式(接口名、数据格式、回调约定、URL 构造),让原生侧与配置服务**零改动**即可对接。
具体机制、handler 清单与源码位置见 `native-bridge-contract` 技能(`.Codex/skills/native-bridge-contract/SKILL.md`)——改这部分前先读它核对,不要臆测接口名或数据格式。
## Cocos Creator 新前端:优先使用 funplay-cocos-mcp
新工程 `cocoscreator_projects/YouleNexus`。**任何涉及该工程的编辑器操作**(场景、节点、组件、prefab、资源、动画、预览/截图等),都必须走 `mcp__funplay_cocos__*` 这组 MCP 工具驱动编辑器。
**绝不允许手改** `.scene` / `.prefab` / `.anim` / `.meta`——它们是 UUID 引用的序列化资源,任何文本编辑都会破坏引用结构,导致「资源导入失败」且只能人工恢复。**MCP 连不上时也不例外**:应提示用户去编辑器启动服务,而不是退而求其次去手改文件。
具体工具用法不写在这里——MCP server 连上后会自注入完整说明(工具清单、action 速查、批量操作),以其为准,**正常操作不需要先读任何仓库文档**。
`cocos-mcp` 技能(`.Codex/skills/cocos-mcp/SKILL.md`)只记录 MCP 自己给不了的部分:前置条件、探活、排障、已知坑。**工具正常时不必读它;一旦 `cocos_*` 工具没出现、调用报连接失败、或行为与预期不符,就去读。**
### 原工程 `Game_Surface_3` → `YouleNexus` 的 UI 迁移
`YouleNexus` 的 UI 资源(`assets/framework/ui/prefabs/`)从**原工程 `projects/Game_Surface_3`** 逐层复刻。数据契约与流程规范:
- 数据模型(权威):`docs/superpowers/specs/原工程数据模型.md`;迁移流程:`docs/superpowers/specs/UI-手工迁移规范.md`(9 步 funplay MCP + 8 项验证清单)。
- 已完成的参照真相源:`assets/framework/ui/prefabs/Login_Layer.prefab`(Layer 2)。
> ⚠️ **索引与类型(易踩坑,实测修正)**:源数据索引**直接按 ID**——`gameabc_Object.json` 的 `ObjectList[ObjectID]`、`gameabc_Image.json` 的 `ImageFileList[ImageFileID]`(**不是** `ID - 1`,旧文档的 off-by-one 是错的);组件类型看 `ObjectType`(`2`=Sprite、`4`=Text),**不是** XML 的 `SpiritType`(那是另一套枚举)。
## 网络协议(`docs/protocol/`,新旧前端共同契约)
`docs/protocol/` 是权威文档,改任何网络层代码前先读对应章节,**不要凭本文件的转述臆测**。
**两个不可妥协的约束:** ① `roomtype` 房间配置编码的类型与结构必须与目标子游戏**逐字节一致**(服务器据此解析规则);② `deskinfo` 对局快照须可序列化/反序列化以支持 `isbattle==1` 时 `Game_Modify.Reconnect(deskinfo)` 断线重连。
## 客户端侧适配约束:远程配置 / 原生数据接口 / 原生↔H5 桥接
继「服务器零改动」之后的**第二类不可妥协约束**:除 WebSocket 协议外,前端还通过「远程配置文件」(`gameserver` → `urlserver`)与「原生接口」(`window.settings` 同步取值、WVJB 异步桥)获取大量数据。新 Cocos 前端必须与原项目**逐字一致**地复刻这些读取/互调/注册方式(接口名、数据格式、回调约定、URL 构造),让原生侧与配置服务**零改动**即可对接。
具体机制、handler 清单与源码位置见 `native-bridge-contract` 技能(`.Codex/skills/native-bridge-contract/SKILL.md`)——改这部分前先读它核对,不要臆测接口名或数据格式。
## Cocos Creator 新前端:优先使用 funplay-cocos-mcp
新工程 `cocoscreator_projects/YouleNexus`。**任何涉及该工程的编辑器操作**(场景、节点、组件、prefab、资源、动画、预览/截图等),都必须走 `mcp__funplay_cocos__*` 这组 MCP 工具驱动编辑器。
**绝不允许手改** `.scene` / `.prefab` / `.anim` / `.meta`——它们是 UUID 引用的序列化资源,任何文本编辑都会破坏引用结构,导致「资源导入失败」且只能人工恢复。**MCP 连不上时也不例外**:应提示用户去编辑器启动服务,而不是退而求其次去手改文件。
具体工具用法不写在这里——MCP server 连上后会自注入完整说明(工具清单、action 速查、批量操作),以其为准,**正常操作不需要先读任何仓库文档**。
`cocos-mcp` 技能(`.Codex/skills/cocos-mcp/SKILL.md`)只记录 MCP 自己给不了的部分:前置条件、探活、排障、已知坑。**工具正常时不必读它;一旦 `cocos_*` 工具没出现、调用报连接失败、或行为与预期不符,就去读。**
@@ -0,0 +1,19 @@
type ClipboardHost=
|{kind:'settings';read:()=>unknown}
|{kind:'bridge';call:(name:'gamepastetext',data:string,callback:(value:unknown)=>void)=>void}
|{kind:'browser';read:()=>Promise<string>};
/** 05_Func: settings takes no arguments; WVJB takes "" and returns through its callback. */
export async function readClipboardText(host:ClipboardHost):Promise<string>{
const value=host.kind==='bridge'?await new Promise<unknown>((resolve,reject)=>{
const timer=setTimeout(()=>reject(Error('读取剪贴板超时,请手动输入')),10000);
try{host.call('gamepastetext','',value=>{clearTimeout(timer);resolve(value);});}
catch(error){clearTimeout(timer);reject(error);}
}):await host.read();
if(typeof value!=='string')throw Error('Invalid clipboard text');
return value;
}
export async function readInviteClipboard(host:ClipboardHost):Promise<string>{
return (await readClipboardText(host)).replace(/[^0-9]/g,'');
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4aeb3de8-f982-4246-95be-7dca058bc99d",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,25 @@
import {requireRecord} from '../../protocol/contracts/validation.ts';
/** Logic.readRoomList/addRoomToList/removeRoomFromList. This is owner-return history, not recently played rooms. */
export class OwnedRoomHistory {
private readonly key:string;
constructor(private readonly storage:Pick<Storage,'getItem'|'setItem'>,gameid:string|number,agentid:string|number){this.key='tsgame_roomlist_'+gameid+agentid;}
read():readonly number[]{
const text=this.storage.getItem(this.key);
if(!text){this.save([]);return [];}
const raw=requireRecord(JSON.parse(text),this.key);
if(!Array.isArray(raw.data)||raw.data.some(n=>typeof n!=='number'||!Number.isSafeInteger(n)||n<=0))throw Error('Invalid '+this.key);
return Object.freeze([...raw.data]);
}
private code(value:unknown):number {
if(!(typeof value==='string'&&/^[0-9]+$/.test(value)||typeof value==='number'))throw Error('Invalid room history roomcode');
const code=Number(value);if(!Number.isSafeInteger(code)||code<=0)throw Error('Invalid room history roomcode');return code;
}
private save(data:readonly number[]):void{this.storage.setItem(this.key,JSON.stringify({data}));}
joined(raw:Readonly<Record<string,unknown>>):void{
if(Number(raw.state)===99||Number(raw.state)===0&&Number(raw.isowner)===1){const code=this.code(raw.roomcode);this.save(this.read().filter(n=>n!==code));}
}
exited(raw:Readonly<Record<string,unknown>>,infinite:number):void{
if(Number(raw.isowner)!==1||infinite!==0)return;
const code=this.code(raw.roomcode),list=this.read();if(!list.includes(code))this.save([code,...list]);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ca074aba-afc9-428f-93ea-199d30dba6ef",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,17 +1,19 @@
import type { ChannelIdentity } from '../identity.ts';
/** Legacy GameData launchWaitTime (04_Data.js): timer units are milliseconds. */
export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10;
/**
* 构建期默认渠道身份(旧 version.js 等价物)。
* gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。
*/
export const BUILD_IDENTITY: ChannelIdentity = {
// This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel.
agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy',
channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG',
gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl',
marketid: 4,
version: 1, // 数字 versionCode(联调时改成本地服要求的版本号)
};
import type { ChannelIdentity } from '../identity.ts';
/** Legacy GameData launchWaitTime (04_Data.js): timer units are milliseconds. */
export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10;
/**
* 构建期默认渠道身份(旧 version.js 等价物)。
* gameid 每子游戏固定写死;agentid/channelid/marketid/version 为默认,运行期由 query/native/profile 覆盖。
*/
export const BUILD_IDENTITY: ChannelIdentity = {
// This game's entry in config_test/update_jsonv2.txt belongs to this agent/channel.
agentid: '1B2h0ccl205c390Y28m1Ajdplkuu4wgy',
channelid: 'aouv0LotK0pYyQ0Pdrx0CsdcaezfzrcG',
// agentid: 'veRa0qrBf0df2K1G4de2tgfmVxB2jxpv',
// channelid: 'FtJf073aa0d6rI1xD8J1Y42fINTm0ziK',
gameid: 'J9r4022t80pxlb01zOK76DN1WD4QewFl',
marketid: 4,
version: 1, // 数字 versionCode(联调时改成本地服要求的版本号)
};
@@ -1,133 +1,181 @@
import type { RuntimeConfig } from '../config/runtime-config.ts';
import { buildFirstSliceEnvelope } from '../protocol/first-slice-routes.ts';
import type { OutboundEnvelope } from '../core/types/envelope.ts';
import {
buildExitRoomRequest,
buildJoinRoomRequest,
buildPrepareRequest,
type JoinRoomEnvironment,
type RoomIdentity,
} from '../protocol/contracts/index.ts';
import { PlatformStore } from './stores/platform-store.ts';
import type { InsideRoomState, PlatformState } from './stores/platform-types.ts';
/** The complete-envelope send capability used by platform command adapters. */
export interface OutboundEnvelopeSendPort {
send(envelope: OutboundEnvelope): void;
}
export interface PlatformCommandsOptions {
readonly runtimeConfig: RuntimeConfig;
readonly store: PlatformStore;
readonly send: OutboundEnvelopeSendPort;
}
export interface JoinRoomCommand extends JoinRoomEnvironment {
readonly roomcode: string;
}
export class UnsupportedGameHostCommandError extends Error {
constructor(message: string) {
super(message);
this.name = 'UnsupportedGameHostCommandError';
}
}
/** Builds platform-owned room requests exclusively through the first-slice contracts. */
export class PlatformCommands {
constructor(private readonly options: PlatformCommandsOptions) {}
getVipRooms(shortcodeList:readonly (string|number)[]):void {
this.taskPlayerId();
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('getInfoByShortCode',{agentid,gameid,shortcodeList:[...shortcodeList]}));
}
joinVipRoom(roomcode:string|number,location:unknown):void {
const playerid=this.taskPlayerId(),state=this.options.store.getState();
const ip=state.players.entities[playerid].ip;
if(typeof ip!=='string')throw Error('VIP join requires player login ip');
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:0,location,ip}));
}
getPlayerTasks(): void {
const playerid=this.taskPlayerId();
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('get_player_task',{agentid,playerid,gameid}));
}
claimTask(taskid:number|string):void {
const playerid=this.taskPlayerId();
const task=this.options.store.getState().tasks?.find(task=>String(task.taskid)===String(taskid));
if(!task||task.state!==1)throw Error('Task is not claimable');
// 11_GameUI claim click sends these three fields; Net._SendData adds none.
this.options.send.send(buildFirstSliceEnvelope('get_task_award',{
agentid:this.options.runtimeConfig.identity.agentid,playerid,taskid:task.taskid,
}));
}
private taskPlayerId():number {
const state=this.options.store.getState();
if(state.app.phase!=='logged-in'||state.room.kind!=='outside')throw Error('Task command requires logged-in lobby');
return this.requireAuthenticatedPlayerId(state);
}
prepare(): void {
const state = this.options.store.getState();
this.options.send.send(buildPrepareRequest(
this.options.runtimeConfig,
this.roomIdentity(state, this.requireInsideRoom(state)),
));
}
exitRoom(): void {
const state = this.options.store.getState();
const room = this.requireInsideRoom(state);
if (room.stage !== 0 && room.infinite !== 1) {
throw new UnsupportedGameHostCommandError('room.exit requires stage=0 unless infinite=1');
}
this.options.send.send(buildExitRoomRequest(
this.options.runtimeConfig,
this.roomIdentity(state, room),
));
}
joinRoom(command: JoinRoomCommand): void {
const state = this.options.store.getState();
if (state.room.kind !== 'outside') {
throw new UnsupportedGameHostCommandError('room.join requires player to be outside a room');
}
const playerid = this.requireAuthenticatedPlayerId(state);
this.options.send.send(buildJoinRoomRequest(
this.options.runtimeConfig,
{ playerid, roomcode: command.roomcode },
command,
));
}
private requireInsideRoom(state: PlatformState): InsideRoomState {
if (state.room.kind !== 'inside') {
throw new UnsupportedGameHostCommandError('room command requires player to be inside a room');
}
return state.room;
}
private requireAuthenticatedPlayerId(state: PlatformState): number {
const playerid = state.players.selfPlayerId;
if (playerid === null) {
throw new UnsupportedGameHostCommandError('room command requires an authenticated player');
}
if (state.players.entities[playerid] === undefined) {
throw new Error('players.entities: missing authenticated player');
}
return playerid;
}
private roomIdentity(state: PlatformState, room: InsideRoomState): RoomIdentity {
return {
playerid: this.requireAuthenticatedPlayerId(state),
roomcode: room.roomcode,
};
}
}
import type { RuntimeConfig } from '../config/runtime-config.ts';
import { buildFirstSliceEnvelope } from '../protocol/first-slice-routes.ts';
import type { OutboundEnvelope } from '../core/types/envelope.ts';
import {
buildCreateRoomRequest,
type CreateRoomEnvironment,
buildExitRoomRequest,
buildJoinRoomRequest,
buildPrepareRequest,
type JoinRoomEnvironment,
type RoomIdentity,
} from '../protocol/contracts/index.ts';
import { cloneAndFreezeRoomtype, PlatformStore } from './stores/platform-store.ts';
import type { InsideRoomState, PlatformState } from './stores/platform-types.ts';
/** The complete-envelope send capability used by platform command adapters. */
export interface OutboundEnvelopeSendPort {
send(envelope: OutboundEnvelope): void;
}
export interface PlatformCommandsOptions {
readonly runtimeConfig: RuntimeConfig;
readonly store: PlatformStore;
readonly send: OutboundEnvelopeSendPort;
}
export interface JoinRoomCommand extends JoinRoomEnvironment {
readonly roomcode: string | number;
}
export interface CreateRoomCommand extends CreateRoomEnvironment {}
export class UnsupportedGameHostCommandError extends Error {
constructor(message: string) {
super(message);
this.name = 'UnsupportedGameHostCommandError';
}
}
/** Builds platform-owned room requests exclusively through the first-slice contracts. */
export class PlatformCommands {
constructor(private readonly options: PlatformCommandsOptions) {}
getPlayerInvitecode():void {
const playerid=this.taskPlayerId(),player=this.options.store.getState().players.entities[playerid];
const {openid,unionid}=player;
if(typeof openid!=='string'||!(typeof unionid==='string'||typeof unionid==='number'&&Number.isFinite(unionid)))throw Error('Invite query requires login openid and unionid');
this.options.send.send(buildFirstSliceEnvelope('get_player_invitecode',{
agentid:this.options.runtimeConfig.identity.agentid,playerid,unionid,openid,
}));
}
bindInvitecode(invitecode:string):void {
const playerid=this.taskPlayerId();
if(typeof invitecode!=='string')throw Error('invitecode must be a string');
this.options.send.send(buildFirstSliceEnvelope('binding_invitecode',{
agentid:this.options.runtimeConfig.identity.agentid,playerid,invitecode,
}));
}
getVipRooms(shortcodeList:readonly (string|number)[]):void {
this.taskPlayerId();
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('getInfoByShortCode',{agentid,gameid,shortcodeList:[...shortcodeList]}));
}
joinVipRoom(roomcode:string|number,location:unknown):void {
const playerid=this.taskPlayerId(),state=this.options.store.getState();
const ip=state.players.entities[playerid].ip;
if(typeof ip!=='string')throw Error('VIP join requires player login ip');
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:0,location,ip}));
}
getPlayerTasks(): void {
const playerid=this.taskPlayerId();
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('get_player_task',{agentid,playerid,gameid}));
}
getVipRankList(limit:number):void {
this.taskPlayerId();if(!Number.isInteger(limit)||limit<=0)throw Error('Invalid vipRankLimit');
this.options.send.send(buildFirstSliceEnvelope('getVipRankList',{agentid:this.options.runtimeConfig.identity.agentid,limit}));
}
joinOwnedRoom(roomcode:number,location:unknown):void {
const playerid=this.taskPlayerId(),state=this.options.store.getState();
if(!Number.isSafeInteger(roomcode)||roomcode<=0)throw Error('Invalid owned roomcode');
const ip=state.players.entities[playerid].ip;if(typeof ip!=='string')throw Error('Owned room join requires player login ip');
const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('self_join_room',{agentid,playerid,gameid,roomcode,joinmode:1,location,ip}));
}
getTreasureList():void {
this.taskPlayerId();const {agentid,gameid}=this.options.runtimeConfig.identity;
this.options.send.send(buildFirstSliceEnvelope('get_treasurelist',{agentid,gameid}));
}
redeemCard(cardno:string):void {
const playerid=this.taskPlayerId();
if(typeof cardno!=='string'||cardno===''||/^[ ]+$/.test(cardno))throw Error('Invalid cardno');
this.options.send.send(buildFirstSliceEnvelope('topup_card',{agentid:this.options.runtimeConfig.identity.agentid,playerid,cardno}));
}
claimTask(taskid:number|string):void {
const playerid=this.taskPlayerId();
const task=this.options.store.getState().tasks?.find(task=>String(task.taskid)===String(taskid));
if(!task||task.state!==1)throw Error('Task is not claimable');
// 11_GameUI claim click sends these three fields; Net._SendData adds none.
this.options.send.send(buildFirstSliceEnvelope('get_task_award',{
agentid:this.options.runtimeConfig.identity.agentid,playerid,taskid:task.taskid,
}));
}
private taskPlayerId():number {
const state=this.options.store.getState();
if(state.app.phase!=='logged-in'||state.room.kind!=='outside')throw Error('Task command requires logged-in lobby');
return this.requireAuthenticatedPlayerId(state);
}
prepare(): void {
const state = this.options.store.getState();
this.options.send.send(buildPrepareRequest(
this.options.runtimeConfig,
this.roomIdentity(state, this.requireInsideRoom(state)),
));
}
exitRoom(): void {
const state = this.options.store.getState();
const room = this.requireInsideRoom(state);
if (room.stage !== 0 && room.infinite !== 1) {
throw new UnsupportedGameHostCommandError('room.exit requires stage=0 unless infinite=1');
}
this.options.send.send(buildExitRoomRequest(
this.options.runtimeConfig,
this.roomIdentity(state, room),
));
}
createRoom(command: CreateRoomCommand): void {
const playerid = this.taskPlayerId();
const roomtype = cloneAndFreezeRoomtype(command.roomtype);
this.options.send.send(buildCreateRoomRequest(this.options.runtimeConfig, playerid, {...command, roomtype}));
}
joinRoom(command: JoinRoomCommand): void {
const state = this.options.store.getState();
if (state.room.kind !== 'outside') {
throw new UnsupportedGameHostCommandError('room.join requires player to be outside a room');
}
const playerid = this.requireAuthenticatedPlayerId(state);
this.options.send.send(buildJoinRoomRequest(
this.options.runtimeConfig,
{ playerid, roomcode: command.roomcode },
command,
));
}
private requireInsideRoom(state: PlatformState): InsideRoomState {
if (state.room.kind !== 'inside') {
throw new UnsupportedGameHostCommandError('room command requires player to be inside a room');
}
return state.room;
}
private requireAuthenticatedPlayerId(state: PlatformState): number {
const playerid = state.players.selfPlayerId;
if (playerid === null) {
throw new UnsupportedGameHostCommandError('room command requires an authenticated player');
}
if (state.players.entities[playerid] === undefined) {
throw new Error('players.entities: missing authenticated player');
}
return playerid;
}
private roomIdentity(state: PlatformState, room: InsideRoomState): RoomIdentity {
return {
playerid: this.requireAuthenticatedPlayerId(state),
roomcode: room.roomcode,
};
}
}
@@ -1,243 +1,276 @@
import type { GameHost } from '../sdk/contracts/index.ts';
import {
extractGameSessionOpenFaultErrors,
GameSessionHost,
} from '../sdk/runtime/game-session-host.ts';
import {
parseLoginResponse,
parseSelfJoinRoomResponse,
type ParsedSelfExitPayload,
type ReconnectSnapshot,
} from '../protocol/contracts/index.ts';
import { PlatformStore } from './stores/platform-store.ts';
import type { PlatformState } from './stores/platform-types.ts';
export interface GameHostLeasePort {
readonly host: GameHost;
invalidate(): void;
}
export type GameHostLeaseFactory = () => GameHostLeasePort;
/** A valid nonzero server outcome, delivered without changing canonical state. */
export interface ServerDenial {
readonly rpc: 'player_login' | 'self_join_room';
readonly state: number;
readonly data: Readonly<Record<string, unknown>>;
}
export interface RuntimeSessionOptions {
readonly store: PlatformStore;
readonly gameSession: GameSessionHost;
readonly createGameHostLease: GameHostLeaseFactory;
readonly onServerDenial: (outcome: ServerDenial) => void;
}
export type RuntimeSessionState = 'operational' | 'faulted' | 'kicked';
export class RuntimeSessionFault extends Error {
readonly errors: readonly unknown[];
constructor(operation: string, errors: readonly unknown[]) {
super(`RuntimeSession fault during ${operation}`);
this.name = 'RuntimeSessionFault';
this.errors = Object.freeze([...errors]);
}
}
/** Coordinates the canonical platform snapshot with the one active game session. */
export class RuntimeSession {
private loginIsPending = false;
private wasKicked = false;
private terminalFault: RuntimeSessionFault | null = null;
private activeLease: GameHostLeasePort | null = null;
constructor(private readonly options: RuntimeSessionOptions) {}
get loginPending(): boolean { return this.loginIsPending; }
get kicked(): boolean { return this.wasKicked; }
get state(): RuntimeSessionState {
if (this.wasKicked) return 'kicked';
return this.terminalFault !== null ? 'faulted' : 'operational';
}
get fault(): Error | null { return this.terminalFault; }
markLoginPending(): void {
this.requireOperational();
this.loginIsPending = true;
}
handleLogin(data: unknown): void {
this.requireOperational();
this.loginIsPending = false;
const parsed = parseLoginResponse(data);
if (parsed.state !== 0) {
this.options.onServerDenial({ rpc: 'player_login', state: parsed.state, data: parsed.raw });
return;
}
const committed = this.options.store.applyLoginSuccess(parsed);
if (parsed.room === null) {
this.releaseOrFault('login lobby replacement');
return;
}
this.openCommittedRoom(committed, parsed.reconnect, 'login room open');
}
handleSelfJoin(data: unknown): void {
this.requireOperational();
const parsed = parseSelfJoinRoomResponse(data);
if (parsed.state !== 0) {
this.options.onServerDenial({ rpc: 'self_join_room', state: parsed.state, data: parsed.raw });
return;
}
const committed = this.options.store.replaceRoom(parsed);
this.openCommittedRoom(committed, parsed.reconnect, 'self join room open');
}
handleSelfExit(parsed: ParsedSelfExitPayload): void {
this.requireOperational();
const state = this.options.store.getState();
if (state.room.kind !== 'inside') {
if (this.options.gameSession.state === 'idle' && this.activeLease === null) return;
const errors = [
new Error('self_exit_room found an active game without room state'),
...this.releaseGame(),
];
this.throwFault('self exit inconsistent state', errors);
}
if (parsed.seat !== undefined && parsed.seat !== state.room.selfSeat) {
throw new RangeError(
`self_exit_room seat mismatch: expected ${state.room.selfSeat}, received ${parsed.seat}`,
);
}
if (this.options.gameSession.state !== 'active' || this.activeLease === null) {
const errors = [
new Error('self_exit_room requires an active game session and host lease'),
...this.releaseGame(),
];
this.throwFault('self exit inconsistent state', errors);
}
try {
this.options.store.clearRoom();
} catch (error) {
this.throwFault('self exit state commit', [error, ...this.releaseGame()]);
}
this.releaseOrFault('self exit cleanup');
}
handleServerSwitch(switchConnection: () => void): void {
this.requireOperational();
const errors = this.releaseGame();
try {
switchConnection();
} catch (error) {
errors.push(error);
}
if (errors.length > 0) this.throwFault('server switch', errors);
}
handleKick(_data: unknown, stopConnection: () => void): void {
if (this.wasKicked) return;
this.requireOperational();
this.loginIsPending = false;
this.wasKicked = true;
const errors = this.releaseGame();
try {
stopConnection();
} catch (error) {
errors.push(error);
}
if (errors.length > 0) this.throwFault('kick terminal cleanup', errors);
}
private openCommittedRoom(
state: PlatformState,
reconnect: ReconnectSnapshot,
operation: string,
): void {
if (state.room.kind !== 'inside') {
this.throwFault(operation, [new Error('Cannot open a game session without committed room state')]);
}
const priorCleanupErrors = this.releaseGame();
if (priorCleanupErrors.length > 0) this.throwFault(operation, priorCleanupErrors);
if (this.gameWasDisposed()) return;
let lease: GameHostLeasePort;
try {
lease = this.options.createGameHostLease();
if (!lease || typeof lease !== 'object' || typeof lease.invalidate !== 'function') {
throw new TypeError('createGameHostLease must return { host, invalidate }');
}
} catch (error) {
this.throwFault(operation, [error]);
}
this.activeLease = lease;
try {
this.options.gameSession.open(lease.host);
if (this.gameWasDisposed()) return;
this.options.gameSession.publish({
type: 'room.entered',
roomtype: state.room.roomtype,
});
if (this.gameWasDisposed()) return;
if (reconnect.present) this.options.gameSession.restore(reconnect.value);
} catch (error) {
const openErrors = extractGameSessionOpenFaultErrors(error);
const errors = openErrors === null
? [error]
: [openErrors[0], openErrors[1]];
errors.push(...this.releaseGame());
this.throwFault(operation, errors);
}
}
private releaseOrFault(operation: string): void {
const errors = this.releaseGame();
if (errors.length > 0) this.throwFault(operation, errors);
}
private gameWasDisposed(): boolean {
return this.options.gameSession.state === 'disposed';
}
private releaseGame(): unknown[] {
const errors: unknown[] = [];
const lease = this.activeLease;
this.activeLease = null;
if (this.options.gameSession.state !== 'idle' && !this.gameWasDisposed()) {
try {
this.options.gameSession.close();
} catch (error) {
errors.push(error);
}
}
if (lease !== null) {
try {
lease.invalidate();
} catch (error) {
errors.push(error);
}
}
return errors;
}
private throwFault(operation: string, errors: readonly unknown[]): never {
const fault = new RuntimeSessionFault(operation, errors);
this.loginIsPending = false;
this.terminalFault = fault;
throw fault;
}
private requireOperational(): void {
if (this.terminalFault !== null) throw this.terminalFault;
if (this.wasKicked) throw new Error('RuntimeSession is kicked');
}
}
import type { GameHost } from '../sdk/contracts/index.ts';
import {
extractGameSessionOpenFaultErrors,
GameSessionHost,
} from '../sdk/runtime/game-session-host.ts';
import {
parseCreateRoomResponse,
parseLoginResponse,
parseSelfJoinRoomResponse,
type ParsedSelfExitPayload,
type ReconnectSnapshot,
} from '../protocol/contracts/index.ts';
import { cloneAndFreezeRoomtype, PlatformStore } from './stores/platform-store.ts';
import type { PlatformState } from './stores/platform-types.ts';
import type {LoginAccountIdentity} from '../protocol/contracts/login-contract.ts';
export interface GameHostLeasePort {
readonly host: GameHost;
invalidate(): void;
}
export type GameHostLeaseFactory = () => GameHostLeasePort;
/** A valid nonzero server outcome, delivered without changing canonical state. */
export interface ServerDenial {
readonly rpc: 'player_login' | 'self_join_room' | 'create_room';
readonly state: number;
readonly data: Readonly<Record<string, unknown>>;
}
export interface RuntimeSessionOptions {
readonly roomHistory?: import('../adapters/native/owned-room-history.ts').OwnedRoomHistory;
readonly store: PlatformStore;
readonly gameSession: GameSessionHost;
readonly createGameHostLease: GameHostLeaseFactory;
readonly onServerDenial: (outcome: ServerDenial) => void;
}
export type RuntimeSessionState = 'operational' | 'faulted' | 'kicked';
export class RuntimeSessionFault extends Error {
readonly errors: readonly unknown[];
constructor(operation: string, errors: readonly unknown[]) {
super(`RuntimeSession fault during ${operation}`);
this.name = 'RuntimeSessionFault';
this.errors = Object.freeze([...errors]);
}
}
/** Coordinates the canonical platform snapshot with the one active game session. */
export class RuntimeSession {
private loginIsPending = false;
private lastJoinedRoomcode: string | null = null;
/** Non-device login owns identity in authorization; device login owns it in the response. */
private authorizedIdentity:Pick<LoginAccountIdentity,'openid'|'unionid'>|undefined;
private wasKicked = false;
private terminalFault: RuntimeSessionFault | null = null;
private activeLease: GameHostLeasePort | null = null;
constructor(private readonly options: RuntimeSessionOptions) {}
get loginPending(): boolean { return this.loginIsPending; }
get kicked(): boolean { return this.wasKicked; }
get state(): RuntimeSessionState {
if (this.wasKicked) return 'kicked';
return this.terminalFault !== null ? 'faulted' : 'operational';
}
get fault(): Error | null { return this.terminalFault; }
markLoginPending(identity?:Pick<LoginAccountIdentity,'openid'|'unionid'>|null): void {
this.requireOperational();
// No argument is a reconnect of the current account; null selects device response identity.
if(identity!==undefined)this.authorizedIdentity=identity===null?undefined:identity;
this.loginIsPending = true;
}
handleLogin(data: unknown): void {
this.requireOperational();
this.loginIsPending = false;
const parsed = parseLoginResponse(data);
if (parsed.state !== 0) {
this.options.onServerDenial({ rpc: 'player_login', state: parsed.state, data: parsed.raw });
return;
}
this.lastJoinedRoomcode = null;
const committed = this.options.store.applyLoginSuccess(parsed,this.authorizedIdentity);
if (parsed.room === null) {
this.releaseOrFault('login lobby replacement');
return;
}
this.openCommittedRoom(committed, parsed.reconnect, 'login room open');
}
handleCreateRoom(data: unknown): void {
this.requireOperational();
const parsed = parseCreateRoomResponse(data);
if (parsed.state !== 0) {
this.options.onServerDenial({rpc: 'create_room', state: parsed.state, data: parsed.raw});
return;
}
if (parsed.room === null) throw new Error('create_room missing parsed room');
const current = this.options.store.getState().room;
if (current.kind === 'inside' && current.roomcode === parsed.room.roomcode
&& this.options.gameSession.state === 'active') return;
// Subgame code receives an owned immutable value, never the server response graph.
const roomtype = cloneAndFreezeRoomtype(parsed.room.roomtype);
const count = this.options.gameSession.entry.resolveSeatCount(roomtype);
const committed = this.options.store.createRoom(parsed, count);
this.openCommittedRoom(committed, {present: false}, 'create room open');
}
handleSelfJoin(data: unknown): void {
this.requireOperational();
const parsed = parseSelfJoinRoomResponse(data);
this.options.roomHistory?.joined(parsed.raw);
if (parsed.state !== 0) {
this.options.onServerDenial({ rpc: 'self_join_room', state: parsed.state, data: parsed.raw });
return;
}
if (parsed.room !== null && this.lastJoinedRoomcode === parsed.room.roomcode
&& this.options.gameSession.state === 'active') return;
const committed = this.options.store.replaceRoom(parsed);
this.openCommittedRoom(committed, parsed.reconnect, 'self join room open');
this.lastJoinedRoomcode = parsed.room!.roomcode;
}
handleSelfExit(parsed: ParsedSelfExitPayload): void {
this.requireOperational();
const state = this.options.store.getState();
if (state.room.kind !== 'inside') {
if (this.options.gameSession.state === 'idle' && this.activeLease === null) return;
const errors = [
new Error('self_exit_room found an active game without room state'),
...this.releaseGame(),
];
this.throwFault('self exit inconsistent state', errors);
}
if (parsed.seat !== undefined && parsed.seat !== state.room.selfSeat) {
throw new RangeError(
`self_exit_room seat mismatch: expected ${state.room.selfSeat}, received ${parsed.seat}`,
);
}
if (this.options.gameSession.state !== 'active' || this.activeLease === null) {
const errors = [
new Error('self_exit_room requires an active game session and host lease'),
...this.releaseGame(),
];
this.throwFault('self exit inconsistent state', errors);
}
try {
this.options.roomHistory?.exited(parsed.raw,state.room.infinite);
this.options.store.clearRoom();
} catch (error) {
this.throwFault('self exit state commit', [error, ...this.releaseGame()]);
}
this.releaseOrFault('self exit cleanup');
}
handleServerSwitch(switchConnection: () => void): void {
this.requireOperational();
const errors = this.releaseGame();
try {
switchConnection();
} catch (error) {
errors.push(error);
}
if (errors.length > 0) this.throwFault('server switch', errors);
}
handleKick(_data: unknown, stopConnection: () => void): void {
if (this.wasKicked) return;
this.requireOperational();
this.loginIsPending = false;
this.wasKicked = true;
const errors = this.releaseGame();
try {
stopConnection();
} catch (error) {
errors.push(error);
}
if (errors.length > 0) this.throwFault('kick terminal cleanup', errors);
}
private openCommittedRoom(
state: PlatformState,
reconnect: ReconnectSnapshot,
operation: string,
): void {
if (state.room.kind !== 'inside') {
this.throwFault(operation, [new Error('Cannot open a game session without committed room state')]);
}
const priorCleanupErrors = this.releaseGame();
if (priorCleanupErrors.length > 0) this.throwFault(operation, priorCleanupErrors);
if (this.gameWasDisposed()) return;
let lease: GameHostLeasePort;
try {
lease = this.options.createGameHostLease();
if (!lease || typeof lease !== 'object' || typeof lease.invalidate !== 'function') {
throw new TypeError('createGameHostLease must return { host, invalidate }');
}
} catch (error) {
this.throwFault(operation, [error]);
}
this.activeLease = lease;
try {
this.options.gameSession.open(lease.host);
if (this.gameWasDisposed()) return;
this.options.gameSession.publish({
type: 'room.entered',
roomtype: state.room.roomtype,
});
if (this.gameWasDisposed()) return;
if (reconnect.present) this.options.gameSession.restore(reconnect.value);
} catch (error) {
const openErrors = extractGameSessionOpenFaultErrors(error);
const errors = openErrors === null
? [error]
: [openErrors[0], openErrors[1]];
errors.push(...this.releaseGame());
this.throwFault(operation, errors);
}
}
private releaseOrFault(operation: string): void {
const errors = this.releaseGame();
if (errors.length > 0) this.throwFault(operation, errors);
}
private gameWasDisposed(): boolean {
return this.options.gameSession.state === 'disposed';
}
private releaseGame(): unknown[] {
this.lastJoinedRoomcode = null;
const errors: unknown[] = [];
const lease = this.activeLease;
this.activeLease = null;
if (this.options.gameSession.state !== 'idle' && !this.gameWasDisposed()) {
try {
this.options.gameSession.close();
} catch (error) {
errors.push(error);
}
}
if (lease !== null) {
try {
lease.invalidate();
} catch (error) {
errors.push(error);
}
}
return errors;
}
private throwFault(operation: string, errors: readonly unknown[]): never {
const fault = new RuntimeSessionFault(operation, errors);
this.loginIsPending = false;
this.terminalFault = fault;
throw fault;
}
private requireOperational(): void {
if (this.terminalFault !== null) throw this.terminalFault;
if (this.wasKicked) throw new Error('RuntimeSession is kicked');
}
}
File diff suppressed because it is too large Load Diff
@@ -1,16 +1,16 @@
import type { ServerDenial } from './runtime-session.ts';
/** Semantic scene intents; the headless runtime never imports Cocos. */
export interface ScenePort {
showLoading(): void;
showLogin(): void;
showLobby(): void;
showRoom(): void;
showReconnect(): void;
showKicked(data: unknown): void;
/** Required when the host supports the server's generic message UI. */
showTips?(message: string, time: number, source?:'show_message'|'update_roomcard'): void;
/** Recoverable server result; the consumer interprets the original failure payload. */
showServerDenial(outcome: ServerDenial): void;
showFatal(error: Error): void;
}
import type { ServerDenial } from './runtime-session.ts';
/** Semantic scene intents; the headless runtime never imports Cocos. */
export interface ScenePort {
showLoading(): void;
showLogin(): void;
showLobby(): void;
showRoom(): void;
showReconnect(): void;
showKicked(data: unknown): void;
/** Required when the host supports the server's generic message UI. */
showTips?(message: string, time: number, source?:'show_message'|'update_roomcard'|'update_bean'|'binding_invitecode'): void;
/** Recoverable server result; the consumer interprets the original failure payload. */
showServerDenial(outcome: ServerDenial): void;
showFatal(error: Error): void;
}
File diff suppressed because it is too large Load Diff
@@ -1,61 +1,68 @@
import type { PlatformGameConnectionPhase } from '../../sdk/contracts/snapshots.ts';
import type { TaskData } from '../../core/types/task.ts';
export type PlatformConnectionPhase =
| 'idle'
| 'connecting'
| PlatformGameConnectionPhase;
export interface PlatformAppState {
readonly phase: PlatformConnectionPhase;
}
export interface PlatformPlayer {
readonly playerid: number;
readonly nickname: string;
readonly avatar: string;
readonly sex: number;
readonly bean: number;
/** Account-only fields are absent on other room players; never default them in views. */
import type { PlatformGameConnectionPhase } from '../../sdk/contracts/snapshots.ts';
import type { TaskData } from '../../core/types/task.ts';
export type PlatformConnectionPhase =
| 'idle'
| 'connecting'
| PlatformGameConnectionPhase;
export interface PlatformAppState {
readonly phase: PlatformConnectionPhase;
}
export interface PlatformPlayer {
readonly playerid: number;
readonly nickname: string;
readonly avatar: string;
readonly sex: number;
readonly bean: number;
/** Account-only fields are absent on other room players; never default them in views. */
readonly roomcard?: number;
readonly taskstate?: number;
readonly advanced?: number;
readonly score?: number;
readonly sign?: string | null;
readonly ip?: string;
readonly isprepare?: number;
readonly onstate?: number;
}
export interface PlayerDirectoryState {
readonly selfPlayerId: number | null;
readonly entities: Readonly<Record<number, PlatformPlayer>>;
}
export interface OutsideRoomState {
readonly kind: 'outside';
}
export interface InsideRoomState {
readonly kind: 'inside';
readonly roomcode: string;
readonly selfSeat: number;
readonly seatPlayerIds: readonly (number | null)[];
readonly readySeats: readonly number[];
readonly offlineSeats: readonly number[];
readonly roomtype: readonly unknown[];
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
}
export type RoomState = OutsideRoomState | InsideRoomState;
readonly openid?: string;
readonly unionid?: string | number;
readonly invitecode?: string | null;
readonly taskstate?: number;
readonly advanced?: number;
readonly score?: number;
readonly sign?: string | null;
readonly ip?: string;
readonly isprepare?: number;
readonly onstate?: number;
}
export interface PlayerDirectoryState {
readonly selfPlayerId: number | null;
readonly entities: Readonly<Record<number, PlatformPlayer>>;
}
export interface OutsideRoomState {
readonly kind: 'outside';
}
export interface InsideRoomState {
readonly kind: 'inside';
readonly roomcode: string;
readonly selfSeat: number;
readonly seatPlayerIds: readonly (number | null)[];
readonly readySeats: readonly number[];
readonly offlineSeats: readonly number[];
readonly roomtype: readonly unknown[];
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
}
export type RoomState = OutsideRoomState | InsideRoomState;
export interface PlatformState {
readonly vipRooms?: readonly import('../../core/types/vip-room.ts').VipRoom[];
/** Absent until a task list response arrives; cleared by login replacement. */
readonly tasks?: readonly TaskData[];
readonly app: PlatformAppState;
readonly players: PlayerDirectoryState;
readonly room: RoomState;
}
readonly vipRank?:{readonly entries:readonly import('../../protocol/contracts/rank-list.ts').RankEntry[]|null};
readonly treasureList?:readonly import('../../protocol/contracts/rank-list.ts').RankEntry[];
/** Receipt identity allows repeated queries with an unchanged code to open the view. */
readonly inviteResponse?: {readonly kind:'query'|'binding'};
readonly vipRooms?: readonly import('../../core/types/vip-room.ts').VipRoom[];
/** Absent until a task list response arrives; cleared by login replacement. */
readonly tasks?: readonly TaskData[];
readonly app: PlatformAppState;
readonly players: PlayerDirectoryState;
readonly room: RoomState;
}
@@ -0,0 +1,25 @@
/** Game_Surface_3 Logic.InputRoomCode/pasteRoomCode and GameUI Spirit602. */
export class JoinRoomInput {
private value='';
private busy=false;
constructor(private readonly submit:(code:number)=>void){}
get code():string{return this.value;}
setBusy(value:boolean):void{this.busy=value;}
prefill(value:string):void{
if(!/^\d{0,6}$/.test(value))throw Error('Invalid saved room shortcode');
this.value=value;
}
digit(value:number):void{
if(!Number.isInteger(value)||value<0||value>9)throw Error('Invalid room digit');
if(this.busy)return;
if(this.value.length<6)this.value+=value;
if(this.value.length===6)this.submit(Number(this.value));
}
remove():void{if(!this.busy)this.value=this.value.slice(0,-1);}
clear():void{if(!this.busy)this.value='';}
paste(value:string):void{
if(this.busy)return;this.value=value.replace(/[^0-9]/g,'').slice(0,6);
if(this.value.length===6)this.submit(Number(this.value));
}
confirm():void{if(!this.busy&&Number(this.value)>0)this.submit(Number(this.value));}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9301c0f4-8779-44f8-8d19-5f540b499e82",
"files": [],
"subMetas": {},
"userData": {}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "a834856f-7a53-445b-b517-6588ad346e59",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,121 +1,131 @@
import type { RemoteConfig } from '../config/remote-config.ts';
export interface LobbyTemplate {
isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean;
}
/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */
export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({
isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false,
});
export interface LobbyContext {versionState:0|1;launchMode:0|1;}
const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string});
const SYS_DEFAULTS=Object.freeze({hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1,
ofcShortCode:null as unknown,hideSpid:null as unknown});
function object(value:unknown,name:string):Record<string,unknown>{
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`);
return value as Record<string,unknown>;
}
/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */
export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly<LobbyTemplate>=LOBBY_TEMPLATE){
const rawHall=remote.getValue('hall_config');
const merged=rawHall==null?{}:object(rawHall,'hall_config');
const hall={...HALL_DEFAULTS,...merged};
for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){
// 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData.
if(key==='actData')continue;
const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value});
}
for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){
if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`);
if(key==='androidPay')hall[key]=Number(hall[key]);
}
for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`);
const sys={...SYS_DEFAULTS};
for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){
const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value});
}
for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){
if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`);
if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]);
}
if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid');
const rawAgent=remote.getValue('agentmode');
const agentmode=rawAgent==null?1:Number(rawAgent);
if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode');
return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template});
import type { RemoteConfig } from '../config/remote-config.ts';
export interface LobbyTemplate {
isGameHall:boolean; mainMenuButton:boolean; hideNotice:boolean; vipInfinite:boolean;
}
/** Template-owned defaults: 02_Const.isGameHall and 00_SubGame_Config.sysConfig. */
export const LOBBY_TEMPLATE:Readonly<LobbyTemplate>=Object.freeze({
isGameHall:false,mainMenuButton:false,hideNotice:false,vipInfinite:false,
});
export interface LobbyContext {versionState:0|1;launchMode:0|1;}
const HALL_DEFAULTS=Object.freeze({androidPay:0,rankList:0 as number|string,isReSetOwnerNote:0 as number|string,storeUrl:'',bagUrl:'',actData:0 as unknown,worldRoomList:0 as number|string,wareHouse:0 as number|string});
const SYS_DEFAULTS=Object.freeze({vipRankLimit:10,hideTaskBtn:0 as number|string,hideWxShare:0 as number|string,miniPro:0 as number|string,hideRoomCard:1,cdKey:1,
ofcShortCode:null as unknown,hideSpid:null as unknown});
function object(value:unknown,name:string):Record<string,unknown>{
if(value===null||typeof value!=='object'||Array.isArray(value))throw Error(`Invalid ${name}: expected object`);
return value as Record<string,unknown>;
}
/** This is the sole menu config source. Defaults reproduce 04_Data, then 12_Logic.getConfig_Succ merges. */
export function resolveLobbyMenuConfig(remote:RemoteConfig,template:Readonly<LobbyTemplate>=LOBBY_TEMPLATE){
const rawHall=remote.getValue('hall_config');
const merged=rawHall==null?{}:object(rawHall,'hall_config');
const hall={...HALL_DEFAULTS,...merged};
for(const key of Object.keys(HALL_DEFAULTS) as (keyof typeof HALL_DEFAULTS)[]){
// 04_Data.hallConfigName contains 'actData:' literally; top-level actData never overrides hall_config.actData.
if(key==='actData')continue;
const value=remote.getValue(key);if(value!=null)Object.assign(hall,{[key]:value});
}
for(const key of ['androidPay','rankList','worldRoomList','wareHouse'] as const){
if(!Number.isFinite(Number(hall[key])))throw Error(`Invalid ${key}`);
if(key==='androidPay')hall[key]=Number(hall[key]);
}
for(const key of ['storeUrl','bagUrl'] as const)if(typeof hall[key]!=='string')throw Error(`Invalid ${key}`);
const sys={...SYS_DEFAULTS};
for(const key of Object.keys(SYS_DEFAULTS) as (keyof typeof SYS_DEFAULTS)[]){
const value=remote.getValue(key);if(value!=null)Object.assign(sys,{[key]:value});
}
if(!Number.isInteger(Number(sys.vipRankLimit))||Number(sys.vipRankLimit)<=0)throw Error('Invalid vipRankLimit');
sys.vipRankLimit=Number(sys.vipRankLimit);
for(const key of ['hideTaskBtn','hideWxShare','miniPro','hideRoomCard','cdKey'] as const){
if(!Number.isFinite(Number(sys[key])))throw Error(`Invalid ${key}`);
if(key==='hideRoomCard'||key==='cdKey')sys[key]=Number(sys[key]);
}
if(sys.hideSpid!==null&&(!Array.isArray(sys.hideSpid)||sys.hideSpid.some(id=>!Number.isInteger(id))))throw Error('Invalid hideSpid');
const rawAgent=remote.getValue('agentmode');
const agentmode=rawAgent==null?1:Number(rawAgent);
if(![1,2,3].includes(agentmode))throw Error('Invalid agentmode');
// Original GameUI draw cases 568/483/3139: missing vipConfig means unopened;
// the earlier matchhtml condition is commented out in the source.
const vipConfig=remote.getValue('vipConfig');
const locked=Object.freeze({568:!hall.worldRoomList,483:!vipConfig,3139:!sys.ofcShortCode});
return Object.freeze({agentmode,hall:Object.freeze(hall),sys:Object.freeze(sys),template,locked});
}
export type LobbyMenuConfig=ReturnType<typeof resolveLobbyMenuConfig>;
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;}
/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */
export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const {hall,sys,template,agentmode}=config;
const visible:Record<number,boolean>={},left:Record<number,number>={},top:Record<number,number>={};
const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;};
// Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X.
if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;}
hide([219,220]);
if(context.versionState===1){
if(context.launchMode===0)hide([234]);
hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604,
286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]);
}else{
if(agentmode===1)hide([10,509]);
visible[235]=false;
if(hall.androidPay===1){visible[10]=true;visible[509]=true;}
visible[3018]=!template.isGameHall&&Array.isArray(hall.actData);
visible[3016]=Boolean(hall.storeUrl);
if(template.isGameHall||template.vipInfinite)hide([3139,679]);
if(template.mainMenuButton)hide([11,12,568,483]);
if(template.hideNotice)hide([16,180,193,194,682,178,179,145]);
if(sys.hideTaskBtn)hide([17]);
if(sys.miniPro||sys.hideWxShare)hide([14]);
if(!template.isGameHall){
const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);});
}
visible[3233]=Boolean(hall.rankList);
visible[3256]=false; // Both original deviceLogin branches hide this entry.
if(template.isGameHall){
hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]);
visible[235]=agentmode!==1;visible[15]=true;visible[5]=true;
const shown=[235,15,5,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;});
}
}
if(sys.hideRoomCard===2)hide([286,10,287,3161]);
visible[3264]=sys.cdKey===2;
return {visible,left,top};
/** Original click messages intentionally use different comma characters. */
export function lobbyLockedTip(config:LobbyMenuConfig,action:'world-rooms'|'match'|'official-rooms'):string|null{
if(action==='world-rooms')return config.locked[568]?'暂未开放,敬请期待':null;
return config.locked[action==='match'?483:3139]?'暂未开放,敬请期待':null;
}
/** openPallet runs its own visibility pass each time before placing visible entries. */
export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const visible:Record<number,boolean>={15:false,604:Boolean(config.hall.rankList),
235:config.agentmode!==1&&!config.sys.miniPro,3017:Boolean(config.hall.bagUrl),3057:Boolean(config.hall.wareHouse),5:true};
const left:Record<number,number>={3121:200},top:Record<number,number>={};
if(context.versionState===1)return {visible:Object.fromEntries(Object.keys(visible).map(id=>[id,false])),left,top};
const shown=[15,604,235,3017,3057,5].filter(id=>visible[id]);
shown.forEach((id,index)=>{left[id]=216+155*index;top[id]=540;});
return {visible,left,top};
}
/** IDs remain source traceability only; Cocos owns hit testing and prefab lifetimes. */
export const LOBBY_MENU_ACTIONS=Object.freeze({
7:'settings',9:'profile',11:'create-room',12:'join-room',13:'records',14:'share',17:'tasks',679:'vip-rooms',
10:'buy-roomcard',286:'buy-roomcard',287:'buy-roomcard',3161:'buy-roomcard',
508:'buy-bean',509:'buy-bean',496:'buy-bean',3162:'buy-bean',
483:'match',568:'world-rooms',3016:'store',3018:'activity',99:'my-rooms',76:'qrcode',
3139:'official-rooms',234:'more-games',3120:'tray',3233:'rank',3232:'vip-rank',3256:'bind-phone',3264:'cdkey',
5:'notice',3017:'bag',235:'agent',604:'rank',3057:'warehouse',15:'feedback',
} as const);
export type LobbyAction=typeof LOBBY_MENU_ACTIONS[keyof typeof LOBBY_MENU_ACTIONS];
export const LOBBY_UNAVAILABLE:Readonly<Partial<Record<LobbyAction,string>>>=Object.freeze({
'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块',
'buy-roomcard':'购买房卡:尚未接入支付模块','buy-bean':'购买星星:尚未接入支付模块',
match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口',
activity:'活动:尚未接入活动身份参数与原生接口','my-rooms':'我的房间:尚未接入房间记录与进房',qrcode:'二维码:尚未接入分享模块',
'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口',
rank:'排行:尚未接入排行榜', 'vip-rank':'VIP 排行:尚未接入排行榜','bind-phone':'绑定手机:尚未接入绑定流程',
cdkey:'兑换码:尚未接入兑换请求',bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口',
warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交',
});
export interface LobbyMenuState {visible:Record<number,boolean>;left:Record<number,number>;top:Record<number,number>;}
/** Ordered projection of JumpMenuScene. Unmentioned nodes retain their authored state. */
export function lobbyMenuState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const {hall,sys,template,agentmode}=config;
const visible:Record<number,boolean>={},left:Record<number,number>={},top:Record<number,number>={};
const hide=(ids:number[])=>{for(const id of ids)visible[id]=false;};
// Source hideSpid moves nodes before JumpMenuScene; later smart-bar placement can override X.
if(Array.isArray(sys.hideSpid))for(const id of sys.hideSpid){left[id]=1300;top[id]=730;}
hide([219,220]);
if(context.versionState===1){
if(context.launchMode===0)hide([234]);
hide([17,14,16,145,340,339,681,682,178,179,180,193,194,235,483,568,604,
286,157,287,10,508,497,496,509,3233,3232,76,3139,679,99,3120,3161,3162,3016,3018,3017]);
}else{
if(agentmode===1)hide([10,509]);
visible[235]=false;
if(hall.androidPay===1){visible[10]=true;visible[509]=true;}
visible[3018]=!template.isGameHall&&Array.isArray(hall.actData);
visible[3016]=Boolean(hall.storeUrl);
if(template.isGameHall||template.vipInfinite)hide([3139,679]);
if(template.mainMenuButton)hide([11,12,568,483]);
if(template.hideNotice)hide([16,180,193,194,682,178,179,145]);
if(sys.hideTaskBtn)hide([17]);
if(sys.miniPro||sys.hideWxShare)hide([14]);
if(!template.isGameHall){
const shown=[3120,14,17,13,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=shown.length===1?558:shown.length===2?[376,740][index]:194+728*index/(shown.length-1);});
}
visible[3233]=Boolean(hall.rankList);
visible[3256]=false; // Both original deviceLogin branches hide this entry.
if(template.isGameHall){
hide([14,13,17,3120,99,3016,76,234,3057,145,3139,3232,3233,604]);
visible[235]=agentmode!==1;visible[15]=true;visible[5]=true;
const shown=[235,15,5,7].filter(id=>visible[id]!==false);
shown.forEach((id,index)=>{left[id]=300+520*index/(shown.length-1);top[id]=642;});
}
}
if(sys.hideRoomCard===2)hide([286,10,287,3161]);
visible[3264]=sys.cdKey===2;
return {visible,left,top};
}
/** openPallet runs its own visibility pass each time before placing visible entries. */
export function lobbyTrayState(config:LobbyMenuConfig,context:LobbyContext):LobbyMenuState{
const visible:Record<number,boolean>={15:false,604:Boolean(config.hall.rankList),
235:config.agentmode!==1&&!config.sys.miniPro,3017:Boolean(config.hall.bagUrl),3057:Boolean(config.hall.wareHouse),5:true};
const left:Record<number,number>={3121:200},top:Record<number,number>={};
if(context.versionState===1)return {visible:Object.fromEntries(Object.keys(visible).map(id=>[id,false])),left,top};
const shown=[15,604,235,3017,3057,5].filter(id=>visible[id]);
shown.forEach((id,index)=>{left[id]=216+155*index;top[id]=540;});
return {visible,left,top};
}
/** IDs remain source traceability only; Cocos owns hit testing and prefab lifetimes. */
export const LOBBY_MENU_ACTIONS=Object.freeze({
7:'settings',9:'profile',11:'create-room',12:'join-room',13:'records',14:'share',17:'tasks',679:'vip-rooms',
10:'buy-roomcard',286:'buy-roomcard',287:'buy-roomcard',3161:'buy-roomcard',
508:'buy-bean',509:'buy-bean',496:'buy-bean',3162:'buy-bean',
483:'match',568:'world-rooms',3016:'store',3018:'activity',99:'my-rooms',76:'qrcode',
3139:'official-rooms',234:'more-games',3120:'tray',3233:'rank',3232:'vip-rank',3256:'bind-phone',3264:'cdkey',
5:'notice',3017:'bag',235:'agent',604:'rank',3057:'warehouse',15:'feedback',
} as const);
export type LobbyAction=typeof LOBBY_MENU_ACTIONS[keyof typeof LOBBY_MENU_ACTIONS];
export const LOBBY_UNAVAILABLE:Readonly<Partial<Record<LobbyAction,string>>>=Object.freeze({
'create-room':'创建房间:尚未接入目标子游戏规则','records':'战绩:尚未接入子游戏战绩模块',
match:'比赛:尚未接入比赛模块','world-rooms':'星星场:尚未接入房间列表',store:'商城:尚未接入商城身份参数与原生接口',
activity:'活动:尚未接入活动身份参数与原生接口',
'official-rooms':'官方试玩:尚未接入微房号列表','more-games':'更多游戏:尚未接入返回大厅与下载入口',
'bind-phone':'绑定手机:尚未接入绑定流程',
bag:'背包:尚未接入背包身份参数与原生接口',agent:'代理:尚未接入代理入口',
warehouse:'保险箱:尚未接入仓库模块',feedback:'反馈:尚未接入意见提交',
});
@@ -1,66 +1,74 @@
import type { RemoteConfig } from '../config/remote-config.ts';
import type { PlatformState } from '../platform/stores/platform-types.ts';
import { selectSelfPlayer } from '../platform/stores/selectors.ts';
// Presentation checks required view inputs without depending on the protocol layer.
function requireNumber(value:unknown,path:string):number {if(typeof value!=='number'||!Number.isFinite(value))throw Error(`${path}: expected number`);return value;}
function requireInteger(value:unknown,path:string):number {const result=requireNumber(value,path);if(!Number.isInteger(result))throw Error(`${path}: expected integer`);return result;}
function requireString(value:unknown,path:string):string {if(typeof value!=='string')throw Error(`${path}: expected string`);return value;}
function requireRecord(value:unknown,path:string):Record<string,unknown> {if(!value||typeof value!=='object'||Array.isArray(value))throw Error(`${path}: expected object`);return value as Record<string,unknown>;}
export function selectLobbyModel(state:PlatformState) {
const self=selectSelfPlayer(state);
if(!self)throw Error('lobby requires authenticated player');
return Object.freeze({...self,
roomcard:requireNumber(self.roomcard,'players.self.roomcard'),
taskstate:requireInteger(self.taskstate,'players.self.taskstate'),
score:requireNumber(self.score,'players.self.score'),
sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~',
canAct:state.app.phase==='logged-in'&&state.room.kind==='outside',
});
}
import type { RemoteConfig } from '../config/remote-config.ts';
import type { PlatformState } from '../platform/stores/platform-types.ts';
import { selectSelfPlayer } from '../platform/stores/selectors.ts';
// Presentation checks required view inputs without depending on the protocol layer.
function requireNumber(value:unknown,path:string):number {if(typeof value!=='number'||!Number.isFinite(value))throw Error(`${path}: expected number`);return value;}
function requireInteger(value:unknown,path:string):number {const result=requireNumber(value,path);if(!Number.isInteger(result))throw Error(`${path}: expected integer`);return result;}
function requireString(value:unknown,path:string):string {if(typeof value!=='string')throw Error(`${path}: expected string`);return value;}
function requireRecord(value:unknown,path:string):Record<string,unknown> {if(!value||typeof value!=='object'||Array.isArray(value))throw Error(`${path}: expected object`);return value as Record<string,unknown>;}
export function selectLobbyModel(state:PlatformState) {
const self=selectSelfPlayer(state);
if(!self)throw Error('lobby requires authenticated player');
return Object.freeze({...self,
roomcard:requireNumber(self.roomcard,'players.self.roomcard'),
taskstate:requireInteger(self.taskstate,'players.self.taskstate'),
score:requireNumber(self.score,'players.self.score'),
sign:self.sign===null?'这个家伙很懒~~':requireString(self.sign,'players.self.sign') || '这个家伙很懒~~',
canAct:state.app.phase==='logged-in'&&state.room.kind==='outside',
});
}
export type LobbyModel=ReturnType<typeof selectLobbyModel>;
export interface Notice {type:0|1; text:string; image:string; url:string; height:number;}
export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string;
starName:string; roomCardName:string; contacts:readonly {label:string;value:string}[];}
/** Source defaults: 04_Data.sysConfig and advertInfo_0/1/2; 12_Logic.get_paravalue. */
export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy {
// 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName.
const cardName=remote.getValue('roomCardName');
const roomCardName=cardName?requireString(cardName,'roomCardName'):'房卡';
const interval=remote.getValue('gameNoticeShow');
const noticeHours=interval==null?0:Number(interval);
if(!Number.isFinite(noticeHours)||noticeHours< -1)throw Error('Invalid gameNoticeShow');
const menuValue=remote.getValue('menunotice');
const menu=menuValue==null?{}:requireRecord(menuValue,'menunotice');
const notices:Notice[]=[];
for(let index=0;index<3;index++){
const value=menu[`advert${index}`];
if(value==null){
const message=index===0?remote.getValue('noticemsg'):null;
notices.push({type:0,text:message==null?'':requireString(message,'noticemsg').replace(/#/g,'\n'),image:'',url:'',height:1});
continue;
}
const data=requireRecord(value,`menunotice.advert${index}`);
const type=requireInteger(data.type,`menunotice.advert${index}.type`);
if(type!==0&&type!==1)throw Error(`menunotice.advert${index}.type must be 0 or 1`);
// Empty text and URL explicitly mean no content/action in the source.
const field=(key:string)=>data[key]==null?'':requireString(data[key],`menunotice.advert${index}.${key}`);
notices.push({type,text:type===0?field('msg').replace(/#/g,'\n'):'',image:type===1?field('imgUrl'):'',
url:field(type===0?'url':'imgAdUrl'),height:type===1?requireNumber(data.height,`menunotice.advert${index}.height`):1});
}
const scroll=remote.getValue('scrollmsg');
const star=remote.getValue('starName');
const contacts:Array<{label:string;value:string}>=[];
for(const [key,title] of [['service_wechat','客服微信:'],['service_qq','投诉建议:'],['service_tel','客服手机:']]){
const value=remote.getValue(key);
if(value!=null&&value!=='')contacts.push({label:title,value:requireString(value,key)});
}
return Object.freeze({roomCardName,noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'),
starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts)});
}
/** Exact JudgeShow expiry: equality is not expired; reconnect never auto-opens. */
export function shouldShowNotice(hours:number,previous:number|null,now:number,reconnect:boolean):boolean {
if(reconnect||hours===-1)return false;
return hours===0||!previous||previous+hours*3600000<now;
/** Game_Config.Notice: speed 0.1 px/ms, 15 px per gblen byte. */
export function announcementMotion(text:string,width:number,elapsedSeconds:number){
if(!Number.isFinite(width)||width<=0)throw Error('Invalid announcement width');
if(!Number.isFinite(elapsedSeconds)||elapsedSeconds<0)throw Error('Invalid announcement elapsed time');
const textWidth=text.replace(/[^\x00-\xff]/g,'**').length*15;
const duration=(width+textWidth)/100;
return {visible:text.length>0,textWidth,duration,left:width-(elapsedSeconds%duration)*100};
}
export interface Notice {type:0|1; text:string; image:string; url:string; height:number;}
export interface LobbyPolicy {noticeHours:number; notices:readonly Notice[]; scroll:string;
starName:string; roomCardName:string; contacts:readonly {label:string;value:string}[];}
/** Source defaults: 04_Data.sysConfig and advertInfo_0/1/2; 12_Logic.get_paravalue. */
export function resolveLobbyPolicy(remote:RemoteConfig):LobbyPolicy {
// 04_Data starts with 房卡; getConfig_Succ replaces only a truthy roomCardName.
const cardName=remote.getValue('roomCardName');
const roomCardName=cardName?requireString(cardName,'roomCardName'):'房卡';
const interval=remote.getValue('gameNoticeShow');
const noticeHours=interval==null?0:Number(interval);
if(!Number.isFinite(noticeHours)||noticeHours< -1)throw Error('Invalid gameNoticeShow');
const menuValue=remote.getValue('menunotice');
const menu=menuValue==null?{}:requireRecord(menuValue,'menunotice');
const notices:Notice[]=[];
for(let index=0;index<3;index++){
const value=menu[`advert${index}`];
if(value==null){
const message=index===0?remote.getValue('noticemsg'):null;
notices.push({type:0,text:message==null?'':requireString(message,'noticemsg').replace(/#/g,'\n'),image:'',url:'',height:1});
continue;
}
const data=requireRecord(value,`menunotice.advert${index}`);
const type=requireInteger(data.type,`menunotice.advert${index}.type`);
if(type!==0&&type!==1)throw Error(`menunotice.advert${index}.type must be 0 or 1`);
// Empty text and URL explicitly mean no content/action in the source.
const field=(key:string)=>data[key]==null?'':requireString(data[key],`menunotice.advert${index}.${key}`);
notices.push({type,text:type===0?field('msg').replace(/#/g,'\n'):'',image:type===1?field('imgUrl'):'',
url:field(type===0?'url':'imgAdUrl'),height:type===1?requireNumber(data.height,`menunotice.advert${index}.height`):1});
}
const scroll=remote.getValue('scrollmsg');
const star=remote.getValue('starName');
const contacts:Array<{label:string;value:string}>=[];
for(const [key,title] of [['service_wechat','客服微信:'],['service_qq','投诉建议:'],['service_tel','客服手机:']]){
const value=remote.getValue(key);
if(value!=null&&value!=='')contacts.push({label:title,value:requireString(value,key)});
}
return Object.freeze({roomCardName,noticeHours,notices:Object.freeze(notices),scroll:scroll==null?'':requireString(scroll,'scrollmsg'),
starName:star==null?'星星':requireString(star,'starName'),contacts:Object.freeze(contacts)});
}
/** Exact JudgeShow expiry: equality is not expired; reconnect never auto-opens. */
export function shouldShowNotice(hours:number,previous:number|null,now:number,reconnect:boolean):boolean {
if(reconnect||hours===-1)return false;
return hours===0||!previous||previous+hours*3600000<now;
}
@@ -0,0 +1,24 @@
import {LEGACY_AUTH_DISTRICTS} from './legacy-auth-districts.ts';
/** Game_Surface_3 05_Func.IDValidator + 11_GameUI cases 484/493.
* Local format validation only. Deliberately retain the source's 15-digit,
* parent-district and month/day range semantics; this is not server identity proof.
*/
export function validateProfileAuth(name:string,id:string):'name'|'id'|null {
if(name.length<=1)return 'name';
const code=id.toUpperCase();
if(code.length!==15&&code.length!==18)return 'id';
const body=code.length===18?code.slice(0,17):code;
const address=body.slice(0,6);
const hasAddress=(key:string)=>Object.prototype.hasOwnProperty.call(LEGACY_AUTH_DISTRICTS,key);
if(!hasAddress(address)&&!hasAddress(address.slice(0,4)+'00')&&!hasAddress(address.slice(0,2)+'0000'))return 'id';
const birth=code.length===18?body.slice(6,14):body.slice(6,12);
const month=parseInt(birth.slice(-4,-2),10),day=parseInt(birth.slice(-2),10);
if(month>12||month===0||day>31||day===0)return 'id';
if(code.length===15)return null;
let sum=0;
for(let index=0;index<body.length;index++)sum+=parseInt(body[index],10)*(Math.pow(2,17-index)%11);
const value=12-sum%11;
const check=value===10?'X':String(value>10?value%11:value);
return check===code.slice(-1)?null:'id';
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "11f5830e-5f4b-46fb-9e06-b2b65edbed89",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,14 @@
export type LoadingRpc='get_player_task'|'get_task_award'|'getVipRankList';
/** Wire receipt, rather than a successful model update, completes request loading. */
export class RequestLoading {
private readonly pending=new Set<LoadingRpc>();
constructor(private readonly show:(visible:boolean)=>void){}
begin(rpc:LoadingRpc):void{const empty=this.pending.size===0;this.pending.add(rpc);if(empty)this.show(true);}
cancel(rpc:LoadingRpc):void{if(this.pending.delete(rpc)&&this.pending.size===0)this.show(false);}
received(message:{route:string;rpc:string;data?:unknown}):void{
if(message.route!=='agent')return;
if(message.rpc==='show_message'){this.clear();return;}
if(message.rpc==='get_player_task'||message.rpc==='get_task_award'||message.rpc==='getVipRankList')this.cancel(message.rpc);
}
clear():void{if(this.pending.size){this.pending.clear();this.show(false);}}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8bb6a294-bfcf-4126-98f1-0845b58c72db",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,19 +1,27 @@
import type { RuntimeConfig } from '../config/runtime-config.ts';
/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */
export function resolveSharePolicy(config:RuntimeConfig){
const remote=config.remoteConfig,raw=remote.getValue('hall_config');
if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config');
const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record<string,unknown>;
for(const key of Object.keys(hall)){const value=remote.getValue(key);if(value!=null)hall[key]=value;}
const text=(value:unknown,name:string)=>{if(typeof value!=='string')throw Error(`Invalid ${name}`);return value;};
const download=remote.getValue('url_download'),name=remote.getValue('sharename'),id=config.identity;
return {url:download?`${text(download,'url_download')}?agentid=${id.agentid}&channelid=${id.channelid}&gameid=${id.gameid}&marketid=${id.marketid}`:'',
title:name?text(name,'sharename'):'',description:hall.shareDesc?text(hall.shareDesc,'shareDesc'):'hello world',
background:text(hall.shareBgUrl,'shareBgUrl'),image:text(hall.shareImgUrl,'shareImgUrl'),imageTimeline:!!hall.shareType};
}
export function sharePayload(policy:ReturnType<typeof resolveSharePolicy>,friend:1|2){
if(friend===2&&!policy.imageTimeline)return null; // Legacy case 75 has no link-sharing call.
return {sharefriend:friend,type:friend===1?1:3,webpageUrl:friend===1?policy.url:policy.image||policy.url,
title:friend===1?policy.title||policy.title+policy.description:policy.title+policy.description,
description:friend===1?policy.description:undefined,sharetype:0};
/** Open_ewm uses this exact config key; absent configuration intentionally does nothing. */
export function resolveQrCodePolicy(config:RuntimeConfig){
const url=config.remoteConfig.getValue('WechatPublicNo_ewm');
if(!url)return null;
if(typeof url!=='string')throw Error('Invalid WechatPublicNo_ewm');
const policy=resolveSharePolicy(config);
return {url,share:{sharefriend:1,type:3,webpageUrl:url,title:policy.title+policy.description,description:undefined,sharetype:0}};
}
/** Source: 12_Logic.getConfig_Succ then 05_Func.sharefriend. */
export function resolveSharePolicy(config:RuntimeConfig){
const remote=config.remoteConfig,raw=remote.getValue('hall_config');
if(raw!=null&&(typeof raw!=='object'||Array.isArray(raw)))throw Error('Invalid hall_config');
const hall={shareDesc:'',shareBgUrl:'',shareImgUrl:'',shareType:0,...raw as object} as Record<string,unknown>;
for(const key of Object.keys(hall)){const value=remote.getValue(key);if(value!=null)hall[key]=value;}
const text=(value:unknown,name:string)=>{if(typeof value!=='string')throw Error(`Invalid ${name}`);return value;};
const download=remote.getValue('url_download'),name=remote.getValue('sharename'),id=config.identity;
return {url:download?`${text(download,'url_download')}?agentid=${id.agentid}&channelid=${id.channelid}&gameid=${id.gameid}&marketid=${id.marketid}`:'',
title:name?text(name,'sharename'):'',description:hall.shareDesc?text(hall.shareDesc,'shareDesc'):'hello world',
background:text(hall.shareBgUrl,'shareBgUrl'),image:text(hall.shareImgUrl,'shareImgUrl'),imageTimeline:!!hall.shareType};
}
export function sharePayload(policy:ReturnType<typeof resolveSharePolicy>,friend:1|2){
if(friend===2&&!policy.imageTimeline)return null; // Legacy case 75 has no link-sharing call.
return {sharefriend:friend,type:friend===1?1:3,webpageUrl:friend===1?policy.url:policy.image||policy.url,
title:friend===1?policy.title||policy.title+policy.description:policy.title+policy.description,
description:friend===1?policy.description:undefined,sharetype:0};
}
@@ -1,52 +1,56 @@
export {
hasOwn,
optionalField,
requireArray,
requireBoolean,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
export {
buildLoginRequest,
parseLoginResponse,
type DeviceLoginMode,
type FirstSliceRuntimeConfig,
type LoginAccountIdentity,
type LoginDeviceSnapshot,
type LoginPlayerIdMode,
type LoginRequestPayload,
type ParsedLoginResponse,
} from './login-contract.ts';
export {
buildExitRoomRequest,
buildJoinRoomRequest,
buildPrepareRequest,
parseConnectAgentserverPayload,
parseConnectRoomserverPayload,
parseKickServerPayload,
parseOtherExitRoomPayload,
parseOtherJoinRoomPayload,
parseOtherOfflinePayload,
parseOtherOnlinePayload,
parsePlayerPreparePayload,
parseSelfExitRoomPayload,
parseSelfJoinRoomResponse,
type JoinRoomEnvironment,
type JoinRoomPayload,
type ParsedAgentserverPayload,
type ParsedKickPayload,
type ParsedOnlinePayload,
type ParsedRoomEntry,
type ParsedRoomResponse,
type ParsedRoomserverPayload,
type ParsedRoomSnapshot,
type ParsedSelfExitPayload,
type ParsedSeatPayload,
type ReconnectSnapshot,
type RoomActionPayload,
type RoomIdentity,
} from './room-contracts.ts';
export {
hasOwn,
optionalField,
requireArray,
requireBoolean,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
export {
buildLoginRequest,
parseLoginResponse,
type DeviceLoginMode,
type FirstSliceRuntimeConfig,
type LoginAccountIdentity,
type LoginDeviceSnapshot,
type LoginPlayerIdMode,
type LoginRequestPayload,
type ParsedLoginResponse,
} from './login-contract.ts';
export {
buildCreateRoomRequest,
parseCreateRoomResponse,
type ParsedCreateRoomResponse,
type CreateRoomEnvironment,
buildExitRoomRequest,
buildJoinRoomRequest,
buildPrepareRequest,
parseConnectAgentserverPayload,
parseConnectRoomserverPayload,
parseKickServerPayload,
parseOtherExitRoomPayload,
parseOtherJoinRoomPayload,
parseOtherOfflinePayload,
parseOtherOnlinePayload,
parsePlayerPreparePayload,
parseSelfExitRoomPayload,
parseSelfJoinRoomResponse,
type JoinRoomEnvironment,
type JoinRoomPayload,
type ParsedAgentserverPayload,
type ParsedKickPayload,
type ParsedOnlinePayload,
type ParsedRoomEntry,
type ParsedRoomResponse,
type ParsedRoomserverPayload,
type ParsedRoomSnapshot,
type ParsedSelfExitPayload,
type ParsedSeatPayload,
type ReconnectSnapshot,
type RoomActionPayload,
type RoomIdentity,
} from './room-contracts.ts';
@@ -0,0 +1,27 @@
import {requireArray,requireRecord,requireString,requireStringOrFiniteNumber} from './validation.ts';
export interface RankEntry {readonly name:string;readonly headimg:string;readonly playerid:string|number;readonly star:string|number;readonly signature:string;readonly notice?:string;readonly destwo?:string;}
/** Server rank entries are consumed as sent. Only rankList1 config replaces # line breaks. */
export function parseRankList(value:unknown,fromConfig=false):readonly RankEntry[]{
return Object.freeze(requireArray(value,'rank list').map((item,index)=>{
const p=`rank list[${index}]`,r=requireRecord(item,p);
const name=requireString(r.name,p+'.name'),signature=r.signature?requireString(r.signature,p+'.signature'):'';
return Object.freeze({name:fromConfig?configRankName(name):name,headimg:requireString(r.headimg,p+'.headimg'),playerid:requireStringOrFiniteNumber(r.playerid,p+'.playerid'),
star:requireStringOrFiniteNumber(r.star,p+'.star'),signature:fromConfig?signature.replace(/#/g,'\n'):signature});
}));
}
/** Func.subString(name,16,true): Latin-1 counts one, other UTF-16 units two. */
function configRankName(name:string):string {
let length=0;
for(let i=0;i<name.length;i++){
length+=name.charCodeAt(i)>255?2:1;
if(length>16)return name.slice(0,i)+'...';
}
return name;
}
export function parseVipRankList(value:unknown):readonly RankEntry[]{
const raw=requireArray(value,'VIP rank list'),entries=parseRankList(raw);
return Object.freeze(entries.map((entry,i)=>{const item=requireRecord(raw[i],'VIP rank entry');
return Object.freeze({...entry,notice:item.notice?requireString(item.notice,'notice'):'',destwo:item.destwo?requireString(item.destwo,'destwo'):''});}));
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "744452fb-2d7e-4a0e-9c49-2e6994f5df84",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,243 +1,302 @@
import {
buildFirstSliceEnvelope,
type FirstSliceEnvelope,
} from '../first-slice-routes.ts';
import {
hasOwn,
optionalField,
requireArray,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
import type { FirstSliceRuntimeConfig } from './login-contract.ts';
export type ReconnectSnapshot =
| { readonly present: false }
| { readonly present: true; readonly value: unknown };
export interface ParsedRoomSnapshot {
readonly roomcode: string;
readonly seat: number;
readonly roomtype: readonly unknown[];
readonly players: readonly unknown[];
}
export interface ParsedRoomEntry {
readonly room: ParsedRoomSnapshot;
readonly reconnect: ReconnectSnapshot;
readonly raw: JsonRecord;
}
export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot {
if (!Boolean(raw.deskinfo)) return { present: false };
return { present: true, value: raw.deskinfo };
}
export function parseRoomEntryFields(raw: JsonRecord): ParsedRoomSnapshot {
const roomcode = requireString(raw.roomcode, '$.data.roomcode');
const seat = requireInteger(raw.seat, '$.data.seat');
const roomtype = requireArray(raw.roomtype, '$.data.roomtype');
const players = requireArray(raw.players, '$.data.players');
if (seat < 0 || seat >= players.length) {
throw new RangeError(`$.data.seat: expected server seat index within players, received number`);
}
return { roomcode, seat, roomtype, players };
}
export interface RoomIdentity {
readonly playerid: number;
readonly roomcode: string;
}
export interface RoomActionPayload extends Readonly<Record<string, unknown>> {
readonly agentid: string | number;
readonly playerid: number;
readonly gameid: string | number;
readonly roomcode: string;
}
function baseRoomAction(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): RoomActionPayload {
const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity');
const roomRecord = requireRecord(room, '$.room');
return {
agentid: requireStringOrFiniteNumber(
identity.agentid,
'$.runtimeConfig.identity.agentid',
),
playerid: requireInteger(roomRecord.playerid, '$.room.playerid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'),
};
}
export function buildPrepareRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): FirstSliceEnvelope<'player_prepare', RoomActionPayload> {
return buildFirstSliceEnvelope('player_prepare', baseRoomAction(runtimeConfig, room));
}
export function buildExitRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): FirstSliceEnvelope<'self_exit_room', RoomActionPayload> {
return buildFirstSliceEnvelope('self_exit_room', baseRoomAction(runtimeConfig, room));
}
export interface JoinRoomEnvironment {
readonly location: unknown;
readonly ip: string;
readonly vipMatch?: 1;
readonly match_id?: string | number;
}
export interface JoinRoomPayload extends RoomActionPayload {
readonly location: unknown;
readonly ip: string;
readonly vipMatch?: 1;
readonly match_id?: string | number;
}
export function buildJoinRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
environment: JoinRoomEnvironment,
): FirstSliceEnvelope<'self_join_room', JoinRoomPayload> {
const environmentRecord = requireRecord(environment, '$.environment');
if (!hasOwn(environmentRecord, 'location') || environmentRecord.location === undefined) {
throw new TypeError('$.environment.location: expected present field, received undefined');
}
if (environmentRecord.location !== null) {
requireRecord(environmentRecord.location, '$.environment.location');
}
const data: Record<string, unknown> = {
...baseRoomAction(runtimeConfig, room),
location: environmentRecord.location,
ip: requireString(environmentRecord.ip, '$.environment.ip'),
};
const vipMatch = optionalField(
environmentRecord,
'vipMatch',
'$.environment',
(value, path): 1 => {
const parsed = requireNumber(value, path);
if (parsed !== 1) {
throw new RangeError(`${path}: expected number 1, received number`);
}
return 1;
},
);
if (vipMatch !== undefined) data.vipMatch = vipMatch;
const matchId = optionalField(
environmentRecord,
'match_id',
'$.environment',
requireStringOrFiniteNumber,
);
if (matchId !== undefined) data.match_id = matchId;
return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload);
}
export interface ParsedRoomResponse {
readonly state: number;
readonly room: ParsedRoomSnapshot | null;
readonly reconnect: ReconnectSnapshot;
readonly stage: 0 | 1;
readonly raw: JsonRecord;
}
export function parseSelfJoinRoomResponse(value: unknown): ParsedRoomResponse {
const raw = requireRecord(value, '$.data');
const state = requireInteger(raw.state, '$.data.state');
const room = state === 0 ? parseRoomEntryFields(raw) : null;
const parsedDeskwar = optionalField(raw, 'deskwar', '$.data', requireInteger);
if (parsedDeskwar !== undefined && parsedDeskwar !== 0 && parsedDeskwar !== 1) {
throw new RangeError('$.data.deskwar: expected protocol flag 0 or 1');
}
const reconnect = reconnectFrom(raw);
const stage = reconnect.present ? 1 : parsedDeskwar ?? 0;
return { state, room, reconnect, stage, raw };
}
export interface ParsedSeatPayload {
readonly seat: number;
readonly raw: JsonRecord;
}
function parseSeatPayload(value: unknown): ParsedSeatPayload {
const raw = requireRecord(value, '$.data');
const seat = requireInteger(raw.seat, '$.data.seat');
if (seat < 0) throw new RangeError('$.data.seat: expected non-negative server seat, received number');
return { seat, raw };
}
export const parsePlayerPreparePayload = parseSeatPayload;
export const parseOtherJoinRoomPayload = parseSeatPayload;
export const parseOtherExitRoomPayload = parseSeatPayload;
export const parseOtherOfflinePayload = parseSeatPayload;
export interface ParsedSelfExitPayload {
readonly seat?: number;
readonly raw: JsonRecord;
}
export function parseSelfExitRoomPayload(value: unknown): ParsedSelfExitPayload {
const raw = requireRecord(value, '$.data');
const seat = optionalField(raw, 'seat', '$.data', requireInteger);
if (seat !== undefined && seat < 0) {
throw new RangeError('$.data.seat: expected non-negative server seat, received number');
}
return { ...(seat === undefined ? {} : { seat }), raw };
}
export interface ParsedOnlinePayload extends ParsedSeatPayload {
readonly ip: string;
}
export function parseOtherOnlinePayload(value: unknown): ParsedOnlinePayload {
const parsed = parseSeatPayload(value);
return { seat: parsed.seat, ip: requireString(parsed.raw.ip, '$.data.ip'), raw: parsed.raw };
}
export interface ParsedRoomserverPayload {
readonly roomserver: string;
readonly raw: JsonRecord;
}
export function parseConnectRoomserverPayload(value: unknown): ParsedRoomserverPayload {
const raw = requireRecord(value, '$.data');
return { roomserver: requireString(raw.roomserver, '$.data.roomserver'), raw };
}
export interface ParsedAgentserverPayload {
readonly agentserver: string;
readonly opt: string;
readonly raw: JsonRecord;
}
export function parseConnectAgentserverPayload(value: unknown): ParsedAgentserverPayload {
const raw = requireRecord(value, '$.data');
return {
agentserver: requireString(raw.agentserver, '$.data.agentserver'),
opt: requireString(raw.opt, '$.data.opt'),
raw,
};
}
export interface ParsedKickPayload {
readonly msg: string;
readonly raw: JsonRecord;
}
export function parseKickServerPayload(value: unknown): ParsedKickPayload {
const raw = requireRecord(value, '$.data');
return { msg: requireString(raw.msg, '$.data.msg'), raw };
}
import {
buildFirstSliceEnvelope,
type FirstSliceEnvelope,
} from '../first-slice-routes.ts';
import {
hasOwn,
optionalField,
requireArray,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
import type { FirstSliceRuntimeConfig } from './login-contract.ts';
export type ReconnectSnapshot =
| { readonly present: false }
| { readonly present: true; readonly value: unknown };
export interface ParsedRoomSnapshot {
readonly roomcode: string;
readonly seat: number;
readonly roomtype: readonly unknown[];
readonly players: readonly unknown[];
}
export interface ParsedRoomEntry {
readonly room: ParsedRoomSnapshot;
readonly reconnect: ReconnectSnapshot;
readonly raw: JsonRecord;
}
export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot {
if (!Boolean(raw.deskinfo)) return { present: false };
return { present: true, value: raw.deskinfo };
}
export function parseRoomEntryFields(raw: JsonRecord): ParsedRoomSnapshot {
const roomcode = requireString(raw.roomcode, '$.data.roomcode');
const seat = requireInteger(raw.seat, '$.data.seat');
const roomtype = requireArray(raw.roomtype, '$.data.roomtype');
const players = requireArray(raw.players, '$.data.players');
if (seat < 0 || seat >= players.length) {
throw new RangeError(`$.data.seat: expected server seat index within players, received number`);
}
return { roomcode, seat, roomtype, players };
}
export interface RoomIdentity {
readonly playerid: number;
readonly roomcode: string;
}
export interface RoomActionPayload extends Readonly<Record<string, unknown>> {
readonly agentid: string | number;
readonly playerid: number;
readonly gameid: string | number;
readonly roomcode: string;
}
function baseRoomAction(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): RoomActionPayload {
const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity');
const roomRecord = requireRecord(room, '$.room');
return {
agentid: requireStringOrFiniteNumber(
identity.agentid,
'$.runtimeConfig.identity.agentid',
),
playerid: requireInteger(roomRecord.playerid, '$.room.playerid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'),
};
}
export function buildPrepareRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): FirstSliceEnvelope<'player_prepare', RoomActionPayload> {
return buildFirstSliceEnvelope('player_prepare', baseRoomAction(runtimeConfig, room));
}
export function buildExitRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: RoomIdentity,
): FirstSliceEnvelope<'self_exit_room', RoomActionPayload> {
return buildFirstSliceEnvelope('self_exit_room', baseRoomAction(runtimeConfig, room));
}
export interface JoinRoomEnvironment {
readonly location: unknown;
readonly ip: string;
readonly vipMatch?: 1;
readonly match_id?: string | number;
}
export interface JoinRoomPayload extends Omit<RoomActionPayload, 'roomcode'> {
readonly roomcode: string | number;
readonly location: unknown;
readonly ip: string;
readonly vipMatch?: 1;
readonly match_id?: string | number;
}
export function buildJoinRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
room: { readonly playerid: number; readonly roomcode: string | number },
environment: JoinRoomEnvironment,
): FirstSliceEnvelope<'self_join_room', JoinRoomPayload> {
const environmentRecord = requireRecord(environment, '$.environment');
if (!hasOwn(environmentRecord, 'location') || environmentRecord.location === undefined) {
throw new TypeError('$.environment.location: expected present field, received undefined');
}
if (environmentRecord.location !== null) {
requireRecord(environmentRecord.location, '$.environment.location');
}
const data: Record<string, unknown> = {
...baseRoomAction(runtimeConfig, {playerid: room.playerid, roomcode: String(room.roomcode)}),
roomcode: requireStringOrFiniteNumber(room.roomcode, '$.room.roomcode'),
location: environmentRecord.location,
ip: requireString(environmentRecord.ip, '$.environment.ip'),
};
const vipMatch = optionalField(
environmentRecord,
'vipMatch',
'$.environment',
(value, path): 1 => {
const parsed = requireNumber(value, path);
if (parsed !== 1) {
throw new RangeError(`${path}: expected number 1, received number`);
}
return 1;
},
);
if (vipMatch !== undefined) data.vipMatch = vipMatch;
const matchId = optionalField(
environmentRecord,
'match_id',
'$.environment',
requireStringOrFiniteNumber,
);
if (matchId !== undefined) data.match_id = matchId;
return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload);
}
export interface ParsedRoomResponse {
readonly state: number;
readonly room: ParsedRoomSnapshot | null;
readonly reconnect: ReconnectSnapshot;
readonly stage: 0 | 1;
readonly raw: JsonRecord;
}
export function parseSelfJoinRoomResponse(value: unknown): ParsedRoomResponse {
const raw = requireRecord(value, '$.data');
const state = requireInteger(raw.state, '$.data.state');
const room = state === 0 ? parseRoomEntryFields(raw) : null;
const parsedDeskwar = optionalField(raw, 'deskwar', '$.data', requireInteger);
if (parsedDeskwar !== undefined && parsedDeskwar !== 0 && parsedDeskwar !== 1) {
throw new RangeError('$.data.deskwar: expected protocol flag 0 or 1');
}
const reconnect = reconnectFrom(raw);
const stage = reconnect.present ? 1 : parsedDeskwar ?? 0;
return { state, room, reconnect, stage, raw };
}
export interface ParsedSeatPayload {
readonly seat: number;
readonly raw: JsonRecord;
}
function parseSeatPayload(value: unknown): ParsedSeatPayload {
const raw = requireRecord(value, '$.data');
const seat = requireInteger(raw.seat, '$.data.seat');
if (seat < 0) throw new RangeError('$.data.seat: expected non-negative server seat, received number');
return { seat, raw };
}
export const parsePlayerPreparePayload = parseSeatPayload;
export const parseOtherJoinRoomPayload = parseSeatPayload;
export const parseOtherExitRoomPayload = parseSeatPayload;
export const parseOtherOfflinePayload = parseSeatPayload;
export interface ParsedSelfExitPayload {
readonly seat?: number;
readonly raw: JsonRecord;
}
export function parseSelfExitRoomPayload(value: unknown): ParsedSelfExitPayload {
const raw = requireRecord(value, '$.data');
const seat = optionalField(raw, 'seat', '$.data', requireInteger);
if (seat !== undefined && seat < 0) {
throw new RangeError('$.data.seat: expected non-negative server seat, received number');
}
return { ...(seat === undefined ? {} : { seat }), raw };
}
export interface ParsedOnlinePayload extends ParsedSeatPayload {
readonly ip: string;
}
export function parseOtherOnlinePayload(value: unknown): ParsedOnlinePayload {
const parsed = parseSeatPayload(value);
return { seat: parsed.seat, ip: requireString(parsed.raw.ip, '$.data.ip'), raw: parsed.raw };
}
export interface ParsedRoomserverPayload {
readonly roomserver: string;
readonly raw: JsonRecord;
}
export function parseConnectRoomserverPayload(value: unknown): ParsedRoomserverPayload {
const raw = requireRecord(value, '$.data');
return { roomserver: requireString(raw.roomserver, '$.data.roomserver'), raw };
}
export interface ParsedAgentserverPayload {
readonly agentserver: string;
readonly opt: string;
readonly raw: JsonRecord;
}
export function parseConnectAgentserverPayload(value: unknown): ParsedAgentserverPayload {
const raw = requireRecord(value, '$.data');
return {
agentserver: requireString(raw.agentserver, '$.data.agentserver'),
opt: requireString(raw.opt, '$.data.opt'),
raw,
};
}
export interface ParsedKickPayload {
readonly msg: string;
readonly raw: JsonRecord;
}
export function parseKickServerPayload(value: unknown): ParsedKickPayload {
const raw = requireRecord(value, '$.data');
return { msg: requireString(raw.msg, '$.data.msg'), raw };
}
export interface CreateRoomEnvironment {
readonly roomtype: readonly unknown[];
readonly location: unknown;
readonly ip: string;
}
/** Transport contract only: roomtype contents belong to the subgame. */
export function buildCreateRoomRequest(
runtimeConfig: FirstSliceRuntimeConfig,
playerid: number,
command: CreateRoomEnvironment,
): FirstSliceEnvelope<'create_room', Readonly<Record<string, unknown>>> {
const identity = requireRecord(runtimeConfig.identity, '$.runtimeConfig.identity');
const raw = requireRecord(command, '$.command');
if (!hasOwn(raw, 'location') || raw.location === undefined) {
throw new TypeError('$.command.location: expected present field');
}
if (raw.location !== null) requireRecord(raw.location, '$.command.location');
return buildFirstSliceEnvelope('create_room', {
agentid: requireStringOrFiniteNumber(identity.agentid, '$.identity.agentid'),
playerid: requireInteger(playerid, '$.playerid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.identity.gameid'),
roomtype: requireArray(raw.roomtype, '$.command.roomtype'),
ip: requireString(raw.ip, '$.command.ip'),
location: raw.location,
});
}
export interface ParsedCreateRoomResponse {
readonly state: number;
readonly room: { readonly roomcode: string; readonly seat: number; readonly roomtype: readonly unknown[] } | null;
readonly needprepare: number;
readonly infinite: number;
readonly raw: JsonRecord;
}
export function parseCreateRoomResponse(value: unknown): ParsedCreateRoomResponse {
const raw = requireRecord(value, '$.data');
const state = requireInteger(raw.state, '$.data.state');
if (state !== 0) return {state, room: null, needprepare: 0, infinite: 0, raw};
// Desk.Init initializes these flags to zero; create_room overrides only when provided.
const flag = (key: string): number => {
if (!hasOwn(raw, key)) return 0;
const value = requireInteger(raw[key], `$.data.${key}`);
if (value !== 0 && value !== 1) throw new RangeError(`$.data.${key}: expected protocol flag`);
return value;
};
return {
state,
room: {roomcode: requireString(raw.roomcode, '$.data.roomcode'),
seat: requireInteger(raw.seat, '$.data.seat'),
roomtype: requireArray(raw.roomtype, '$.data.roomtype')},
needprepare: flag('needprepare'), infinite: flag('infinite'), raw,
};
}
@@ -1,77 +1,91 @@
import { APP } from '../core/constants.ts';
/** Legacy 12_Logic dispatches login replies from all three platform routes by rpc.
* This inbound contract is independent of the outbound agent route below. */
export function isLoginResponse(message:{route:string;rpc:string}):boolean{
return message.rpc==='player_login'&&(message.route==='agent'||message.route==='room'||message.route==='platform');
}
export type FirstSliceRpc =
| 'getInfoByShortCode'
| 'get_player_task'
| 'get_task_award'
| 'update_roomcard'
| 'player_finish_task'
| 'player_login'
| 'self_join_room'
| 'player_prepare'
| 'self_exit_room'
| 'other_join_room'
| 'other_exit_room'
| 'other_offline'
| 'other_online'
| 'connect_roomserver'
| 'connect_agentserver'
| 'kick_server'
| 'kick_offline'
| 'show_message';
export type FirstSliceRoute = 'platform' | 'agent' | 'room';
/** 第一纵向切片 rpc → route 的唯一权威映射。 */
export const FIRST_SLICE_RPC_ROUTE = {
getInfoByShortCode: 'agent',
get_player_task: 'agent',
get_task_award: 'agent',
update_roomcard: 'agent',
player_finish_task: 'agent',
player_login: 'agent',
self_join_room: 'agent',
player_prepare: 'room',
self_exit_room: 'room',
other_join_room: 'room',
other_exit_room: 'room',
other_offline: 'room',
other_online: 'room',
connect_roomserver: 'room',
connect_agentserver: 'agent',
kick_server: 'agent',
kick_offline: 'agent',
show_message: 'agent',
} as const satisfies Record<FirstSliceRpc, 'platform' | 'agent' | 'room'>;
export function routeForFirstSlice<Rpc extends FirstSliceRpc>(
rpc: Rpc,
): (typeof FIRST_SLICE_RPC_ROUTE)[Rpc] {
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, rpc)) {
throw new Error(`FIRST_SLICE_RPC_ROUTE 未定义 rpc: ${String(rpc)}`);
}
return FIRST_SLICE_RPC_ROUTE[rpc];
}
export interface FirstSliceEnvelope<
Rpc extends FirstSliceRpc,
Data extends Readonly<Record<string, unknown>>,
> {
readonly app: 'youle';
readonly route: (typeof FIRST_SLICE_RPC_ROUTE)[Rpc];
readonly rpc: Rpc;
readonly data: Data;
}
export function buildFirstSliceEnvelope<
Rpc extends FirstSliceRpc,
Data extends Readonly<Record<string, unknown>>,
>(rpc: Rpc, data: Data): FirstSliceEnvelope<Rpc, Data> {
return { app: APP, route: routeForFirstSlice(rpc), rpc, data };
}
import { APP } from '../core/constants.ts';
/** Legacy 12_Logic dispatches login replies from all three platform routes by rpc.
* This inbound contract is independent of the outbound agent route below. */
export function isLoginResponse(message:{route:string;rpc:string}):boolean{
return message.rpc==='player_login'&&(message.route==='agent'||message.route==='room'||message.route==='platform');
}
export type FirstSliceRpc =
| 'getVipRankList'
| 'get_treasurelist'
| 'topup_card'
| 'get_player_invitecode'
| 'binding_invitecode'
| 'getInfoByShortCode'
| 'get_player_task'
| 'get_task_award'
| 'update_bean'
| 'update_roomcard'
| 'player_finish_task'
| 'player_login'
| 'create_room'
| 'self_join_room'
| 'player_prepare'
| 'self_exit_room'
| 'other_join_room'
| 'other_exit_room'
| 'other_offline'
| 'other_online'
| 'connect_roomserver'
| 'connect_agentserver'
| 'kick_server'
| 'kick_offline'
| 'show_message';
export type FirstSliceRoute = 'platform' | 'agent' | 'room';
/** 第一纵向切片 rpc → route 的唯一权威映射。 */
export const FIRST_SLICE_RPC_ROUTE = {
getVipRankList: 'agent',
get_treasurelist: 'agent',
topup_card: 'agent',
get_player_invitecode: 'agent',
binding_invitecode: 'agent',
getInfoByShortCode: 'agent',
get_player_task: 'agent',
get_task_award: 'agent',
update_bean: 'agent',
update_roomcard: 'agent',
player_finish_task: 'agent',
player_login: 'agent',
create_room: 'agent',
self_join_room: 'agent',
player_prepare: 'room',
self_exit_room: 'room',
other_join_room: 'room',
other_exit_room: 'room',
other_offline: 'room',
other_online: 'room',
connect_roomserver: 'room',
connect_agentserver: 'agent',
kick_server: 'agent',
kick_offline: 'agent',
show_message: 'agent',
} as const satisfies Record<FirstSliceRpc, 'platform' | 'agent' | 'room'>;
export function routeForFirstSlice<Rpc extends FirstSliceRpc>(
rpc: Rpc,
): (typeof FIRST_SLICE_RPC_ROUTE)[Rpc] {
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, rpc)) {
throw new Error(`FIRST_SLICE_RPC_ROUTE 未定义 rpc: ${String(rpc)}`);
}
return FIRST_SLICE_RPC_ROUTE[rpc];
}
export interface FirstSliceEnvelope<
Rpc extends FirstSliceRpc,
Data extends Readonly<Record<string, unknown>>,
> {
readonly app: 'youle';
readonly route: (typeof FIRST_SLICE_RPC_ROUTE)[Rpc];
readonly rpc: Rpc;
readonly data: Data;
}
export function buildFirstSliceEnvelope<
Rpc extends FirstSliceRpc,
Data extends Readonly<Record<string, unknown>>,
>(rpc: Rpc, data: Data): FirstSliceEnvelope<Rpc, Data> {
return { app: APP, route: routeForFirstSlice(rpc), rpc, data };
}
@@ -1,206 +1,228 @@
import type { InboundMessage } from '../core/types/envelope.ts';
import type { GameSessionHost } from '../sdk/runtime/game-session-host.ts';
import type { ConnectionControlPort } from '../platform/connection-intent.ts';
import type { RuntimeSession } from '../platform/runtime-session.ts';
import type { PlatformStore } from '../platform/stores/platform-store.ts';
import { requireRecord, requireString } from './contracts/validation.ts';
import {
parseConnectAgentserverPayload,
parseConnectRoomserverPayload,
parseKickServerPayload,
parseOtherExitRoomPayload,
parseOtherJoinRoomPayload,
parseOtherOfflinePayload,
parseOtherOnlinePayload,
parsePlayerPreparePayload,
parseSelfExitRoomPayload,
} from './contracts/index.ts';
import {
buildFirstSliceEnvelope,
isLoginResponse,
FIRST_SLICE_RPC_ROUTE,
routeForFirstSlice,
type FirstSliceRpc,
} from './first-slice-routes.ts';
export interface PlatformHandlersOptions {
readonly store: PlatformStore;
readonly gameSession: GameSessionHost;
readonly session: RuntimeSession;
readonly connection: ConnectionControlPort;
readonly showMessage?: (message:string,time:number,source:'show_message'|'update_roomcard')=>void;
}
export type PlatformDispatchResult = 'handled-platform';
/** Strict first-slice platform RPC dispatcher. */
export class PlatformHandlers {
constructor(private readonly options: PlatformHandlersOptions) {}
dispatch(message: InboundMessage): PlatformDispatchResult {
if (!isPlatformRoute(message.route)) {
throw new Error(
`PlatformHandlers received non-platform route: route=${message.route} rpc=${message.rpc}`,
);
}
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, message.rpc)) {
throw new Error(
`Unsupported platform rpc: route=${message.route} rpc=${message.rpc}`,
);
}
const rpc = message.rpc as FirstSliceRpc;
const expectedRoute = routeForFirstSlice(rpc);
if (message.route !== expectedRoute && !isLoginResponse(message)) {
throw new Error(
`Platform rpc route mismatch: rpc=${rpc} expected=${expectedRoute} received=${message.route}`,
);
}
switch (rpc) {
case 'getInfoByShortCode':this.options.store.applyVipRooms(message.data);break;
case 'update_roomcard': {
const raw=requireRecord(message.data,'$.data');
const text=raw.text?requireString(raw.text,'$.data.text'):null;
this.options.store.applyRoomcardUpdate(raw);
if(raw.change===undefined&&text){
if(!this.options.showMessage)throw Error('update_roomcard requires a message UI');
this.options.showMessage(text,0,'update_roomcard');
}
break;
}
case 'get_player_task':this.options.store.applyTaskList(message.data);break;
case 'get_task_award':this.options.store.applyTaskAward(message.data);break;
case 'player_finish_task':this.options.store.applyTaskFinished(message.data);break;
case 'player_login':
this.options.session.handleLogin(message.data);
break;
case 'self_join_room':
this.options.session.handleSelfJoin(message.data);
break;
case 'player_prepare': {
const parsed = parsePlayerPreparePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerReady(parsed);
this.options.gameSession.publish({ type: 'room.player-ready', seat: parsed.seat });
break;
}
case 'self_exit_room': {
const parsed = parseSelfExitRoomPayload(message.data);
this.options.session.handleSelfExit(parsed);
break;
}
case 'other_join_room': {
const parsed = parseOtherJoinRoomPayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerJoin(parsed);
this.options.gameSession.publish({ type: 'room.player-joined', seat: parsed.seat });
break;
}
case 'other_exit_room': {
const parsed = parseOtherExitRoomPayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerExit(parsed);
this.options.gameSession.publish({ type: 'room.player-left', seat: parsed.seat });
break;
}
case 'other_offline': {
const parsed = parseOtherOfflinePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerOffline(parsed);
this.options.gameSession.publish({ type: 'room.player-offline', seat: parsed.seat });
break;
}
case 'other_online': {
const parsed = parseOtherOnlinePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerOnline(parsed);
this.options.gameSession.publish({ type: 'room.player-online', seat: parsed.seat });
break;
}
case 'connect_roomserver': {
const parsed = parseConnectRoomserverPayload(message.data);
const target = parseSwitchTarget(parsed.roomserver, '$.data.roomserver');
const resend = buildFirstSliceEnvelope('connect_roomserver', parsed.raw);
this.options.session.handleServerSwitch(
() => this.options.connection.switchServer(target, resend),
);
break;
}
case 'connect_agentserver': {
const parsed = parseConnectAgentserverPayload(message.data);
const target = parseSwitchTarget(parsed.agentserver, '$.data.agentserver');
const resend = buildFirstSliceEnvelope('connect_agentserver', parsed.raw);
this.options.session.handleServerSwitch(
() => this.options.connection.switchServer(target, resend),
);
break;
}
case 'show_message': {
const data=requireRecord(message.data,'$.data');
const text=requireString(data.msg,'$.data.msg');
// Desk.show_message converts the original time with Number; only >0 auto-closes.
const time=Number(data.time);
if(!this.options.showMessage)throw Error('show_message requires a message UI');
this.options.showMessage(text,time,'show_message');
break;
}
case 'kick_offline': {
const data=requireRecord(message.data,'$.data');
this.options.session.handleKick(data,()=>this.options.connection.stopForKick(data));
break;
}
case 'kick_server': {
const parsed = parseKickServerPayload(message.data);
this.options.session.handleKick(
parsed.raw,
() => this.options.connection.stopForKick(parsed.raw),
);
break;
}
}
return 'handled-platform';
}
private requireActiveGame(rpc: string): void {
if (this.options.gameSession.state !== 'active') {
throw new Error(`${rpc} requires an active game session`);
}
}
}
function isPlatformRoute(route: string): route is 'platform' | 'agent' | 'room' {
return route === 'platform' || route === 'agent' || route === 'room';
}
function parseSwitchTarget(address: string, path: string): string {
if (address.length === 0 || address.trim() !== address) {
throw new TypeError(`${path}: expected a non-empty WebSocket server address`);
}
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(address);
const target = scheme ? address : `ws://${address}`;
const authority = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]*)/.exec(target)?.[1];
if (authority === undefined || authority.length === 0) {
throw new TypeError(`${path}: expected a WebSocket address with a non-empty host`);
}
if (target.includes('#') || authority.includes('@')) {
throw new TypeError(`${path}: expected a WebSocket address without fragment or userinfo`);
}
let parsed: URL;
try {
parsed = new URL(target);
} catch {
throw new TypeError(`${path}: expected a valid WebSocket server address`);
}
const protocol = parsed.protocol.toLowerCase();
if (
(protocol !== 'ws:' && protocol !== 'wss:')
|| parsed.hostname.length === 0
|| parsed.username.length > 0
|| parsed.password.length > 0
|| parsed.hash.length > 0
) {
throw new TypeError(`${path}: expected a valid WebSocket server address`);
}
return target;
}
import type { InboundMessage } from '../core/types/envelope.ts';
import type { GameSessionHost } from '../sdk/runtime/game-session-host.ts';
import type { ConnectionControlPort } from '../platform/connection-intent.ts';
import type { RuntimeSession } from '../platform/runtime-session.ts';
import type { PlatformStore } from '../platform/stores/platform-store.ts';
import { requireRecord, requireString } from './contracts/validation.ts';
import {
parseConnectAgentserverPayload,
parseConnectRoomserverPayload,
parseKickServerPayload,
parseOtherExitRoomPayload,
parseOtherJoinRoomPayload,
parseOtherOfflinePayload,
parseOtherOnlinePayload,
parsePlayerPreparePayload,
parseSelfExitRoomPayload,
} from './contracts/index.ts';
import {
buildFirstSliceEnvelope,
isLoginResponse,
FIRST_SLICE_RPC_ROUTE,
routeForFirstSlice,
type FirstSliceRpc,
} from './first-slice-routes.ts';
export interface PlatformHandlersOptions {
readonly store: PlatformStore;
readonly gameSession: GameSessionHost;
readonly session: RuntimeSession;
readonly connection: ConnectionControlPort;
readonly showMessage?: (message:string,time:number,source:'show_message'|'update_roomcard'|'update_bean'|'binding_invitecode')=>void;
}
export type PlatformDispatchResult = 'handled-platform';
/** Strict first-slice platform RPC dispatcher. */
export class PlatformHandlers {
constructor(private readonly options: PlatformHandlersOptions) {}
dispatch(message: InboundMessage): PlatformDispatchResult {
if (!isPlatformRoute(message.route)) {
throw new Error(
`PlatformHandlers received non-platform route: route=${message.route} rpc=${message.rpc}`,
);
}
if (!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE, message.rpc)) {
throw new Error(
`Unsupported platform rpc: route=${message.route} rpc=${message.rpc}`,
);
}
const rpc = message.rpc as FirstSliceRpc;
const expectedRoute = routeForFirstSlice(rpc);
if (message.route !== expectedRoute && !isLoginResponse(message) && !(rpc==='update_bean'&&message.route==='room')) {
throw new Error(
`Platform rpc route mismatch: rpc=${rpc} expected=${expectedRoute} received=${message.route}`,
);
}
switch (rpc) {
case 'getVipRankList':this.options.store.applyVipRank(message.data);break;
case 'get_treasurelist':this.options.store.applyTreasureList(message.data);break;
case 'topup_card':break; // Original response is empty; updates arrive separately.
case 'get_player_invitecode':this.options.store.applyInvitecode(message.data);break;
case 'binding_invitecode':{
if(!this.options.showMessage)throw Error('binding_invitecode requires a message UI');
const text=this.options.store.applyInviteBinding(message.data);
this.options.showMessage(text,3000,'binding_invitecode');break;
}
case 'getInfoByShortCode':this.options.store.applyVipRooms(message.data);break;
case 'update_bean': {
const raw=requireRecord(message.data,'$.data');
const text=raw.text?requireString(raw.text,'$.data.text'):null;
this.options.store.applyBeanUpdate(raw);
if(raw.change===undefined&&raw.seat===undefined&&text){
if(!this.options.showMessage)throw Error('update_bean requires a message UI');
this.options.showMessage(text,0,'update_bean');
}
break;
}
case 'update_roomcard': {
const raw=requireRecord(message.data,'$.data');
const text=raw.text?requireString(raw.text,'$.data.text'):null;
this.options.store.applyRoomcardUpdate(raw);
if(raw.change===undefined&&text){
if(!this.options.showMessage)throw Error('update_roomcard requires a message UI');
this.options.showMessage(text,0,'update_roomcard');
}
break;
}
case 'get_player_task':this.options.store.applyTaskList(message.data);break;
case 'get_task_award':this.options.store.applyTaskAward(message.data);break;
case 'player_finish_task':this.options.store.applyTaskFinished(message.data);break;
case 'player_login':
this.options.session.handleLogin(message.data);
break;
case 'create_room':
this.options.session.handleCreateRoom(message.data);
break;
case 'self_join_room':
this.options.session.handleSelfJoin(message.data);
break;
case 'player_prepare': {
const parsed = parsePlayerPreparePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerReady(parsed);
this.options.gameSession.publish({ type: 'room.player-ready', seat: parsed.seat });
break;
}
case 'self_exit_room': {
const parsed = parseSelfExitRoomPayload(message.data);
this.options.session.handleSelfExit(parsed);
break;
}
case 'other_join_room': {
const parsed = parseOtherJoinRoomPayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerJoin(parsed);
this.options.gameSession.publish({ type: 'room.player-joined', seat: parsed.seat });
break;
}
case 'other_exit_room': {
const parsed = parseOtherExitRoomPayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerExit(parsed);
this.options.gameSession.publish({ type: 'room.player-left', seat: parsed.seat });
break;
}
case 'other_offline': {
const parsed = parseOtherOfflinePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerOffline(parsed);
this.options.gameSession.publish({ type: 'room.player-offline', seat: parsed.seat });
break;
}
case 'other_online': {
const parsed = parseOtherOnlinePayload(message.data);
this.requireActiveGame(rpc);
this.options.store.playerOnline(parsed);
this.options.gameSession.publish({ type: 'room.player-online', seat: parsed.seat });
break;
}
case 'connect_roomserver': {
const parsed = parseConnectRoomserverPayload(message.data);
const target = parseSwitchTarget(parsed.roomserver, '$.data.roomserver');
const resend = buildFirstSliceEnvelope('connect_roomserver', parsed.raw);
this.options.session.handleServerSwitch(
() => this.options.connection.switchServer(target, resend),
);
break;
}
case 'connect_agentserver': {
const parsed = parseConnectAgentserverPayload(message.data);
const target = parseSwitchTarget(parsed.agentserver, '$.data.agentserver');
const resend = buildFirstSliceEnvelope('connect_agentserver', parsed.raw);
this.options.session.handleServerSwitch(
() => this.options.connection.switchServer(target, resend),
);
break;
}
case 'show_message': {
const data=requireRecord(message.data,'$.data');
const text=requireString(data.msg,'$.data.msg');
// Desk.show_message converts the original time with Number; only >0 auto-closes.
const time=Number(data.time);
if(!this.options.showMessage)throw Error('show_message requires a message UI');
this.options.showMessage(text,time,'show_message');
break;
}
case 'kick_offline': {
const data=requireRecord(message.data,'$.data');
this.options.session.handleKick(data,()=>this.options.connection.stopForKick(data));
break;
}
case 'kick_server': {
const parsed = parseKickServerPayload(message.data);
this.options.session.handleKick(
parsed.raw,
() => this.options.connection.stopForKick(parsed.raw),
);
break;
}
}
return 'handled-platform';
}
private requireActiveGame(rpc: string): void {
if (this.options.gameSession.state !== 'active') {
throw new Error(`${rpc} requires an active game session`);
}
}
}
function isPlatformRoute(route: string): route is 'platform' | 'agent' | 'room' {
return route === 'platform' || route === 'agent' || route === 'room';
}
function parseSwitchTarget(address: string, path: string): string {
if (address.length === 0 || address.trim() !== address) {
throw new TypeError(`${path}: expected a non-empty WebSocket server address`);
}
const scheme = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(address);
const target = scheme ? address : `ws://${address}`;
const authority = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]*)/.exec(target)?.[1];
if (authority === undefined || authority.length === 0) {
throw new TypeError(`${path}: expected a WebSocket address with a non-empty host`);
}
if (target.includes('#') || authority.includes('@')) {
throw new TypeError(`${path}: expected a WebSocket address without fragment or userinfo`);
}
let parsed: URL;
try {
parsed = new URL(target);
} catch {
throw new TypeError(`${path}: expected a valid WebSocket server address`);
}
const protocol = parsed.protocol.toLowerCase();
if (
(protocol !== 'ws:' && protocol !== 'wss:')
|| parsed.hostname.length === 0
|| parsed.username.length > 0
|| parsed.password.length > 0
|| parsed.hash.length > 0
) {
throw new TypeError(`${path}: expected a valid WebSocket server address`);
}
return target;
}
@@ -0,0 +1,12 @@
/** Pre-room capability: a game owns its page and validated, opaque wire configuration. */
export interface CreateRoomPageContext {
readonly previousRoomtype:unknown;
submit(roomtype:readonly unknown[]):void;
cancel():void;
reportError(message:string):void;
}
export interface CreateRoomPagePort {
open(context:CreateRoomPageContext):void;
setBusy(busy:boolean):void;
close():void;
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "fd4c9495-d6d5-49da-8476-a510df24f1c4",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,6 +1,7 @@
export * from './game-entry.ts';
export * from './game-host.ts';
export * from './game-module.ts';
export * from './game-seat-mapper.ts';
export * from './platform-events.ts';
export * from './snapshots.ts';
export * from './create-room-page.ts';
export * from './game-host.ts';
export * from './game-module.ts';
export * from './game-seat-mapper.ts';
export * from './platform-events.ts';
export * from './snapshots.ts';
@@ -0,0 +1,91 @@
[
{
"__type__": "cc.AnimationClip",
"_name": "AnnouncementHorn",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"sample": 2,
"speed": 1,
"wrapMode": 2,
"enableTrsBlending": false,
"_duration": 2,
"_hash": 0,
"_tracks": [
{
"__id__": 1
}
],
"_exoticAnimation": null,
"_events": [],
"_embeddedPlayers": [],
"_additiveSettings": {
"__id__": 6
},
"_auxiliaryCurveEntries": []
},
{
"__type__": "cc.animation.ObjectTrack",
"_binding": {
"__type__": "cc.animation.TrackBinding",
"path": {
"__id__": 2
},
"proxy": null
},
"_channel": {
"__id__": 4
}
},
{
"__type__": "cc.animation.TrackPath",
"_paths": [
{
"__id__": 3
},
"spriteFrame"
]
},
{
"__type__": "cc.animation.ComponentPath",
"component": "cc.Sprite"
},
{
"__type__": "cc.animation.Channel",
"_curve": {
"__id__": 5
}
},
{
"__type__": "cc.ObjectCurve",
"_times": [
0,
0.5,
1,
1.5
],
"_values": [
{
"__uuid__": "151439f3-c9bd-4fb2-97b5-5afc295d76e8@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "5647b603-4a35-4bd8-b955-a8ceca2a3f43@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "151439f3-c9bd-4fb2-97b5-5afc295d76e8@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "151439f3-c9bd-4fb2-97b5-5afc295d76e8@f9941",
"__expectedType__": "cc.SpriteFrame"
}
]
},
{
"__type__": "cc.AnimationClipAdditiveSettings",
"enabled": false,
"refClip": null
}
]
@@ -0,0 +1,13 @@
{
"ver": "2.0.4",
"importer": "animation-clip",
"imported": true,
"uuid": "aed03d13-bce1-42a0-9d23-ebd829d9ee55",
"files": [
".bin"
],
"subMetas": {},
"userData": {
"name": "AnnouncementHorn"
}
}
@@ -0,0 +1,92 @@
[
{
"__type__": "cc.AnimationClip",
"_name": "RequestLoading",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"sample": 60,
"speed": 1,
"wrapMode": 2,
"enableTrsBlending": false,
"_duration": 1,
"_hash": 0,
"_tracks": [
{
"__id__": 1
}
],
"_exoticAnimation": null,
"_events": [],
"_embeddedPlayers": [],
"_additiveSettings": {
"__id__": 5
},
"_auxiliaryCurveEntries": []
},
{
"__type__": "cc.animation.RealTrack",
"_binding": {
"__type__": "cc.animation.TrackBinding",
"path": {
"__id__": 2
},
"proxy": null
},
"_channel": {
"__id__": 3
}
},
{
"__type__": "cc.animation.TrackPath",
"_paths": [
"angle"
]
},
{
"__type__": "cc.animation.Channel",
"_curve": {
"__id__": 4
}
},
{
"__type__": "cc.RealCurve",
"_times": [
0,
1
],
"_values": [
{
"__type__": "cc.RealKeyframeValue",
"interpolationMode": 0,
"tangentWeightMode": 0,
"value": 0,
"rightTangent": 0,
"rightTangentWeight": 0,
"leftTangent": 0,
"leftTangentWeight": 0,
"easingMethod": 0,
"__editorExtras__": null
},
{
"__type__": "cc.RealKeyframeValue",
"interpolationMode": 0,
"tangentWeightMode": 0,
"value": -360,
"rightTangent": 0,
"rightTangentWeight": 0,
"leftTangent": 0,
"leftTangentWeight": 0,
"easingMethod": 0,
"__editorExtras__": null
}
],
"preExtrapolation": 1,
"postExtrapolation": 1
},
{
"__type__": "cc.AnimationClipAdditiveSettings",
"enabled": false,
"refClip": null
}
]
@@ -0,0 +1,13 @@
{
"ver": "2.0.4",
"importer": "animation-clip",
"imported": true,
"uuid": "987771c5-7f29-487a-b61a-476981cc9216",
"files": [
".bin"
],
"subMetas": {},
"userData": {
"name": "RequestLoading"
}
}
@@ -1,134 +1,134 @@
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"displayName": "00042",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941",
"displayName": "00042",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 108,
"height": 98,
"rawWidth": 108,
"rawHeight": 98,
"borderTop": 48,
"borderBottom": 48,
"borderLeft": 53,
"borderRight": 53,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-54,
-49,
0,
54,
-49,
0,
-54,
49,
0,
54,
49,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
98,
108,
98,
0,
0,
108,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-54,
-49,
0
],
"maxPos": [
54,
49,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"atlasUuid": "",
"trimType": "none"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a"
}
}
{
"ver": "1.0.27",
"importer": "image",
"imported": true,
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6",
"files": [
".json",
".png"
],
"subMetas": {
"6c48a": {
"importer": "texture",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"displayName": "00042",
"id": "6c48a",
"name": "texture",
"userData": {
"wrapModeS": "clamp-to-edge",
"wrapModeT": "clamp-to-edge",
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6",
"isUuid": true,
"visible": false,
"minfilter": "linear",
"magfilter": "linear",
"mipfilter": "none",
"anisotropy": 0
},
"ver": "1.0.22",
"imported": true,
"files": [
".json"
],
"subMetas": {}
},
"f9941": {
"importer": "sprite-frame",
"uuid": "34daf5f4-3869-4f71-84c4-d09103701be6@f9941",
"displayName": "00042",
"id": "f9941",
"name": "spriteFrame",
"userData": {
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 108,
"height": 98,
"rawWidth": 108,
"rawHeight": 98,
"borderTop": 26,
"borderBottom": 34,
"borderLeft": 30,
"borderRight": 31,
"packable": true,
"pixelsToUnit": 100,
"pivotX": 0.5,
"pivotY": 0.5,
"meshType": 0,
"vertices": {
"rawPosition": [
-54,
-49,
0,
54,
-49,
0,
-54,
49,
0,
54,
49,
0
],
"indexes": [
0,
1,
2,
2,
1,
3
],
"uv": [
0,
98,
108,
98,
0,
0,
108,
0
],
"nuv": [
0,
0,
1,
0,
0,
1,
1,
1
],
"minPos": [
-54,
-49,
0
],
"maxPos": [
54,
49,
0
]
},
"isUuid": true,
"imageUuidOrDatabaseUri": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a",
"atlasUuid": "",
"trimType": "none"
},
"ver": "1.0.12",
"imported": true,
"files": [
".json"
],
"subMetas": {}
}
},
"userData": {
"type": "sprite-frame",
"fixAlphaTransparencyArtifacts": false,
"hasAlpha": true,
"redirect": "34daf5f4-3869-4f71-84c4-d09103701be6@6c48a"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -45,9 +45,9 @@
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1600,
"width": 1280,
"height": 720,
"rawWidth": 1600,
"rawWidth": 1280,
"rawHeight": 720,
"borderTop": 0,
"borderBottom": 0,
@@ -60,16 +60,16 @@
"meshType": 0,
"vertices": {
"rawPosition": [
-800,
-640,
-360,
0,
800,
640,
-360,
0,
-800,
-640,
360,
0,
800,
640,
360,
0
],
@@ -84,11 +84,11 @@
"uv": [
0,
720,
1600,
1280,
720,
0,
0,
1600,
1280,
0
],
"nuv": [
@@ -102,12 +102,12 @@
1
],
"minPos": [
-800,
-640,
-360,
0
],
"maxPos": [
800,
640,
360,
0
]
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "ad5e871f-b5d8-439a-852b-53d4d1d9f61b",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,538 @@
[
{
"__type__": "cc.Prefab",
"_name": "OwnerRoomItem",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "OwnerRoomItem",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 8
},
{
"__id__": 14
}
],
"_active": true,
"_components": [
{
"__id__": 20
},
{
"__id__": 22
}
],
"_prefab": {
"__id__": 24
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "5dxzP/UG5Fea3nNr557ORP"
},
{
"__type__": "cc.Node",
"_name": "Spirit100",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 3
},
{
"__id__": 5
}
],
"_prefab": {
"__id__": 7
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "9blwBxZBxC4LDYs+VIpNy1"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 4
},
"_contentSize": {
"__type__": "cc.Size",
"width": 264,
"height": 80
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "90IWdamq1CaK2+fcIY8I4G"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "l3ocOicG3kovqa6+Yow7SQ"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": {
"__id__": 6
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "5f783910-7fd0-4482-a206-8dcb57238c74@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": "e2Q9Z8vy1NVpG+xdDhdD9s"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "VMQNp5XtZ86iR1Z+hSYFOg"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "0lhvW5OWHIVi/ncFScuMKQ",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Spirit102",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": false,
"_components": [
{
"__id__": 9
},
{
"__id__": 11
}
],
"_prefab": {
"__id__": 13
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 39,
"y": 2.5,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "89d19HpytGVox/SvGWJFDr"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": {
"__id__": 10
},
"_contentSize": {
"__type__": "cc.Size",
"width": 14,
"height": 19
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "50k3QzjPhHhJYWzNgtUGBk"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "1sIXmawSIfDNdF9tSIh6ig"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 8
},
"_enabled": true,
"__prefab": {
"__id__": 12
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "9d49bb70-608e-46d2-8672-5afb78bbd8d7@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": "32xJfAFBFPL57FrZT1Njpw"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "dYeagt3oXF7LLcLyBNmxcQ"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "xOj6F8iDH5ro1bLGRx3c5g",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "Digits",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 15
},
{
"__id__": 17
}
],
"_prefab": {
"__id__": 19
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 32,
"y": 2.5,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "85eISb6stE0oj1mZHpP6zK"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 14
},
"_enabled": true,
"__prefab": {
"__id__": 16
},
"_contentSize": {
"__type__": "cc.Size",
"width": 14,
"height": 19
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0.5
},
"_id": "44soAEXzBJSaDlfZI1MuoB"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "41Li0cKhzYiSg6SbbaoAGg"
},
{
"__type__": "cc.Layout",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 14
},
"_enabled": true,
"__prefab": {
"__id__": 18
},
"_resizeMode": 1,
"_layoutType": 1,
"_cellSize": {
"__type__": "cc.Size",
"width": 40,
"height": 40
},
"_startAxis": 0,
"_paddingLeft": 0,
"_paddingRight": 0,
"_paddingTop": 0,
"_paddingBottom": 0,
"_spacingX": 0,
"_spacingY": 0,
"_verticalDirection": 1,
"_horizontalDirection": 0,
"_constraint": 0,
"_constraintNum": 2,
"_affectedByScale": false,
"_isAlign": false,
"_id": "53I4gVRnlCNo+Wqps6VeDy"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "k0owfG3taNqw+EIUHZ8V9g"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "lC5WHe9437aSbTecw6lUiQ",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 21
},
"_contentSize": {
"__type__": "cc.Size",
"width": 264,
"height": 80
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "77ZqoCi+xJ/7Bjxxb0o4bl"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "KLcB/F2HEtr5tYEndV8qbg"
},
{
"__type__": "e40a5gQKgNA6a/yNIkJZlsD",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 23
},
"digitTemplate": {
"__id__": 11
},
"digitsRoot": {
"__id__": 14
},
"_id": "306fBHxpJHuLQI/kcAnGQS"
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "RCmOaA1Wm5nUHFrcVQJpOw"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "0oOd9F3D7yWBfO1u4tL3BA",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
}
]
@@ -2,12 +2,12 @@
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b13f487a-451e-41ce-a810-1fab0d52248f",
"uuid": "67117675-9f6d-4def-a67c-db0e9fb19b98",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Layer9_Feedback"
"syncNodeName": "OwnerRoomItem"
}
}
@@ -0,0 +1,170 @@
[
{
"__type__": "cc.Prefab",
"_name": "PlayerInfoTextItem",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"persistent": false
},
{
"__type__": "cc.Node",
"_name": "PlayerInfoTextItem",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [],
"_active": true,
"_components": [
{
"__id__": 2
},
{
"__id__": 4
}
],
"_prefab": {
"__id__": 6
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 10,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 3
},
"_contentSize": {
"__type__": "cc.Size",
"width": 371,
"height": 22.680000000000003
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0,
"y": 1
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "JYB5ndvc0hWZFdTsDPujQg"
},
{
"__type__": "cc.Label",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 1
},
"_enabled": true,
"__prefab": {
"__id__": 5
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_string": "资料文字",
"_horizontalAlign": 0,
"_verticalAlign": 0,
"_actualFontSize": 18.975,
"_fontSize": 18,
"_fontFamily": "Arial",
"_lineHeight": 18,
"_overflow": 3,
"_enableWrapText": true,
"_font": null,
"_isSystemFontUsed": true,
"_spacingX": 0,
"_isItalic": false,
"_isBold": false,
"_isUnderline": false,
"_underlineHeight": 2,
"_cacheMode": 0,
"_enableOutline": false,
"_outlineColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_outlineWidth": 2,
"_enableShadow": false,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_shadowOffset": {
"__type__": "cc.Vec2",
"x": 2,
"y": 2
},
"_shadowBlur": 2,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "wX+Ww4pBz/TnLcrPEXSuAA"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "Pn6tcB1uaJ1EOUf2nLgpzw",
"instance": null,
"targetOverrides": null
}
]
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "9e6dbfc6-26c5-4eac-9cc8-8060a677d5ee",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "PlayerInfoTextItem"
}
}
@@ -1,13 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b9f6f400-7b1b-4e51-a841-cc84dcddca82",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "TaskItem"
}
}
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b9f6f400-7b1b-4e51-a841-cc84dcddca82",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "TaskItem"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "5925d1d2-f4b2-44c1-a37a-fb2bcdcf664e",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRankItem"
}
}
@@ -1,13 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "fed2e71c-22ac-4e48-a42a-68e48a1929a7",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRoomItem1"
}
}
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "fed2e71c-22ac-4e48-a42a-68e48a1929a7",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRoomItem1"
}
}
@@ -1,13 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "8cb43df4-d8fa-4b3c-ab01-497b23c68435",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRoomItem2"
}
}
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "8cb43df4-d8fa-4b3c-ab01-497b23c68435",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "VipRoomItem2"
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -25,14 +25,14 @@
"_active": true,
"_components": [
{
"__id__": 24
"__id__": 25
},
{
"__id__": 26
"__id__": 27
}
],
"_prefab": {
"__id__": 28
"__id__": 29
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -82,14 +82,14 @@
"_active": true,
"_components": [
{
"__id__": 19
"__id__": 20
},
{
"__id__": 21
"__id__": 22
}
],
"_prefab": {
"__id__": 23
"__id__": 24
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -186,6 +186,7 @@
"__id__": 5
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1600,
"height": 720
},
@@ -313,10 +314,13 @@
},
{
"__id__": 16
},
{
"__id__": 18
}
],
"_prefab": {
"__id__": 18
"__id__": 19
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -360,6 +364,7 @@
"__id__": 13
},
"_contentSize": {
"__type__": "cc.Size",
"width": 150,
"height": 150
},
@@ -455,6 +460,29 @@
"__type__": "cc.CompPrefabInfo",
"fileId": "3wlBO4ciKrxeTYsQdlQvBg"
},
{
"__type__": "cc.Animation",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 11
},
"_enabled": true,
"__prefab": null,
"playOnLoad": false,
"_clips": [
{
"__uuid__": "987771c5-7f29-487a-b61a-476981cc9216",
"__expectedType__": "cc.AnimationClip"
}
],
"_defaultClip": {
"__uuid__": "987771c5-7f29-487a-b61a-476981cc9216",
"__expectedType__": "cc.AnimationClip"
},
"_id": "1b3VMKLpRFPKzD9o608yTZ"
},
{
"__type__": "cc.PrefabInfo",
"root": {
@@ -478,9 +506,10 @@
},
"_enabled": true,
"__prefab": {
"__id__": 20
"__id__": 21
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1600,
"height": 720
},
@@ -505,7 +534,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 22
"__id__": 23
},
"_alignFlags": 45,
"_target": null,
@@ -554,9 +583,10 @@
},
"_enabled": true,
"__prefab": {
"__id__": 25
"__id__": 26
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1600,
"height": 720
},
@@ -581,7 +611,7 @@
},
"_enabled": true,
"__prefab": {
"__id__": 27
"__id__": 28
},
"_alignFlags": 45,
"_target": null,
@@ -1,7 +1,7 @@
[
{
"__type__": "cc.Prefab",
"_name": "Login_Layer",
"_name": "Login",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
@@ -13,7 +13,7 @@
},
{
"__type__": "cc.Node",
"_name": "Login_Layer",
"_name": "Login",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
@@ -1,14 +1,14 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b209cefb-b381-4c7c-b765-94a5ab75a89c",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Login_Layer",
"hasIcon": false
}
}
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "b209cefb-b381-4c7c-b765-94a5ab75a89c",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "Login",
"hasIcon": false
}
}
@@ -8,6 +8,6 @@
],
"subMetas": {},
"userData": {
"syncNodeName": "Layer4_MainMenu"
"syncNodeName": "MainMenu"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "e7a05100-7de4-4935-896e-a59061377b87",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "NoticeBoard"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "260ea2dc-696c-4a65-a2fd-109dd794bb45",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "OwnedRoomList"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "95ee9b79-8c30-4f54-a2cf-19d02083cd28",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "QrCode"
}
}
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "ada34847-93c7-48f2-bb0a-5c1db4b112f6",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "RedeemCode"
}
}
@@ -1,7 +1,7 @@
[
{
"__type__": "cc.Prefab",
"_name": "Layer1_Logo",
"_name": "ResourceLoading",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
@@ -13,22 +13,19 @@
},
{
"__type__": "cc.Node",
"_name": "Layer1_Logo",
"_name": "ResourceLoading",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
},
{
"__id__": 50
}
],
"_active": true,
"_components": [],
"_prefab": {
"__id__": 60
"__id__": 50
},
"_lpos": {
"__type__": "cc.Vec3",
@@ -1130,241 +1127,6 @@
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.Node",
"_name": "group-32",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 51
}
],
"_active": true,
"_components": [],
"_prefab": {
"__id__": 59
},
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "Spirit221",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 50
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 52
},
{
"__id__": 54
},
{
"__id__": 56
}
],
"_prefab": {
"__id__": 58
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -1612.5,
"y": 1099.952,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": ""
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
},
"_enabled": true,
"__prefab": {
"__id__": 53
},
"_contentSize": {
"__type__": "cc.Size",
"width": 63,
"height": 63
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "Y8tq/cDuhVvttPn+x9+bww"
},
{
"__type__": "cc.Sprite",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
},
"_enabled": true,
"__prefab": {
"__id__": 55
},
"_customMaterial": null,
"_srcBlendFactor": 2,
"_dstBlendFactor": 4,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_spriteFrame": {
"__uuid__": "974bf7e8-69fd-49ff-95fa-ed7ddb8d6a14@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"_type": 0,
"_fillType": 0,
"_sizeMode": 0,
"_fillCenter": {
"__type__": "cc.Vec2",
"x": 0,
"y": 0
},
"_fillStart": 0,
"_fillRange": 0,
"_isTrimmedMode": true,
"_useGrayscale": false,
"_atlas": null,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "t+vRkZQc/kDtmwdvT1STKA"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 51
},
"_enabled": true,
"__prefab": {
"__id__": 57
},
"_alignFlags": 16,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_horizontalCenter": -1612.5,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": ""
},
{
"__type__": "cc.CompPrefabInfo",
"fileId": "K2+PudUKVOrBYR09Lp3sAA"
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "J1IKV6k3EB5NkpQVaZVxiA",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__id__": 0
},
"fileId": "JccA+NmPM7lVqq5kAuMdjQ",
"instance": null,
"targetOverrides": null,
"nestedPrefabInstanceRoots": null
},
{
"__type__": "cc.PrefabInfo",
"root": {
@@ -8,6 +8,6 @@
],
"subMetas": {},
"userData": {
"syncNodeName": "Layer1_Logo"
"syncNodeName": "ResourceLoading"
}
}
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "bfaf200b-f8f5-4fcb-b9e3-7e4e831177ce",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "c593ddf6-0173-48e8-96cb-dab1273e43d8",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,38 @@
import {_decorator,Button,Label,Toggle,ToggleContainer} from 'cc';
import {CreateRoomPage} from '../../scripts/platform-login/CreateRoomPage.ts';
import type {CreateRoomPageContext} from '../../framework/sdk/contracts/create-room-page.ts';
import {TemplateRoomRules} from './template-room-rules.ts';
const {ccclass,property}=_decorator;
@ccclass('TemplateCreateRoomView')
export class TemplateCreateRoomView extends CreateRoomPage {
@property([ToggleContainer]) groups:ToggleContainer[]=[];
@property([Toggle]) options:Toggle[]=[];
@property([Label]) optionLabels:Label[]=[];
@property(Button) submitButton:Button|null=null;
@property([Button]) cancelButtons:Button[]=[];
private readonly rules=new TemplateRoomRules();
private context:CreateRoomPageContext|null=null;
private busy=false;
private releases:Array<()=>void>=[];
open(context:CreateRoomPageContext):void{
this.close();
if(this.groups.length!==2||this.options.length!==4||this.optionLabels.length!==4||!this.submitButton||this.cancelButtons.length!==2)throw Error('TemplateCreateRoomView requires serialized controls');
this.rules.open(context.previousRoomtype);this.context=context;
// A saved source index may have no matching option; initialize that state explicitly.
this.groups.forEach(group=>group.allowSwitchOff=true);
this.optionLabels.forEach((label,i)=>label.string=this.rules.labels[i]);
this.options.forEach((toggle,i)=>{
toggle.isChecked=this.rules.selection[Math.floor(i/2)]===i%2;
const changed=()=>{if(!this.busy&&toggle.isChecked)this.rules.select(Math.floor(i/2) as 0|1,i%2);};
toggle.node.on(Toggle.EventType.TOGGLE,changed);this.releases.push(()=>toggle.node.off(Toggle.EventType.TOGGLE,changed));
});
this.groups.forEach(group=>group.allowSwitchOff=false);
const bind=(button:Button,action:()=>void)=>{const fn=()=>{if(!this.busy&&this.context)try{action();}catch(error){context.reportError(error instanceof Error?error.message:String(error));}};
button.node.on(Button.EventType.CLICK,fn);this.releases.push(()=>button.node.off(Button.EventType.CLICK,fn));};
bind(this.submitButton,()=>context.submit(this.rules.build()));
this.cancelButtons.forEach(button=>{const cancel=()=>context.cancel();button.node.on(Button.EventType.CLICK,cancel);this.releases.push(()=>button.node.off(Button.EventType.CLICK,cancel));});this.setBusy(false);
}
setBusy(busy:boolean):void{this.busy=busy;this.options.forEach(toggle=>toggle.interactable=!busy);if(this.submitButton)this.submitButton.interactable=!busy;}
close():void{for(const release of this.releases.splice(0))release();this.context=null;}
onDestroy():void{this.close();}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "ebbef7d9-7bda-4c61-9b0a-1ee56e7817fb",
"files": [],
"subMetas": {},
"userData": {}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,13 @@
{
"ver": "1.1.50",
"importer": "prefab",
"imported": true,
"uuid": "f4582832-23e2-4981-be26-08dfe832aa96",
"files": [
".json"
],
"subMetas": {},
"userData": {
"syncNodeName": "TemplateRoom"
}
}
@@ -0,0 +1,39 @@
import {_decorator,Button,Component,Label} from 'cc';
import type {PlatformGameSnapshot} from '../../framework/sdk/contracts/index.ts';
import type {TemplateRoomActions,TemplateRoomPresentation} from './template-game.ts';
const {ccclass,property}=_decorator;
/** Template lobby-to-room handoff surface; concrete subgames replace this presentation. */
@ccclass('TemplateRoomView')
export class TemplateRoomView extends Component implements TemplateRoomPresentation {
@property(Label) roomLabel:Label|null=null;
@property(Label) playersLabel:Label|null=null;
@property(Button) prepareButton:Button|null=null;
@property(Button) exitButton:Button|null=null;
private actions:TemplateRoomActions|null=null;
private bound=false;
private pending:'prepare'|'exit'|null=null;
private timer:ReturnType<typeof setTimeout>|null=null;
private snapshot:PlatformGameSnapshot|null=null;
private readonly prepare=()=>this.invoke('prepare');
private readonly exit=()=>this.invoke('exit');
render(snapshot:PlatformGameSnapshot,actions:TemplateRoomActions):void{
if(!this.roomLabel||!this.playersLabel||!this.prepareButton||!this.exitButton)throw Error('TemplateRoomView missing controls');
this.actions=actions;this.snapshot=snapshot;
if(!this.bound){this.prepareButton.node.on(Button.EventType.CLICK,this.prepare);this.exitButton.node.on(Button.EventType.CLICK,this.exit);this.bound=true;}
this.roomLabel.string=`房间号:${snapshot.room.roomcode}`;
this.playersLabel.string=snapshot.seats.map(p=>`${p.seat+1} ${p.nickname}${p.playerId===snapshot.self.playerId?'(我)':''} ${!p.online?'离线':p.ready?'已准备':'未准备'}`).join('\n');
const self=snapshot.seats.find(p=>p.playerId===snapshot.self.playerId);
if(self?.ready||snapshot.connection.phase!=='logged-in')this.clearPending();
this.prepareButton.interactable=this.pending===null&&snapshot.connection.phase==='logged-in'&&snapshot.room.stage===0&&!self?.ready;
this.exitButton.interactable=this.pending===null&&snapshot.connection.phase==='logged-in';
}
private invoke(kind:'prepare'|'exit'):void{
if(!this.actions||this.pending)return;
try{this.actions[kind]();this.pending=kind;this.prepareButton!.interactable=false;this.exitButton!.interactable=false;
this.timer=setTimeout(()=>{this.clearPending();if(this.snapshot&&this.actions)this.render(this.snapshot,this.actions);this.node.emit('room-action-timeout');},15000);
}catch(error){this.node.emit('room-action-error',error);}
}
private clearPending():void{if(this.timer!==null)clearTimeout(this.timer);this.timer=null;this.pending=null;}
close():void{this.clearPending();if(this.bound){this.prepareButton?.node.off(Button.EventType.CLICK,this.prepare);this.exitButton?.node.off(Button.EventType.CLICK,this.exit);}this.bound=false;this.actions=null;}
onDestroy():void{this.close();}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e0dc1f88-a719-4b19-9827-2cf9e4452be9",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,29 @@
import type {GameEntry,GameHost,GameModule,PlatformGameSnapshot} from '../../framework/sdk/contracts/index.ts';
import {TemplateRoomRules} from './template-room-rules.ts';
export interface TemplateRoomActions {prepare():void;exit():void;}
export interface TemplateRoomPresentation {render(snapshot:PlatformGameSnapshot,actions:TemplateRoomActions):void;close():void;}
/** Template hooks are intentionally empty, matching 02_SubGame_Input.js. No gameplay is inferred. */
export function createTemplateGame(gameId:string|number){
let active:TemplateModule|null=null;
class TemplateModule implements GameModule {
private host:GameHost|null=null;
private unsubscribe:(()=>void)|null=null;
private view:TemplateRoomPresentation|null=null;
restored:unknown=null;
attach(host:GameHost):void{
if(this.host||active)throw Error('Template module already attached');
this.host=host;active=this;this.unsubscribe=host.subscribe(()=>this.render());
}
mount(view:TemplateRoomPresentation):void{this.view?.close();this.view=view;this.render();}
private requireHost():GameHost{if(!this.host)throw Error('Template room session disposed');return this.host;}
private render():void{this.view?.render(this.requireHost().getSnapshot(),{prepare:()=>this.requireHost().execute({type:'room.prepare'}),exit:()=>this.requireHost().execute({type:'room.exit'})});}
handlePlatformEvent():void{this.render();}
handleGameMessage():void{/* Original template _ReceiveData hook is empty. */}
restore(deskinfo:unknown):void{this.requireHost();this.restored=deskinfo;}
dispose():void{this.unsubscribe?.();this.unsubscribe=null;this.view?.close();this.view=null;this.host=null;this.restored=null;if(active===this)active=null;}
}
const rules=new TemplateRoomRules();
const entry:GameEntry={key:'Game_Surface_3',gameId,route:'Game_Surface_3',resolveSeatCount:roomtype=>rules.seatCount(roomtype),createModule:()=>new TemplateModule()};
return {entry,mount(view:TemplateRoomPresentation){if(!active)throw Error('Template room not attached');active.mount(view);},
get restoredSnapshot():unknown{return active?.restored;}};
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "c6a24a57-c9c3-4eda-9f82-73f1990703fb",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,21 @@
/** Source: 01_SubGame_modify.js. Options are presentation state, not a rule-array encoder. */
export class TemplateRoomRules {
readonly labels=['10局(房卡X1)','20局(房卡X2)','房主扣卡','每人扣卡'] as const;
private selected:[number,number]=[0,0];
get selection():readonly number[]{return [...this.selected];}
open(previous:unknown=null):void{
if(previous===null){this.selected=[0,0];return;}
if(!Array.isArray(previous)||!Number.isInteger(previous[0])||!Number.isInteger(previous[1]))throw Error('Invalid template saved roomtype');
this.selected=[previous[0]-1,previous[1]-1];
}
select(group:0|1,index:number):void{
if(!Number.isInteger(index)||index<0||index>1)throw Error('Invalid template option');
this.selected[group]=index;
}
build():unknown[]{return [1,4,1,2,2,[1,1,[1,2000,10],null,null,1],[1,0,5]];}
seatCount(roomtype:readonly unknown[]):number{
if(!Array.isArray(roomtype))throw Error('Template roomtype must be an array');
// Desk.Create uses Game_Config.Max.PlayerCnt because the template defines no getMaxPlayerCount hook.
return 10;
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "0cad77f9-c126-4532-9ffb-70e650008a34",
"files": [],
"subMetas": {},
"userData": {}
}
File diff suppressed because it is too large Load Diff
@@ -52,7 +52,7 @@
},
"autoReleaseAssets": false,
"_globals": {
"__id__": 5
"__id__": 6
},
"_id": "1c10c2ee-adc2-496f-9005-d3e0e6ebbb56"
},
@@ -72,6 +72,9 @@
},
{
"__id__": 4
},
{
"__id__": 5
}
],
"_prefab": null,
@@ -182,8 +185,24 @@
"__uuid__": "e1b53f04-3e2f-4127-9a3f-f629bed6905e",
"__expectedType__": "cc.Prefab"
},
"notices": {
"__uuid__": "b13f487a-451e-41ce-a810-1fab0d52248f",
"redeemCode": {
"__uuid__": "ada34847-93c7-48f2-bb0a-5c1db4b112f6",
"__expectedType__": "cc.Prefab"
},
"qrCode": {
"__uuid__": "95ee9b79-8c30-4f54-a2cf-19d02083cd28",
"__expectedType__": "cc.Prefab"
},
"noticeBoard": {
"__uuid__": "e7a05100-7de4-4935-896e-a59061377b87",
"__expectedType__": "cc.Prefab"
},
"ownedRoomList": {
"__uuid__": "260ea2dc-696c-4a65-a2fd-109dd794bb45",
"__expectedType__": "cc.Prefab"
},
"invite": {
"__uuid__": "a3db6daa-a98e-4a73-b0f7-cd122bdb11c5",
"__expectedType__": "cc.Prefab"
},
"profile": {
@@ -206,6 +225,100 @@
"__uuid__": "f4af0cc2-2514-4bd5-a0de-7a3dbb32438b",
"__expectedType__": "cc.Prefab"
},
"rank": {
"__uuid__": "3eafe9c2-475b-4c81-b71a-37ffa2717c71",
"__expectedType__": "cc.Prefab"
},
"rankItem": {
"__uuid__": "5925d1d2-f4b2-44c1-a37a-fb2bcdcf664e",
"__expectedType__": "cc.Prefab"
},
"rankMedals": [
{
"__uuid__": "fb86ab44-8140-4d8e-82f1-84ae4b8f3216@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "10653735-f46a-4e0f-906f-dacb093dad23@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "87749e95-72aa-435e-93bb-1f41553922f0@f9941",
"__expectedType__": "cc.SpriteFrame"
}
],
"rankTitle": {
"__uuid__": "ecb18612-2066-42d2-8fa0-c0161124cf1e@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"vipRankTitle": {
"__uuid__": "97b4bd7f-8716-497f-a375-d9c841cf34e9@f9941",
"__expectedType__": "cc.SpriteFrame"
},
"ownedRoomDigits": [
{
"__uuid__": "9d49bb70-608e-46d2-8672-5afb78bbd8d7@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "e71157d8-1261-4917-bac1-1b48ab5b6ab9@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "bade804f-d16a-46b6-8746-600ff6b93cce@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "dae7297a-cb1a-49d7-98a9-a3008a8afc7a@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "f1365825-aa47-4efe-936c-81284b0d5e1f@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "b4d68ae8-0799-47c1-8b30-26b2daed2d59@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "be16e577-8add-4c32-a56b-948a52e0180b@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "2fd355d7-493d-474c-b753-676c7ee9e50d@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "3c1709f3-a24f-4a9e-814c-04bc1441da55@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "4dd36de5-fadb-4852-8334-e9b5834a3957@f9941",
"__expectedType__": "cc.SpriteFrame"
}
],
"rankSymbols": [
{
"__uuid__": "6b9ad554-bf4a-4493-82dd-92b4328cb8a4@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "2f1e97ec-1bad-4731-9563-d90ae5b93111@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "a1016858-72af-4a2c-8d8e-7098eb82a310@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "a6986c72-2bef-4f50-b215-86d4de943b68@f9941",
"__expectedType__": "cc.SpriteFrame"
},
{
"__uuid__": "df99cc0a-0b2a-4fdd-b271-7932b2bea237@f9941",
"__expectedType__": "cc.SpriteFrame"
}
],
"digits": [
{
"__uuid__": "fc6e9a8e-0a6d-4555-bc3e-88fd427b9f3a@f9941",
@@ -272,32 +385,56 @@
],
"_id": "abZXhqPblEq57+36N2j8xH"
},
{
"__type__": "ec19eRwzzZA6LQg5DFOX78H",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"createRoom": {
"__uuid__": "584dbb85-4acc-4bea-9d9b-e0cbeb8741da",
"__expectedType__": "cc.Prefab"
},
"room": {
"__uuid__": "f4582832-23e2-4981-be26-08dfe832aa96",
"__expectedType__": "cc.Prefab"
},
"roomScene": {
"__uuid__": "18f06ada-9d36-485c-9f44-556d6980ca7a",
"__expectedType__": "cc.SceneAsset"
},
"_id": "c2sXHnthJJQoYDj00RAeBa"
},
{
"__type__": "cc.SceneGlobals",
"ambient": {
"__id__": 6
},
"shadows": {
"__id__": 7
},
"_skybox": {
"shadows": {
"__id__": 8
},
"fog": {
"_skybox": {
"__id__": 9
},
"octree": {
"fog": {
"__id__": 10
},
"skin": {
"octree": {
"__id__": 11
},
"lightProbeInfo": {
"skin": {
"__id__": 12
},
"postSettings": {
"lightProbeInfo": {
"__id__": 13
},
"postSettings": {
"__id__": 14
},
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
@@ -0,0 +1,449 @@
[
{
"__type__": "cc.SceneAsset",
"_name": "TemplateRoom",
"_objFlags": 0,
"__editorExtras__": {},
"_native": "",
"scene": {
"__id__": 1
}
},
{
"__type__": "cc.Scene",
"_name": "TemplateRoom",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"autoReleaseAssets": false,
"_globals": {
"__id__": 8
},
"_id": "18f06ada-9d36-485c-9f44-556d6980ca7a"
},
{
"__type__": "cc.Node",
"_name": "Canvas",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 3
}
],
"_active": true,
"_components": [
{
"__id__": 5
},
{
"__id__": 6
},
{
"__id__": 7
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 800,
"y": 360.00000000000006,
"z": 0
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 33554432,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "1cgbnsENBDoLhHJwvyJnKB"
},
{
"__type__": "cc.Node",
"_name": "Camera",
"_objFlags": 0,
"__editorExtras__": {},
"_parent": {
"__id__": 2
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 4
}
],
"_prefab": null,
"_lpos": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 1000
},
"_lrot": {
"__type__": "cc.Quat",
"x": 0,
"y": 0,
"z": 0,
"w": 1
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1,
"y": 1,
"z": 1
},
"_mobility": 0,
"_layer": 1073741824,
"_euler": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_id": "9atEuWMGdBq70ouQQ4tRnO"
},
{
"__type__": "cc.Camera",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 3
},
"_enabled": true,
"__prefab": null,
"_projection": 0,
"_priority": 0,
"_fov": 45,
"_fovAxis": 0,
"_orthoHeight": 360,
"_near": 0,
"_far": 2000,
"_color": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 255
},
"_depth": 1,
"_stencil": 0,
"_clearFlags": 7,
"_rect": {
"__type__": "cc.Rect",
"x": 0,
"y": 0,
"width": 1,
"height": 1
},
"_aperture": 19,
"_shutter": 7,
"_iso": 0,
"_screenScale": 1,
"_visibility": 1108344832,
"_targetTexture": null,
"_postProcess": null,
"_usePostProcess": false,
"_cameraType": -1,
"_trackingType": 0,
"_id": "4780+rx15BKqkTCexT+Fi8"
},
{
"__type__": "cc.UITransform",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_contentSize": {
"__type__": "cc.Size",
"width": 1600,
"height": 720
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_id": "9cHcVMc5JHVJoaZqnwhdaW"
},
{
"__type__": "cc.Canvas",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_cameraComponent": {
"__id__": 4
},
"_alignCanvasWithScreen": true,
"_id": "f7IXBbP6ZNjqIM4Q5/I3dI"
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"__editorExtras__": {},
"node": {
"__id__": 2
},
"_enabled": true,
"__prefab": null,
"_alignFlags": 45,
"_target": null,
"_left": 0,
"_right": 0,
"_top": 5.684341886080802e-14,
"_bottom": 5.684341886080802e-14,
"_horizontalCenter": 0,
"_verticalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_alignMode": 2,
"_lockFlags": 0,
"_id": "9fI6665VZLG5Uuoch5UpgC"
},
{
"__type__": "cc.SceneGlobals",
"ambient": {
"__id__": 9
},
"shadows": {
"__id__": 10
},
"_skybox": {
"__id__": 11
},
"fog": {
"__id__": 12
},
"octree": {
"__id__": 13
},
"skin": {
"__id__": 14
},
"lightProbeInfo": {
"__id__": 15
},
"postSettings": {
"__id__": 16
},
"bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false
},
{
"__type__": "cc.AmbientInfo",
"_skyColorHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 1
},
"_skyColor": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 1
},
"_skyIllumHDR": 20000,
"_skyIllum": 20000,
"_groundAlbedoHDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_groundAlbedo": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
},
"_skyColorLDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.5,
"z": 0.8,
"w": 1
},
"_skyIllumLDR": 20000,
"_groundAlbedoLDR": {
"__type__": "cc.Vec4",
"x": 0.2,
"y": 0.2,
"z": 0.2,
"w": 1
}
},
{
"__type__": "cc.ShadowsInfo",
"_enabled": false,
"_type": 0,
"_normal": {
"__type__": "cc.Vec3",
"x": 0,
"y": 1,
"z": 0
},
"_distance": 0,
"_planeBias": 1,
"_shadowColor": {
"__type__": "cc.Color",
"r": 0,
"g": 0,
"b": 0,
"a": 76
},
"_maxReceived": 4,
"_size": {
"__type__": "cc.Vec2",
"x": 1024,
"y": 1024
}
},
{
"__type__": "cc.SkyboxInfo",
"_envLightingType": 0,
"_envmapHDR": null,
"_envmap": null,
"_envmapLDR": null,
"_diffuseMapHDR": null,
"_diffuseMapLDR": null,
"_enabled": false,
"_useHDR": true,
"_editableMaterial": null,
"_reflectionHDR": null,
"_reflectionLDR": null,
"_rotationAngle": 0
},
{
"__type__": "cc.FogInfo",
"_type": 0,
"_fogColor": {
"__type__": "cc.Color",
"r": 200,
"g": 200,
"b": 200,
"a": 255
},
"_enabled": false,
"_fogDensity": 0.3,
"_fogStart": 0.5,
"_fogEnd": 300,
"_fogAtten": 5,
"_fogTop": 1.5,
"_fogRange": 1.2,
"_accurate": false
},
{
"__type__": "cc.OctreeInfo",
"_enabled": false,
"_minPos": {
"__type__": "cc.Vec3",
"x": -1024,
"y": -1024,
"z": -1024
},
"_maxPos": {
"__type__": "cc.Vec3",
"x": 1024,
"y": 1024,
"z": 1024
},
"_depth": 8
},
{
"__type__": "cc.SkinInfo",
"_enabled": true,
"_blurRadius": 0.01,
"_sssIntensity": 3
},
{
"__type__": "cc.LightProbeInfo",
"_giScale": 1,
"_giSamples": 1024,
"_bounces": 2,
"_reduceRinging": 0,
"_showProbe": true,
"_showWireframe": true,
"_showConvex": false,
"_data": null,
"_lightProbeSphereVolume": 1
},
{
"__type__": "cc.PostSettingsInfo",
"_toneMappingType": 0
}
]
@@ -0,0 +1,11 @@
{
"ver": "1.1.50",
"importer": "scene",
"imported": true,
"uuid": "18f06ada-9d36-485c-9f44-556d6980ca7a",
"files": [
".json"
],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,50 @@
import { _decorator, Component, Label, Mask, Node, UITransform, Widget } from 'cc';
import { announcementMotion } from '../../framework/presentation/lobby-model.ts';
import { requireNode } from './node-bindings.ts';
const {ccclass}=_decorator;
function required<T>(value:T|null,name:string):T {if(value===null)throw Error(`Announcement missing ${name}`);return value;}
/** Native clipping and movement of the authored Announcement children. */
@ccclass('AnnouncementView')
export class AnnouncementView extends Component {
private background:Node|null=null;
private textNode:Node|null=null;
private viewport:Node|null=null;
private message='';
private elapsed=0;
private textY=0;
show(message:string):void {
if(!this.viewport){
const bg=requireNode(this.node,'Spirit219'),text=requireNode(this.node,'Spirit220');
const bgUI=required(bg.getComponent(UITransform),'Spirit219 UITransform');
// The text is animated; an authored Widget must not reset its X every frame.
const widget=text.getComponent(Widget);if(widget)widget.enabled=false;
const viewport=new Node('AnnouncementClip');viewport.layer=this.node.layer;viewport.parent=bg;
const clipUI=viewport.addComponent(UITransform);clipUI.setAnchorPoint(0,0);
clipUI.setContentSize(bgUI.contentSize);
viewport.setPosition(-bgUI.width*bgUI.anchorX,-bgUI.height*bgUI.anchorY,0);
viewport.addComponent(Mask);
this.textY=text.position.y-bg.position.y+bgUI.height*bgUI.anchorY;
text.parent=viewport;
const textUI=required(text.getComponent(UITransform),'Spirit220 UITransform');
textUI.setAnchorPoint(0,textUI.anchorY);
const label=required(text.getComponent(Label),'Spirit220 Label');
label.enableWrapText=false;label.overflow=Label.Overflow.NONE;label.horizontalAlign=Label.HorizontalAlign.LEFT;
this.background=bg;this.textNode=text;this.viewport=viewport;
}
this.message=message;this.elapsed=0;
required(required(this.textNode,'text').getComponent(Label),'label').string=message;
this.draw();
}
update(dt:number):void {if(this.viewport){this.elapsed+=dt;this.draw();}}
private draw():void {
const text=required(this.textNode,'text'),bg=required(this.background,'background');
const width=required(bg.getComponent(UITransform),'background UITransform').width;
const state=announcementMotion(this.message,width,this.elapsed);
bg.active=state.visible;text.active=state.visible;
text.setPosition(state.left,this.textY,0);
}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "4b4591ab-307a-4061-9690-7102b996a3ae",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,10 @@
import {_decorator,Component} from 'cc';
import type {CreateRoomPageContext,CreateRoomPagePort} from '../../framework/sdk/contracts/create-room-page.ts';
const {ccclass}=_decorator;
/** Inherit this component on the subgame prefab root. Platform never inspects its children. */
@ccclass('CreateRoomPage')
export class CreateRoomPage extends Component implements CreateRoomPagePort {
open(_context:CreateRoomPageContext):void{throw Error('Subgame must implement CreateRoomPage.open');}
setBusy(_busy:boolean):void{throw Error('Subgame must implement CreateRoomPage.setBusy');}
close():void{throw Error('Subgame must implement CreateRoomPage.close');}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "956dedf9-3650-418b-af8a-a755a26d991c",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -0,0 +1,37 @@
import {_decorator,Button,Component,Label} from 'cc';
import {JoinRoomInput} from '../../framework/presentation/join-room-input.ts';
const {ccclass,property}=_decorator;
export interface JoinRoomViewContext {initialCode:string;submit(code:number):void;cancel():void;paste():Promise<string>;reportError(message:string):void;}
@ccclass('JoinRoomView')
export class JoinRoomView extends Component {
@property([Button]) digitButtons:Button[]=[];
@property([Label]) slots:Label[]=[];
@property(Button) clearButton:Button|null=null;
@property(Button) deleteButton:Button|null=null;
@property(Button) pasteButton:Button|null=null;
@property(Button) submitButton:Button|null=null;
@property([Button]) cancelButtons:Button[]=[];
private input:JoinRoomInput|null=null;
private releases:Array<()=>void>=[];
private generation=0;
private busy=false;
private pasting=false;
open(context:JoinRoomViewContext):void{
this.close();const generation=this.generation;
if(this.digitButtons.length!==10||this.slots.length!==6||!this.clearButton||!this.deleteButton||!this.pasteButton||!this.submitButton||this.cancelButtons.length!==2)throw Error('JoinRoomView requires serialized controls');
const input=this.input=new JoinRoomInput(code=>context.submit(code));input.prefill(context.initialCode);
const bind=(button:Button,action:()=>void)=>{const fn=()=>{if(this.busy||this.pasting)return;try{action();}catch(error){context.reportError(error instanceof Error?error.message:String(error));}finally{this.render();}};
button.node.on(Button.EventType.CLICK,fn);this.releases.push(()=>button.node.off(Button.EventType.CLICK,fn));};
this.digitButtons.forEach((button,digit)=>bind(button,()=>input.digit(digit)));
bind(this.clearButton,()=>input.clear());bind(this.deleteButton,()=>input.remove());bind(this.submitButton,()=>input.confirm());
bind(this.pasteButton,()=>{this.pasting=true;void context.paste().then(text=>{if(this.generation===generation){input.paste(text);this.render();}})
.catch(error=>{if(this.generation===generation)context.reportError(error instanceof Error?error.message:String(error));})
.finally(()=>{if(this.generation===generation)this.pasting=false;});});
this.cancelButtons.forEach(button=>{const fn=()=>context.cancel();button.node.on(Button.EventType.CLICK,fn);this.releases.push(()=>button.node.off(Button.EventType.CLICK,fn));});
this.setBusy(false);this.render();
}
setBusy(busy:boolean):void{this.busy=busy;this.input?.setBusy(busy);for(const b of [...this.digitButtons,this.clearButton,this.deleteButton,this.submitButton,this.pasteButton])if(b)b.interactable=!busy;}
private render():void{const code=this.input?.code;if(code===undefined)return;this.slots.forEach((slot,i)=>{slot.node.active=i<code.length;slot.string=code[i]??'';});}
close():void{this.generation++;this.pasting=false;for(const release of this.releases.splice(0))release();this.input=null;}
onDestroy():void{this.close();}
}
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "033ab9dd-e1a1-48fb-9fd6-5474c1a45eb2",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,17 +1,28 @@
import { _decorator, Component, Prefab, SpriteFrame } from 'cc';
const {ccclass,property}=_decorator;
/** Editor-owned asset references; no runtime UUID/path guessing. */
@ccclass('LobbyAssets')
export class LobbyAssets extends Component {
@property(Prefab) menu:Prefab|null=null;
@property(Prefab) header:Prefab|null=null;
@property(Prefab) noticeMenu:Prefab|null=null;
@property(Prefab) notices:Prefab|null=null;
@property(Prefab) profile:Prefab|null=null;
@property(Prefab) settings:Prefab|null=null;
@property(Prefab) join:Prefab|null=null;
@property(Prefab) tasks:Prefab|null=null;
import { _decorator, Component, Prefab, SpriteFrame } from 'cc';
const {ccclass,property}=_decorator;
/** Editor-owned asset references; no runtime UUID/path guessing. */
@ccclass('LobbyAssets')
export class LobbyAssets extends Component {
@property(Prefab) menu:Prefab|null=null;
@property(Prefab) header:Prefab|null=null;
@property(Prefab) noticeMenu:Prefab|null=null;
@property(Prefab) redeemCode:Prefab|null=null;
@property(Prefab) qrCode:Prefab|null=null;
@property(Prefab) noticeBoard:Prefab|null=null;
@property(Prefab) ownedRoomList:Prefab|null=null;
@property(Prefab) invite:Prefab|null=null;
@property(Prefab) profile:Prefab|null=null;
@property(Prefab) settings:Prefab|null=null;
@property(Prefab) join:Prefab|null=null;
@property(Prefab) tasks:Prefab|null=null;
@property(Prefab) share:Prefab|null=null;
@property([SpriteFrame]) digits:SpriteFrame[]=[];
@property([SpriteFrame]) switches:SpriteFrame[]=[];
}
@property(Prefab) rank:Prefab|null=null;
@property(Prefab) rankItem:Prefab|null=null;
@property([SpriteFrame]) rankMedals:SpriteFrame[]=[];
@property(SpriteFrame) rankTitle:SpriteFrame|null=null;
@property(SpriteFrame) vipRankTitle:SpriteFrame|null=null;
@property([SpriteFrame]) ownedRoomDigits:SpriteFrame[]=[];
@property([SpriteFrame]) rankSymbols:SpriteFrame[]=[];
@property([SpriteFrame]) digits:SpriteFrame[]=[];
@property([SpriteFrame]) switches:SpriteFrame[]=[];
}

Some files were not shown because too many files have changed in this diff Show More