feat: integrate erqiwang rooms and use spec-compliant Web compilation

This commit is contained in:
2026-09-07 22:36:24 +08:00
parent 30a1b4114c
commit a7ec8aa6a7
49 changed files with 11603 additions and 1183 deletions
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
export type VipCode = string | number;
/** min_ary_indexof in the legacy source compares short codes with ==. */
export const sameVipCode=(left:VipCode,right:VipCode):boolean=>left==right;
@@ -5,7 +6,7 @@ export interface VipRoom {
readonly shortCode: VipCode;
readonly title: string;
readonly owner: string;
readonly roomcodes: { readonly roomtype: readonly unknown[]; readonly roomlist: readonly VipCode[] } | null;
readonly roomcodes: { readonly roomtype: Roomtype; readonly roomlist: readonly VipCode[] } | null;
readonly playerList: readonly (readonly unknown[])[];
readonly deskState: readonly unknown[];
readonly seatCount: readonly VipCode[];
@@ -1,3 +1,4 @@
import type {Roomtype} from '../sdk/contracts/roomtype.ts';
import { APP } from '../core/constants.ts';
import type { OutboundEnvelope } from '../core/types/envelope.ts';
import type {
@@ -48,7 +49,7 @@ type SubscriptionReleaseResult =
interface BoundRoomGeneration {
readonly roomcode: string;
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly selfSeat: number;
readonly seatCount: number;
}
@@ -341,7 +341,7 @@ export class PlatformRuntime {
this.wireIsOpen = false;
const errors: unknown[] = [];
for (const off of [...this.stateObservers]) {
for (const off of [...this.stateObservers]) {
this.captureCleanupError(errors, off);
}
this.captureCleanupError(errors, () => { this.clearLoginGuard(); });
@@ -556,7 +556,7 @@ export class PlatformRuntime {
this.connectionIntent = { type: 'none' };
this.wireIsOpen = false;
const errors: unknown[] = [];
for (const off of [...this.stateObservers]) {
for (const off of [...this.stateObservers]) {
this.captureCleanupError(errors, off);
}
this.captureCleanupError(errors, () => { this.store.setConnectionPhase('kicked'); });
@@ -722,7 +722,7 @@ export class PlatformRuntime {
this.wireIsOpen = false;
const cleanupErrors: unknown[] = [];
for (const off of [...this.stateObservers]) {
for (const off of [...this.stateObservers]) {
this.captureCleanupError(cleanupErrors, off);
}
this.captureCleanupError(cleanupErrors, () => { this.clearLoginGuard(); });
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
import type { Unsubscribe } from '../../core/reactive.ts';
import {parseVipRooms} from '../../protocol/contracts/vip-room.ts';
import {parseRankList,parseVipRankList} from '../../protocol/contracts/rank-list.ts';
@@ -6,7 +7,8 @@ import type {LoginAccountIdentity} from '../../protocol/contracts/login-contract
import {requireStringOrFiniteNumber} from '../../protocol/contracts/validation.ts';
import {
hasOwn,
requireArray,
requireArray,
requireRoomtype,
requireBoolean,
requireInteger,
requireNumber,
@@ -152,7 +154,7 @@ function verifyRoomtypeSourceGraph(graph: readonly RoomtypeObjectSnapshot[]): vo
}
}
export function cloneAndFreezeRoomtype(value: readonly unknown[]): readonly unknown[] {
export function cloneAndFreezeRoomtype(value: Roomtype): Roomtype {
const copies = new WeakMap<object, object>();
const active = new WeakSet<object>();
const ownedGraph: object[] = [];
@@ -241,7 +243,7 @@ export function cloneAndFreezeRoomtype(value: readonly unknown[]): readonly unkn
return target;
};
const canonical = copy(value, 'roomtype') as readonly unknown[];
const canonical = copy(value, 'roomtype') as Roomtype;
verifyRoomtypeSourceGraph(sourceGraph);
for (let index = ownedGraph.length - 1; index >= 0; index -= 1) {
Object.freeze(ownedGraph[index]);
@@ -287,7 +289,7 @@ function buildRoom(
const raw = requireRecord(rawValue, '$.raw');
const roomcode = requireString(room.roomcode, '$.room.roomcode');
const selfSeat = requireInteger(room.seat, '$.room.seat');
const roomtype = requireArray(room.roomtype, '$.room.roomtype');
const roomtype = requireRoomtype(room.roomtype, '$.room.roomtype');
const sourcePlayers = requireArray(room.players, '$.room.players');
if (requireString(raw.roomcode, '$.raw.roomcode') !== roomcode) {
throw new RangeError('$.room.roomcode: expected value parsed from raw roomcode');
@@ -569,7 +571,7 @@ export class PlatformStore {
this.transitions.push({
state,
previous,
listeners: [...this.listeners],
listeners: [...this.listeners],
});
if (this.publishing) return state;
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
import type { PlatformGameConnectionPhase } from '../../sdk/contracts/snapshots.ts';
import type { TaskData } from '../../core/types/task.ts';
@@ -46,7 +47,7 @@ export interface InsideRoomState {
readonly seatPlayerIds: readonly (number | null)[];
readonly readySeats: readonly number[];
readonly offlineSeats: readonly number[];
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
@@ -1,7 +1,8 @@
export {
hasOwn,
optionalField,
requireArray,
requireArray,
requireRoomtype,
requireBoolean,
requireInteger,
requireNumber,
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
import {
buildFirstSliceEnvelope,
type FirstSliceEnvelope,
@@ -5,7 +6,8 @@ import {
import {
hasOwn,
optionalField,
requireArray,
requireArray,
requireRoomtype,
requireInteger,
requireNumber,
requireRecord,
@@ -22,7 +24,7 @@ export type ReconnectSnapshot =
export interface ParsedRoomSnapshot {
readonly roomcode: string;
readonly seat: number;
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly players: readonly unknown[];
}
@@ -40,7 +42,7 @@ export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot {
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 roomtype = requireRoomtype(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`);
@@ -246,7 +248,7 @@ export function parseKickServerPayload(value: unknown): ParsedKickPayload {
export interface CreateRoomEnvironment {
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly location: unknown;
readonly ip: string;
}
@@ -267,7 +269,7 @@ export function buildCreateRoomRequest(
agentid: requireStringOrFiniteNumber(identity.agentid, '$.identity.agentid'),
playerid: requireInteger(playerid, '$.playerid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.identity.gameid'),
roomtype: requireArray(raw.roomtype, '$.command.roomtype'),
roomtype: requireRoomtype(raw.roomtype, '$.command.roomtype'),
ip: requireString(raw.ip, '$.command.ip'),
location: raw.location,
});
@@ -275,7 +277,7 @@ export function buildCreateRoomRequest(
export interface ParsedCreateRoomResponse {
readonly state: number;
readonly room: { readonly roomcode: string; readonly seat: number; readonly roomtype: readonly unknown[] } | null;
readonly room: { readonly roomcode: string; readonly seat: number; readonly roomtype: Roomtype } | null;
readonly needprepare: number;
readonly infinite: number;
readonly raw: JsonRecord;
@@ -296,7 +298,7 @@ export function parseCreateRoomResponse(value: unknown): ParsedCreateRoomRespons
state,
room: {roomcode: requireString(raw.roomcode, '$.data.roomcode'),
seat: requireInteger(raw.seat, '$.data.seat'),
roomtype: requireArray(raw.roomtype, '$.data.roomtype')},
roomtype: requireRoomtype(raw.roomtype, '$.data.roomtype')},
needprepare: flag('needprepare'), infinite: flag('infinite'), raw,
};
}
@@ -1,3 +1,4 @@
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
export type JsonRecord = Readonly<Record<string, unknown>>;
function receivedType(value: unknown): string {
@@ -66,3 +67,9 @@ export function optionalField<T>(
if (!hasOwn(record, key)) return undefined;
return validate(record[key], `${path}.${key}`);
}
/** Validate the transport shape without interpreting game-specific options. */
export function requireRoomtype(value: unknown, path: string): Roomtype {
if (typeof value === 'string' || Array.isArray(value)) return value;
return invalid(path, 'string or array', value);
}
@@ -1,4 +1,4 @@
import {requireArray,requireRecord,requireString} from './validation.ts';
import {requireArray,requireRoomtype,requireRecord,requireString} from './validation.ts';
import type {VipCode,VipRoom} from '../../core/types/vip-room.ts';
export function vipCode(value:unknown,path:string):VipCode {
if(typeof value==='string'&&value.length>0)return value;
@@ -12,7 +12,8 @@ export function parseVipRooms(payload:unknown):readonly VipRoom[]{
const p=`$.data.roomInfo[${index}]`,r=requireRecord(value,p);
const playerList=requireArray(r.playerList,p+'.playerList').map((v,i)=>Object.freeze([...requireArray(v,`${p}.playerList[${i}]`)]));
const codes=r.roomcodes?requireRecord(r.roomcodes,p+'.roomcodes'):null;
const roomcodes=codes?Object.freeze({roomtype:Object.freeze([...requireArray(codes.roomtype,p+'.roomcodes.roomtype')]),roomlist:Object.freeze(requireArray(codes.roomlist,p+'.roomcodes.roomlist').map((v,i)=>vipCode(v,`${p}.roomcodes.roomlist[${i}]`)))}):null;
const rawType=codes?requireRoomtype(codes.roomtype,p+'.roomcodes.roomtype'):null;
const roomcodes=codes?Object.freeze({roomtype:typeof rawType==='string'?rawType:Object.freeze([...(rawType as readonly unknown[])]),roomlist:Object.freeze(requireArray(codes.roomlist,p+'.roomcodes.roomlist').map((v,i)=>vipCode(v,`${p}.roomcodes.roomlist[${i}]`)))}):null;
const deskState=roomcodes?requireArray(r.deskState,p+'.deskState'):[];
const seatCount=roomcodes?requireArray(r.seatCount,p+'.seatCount').map((v,i)=>vipCode(v,`${p}.seatCount[${i}]`)):[];
if(roomcodes&&[roomcodes.roomlist.length,deskState.length,seatCount.length].some(n=>n!==playerList.length))throw TypeError(p+': desk arrays must have matching lengths');
@@ -1,7 +1,8 @@
import type {Roomtype} from './roomtype.ts';
/** Pre-room capability: a game owns its page and validated, opaque wire configuration. */
export interface CreateRoomPageContext {
readonly previousRoomtype:unknown;
submit(roomtype:readonly unknown[]):void;
submit(roomtype: Roomtype):void;
cancel():void;
reportError(message:string):void;
}
@@ -1,3 +1,4 @@
import type {Roomtype} from './roomtype.ts';
import type { GameModule } from './game-module.ts';
/** The one compile-time game identity and module factory for an app. */
@@ -6,6 +7,6 @@ export interface GameEntry {
readonly gameId: string | number;
readonly route: string;
/** Interprets this game's actual server roomtype; no synthetic/default input is required. */
resolveSeatCount(roomtype: readonly unknown[]): number;
resolveSeatCount(roomtype: Roomtype): number;
createModule(): GameModule;
}
@@ -5,3 +5,5 @@ export * from './game-module.ts';
export * from './game-seat-mapper.ts';
export * from './platform-events.ts';
export * from './snapshots.ts';
export type {Roomtype} from './roomtype.ts';
@@ -1,6 +1,7 @@
import type {Roomtype} from './roomtype.ts';
/** Platform lifecycle events delivered to the current game module. */
export type PlatformToGameEvent =
| { readonly type: 'room.entered'; readonly roomtype: readonly unknown[] }
| { readonly type: 'room.entered'; readonly roomtype: Roomtype }
| { readonly type: 'room.player-joined'; readonly seat: number }
| { readonly type: 'room.player-left'; readonly seat: number }
| { readonly type: 'room.player-ready'; readonly seat: number }
@@ -0,0 +1,2 @@
/** Opaque subgame wire configuration. The game defines its contents. */
export type Roomtype = string | readonly unknown[];
@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "69bfa9e3-15e3-4ed5-a26a-1fda48ffc906",
"files": [],
"subMetas": {},
"userData": {}
}
@@ -1,3 +1,4 @@
import type {Roomtype} from './roomtype.ts';
/** Public connection phases available to a game. */
export type PlatformGameConnectionPhase =
| 'connected'
@@ -20,7 +21,7 @@ export interface PlatformGameSelfSnapshot {
/** Plain public room projection. `roomtype` remains opaque server data. */
export interface PlatformGameRoomSnapshot {
readonly roomcode: string;
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly stage: number;
readonly needprepare: number;
readonly infinite: number;
@@ -1,3 +1,4 @@
import type {Roomtype} from '../contracts/roomtype.ts';
import type { strict } from 'node:assert';
import type {
GameEntry,
@@ -16,7 +17,7 @@ export type GameContractHostFactory = () => GameContractHost;
/** The game owns each room configuration, its expected count and matching Host fixture. */
export interface GameContractSample {
readonly roomtype: readonly unknown[];
readonly roomtype: Roomtype;
readonly expectedSeatCount: number;
readonly makeHost: GameContractHostFactory;
}
@@ -701,7 +701,7 @@
},
"_lpos": {
"__type__": "cc.Vec3",
"x": -229.49841260344616,
"x": -231.99801937299284,
"y": 178,
"z": 0
},
@@ -742,8 +742,8 @@
},
"_contentSize": {
"__type__": "cc.Size",
"width": 19.996825206892293,
"height": 25.200000000000003
"width": 24.996038745985672,
"height": 31.500000000000004
},
"_anchorPoint": {
"__type__": "cc.Vec2",
@@ -781,10 +781,10 @@
"_string": "消",
"_horizontalAlign": 0,
"_verticalAlign": 1,
"_actualFontSize": 21.083333333333336,
"_fontSize": 20,
"_actualFontSize": 26.354166666666668,
"_fontSize": 25,
"_fontFamily": "Arial",
"_lineHeight": 20,
"_lineHeight": 25,
"_overflow": 0,
"_enableWrapText": true,
"_font": null,