feat: integrate room framework and isolated subgame bundles
Complete room UI and protocol integration, move game definitions and resources behind bundle entries, publish authoritative version XML, and document single-game builds. Include all current resource changes and experiment artifacts.
This commit is contained in:
+4
@@ -18,6 +18,10 @@ export class OwnedRoomHistory {
|
||||
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));}
|
||||
}
|
||||
broken(raw:Readonly<Record<string,unknown>>):void{
|
||||
if(raw.roomcode===undefined)return;
|
||||
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,35 @@
|
||||
import {logNativeCall} from '../../core/native-call-log.ts';
|
||||
import type {NativeCallName} from './native-contracts.ts';
|
||||
|
||||
export type PlatformNativeHost=
|
||||
|{kind:'browser'}
|
||||
|{kind:'settings';settings:object}
|
||||
|{kind:'bridge';call(name:NativeCallName,data:unknown):void};
|
||||
|
||||
// 05_Func.startlocation/getlocation: the browser source owns this test fixture.
|
||||
// Native hosts always supply their own result through getlocationinfo.
|
||||
const BROWSER_LOCATION=Object.freeze({
|
||||
address:'江西省南昌市青云谱区施尧路靠近上海浦东发展银行(长天支行)',city:'南昌市',cityCode:'0791',country:'中国',
|
||||
district:'青云谱区',latitude:28.623546,longitude:115.900333,province:'江西省',street:'施尧路',errorCode:0,
|
||||
});
|
||||
|
||||
function invoke(host:PlatformNativeHost,name:NativeCallName,data:unknown,args:unknown[]):void {
|
||||
if(host.kind==='bridge'){host.call(name,data);return;}
|
||||
if(host.kind==='browser'){logNativeCall('browser',name,args,false);return;}
|
||||
const method=(host.settings as Record<string,unknown>)[name];
|
||||
logNativeCall('settings',name,args,typeof method==='function');
|
||||
if(typeof method!=='function')throw Error(`Native settings missing ${name}`);
|
||||
Reflect.apply(method,host.settings,args);
|
||||
}
|
||||
|
||||
export function startPlatformLocation(host:PlatformNativeHost,onLocation:(data:unknown)=>void):void {
|
||||
invoke(host,'startlocation',2,[2]);
|
||||
if(host.kind==='browser')onLocation(JSON.stringify(BROWSER_LOCATION));
|
||||
}
|
||||
|
||||
export function callPlatformSetting(host:PlatformNativeHost,name:'stopshake'):void;
|
||||
export function callPlatformSetting(host:PlatformNativeHost,name:'voicePlaying'|'SwitchShake',value:0|1):void;
|
||||
export function callPlatformSetting(host:PlatformNativeHost,name:'stopshake'|'voicePlaying'|'SwitchShake',value?:0|1):void {
|
||||
// Settings stopshake takes no args; WVJB explicitly takes an empty string.
|
||||
invoke(host,name,name==='stopshake'?'':value,name==='stopshake'?[]:[value]);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "41e1b7ed-8004-4b1e-b41d-3013e5716ac2",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import {logNativeCall} from '../../core/native-call-log.ts';
|
||||
import type {NativeBridgePort, NativeHandler, RegisteredNativeHandlerName} from './native-contracts.ts';
|
||||
import type {PlatformNativeHost} from './platform-native.ts';
|
||||
|
||||
export interface RoomDeviceStatus {
|
||||
readonly battery:number|null;
|
||||
readonly network:1|2|3|null;
|
||||
readonly wifiLevel:number|null;
|
||||
readonly wifiFrame:1|2|3|4|null;
|
||||
readonly batteryFrame:2|3|null;
|
||||
}
|
||||
export interface RoomDeviceStatusOptions {
|
||||
host:PlatformNativeHost;
|
||||
subscribe(name:RegisteredNativeHandlerName,handler:NativeHandler):()=>void;
|
||||
}
|
||||
|
||||
// Browser fixture belongs to this source: 05_Func.getbattery/getnetwork return
|
||||
// 1 in plain H5; MainScene initially maps every non-WiFi network to frame 4.
|
||||
const BROWSER_STATUS:RoomDeviceStatus=Object.freeze({battery:1,network:1,wifiLevel:null,wifiFrame:4,batteryFrame:2});
|
||||
|
||||
/** Settings calls globals; WVJB registers the same names with different WiFi args. */
|
||||
export function createRoomDeviceStatusSubscriptions(
|
||||
host:PlatformNativeHost,globals:Record<string,unknown>,bridge?:Pick<NativeBridgePort,'register'>,
|
||||
):RoomDeviceStatusOptions['subscribe'] {
|
||||
if(host.kind==='bridge'&&!bridge)throw Error('Room device status bridge registration is required');
|
||||
return (name,handler)=>{
|
||||
if(name!=='getBattery'&&name!=='getwifiLevel'&&name!=='getnetwork')throw Error(`Unexpected room device status handler: ${name}`);
|
||||
let active=true;
|
||||
const guarded:NativeHandler=(...args)=>{if(active)return handler(...args);};
|
||||
if(host.kind==='bridge'){
|
||||
bridge!.register(name,guarded);
|
||||
// WVJB exposes no unregister contract; native-retained callbacks become inert.
|
||||
return ()=>{active=false;};
|
||||
}
|
||||
if(host.kind==='browser')return ()=>{active=false;};
|
||||
const previous=Object.getOwnPropertyDescriptor(globals,name);
|
||||
globals[name]=guarded;
|
||||
return ()=>{
|
||||
active=false;
|
||||
if(globals[name]!==guarded)return;
|
||||
if(previous)Object.defineProperty(globals,name,previous);
|
||||
else delete globals[name];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function sourceNumber(value:unknown,name:string,min:number,max:number,integer=false):number {
|
||||
if((typeof value!=='number'&&typeof value!=='string')||String(value).trim()==='')throw Error(`Invalid native ${name}`);
|
||||
const number=Number(value);
|
||||
if(!Number.isFinite(number)||number<min||number>max||(integer&&!Number.isInteger(number)))throw Error(`Invalid native ${name}: ${String(value)}`);
|
||||
return number;
|
||||
}
|
||||
|
||||
export class RoomDeviceStatusAdapter {
|
||||
private state:RoomDeviceStatus={battery:null,network:null,wifiLevel:null,wifiFrame:null,batteryFrame:null};
|
||||
private readonly listeners=new Set<(status:RoomDeviceStatus)=>void>();
|
||||
private readonly unsubscribers:Array<()=>void>=[];
|
||||
private disposed=false;
|
||||
private readonly options:RoomDeviceStatusOptions;
|
||||
|
||||
constructor(options:RoomDeviceStatusOptions) {
|
||||
this.options=options;
|
||||
try {
|
||||
this.unsubscribers.push(options.subscribe('getBattery',value=>{if(!this.disposed)this.receiveBattery(value);}));
|
||||
this.unsubscribers.push(options.subscribe('getnetwork',value=>{if(!this.disposed)this.receiveNetwork(value,false);}));
|
||||
this.unsubscribers.push(options.subscribe('getwifiLevel',(...args)=>{if(!this.disposed)this.receiveWifi(args);}));
|
||||
this.refresh();
|
||||
}catch(error){this.dispose();throw error;}
|
||||
}
|
||||
|
||||
snapshot():RoomDeviceStatus {return this.state;}
|
||||
|
||||
/** Changes only; use snapshot() for the initial render. */
|
||||
subscribe(listener:(status:RoomDeviceStatus)=>void):()=>void {
|
||||
this.assertUsable();this.listeners.add(listener);return()=>{this.listeners.delete(listener);};
|
||||
}
|
||||
|
||||
refresh():void {
|
||||
this.assertUsable();
|
||||
const host=this.options.host;
|
||||
if(host.kind==='browser'){this.publish(BROWSER_STATUS);return;}
|
||||
// Legacy WVJB Func getters return callback-populated app_* variables only.
|
||||
// There are no getnetwork/getbattery/getwifisignalLevel callHandler methods.
|
||||
if(host.kind==='bridge')return;
|
||||
this.receiveNetwork(this.setting('getnetwork'),true);
|
||||
this.receiveBattery(this.setting('getbattery'));
|
||||
}
|
||||
|
||||
dispose():void {
|
||||
if(this.disposed)return;
|
||||
this.disposed=true;this.listeners.clear();
|
||||
const errors:unknown[]=[];
|
||||
for(const off of this.unsubscribers.splice(0)){try{off();}catch(error){errors.push(error);}}
|
||||
if(errors.length)throw errors[0];
|
||||
}
|
||||
|
||||
private receiveBattery(value:unknown):void {
|
||||
const battery=sourceNumber(value,'battery',0,1);
|
||||
this.publish({...this.state,battery,batteryFrame:battery<=0.2?3:2});
|
||||
}
|
||||
|
||||
private receiveNetwork(value:unknown,entry:boolean):void {
|
||||
const network=sourceNumber(value,'network',1,3,true) as 1|2|3;
|
||||
// 05_Func.getnetwork callback type 1 preserves the displayed frame.
|
||||
const wifiFrame=network===2?3:network===3||entry?4:this.state.wifiFrame;
|
||||
this.publish({...this.state,network,wifiFrame});
|
||||
if(network===2&&this.options.host.kind==='settings')this.setting('getwifisignalLevel');
|
||||
}
|
||||
|
||||
private receiveWifi(args:unknown[]):void {
|
||||
let raw:unknown;
|
||||
if(this.options.host.kind==='settings')raw={ssidname:args[0],signalLevel:args[1]};
|
||||
else raw=typeof args[0]==='string'?JSON.parse(args[0]):args[0];
|
||||
if(!raw||typeof raw!=='object')throw Error('Invalid native wifi payload');
|
||||
const wifiLevel=sourceNumber((raw as Record<string,unknown>).signalLevel,'wifi signalLevel',0,4,true);
|
||||
this.publish({...this.state,wifiLevel,wifiFrame:wifiLevel===0?1:wifiLevel<=2?2:3});
|
||||
}
|
||||
|
||||
private setting(name:'getnetwork'|'getbattery'|'getwifisignalLevel'):unknown {
|
||||
const host=this.options.host;
|
||||
if(host.kind!=='settings')throw Error('Device status settings host required');
|
||||
const method=(host.settings as Record<string,unknown>)[name];
|
||||
logNativeCall('settings',name,[],typeof method==='function');
|
||||
if(typeof method!=='function')throw Error(`Native settings missing ${name}`);
|
||||
return Reflect.apply(method,host.settings,[]);
|
||||
}
|
||||
|
||||
private publish(state:RoomDeviceStatus):void {
|
||||
this.state=Object.freeze(state);
|
||||
for(const listener of this.listeners)listener(this.state);
|
||||
}
|
||||
|
||||
private assertUsable():void {if(this.disposed)throw Error('Room device status adapter is disposed');}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e58a8fa9-83ef-4325-a40c-99e91f716092",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import {logNativeCall} from '../../core/native-call-log.ts';
|
||||
import type {NativeBridgePort, NativeHandler, RegisteredNativeHandlerName} from './native-contracts.ts';
|
||||
import type {PlatformNativeHost} from './platform-native.ts';
|
||||
|
||||
export interface RoomVoiceOptions {
|
||||
host:PlatformNativeHost;
|
||||
subscribe(name:RegisteredNativeHandlerName,handler:NativeHandler):()=>void;
|
||||
/** Preserve the native duration's wire type; the runtime owns the envelope. */
|
||||
sendVoice(voiceurl:string,time:string|number):void;
|
||||
onPlayback(user:number,playing:boolean):void;
|
||||
}
|
||||
|
||||
/** Settings calls top-level window functions; WVJB calls registered handlers. */
|
||||
export function createRoomVoiceSubscriptions(
|
||||
host:PlatformNativeHost,
|
||||
globals:Record<string,unknown>,
|
||||
bridge?:Pick<NativeBridgePort,'register'>,
|
||||
):RoomVoiceOptions['subscribe'] {
|
||||
if(host.kind==='bridge'&&!bridge)throw Error('Room voice bridge registration is required');
|
||||
return (name,handler)=>{
|
||||
if(name!=='getaudiourl'&&name!=='gameui_play_voice'&&name!=='gameui_stop_voice')throw Error(`Unexpected room voice handler: ${name}`);
|
||||
let active=true;
|
||||
const guarded:NativeHandler=(...args)=>{if(active)return handler(...args);};
|
||||
if(host.kind==='bridge'){
|
||||
bridge!.register(name,guarded);
|
||||
// WVJB has no unregister contract. Retained native callbacks become inert.
|
||||
return ()=>{active=false;};
|
||||
}
|
||||
if(host.kind==='browser')return ()=>{active=false;};
|
||||
const previous=Object.getOwnPropertyDescriptor(globals,name);
|
||||
globals[name]=guarded;
|
||||
return ()=>{
|
||||
active=false;
|
||||
if(globals[name]!==guarded)return;
|
||||
if(previous)Object.defineProperty(globals,name,previous);
|
||||
else delete globals[name];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/** Native owns recorder UI and upload. Legacy defines no native stop/cancel command. */
|
||||
export class RoomVoiceAdapter {
|
||||
private pending=false;
|
||||
private cancelled=false;
|
||||
private disposed=false;
|
||||
private readonly unsubscribers:Array<()=>void>=[];
|
||||
private readonly options:RoomVoiceOptions;
|
||||
|
||||
constructor(options:RoomVoiceOptions) {
|
||||
this.options=options;
|
||||
try {
|
||||
this.unsubscribers.push(options.subscribe('getaudiourl',(...args)=>this.uploaded(args)));
|
||||
for(const [name,playing] of [['gameui_play_voice',true],['gameui_stop_voice',false]] as const){
|
||||
this.unsubscribers.push(options.subscribe(name,(value)=>{
|
||||
if(this.disposed)return;
|
||||
if((typeof value!=='string'&&typeof value!=='number')||String(value).trim()===''||!Number.isInteger(Number(value)))throw Error('Invalid native voice user');
|
||||
options.onPlayback(Number(value),playing);
|
||||
}));
|
||||
}
|
||||
} catch(error) {this.dispose();throw error;}
|
||||
}
|
||||
|
||||
startRecording():void {
|
||||
this.assertUsable();
|
||||
if(this.pending)throw Error('Native voice recording is pending');
|
||||
this.pending=true;this.cancelled=false;
|
||||
try {this.invoke('prepareaudio','',[]);}catch(error){this.pending=false;throw error;}
|
||||
}
|
||||
|
||||
/** Pointer release does not end the native recorder; upload remains eligible. */
|
||||
stopRecording():void {this.assertUsable();}
|
||||
|
||||
/** Local abandonment only. Keep the pending slot until native completes. */
|
||||
cancelRecording():void {this.assertUsable();if(this.pending)this.cancelled=true;}
|
||||
|
||||
playVoice(audiourl:string,type:number,user:number):void {
|
||||
this.assertUsable();
|
||||
if(typeof audiourl!=='string'||audiourl.length===0)throw Error('Invalid native voice audiourl');
|
||||
if(!Number.isInteger(type)||!Number.isInteger(user))throw Error('Invalid native voice type/user');
|
||||
this.invoke('mediaTypeAudio',{audiourl,type,user},[audiourl,type,user]);
|
||||
}
|
||||
|
||||
dispose():void {
|
||||
if(this.disposed)return;
|
||||
this.disposed=true;this.pending=false;
|
||||
const errors:unknown[]=[];
|
||||
for(const unsubscribe of this.unsubscribers.splice(0)){try{unsubscribe();}catch(error){errors.push(error);}}
|
||||
if(errors.length)throw errors[0];
|
||||
}
|
||||
|
||||
private uploaded(args:unknown[]):void {
|
||||
if(this.disposed||!this.pending)return;
|
||||
this.pending=false;
|
||||
if(this.cancelled)return;
|
||||
let raw:unknown;
|
||||
if(this.options.host.kind==='settings')raw={audiourl:args[0],time:args[1]};
|
||||
else raw=typeof args[0]==='string'?JSON.parse(args[0]):args[0];
|
||||
if(!raw||typeof raw!=='object')throw Error('Invalid native voice upload');
|
||||
const {audiourl,time}=raw as Record<string,unknown>;
|
||||
if((typeof time!=='number'&&typeof time!=='string')||String(time).trim()===''||!Number.isFinite(Number(time)))throw Error('Invalid native voice time');
|
||||
// Legacy suppresses zero and negative duration callbacks.
|
||||
if(Number(time)<=0)return;
|
||||
if(typeof audiourl!=='string'||audiourl.length===0)throw Error('Invalid native voice audiourl');
|
||||
this.options.sendVoice(audiourl,time);
|
||||
}
|
||||
|
||||
private invoke(name:'prepareaudio'|'mediaTypeAudio',data:unknown,args:unknown[]):void {
|
||||
const host=this.options.host;
|
||||
if(host.kind==='bridge'){host.call(name,data);return;}
|
||||
if(host.kind==='browser'){
|
||||
logNativeCall('browser',name,args,false);
|
||||
throw Error('当前浏览器不支持原生语音录制或播放,请在客户端使用');
|
||||
}
|
||||
const method=(host.settings as Record<string,unknown>)[name];
|
||||
logNativeCall('settings',name,args,typeof method==='function');
|
||||
if(typeof method!=='function')throw Error(`Native settings missing ${name}`);
|
||||
Reflect.apply(method,host.settings,args);
|
||||
}
|
||||
|
||||
private assertUsable():void {if(this.disposed)throw Error('Room voice adapter is disposed');}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "17ae6470-6771-4c3a-9db6-409deca12dab",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -34,11 +34,13 @@ function bootstrapHostKind(options: BootstrapOptions): HostKind {
|
||||
/** 兼容启动编排外观;所有运行契约均由唯一的 RuntimeConfig 边界产出。 */
|
||||
export async function resolveBootstrap(options: BootstrapOptions): Promise<BootstrapResult> {
|
||||
const runtime = await resolveRuntimeConfig({
|
||||
buildIdentity: options.buildIdentity,
|
||||
startupConfig: options.startupConfig,
|
||||
hostKind: bootstrapHostKind(options),
|
||||
win: options.win,
|
||||
search: options.search,
|
||||
fetcher: options.fetcher,
|
||||
localFetcher: options.localFetcher,
|
||||
cacheBust: options.cacheBust,
|
||||
});
|
||||
return Object.freeze({
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Explicit client lifecycle budget; no protocol fields or game rules are inferred. */
|
||||
export const ROOM_LIFECYCLE_CONFIG=Object.freeze({capacity:256,maxWaitMs:30000});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "57882ffe-3e00-46d9-ab47-362e6dd4467d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChannelIdentity, IdentitySource } from './identity.ts';
|
||||
import type { ChannelIdentity } from './identity.ts';
|
||||
import { resolveIdentity } from './identity.ts';
|
||||
import {
|
||||
STARTUP_CONFIG,
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type RemoteConfig,
|
||||
} from './remote-config.ts';
|
||||
import type { RuntimeMode } from './runtime-mode.ts';
|
||||
import { BUILD_IDENTITY } from './sources/defaults.ts';
|
||||
import {
|
||||
getothername,
|
||||
nativeIdentitySource,
|
||||
@@ -40,6 +39,7 @@ export interface RuntimeConfig {
|
||||
}
|
||||
|
||||
export interface ResolveRuntimeConfigOptions {
|
||||
readonly buildIdentity: Readonly<ChannelIdentity>;
|
||||
readonly startupConfig?: Readonly<StartupConfig>;
|
||||
readonly hostKind: HostKind;
|
||||
readonly win: NativeHost;
|
||||
@@ -50,16 +50,14 @@ export interface ResolveRuntimeConfigOptions {
|
||||
readonly cacheBust?: () => string;
|
||||
}
|
||||
|
||||
function buildIdentitySource(): IdentitySource {
|
||||
return () => ({ gameid: BUILD_IDENTITY.gameid, version: BUILD_IDENTITY.version });
|
||||
}
|
||||
|
||||
function resolveHostIdentity(options: ResolveRuntimeConfigOptions): Readonly<ChannelIdentity> {
|
||||
if (!options.buildIdentity || typeof options.buildIdentity !== 'object') throw Error('Missing buildIdentity from application composition');
|
||||
const buildIdentity = resolveIdentity([() => options.buildIdentity]);
|
||||
if (options.hostKind === 'h5') {
|
||||
return resolveIdentity([() => BUILD_IDENTITY, queryStringSource(options.search)]);
|
||||
return resolveIdentity([() => buildIdentity, queryStringSource(options.search)]);
|
||||
}
|
||||
return resolveIdentity([
|
||||
buildIdentitySource(),
|
||||
() => ({gameid:buildIdentity.gameid,version:buildIdentity.version}),
|
||||
nativeIdentitySource(options.win, options.hostKind),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,2 @@
|
||||
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(联调时改成本地服要求的版本号)
|
||||
};
|
||||
/** Legacy launchWaitTime, in milliseconds. */
|
||||
export const DEFAULT_LAUNCH_WAIT_TIME_MS = 10;
|
||||
|
||||
@@ -362,7 +362,9 @@ export class WireClient {
|
||||
if (!this.open || this.stopped || this.transport === null) {
|
||||
throw new Error('connection is not open');
|
||||
}
|
||||
this.transport.send(serializeOutbound(envelope));
|
||||
const frame = serializeOutbound(envelope);
|
||||
this.logPacket('发送', frame);
|
||||
this.transport.send(frame);
|
||||
}
|
||||
|
||||
switchServer(url: string): void {
|
||||
@@ -454,12 +456,19 @@ export class WireClient {
|
||||
|
||||
private onMessage(frame: string): void {
|
||||
const decoded = decodeFrame(frame);
|
||||
if (decoded.kind !== 'heartbeat') this.logPacket('接收', frame);
|
||||
if (decoded.kind === 'ignore') return;
|
||||
this.watchdog.feed();
|
||||
if (decoded.kind !== 'message') return;
|
||||
this.emit({ type: 'message', message: decoded.message });
|
||||
}
|
||||
|
||||
private logPacket(direction: '发送' | '接收', frame: string): void {
|
||||
if (STARTUP_CONFIG.debugIO) {
|
||||
console.log(`[收发包] ${direction}`, {server:this.policy.current(),time:new Date().toISOString()}, frame);
|
||||
}
|
||||
}
|
||||
|
||||
private onReceiveTimeout(): void {
|
||||
if (!this.open || this.stopped) return;
|
||||
this.emit({ type: 'slow' });
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { RuntimeConfig } from '../config/runtime-config.ts';
|
||||
import type { RuntimeConfig } from '../config/runtime-config.ts';
|
||||
import type {GameRoomProjection} from '../sdk/contracts/game-room-projection.ts';
|
||||
import {gameAllowsRoomAction} from '../sdk/room-action-policy.ts';
|
||||
import { buildFirstSliceEnvelope } from '../protocol/first-slice-routes.ts';
|
||||
import type { OutboundEnvelope } from '../core/types/envelope.ts';
|
||||
import {
|
||||
@@ -18,7 +20,9 @@ export interface OutboundEnvelopeSendPort {
|
||||
send(envelope: OutboundEnvelope): void;
|
||||
}
|
||||
|
||||
export interface PlatformCommandsOptions {
|
||||
export interface PlatformCommandsOptions {
|
||||
/** Omitted/null explicitly means the game provides no additional restrictions. */
|
||||
readonly getRoomProjection?:()=>GameRoomProjection|null;
|
||||
readonly runtimeConfig: RuntimeConfig;
|
||||
readonly store: PlatformStore;
|
||||
readonly send: OutboundEnvelopeSendPort;
|
||||
@@ -115,20 +119,75 @@ export class PlatformCommands {
|
||||
return this.requireAuthenticatedPlayerId(state);
|
||||
}
|
||||
|
||||
prepare(): void {
|
||||
const state = this.options.store.getState();
|
||||
prepare(): void {
|
||||
const state = this.options.store.getState();
|
||||
this.requireInsideRoom(state);
|
||||
this.requireGameAction('prepare');
|
||||
this.options.send.send(buildPrepareRequest(
|
||||
this.options.runtimeConfig,
|
||||
this.roomIdentity(state, this.requireInsideRoom(state)),
|
||||
));
|
||||
}
|
||||
|
||||
sendRoomText(text:string,announcement=false):void {
|
||||
if(typeof text!=='string'||text.length===0)throw Error('room text must be nonempty');
|
||||
this.sendSocial('send_text',{text,type:announcement?1:0});
|
||||
}
|
||||
sendRoomPhrase(text:string):void {
|
||||
if(typeof text!=='string'||text.length===0)throw Error('room phrase must be nonempty');
|
||||
this.sendSocial('send_text',{text});
|
||||
}
|
||||
sendRoomVoice(voiceurl:string,time:string|number):void {
|
||||
if(typeof voiceurl!=='string'||voiceurl.length===0||!(typeof time==='number'||typeof time==='string'&&/^[0-9]+(?:\.[0-9]+)?$/.test(time))||!Number.isFinite(Number(time))||Number(time)<=0)throw Error('room voice requires uploaded URL and positive duration');
|
||||
this.sendSocial('send_voice',{voiceurl,time});
|
||||
}
|
||||
sendRoomExpression(id:number):void {
|
||||
if(!Number.isSafeInteger(id)||id<=0)throw Error('room expression requires positive ID');
|
||||
this.sendSocial('send_phiz',{text:id});
|
||||
}
|
||||
breakRoom():void {
|
||||
const room=this.requireInsideRoom(this.options.store.getState());
|
||||
if(room.stage!==0||room.status!==1)throw Error('breakRoom requires pregame owner');
|
||||
this.sendSocial('self_break_room',{});
|
||||
}
|
||||
surrenderRoom(count:number):void {
|
||||
if(!Number.isFinite(count)||count<=0)throw Error('surrender count must be positive');
|
||||
this.sendSocial('beanroom_surrender',{count});
|
||||
}
|
||||
changeRoom():void {this.sendSocial('change_room',{});}
|
||||
private sendSocial(rpc:'send_text'|'send_voice'|'send_phiz'|'self_break_room'|'change_room'|'beanroom_surrender',data:Readonly<Record<string,unknown>>):void {
|
||||
const state=this.options.store.getState(),room=this.requireInsideRoom(state);
|
||||
if(state.app.phase!=='logged-in')throw Error('room command requires logged-in connection');
|
||||
const {agentid,gameid}=this.options.runtimeConfig.identity;
|
||||
this.options.send.send(buildFirstSliceEnvelope(rpc,{agentid,gameid,...this.roomIdentity(state,room),...data}));
|
||||
}
|
||||
|
||||
applyDissolution():void {this.sendDissolution('self_apply_free_room');}
|
||||
agreeDissolution():void {this.sendDissolution('self_agree_free_room');}
|
||||
refuseDissolution():void {this.sendDissolution('self_refuse_free_room');}
|
||||
|
||||
private sendDissolution(rpc:'self_apply_free_room'|'self_agree_free_room'|'self_refuse_free_room'):void {
|
||||
const state=this.options.store.getState(),room=this.requireInsideRoom(state);
|
||||
if(state.app.phase!=='logged-in')throw Error('dissolution requires logged-in connection');
|
||||
if(rpc==='self_apply_free_room'){
|
||||
if(room.stage!==1)throw Error('dissolution application requires stage=1');
|
||||
if(state.dissolution)throw Error('dissolution vote or result is already present');
|
||||
this.requireGameAction('exit');
|
||||
}else{
|
||||
const vote=state.dissolution;
|
||||
if(vote?.kind!=='active'||vote.participants.find(player=>player.seat===room.selfSeat)?.state!==-1)throw Error('dissolution response requires own waiting vote');
|
||||
}
|
||||
const {agentid,gameid}=this.options.runtimeConfig.identity;
|
||||
this.options.send.send(buildFirstSliceEnvelope(rpc,{agentid,gameid,...this.roomIdentity(state,room)}));
|
||||
}
|
||||
|
||||
exitRoom(): void {
|
||||
const state = this.options.store.getState();
|
||||
const room = this.requireInsideRoom(state);
|
||||
if (room.stage !== 0 && room.infinite !== 1) {
|
||||
if (room.stage !== 0 && room.infinite !== 1) {
|
||||
throw new UnsupportedGameHostCommandError('room.exit requires stage=0 unless infinite=1');
|
||||
}
|
||||
}
|
||||
this.requireGameAction('exit');
|
||||
this.options.send.send(buildExitRoomRequest(
|
||||
this.options.runtimeConfig,
|
||||
this.roomIdentity(state, room),
|
||||
@@ -154,12 +213,17 @@ export class PlatformCommands {
|
||||
));
|
||||
}
|
||||
|
||||
private requireInsideRoom(state: PlatformState): InsideRoomState {
|
||||
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 requireGameAction(action:'prepare'|'exit'):void {
|
||||
const projection=this.options.getRoomProjection===undefined?null:this.options.getRoomProjection();
|
||||
if(!gameAllowsRoomAction(true,projection,action))throw new UnsupportedGameHostCommandError(`Game currently disallows room.${action}`);
|
||||
}
|
||||
|
||||
private requireAuthenticatedPlayerId(state: PlatformState): number {
|
||||
const playerid = state.players.selfPlayerId;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type {NumericToolsPort,RoomCapabilities,InputResult,InputCancelReason,FeedbackOwnerPort,OwnerHandle} from '../sdk/contracts/game-capabilities.ts';
|
||||
export type {NumericToolsPort} from '../sdk/contracts/game-capabilities.ts';
|
||||
import {roomExitAction} from '../sdk/room-exit-action.ts';
|
||||
import {requireRoomtype} from '../protocol/contracts/validation.ts';
|
||||
import type {Roomtype} from '../sdk/contracts/roomtype.ts';
|
||||
import { APP } from '../core/constants.ts';
|
||||
import type { OutboundEnvelope } from '../core/types/envelope.ts';
|
||||
@@ -25,7 +29,10 @@ export class GameSessionDisposedError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface CreateGameHostAdapterOptions {
|
||||
readonly numericTools?:NumericToolsPort;
|
||||
readonly feedback?:FeedbackOwnerPort;
|
||||
readonly gameEntry: GameEntry;
|
||||
readonly store: PlatformStore;
|
||||
readonly commands: PlatformCommands;
|
||||
@@ -75,8 +82,16 @@ class SessionBoundGameHost {
|
||||
private readonly subscriptions = new Set<SubscriptionRecord>();
|
||||
private invalidationErrors: unknown[] | null = null;
|
||||
private disposed = false;
|
||||
private numericTools:NumericToolsPort|null;
|
||||
private feedback:FeedbackOwnerPort|null;
|
||||
private numericToolsUsed=false;
|
||||
private inputGeneration=0;
|
||||
private multiplierGeneration=0;
|
||||
private readonly cleanups=new Set<()=>void>();
|
||||
private readonly pendingInputs=new Map<'input'|'multiplier',{cancel(reason:InputCancelReason):void}>();
|
||||
|
||||
constructor(options: CreateGameHostAdapterOptions) {
|
||||
this.numericTools=options.numericTools??null;this.feedback=options.feedback??null;
|
||||
const route = options.gameEntry.route;
|
||||
assertGameRoute(route);
|
||||
const state = options.store.getState();
|
||||
@@ -111,7 +126,20 @@ class SessionBoundGameHost {
|
||||
},
|
||||
});
|
||||
|
||||
const capabilities:RoomCapabilities=Object.freeze({
|
||||
room:Object.freeze({seat,getSnapshot:()=>this.getSnapshot(),subscribe:(listener:(snapshot:PlatformGameSnapshot)=>void)=>this.subscribe(listener),
|
||||
prepare:()=>this.execute({type:'room.prepare'}),requestExit:()=>this.execute({type:roomExitAction(this.getSnapshot().room).command}),
|
||||
applyDissolution:()=>this.execute({type:'room.apply-dissolution'})}),
|
||||
messages:Object.freeze({send:(rpc:string,data:import('../sdk/contracts/roomtype.ts').JsonValue)=>{this.requireCurrentRoom();requireRoomtype(data,'$.game.data');this.sendGameMessage(rpc,data);}}),
|
||||
ui:Object.freeze({beginLoading:()=>this.ownFeedback(port=>port.beginLoading()),showBusinessTip:(message:string,time:number)=>this.ownFeedback(port=>port.showBusinessTip(message,time)),requestNumber:(request:{message:string})=>this.requestNumber<number>(request.message,false),
|
||||
requestDigitText:(request:{message:string})=>this.requestNumber<string>(request.message,true),
|
||||
requestMultiplier:(request:{values:readonly number[]})=>this.requestMultiplier(request.values),
|
||||
cancelNumber:()=>this.execute({type:'ui.close-number-input'}),cancelMultiplier:()=>this.execute({type:'ui.hide-multiplier'})}),
|
||||
scope:Object.freeze({get active(){return owner.isActiveRoom();},defer:(cleanup:()=>void)=>this.defer(cleanup)}),
|
||||
});
|
||||
const owner=this;
|
||||
this.host = Object.freeze({
|
||||
capabilities,
|
||||
seat,
|
||||
getSnapshot: (): PlatformGameSnapshot => this.getSnapshot(),
|
||||
subscribe: (listener: (snapshot: PlatformGameSnapshot) => void): (() => void) => (
|
||||
@@ -132,9 +160,19 @@ class SessionBoundGameHost {
|
||||
this.commands = null;
|
||||
this.sendPort = null;
|
||||
this.route = null;
|
||||
this.feedback=null;
|
||||
this.roomGeneration = null;
|
||||
|
||||
const errors: unknown[] = [];
|
||||
for(const pending of this.pendingInputs.values())pending.cancel('scope-ended');
|
||||
this.pendingInputs.clear();
|
||||
for(const cleanup of [...this.cleanups].reverse()){this.cleanups.delete(cleanup);try{cleanup();}catch(error){errors.push(error);}}
|
||||
const numericTools=this.numericTools;
|
||||
this.numericTools=null;
|
||||
this.inputGeneration++;this.multiplierGeneration++;
|
||||
if(this.numericToolsUsed&&numericTools!==null){
|
||||
try{numericTools.close();}catch(error){errors.push(error);}
|
||||
}
|
||||
this.invalidationErrors = errors;
|
||||
try {
|
||||
for (const record of subscriptions) {
|
||||
@@ -206,10 +244,38 @@ class SessionBoundGameHost {
|
||||
const commands = this.commands;
|
||||
if (commands === null) throw new GameSessionDisposedError();
|
||||
if (command !== null && typeof command === 'object') {
|
||||
if(command.type==='ui.open-number-input'||command.type==='ui.close-number-input'
|
||||
||command.type==='ui.show-multiplier'||command.type==='ui.hide-multiplier'){
|
||||
const tools=this.numericTools;
|
||||
if(tools===null)throw new UnsupportedGameHostCommandError('Numeric tools presentation is unavailable');
|
||||
this.numericToolsUsed=true;
|
||||
if(command.type==='ui.close-number-input'){this.cancelInput('input','user');this.inputGeneration++;tools.closeNumberInput();return;}
|
||||
if(command.type==='ui.hide-multiplier'){this.cancelInput('multiplier','user');this.multiplierGeneration++;tools.hideMultiplier();return;}
|
||||
if(typeof command.onConfirm!=='function')throw TypeError('Numeric tools require a confirmation callback');
|
||||
const current=():boolean=>!this.disposed&&this.store!==null&&this.isCurrentRoom(this.store.getState());
|
||||
if(command.type==='ui.open-number-input'){
|
||||
this.cancelInput('input','replaced');
|
||||
const generation=++this.inputGeneration;
|
||||
tools.openNumberInput({message:command.message,begin0:command.begin0,onConfirm:value=>{
|
||||
if(current()&&generation===this.inputGeneration)command.onConfirm(value);
|
||||
}});
|
||||
}else{
|
||||
this.cancelInput('multiplier','replaced');
|
||||
const generation=++this.multiplierGeneration;
|
||||
tools.showMultiplier({values:[...command.values],onConfirm:values=>{
|
||||
if(current()&&generation===this.multiplierGeneration)command.onConfirm(values);
|
||||
}});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (command.type === 'room.prepare') {
|
||||
commands.prepare();
|
||||
return;
|
||||
}
|
||||
if(command.type==='room.apply-dissolution'){
|
||||
commands.applyDissolution();
|
||||
return;
|
||||
}
|
||||
if (command.type === 'room.exit') {
|
||||
commands.exitRoom();
|
||||
return;
|
||||
@@ -218,6 +284,72 @@ class SessionBoundGameHost {
|
||||
throw new UnsupportedGameHostCommandError('unsupported GameHost command');
|
||||
}
|
||||
|
||||
private ownFeedback(acquire:(port:FeedbackOwnerPort)=>OwnerHandle):OwnerHandle {
|
||||
this.requireCurrentRoom();const port=this.feedback;if(!port)throw new UnsupportedGameHostCommandError('Owned feedback presentation is unavailable');
|
||||
const handle=acquire(port);if(!handle||typeof handle.close!=='function')throw TypeError('Feedback must return an owner handle');
|
||||
if(!this.isActiveRoom()){handle.close();throw new GameSessionDisposedError();}
|
||||
let closed=false;let unregister=()=>{};
|
||||
const owned=Object.freeze({close:()=>{if(closed)return;closed=true;unregister();handle.close();}});
|
||||
unregister=this.defer(()=>owned.close());return owned;
|
||||
}
|
||||
|
||||
private isActiveRoom():boolean {return !this.disposed&&this.store!==null&&this.isCurrentRoom(this.store.getState());}
|
||||
|
||||
private defer(cleanup:()=>void):()=>void {
|
||||
this.requireCurrentRoom();if(typeof cleanup!=='function')throw TypeError('Scope cleanup must be a function');
|
||||
const registered=()=>cleanup();this.cleanups.add(registered);return ()=>{this.cleanups.delete(registered);};
|
||||
}
|
||||
|
||||
private cancelInput(kind:'input'|'multiplier',reason:InputCancelReason):void {this.pendingInputs.get(kind)?.cancel(reason);}
|
||||
|
||||
private requestNumber<T extends number|string>(message:string,text:boolean):Promise<InputResult<T>> {
|
||||
return this.requestInput<T>('input',(tools,confirm,cancel,fail)=>{
|
||||
if(typeof message!=='string')throw TypeError('Number input message must be a string');
|
||||
tools.openNumberInput({message,begin0:text,onCancel:cancel,onConfirm:value=>{
|
||||
if(typeof value!==(text?'string':'number')||(typeof value==='number'&&!Number.isFinite(value))){fail(TypeError('Number input returned the wrong result type'));return;}
|
||||
confirm(value as T);
|
||||
}});
|
||||
});
|
||||
}
|
||||
|
||||
private requestMultiplier(values:readonly number[]):Promise<InputResult<readonly number[]>> {
|
||||
return this.requestInput<readonly number[]>('multiplier',(tools,confirm,cancel,fail)=>{
|
||||
if(!Array.isArray(values)||!values.every(Number.isFinite))throw TypeError('Multiplier requires finite values');
|
||||
tools.showMultiplier({values:Object.freeze([...values]),onCancel:cancel,onConfirm:result=>{
|
||||
if(!Array.isArray(result)||!result.every(Number.isFinite)){fail(TypeError('Multiplier returned non-finite values'));return;}
|
||||
confirm(Object.freeze([...result]));
|
||||
}});
|
||||
});
|
||||
}
|
||||
|
||||
private requestInput<T>(kind:'input'|'multiplier',open:(tools:NumericToolsPort,confirm:(value:T)=>void,cancel:()=>void,fail:(error:unknown)=>void)=>void):Promise<InputResult<T>> {
|
||||
return new Promise((resolve,reject)=>{
|
||||
try{
|
||||
this.requireCurrentRoom();
|
||||
if(this.pendingInputs.has(kind))throw Error('An input request of this kind is already pending');
|
||||
const tools=this.numericTools;if(tools===null)throw new UnsupportedGameHostCommandError('Numeric tools presentation is unavailable');
|
||||
this.numericToolsUsed=true;
|
||||
if(kind==='input')this.inputGeneration++;else this.multiplierGeneration++;
|
||||
let settled=false;
|
||||
const finish=(result:InputResult<T>):void=>{if(settled)return;settled=true;this.pendingInputs.delete(kind);resolve(Object.freeze(result));};
|
||||
this.pendingInputs.set(kind,{cancel:reason=>finish({kind:'cancelled',reason})});
|
||||
const fail=(error:unknown):void=>{
|
||||
if(settled)return;settled=true;this.pendingInputs.delete(kind);
|
||||
try{if(kind==='input')tools.closeNumberInput();else tools.hideMultiplier();}
|
||||
catch(cleanup){reject(Object.assign(new Error('Input request and cleanup failed'),{errors:Object.freeze([error,cleanup])}));return;}
|
||||
reject(error);
|
||||
};
|
||||
try{open(tools,value=>{
|
||||
if(settled)return;
|
||||
if(!this.isActiveRoom()){finish({kind:'cancelled',reason:'scope-ended'});return;}
|
||||
settled=true;this.pendingInputs.delete(kind);
|
||||
try{if(kind==='input')tools.closeNumberInput();else tools.hideMultiplier();resolve(Object.freeze({kind:'confirmed',value}));}catch(error){reject(error);}
|
||||
},()=>finish({kind:'cancelled',reason:'user'}),fail);}
|
||||
catch(error){fail(error);}
|
||||
}catch(error){reject(error);}
|
||||
});
|
||||
}
|
||||
|
||||
private requireStore(): PlatformStore {
|
||||
this.requireActive();
|
||||
const store = this.store;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "366456c6-a70b-47c0-8ad8-ffe01393a51e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import {RoomScope,type RoomDisposer} from './room-scope.ts';
|
||||
import {RoomMailbox,assertRoomMailboxOptions,type RoomMailboxOptions} from './room-mailbox.ts';
|
||||
|
||||
export type RoomCoordinatorState='outside'|'entering'|'active'|'failed';
|
||||
export type RoomMountResult=void|RoomDisposer;
|
||||
export interface RoomEntryOptions<T> {
|
||||
readonly invalidate:RoomDisposer;
|
||||
readonly mount:(scope:RoomScope)=>RoomMountResult|PromiseLike<RoomMountResult>;
|
||||
readonly ready:()=>void;
|
||||
readonly handle:(message:T)=>void;
|
||||
}
|
||||
export class RoomCoordinatorFailure extends Error {
|
||||
readonly errors:readonly unknown[];
|
||||
constructor(readonly generation:number,errors:readonly unknown[]){
|
||||
super(`Room generation ${generation} failed`);this.name='RoomCoordinatorFailure';this.errors=Object.freeze([...errors]);
|
||||
}
|
||||
}
|
||||
export interface RoomCoordinatorOptions extends RoomMailboxOptions {
|
||||
readonly onFailure:(failure:RoomCoordinatorFailure)=>void;
|
||||
}
|
||||
interface Session<T>{scope:RoomScope;mailbox:RoomMailbox<T>;entry:RoomEntryOptions<T>;draining:boolean;}
|
||||
|
||||
/** Serializes room input across async scene mounting and revokes stale generations immediately. */
|
||||
export class RoomCoordinator<T> {
|
||||
private current:Session<T>|null=null;
|
||||
private nextGeneration=0;
|
||||
private status:RoomCoordinatorState='outside';
|
||||
private lastFailure:RoomCoordinatorFailure|null=null;
|
||||
private lastDiagnostic:RoomCoordinatorFailure|null=null;
|
||||
constructor(private readonly options:RoomCoordinatorOptions){assertRoomMailboxOptions(options);if(typeof options.onFailure!=='function')throw Error('Room coordinator requires a diagnostic failure handler');}
|
||||
get state():RoomCoordinatorState{return this.status;}
|
||||
get scope():RoomScope|null{return this.current?.scope??null;}
|
||||
get generation():number{return this.nextGeneration;}
|
||||
get failure():RoomCoordinatorFailure|null{return this.lastFailure;}
|
||||
get diagnosticFailure():RoomCoordinatorFailure|null{return this.lastDiagnostic;}
|
||||
enter(entry:RoomEntryOptions<T>):RoomScope{
|
||||
const cleanup=this.cancel();
|
||||
const scope=new RoomScope(++this.nextGeneration,entry.invalidate);
|
||||
const session:Session<T>={scope,entry,draining:false,mailbox:new RoomMailbox<T>(this.options,error=>this.fail(session,error))};
|
||||
scope.own(()=>session.mailbox.dispose());
|
||||
this.current=session;this.status='entering';this.lastFailure=null;
|
||||
if(cleanup.length){this.fail(session,...cleanup);return scope;}
|
||||
try{
|
||||
const result=entry.mount(scope);
|
||||
if(result!==undefined&&typeof result!=='function'&&typeof result.then==='function'){
|
||||
void Promise.resolve(result).then(resource=>this.mounted(session,resource),error=>this.mountRejected(session,error));
|
||||
}else this.mounted(session,result as RoomMountResult);
|
||||
}catch(error){if(this.isCurrent(session))this.fail(session,error);else throw error;}
|
||||
return scope;
|
||||
}
|
||||
dispatch(message:T):void{
|
||||
const session=this.current;
|
||||
if(!session||!session.scope.active)throw Error(`Cannot dispatch room input while ${this.status}`);
|
||||
try{
|
||||
session.mailbox.enqueue(message);
|
||||
if(this.status==='active')this.drain(session);
|
||||
}catch(error){if(this.isCurrent(session))this.fail(session,error);else throw error;}
|
||||
}
|
||||
/** Callers aggregate explicit cancellation cleanup errors with their existing teardown errors. */
|
||||
cancel():readonly unknown[]{
|
||||
const session=this.current;this.current=null;this.status='outside';
|
||||
return session?.scope.dispose()??[];
|
||||
}
|
||||
private isCurrent(session:Session<T>):boolean{return this.current===session&&session.scope.active;}
|
||||
private mounted(session:Session<T>,resource:RoomMountResult):void{
|
||||
try{
|
||||
if(resource!==undefined)session.scope.own(resource);
|
||||
if(!this.isCurrent(session))return;
|
||||
session.entry.ready();
|
||||
if(!this.isCurrent(session))return;
|
||||
this.status='active';this.drain(session);
|
||||
}catch(error){if(this.isCurrent(session))this.fail(session,error);else this.report(new RoomCoordinatorFailure(session.scope.generation,[error]));}
|
||||
}
|
||||
private mountRejected(session:Session<T>,error:unknown):void{
|
||||
if(this.isCurrent(session))this.fail(session,error);
|
||||
// Rejected stale loads own no mount resource; their superseded scope was already released.
|
||||
}
|
||||
private drain(session:Session<T>):void{
|
||||
if(session.draining)return;
|
||||
session.draining=true;
|
||||
try{session.mailbox.drain(session.entry.handle,()=>this.isCurrent(session));}
|
||||
finally{session.draining=false;}
|
||||
}
|
||||
private fail(session:Session<T>,...errors:unknown[]):void{
|
||||
if(!this.isCurrent(session))return;
|
||||
this.current=null;this.status='failed';
|
||||
const failure=new RoomCoordinatorFailure(session.scope.generation,[...errors,...session.scope.dispose()]);
|
||||
this.lastFailure=failure;this.report(failure);
|
||||
}
|
||||
private report(failure:RoomCoordinatorFailure):void{
|
||||
this.lastDiagnostic=failure;
|
||||
try{this.options.onFailure(failure);}catch(error){
|
||||
// Preserve diagnostic callback faults without creating an unhandled async rejection.
|
||||
const diagnostic=new RoomCoordinatorFailure(failure.generation,[...failure.errors,error]);
|
||||
this.lastDiagnostic=diagnostic;
|
||||
if(this.nextGeneration===failure.generation)this.lastFailure=diagnostic;
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e56782fe-72cd-4748-a2b4-8907281be052",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export interface RoomClock {
|
||||
setTimeout(callback:()=>void,milliseconds:number):unknown;
|
||||
clearTimeout(handle:unknown):void;
|
||||
}
|
||||
export interface RoomMailboxOptions {
|
||||
readonly capacity:number;
|
||||
readonly maxWaitMs:number;
|
||||
readonly clock:RoomClock;
|
||||
}
|
||||
export function assertRoomMailboxOptions(options:RoomMailboxOptions):void{
|
||||
if(!Number.isSafeInteger(options.capacity)||options.capacity<1)throw Error('Room mailbox capacity must be a positive safe integer');
|
||||
if(!Number.isFinite(options.maxWaitMs)||options.maxWaitMs<=0)throw Error('Room mailbox maxWaitMs must be positive and finite');
|
||||
if(typeof options.clock?.setTimeout!=='function'||typeof options.clock?.clearTimeout!=='function')throw Error('Room mailbox requires a clock');
|
||||
}
|
||||
/** FIFO queue owned by a room scope; its first pending message starts the wait deadline. */
|
||||
export class RoomMailbox<T> {
|
||||
private readonly messages:T[]=[];
|
||||
private timer:{handle:unknown}|null=null;
|
||||
private disposed=false;
|
||||
constructor(private readonly options:RoomMailboxOptions,private readonly onTimeout:(error:Error)=>void){assertRoomMailboxOptions(options);}
|
||||
get size():number{return this.messages.length;}
|
||||
enqueue(message:T):void{
|
||||
if(this.disposed)throw Error('Room mailbox is disposed');
|
||||
if(this.messages.length>=this.options.capacity)throw Error(`Room mailbox capacity exceeded (${this.options.capacity})`);
|
||||
this.messages.push(message);
|
||||
if(this.timer===null){this.timer={handle:this.options.clock.setTimeout(()=>{
|
||||
this.timer=null;
|
||||
if(!this.disposed&&this.messages.length>0)this.onTimeout(Error(`Room mailbox timed out after ${this.options.maxWaitMs} ms`));
|
||||
},this.options.maxWaitMs)};}
|
||||
}
|
||||
drain(handle:(message:T)=>void,active:()=>boolean):void{
|
||||
while(!this.disposed&&active()&&this.messages.length>0){
|
||||
const message=this.messages.shift() as T;
|
||||
if(this.messages.length===0)this.clearTimer();
|
||||
handle(message);
|
||||
}
|
||||
}
|
||||
dispose():void{this.disposed=true;this.messages.length=0;this.clearTimer();}
|
||||
private clearTimer():void{
|
||||
const timer=this.timer;this.timer=null;
|
||||
if(timer!==null)this.options.clock.clearTimeout(timer.handle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "ca845f16-3594-4150-b744-315e56d38814",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export type RoomDisposer=()=>void;
|
||||
|
||||
/** Owns one room generation; the injected invalidator revokes the existing host lease. */
|
||||
export class RoomScope {
|
||||
private live=true;
|
||||
private readonly disposers=new Set<RoomDisposer>();
|
||||
constructor(readonly generation:number,private readonly invalidate:RoomDisposer){
|
||||
if(!Number.isSafeInteger(generation)||generation<1)throw Error('Invalid room scope generation');
|
||||
if(typeof invalidate!=='function')throw Error('Room scope requires a lease invalidator');
|
||||
}
|
||||
get active():boolean{return this.live;}
|
||||
get resourceCount():number{return this.disposers.size;}
|
||||
/** The returned release is idempotent and unregisters before invoking user cleanup. */
|
||||
own(dispose:RoomDisposer):RoomDisposer{
|
||||
if(typeof dispose!=='function')throw Error('Room scope requires a disposer');
|
||||
let owned=true;
|
||||
const release=()=>{if(!owned)return;owned=false;this.disposers.delete(release);dispose();};
|
||||
if(this.live)this.disposers.add(release);else release();
|
||||
return release;
|
||||
}
|
||||
dispose():readonly unknown[]{
|
||||
if(!this.live)return [];
|
||||
this.live=false;
|
||||
const errors:unknown[]=[];
|
||||
try{this.invalidate();}catch(error){errors.push(error);}
|
||||
for(const release of [...this.disposers].reverse())try{release();}catch(error){errors.push(error);}
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "11761871-986b-4241-b53b-1f277f35ce1d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type {GameEntry,GameHost,PlatformToGameEvent,GameServerMessage} from '../../sdk/contracts/index.ts';
|
||||
import {GameSessionHost} from '../../sdk/runtime/game-session-host.ts';
|
||||
import type {ScenePort} from '../scene-port.ts';
|
||||
import {RoomCoordinator,type RoomCoordinatorOptions} from './room-coordinator.ts';
|
||||
|
||||
/** Module attachment precedes view binding; all room input waits for the bound view. */
|
||||
export class SceneOrderedGameSessionHost extends GameSessionHost {
|
||||
private readonly coordinator:RoomCoordinator<()=>void>;
|
||||
private synchronousErrors:unknown[]|null=null;
|
||||
private packetDepth=0;
|
||||
constructor(entry:GameEntry,private readonly scene:ScenePort,options:RoomCoordinatorOptions,
|
||||
private readonly invalidateHost:(host:GameHost)=>void){
|
||||
super(entry);
|
||||
this.coordinator=new RoomCoordinator({...options,onFailure:failure=>{
|
||||
if(this.synchronousErrors!==null)this.synchronousErrors.push(...failure.errors);
|
||||
else options.onFailure(failure);
|
||||
}});
|
||||
}
|
||||
open(host:GameHost):void{
|
||||
super.open(host);
|
||||
if(this.state!=='active')return;
|
||||
this.synchronous(()=>this.coordinator.enter({invalidate:()=>this.invalidateHost(host),
|
||||
mount:scope=>this.scene.showRoom(scope),ready:()=>{},handle:action=>action()}));
|
||||
}
|
||||
publish(event:PlatformToGameEvent):void{this.dispatch(()=>super.publish(event));}
|
||||
dispatchGameMessage(route:string,message:GameServerMessage):void{this.dispatch(()=>super.dispatchGameMessage(route,message));}
|
||||
restore(deskinfo:unknown):void{this.dispatch(()=>super.restore(deskinfo));}
|
||||
get isRoomScopeAvailable():boolean{return this.coordinator.scope?.active===true;}
|
||||
/** Preserve a complete packet's Store commit and game callbacks as one FIFO transaction. */
|
||||
queueRoomPacket(action:()=>void):void{
|
||||
this.synchronous(()=>this.coordinator.dispatch(()=>{
|
||||
this.packetDepth++;
|
||||
try{action();}finally{this.packetDepth--;}
|
||||
}));
|
||||
}
|
||||
showRoomAfterOpen():void{
|
||||
if(this.coordinator.state!=='entering'&&this.coordinator.state!=='active'&&this.state!=='disposed')throw Error('Cannot show room without a live room scope');
|
||||
}
|
||||
/** Connection loss cancels slow mount immediately; an already mounted settlement remains owned. */
|
||||
cancelPendingMount():void{if(this.coordinator.state==='entering')this.close();}
|
||||
close():void{this.release(false);}
|
||||
dispose():void{this.release(true);}
|
||||
private release(terminal:boolean):void{
|
||||
const errors=[...this.coordinator.cancel()];
|
||||
try{if(terminal)super.dispose();else super.close();}catch(error){errors.push(error);}
|
||||
this.throwErrors(errors);
|
||||
}
|
||||
private dispatch(action:()=>void):void{
|
||||
if(this.state==='disposed')throw Error('Cannot dispatch to a disposed game session');
|
||||
if(this.packetDepth>0){action();return;}
|
||||
this.synchronous(()=>this.coordinator.dispatch(action));
|
||||
}
|
||||
/** Wire callbacks remain synchronous faults; only deferred work uses onFailure. */
|
||||
private synchronous(action:()=>unknown):void{
|
||||
const previous=this.synchronousErrors,errors:unknown[]=[];this.synchronousErrors=errors;
|
||||
try{action();}catch(error){errors.push(error);}finally{this.synchronousErrors=previous;}
|
||||
this.throwErrors(errors);
|
||||
}
|
||||
private throwErrors(errors:readonly unknown[]):void{
|
||||
if(errors.length===1)throw errors[0];
|
||||
if(errors.length>1)throw Object.assign(Error('Room scope operation failed'),{errors});
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "19c227d9-98e6-42f2-9c1e-9c6021eb44b7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import type {RoomEntryInput,RoomStartMessage} from '../sdk/contracts/platform-events.ts';
|
||||
import {requireRecord} from '../protocol/contracts/validation.ts';
|
||||
import type { GameHost } from '../sdk/contracts/index.ts';
|
||||
import {
|
||||
extractGameSessionOpenFaultErrors,
|
||||
@@ -12,6 +14,7 @@ import {
|
||||
} from '../protocol/contracts/index.ts';
|
||||
import { cloneAndFreezeRoomtype, PlatformStore } from './stores/platform-store.ts';
|
||||
import type { PlatformState } from './stores/platform-types.ts';
|
||||
import type {DissolutionResult} from './stores/platform-types.ts';
|
||||
import type {LoginAccountIdentity} from '../protocol/contracts/login-contract.ts';
|
||||
|
||||
export interface GameHostLeasePort {
|
||||
@@ -42,7 +45,7 @@ export class RuntimeSessionFault extends Error {
|
||||
readonly errors: readonly unknown[];
|
||||
|
||||
constructor(operation: string, errors: readonly unknown[]) {
|
||||
super(`RuntimeSession fault during ${operation}`);
|
||||
super(`RuntimeSession fault during ${operation}: ${errors.map(error => String(error)).join('; ')}`);
|
||||
this.name = 'RuntimeSessionFault';
|
||||
this.errors = Object.freeze([...errors]);
|
||||
}
|
||||
@@ -92,7 +95,7 @@ export class RuntimeSession {
|
||||
this.releaseOrFault('login lobby replacement');
|
||||
return;
|
||||
}
|
||||
this.openCommittedRoom(committed, parsed.reconnect, 'login room open');
|
||||
this.openCommittedRoom(committed, parsed.reconnect, 'login room open',parsed.reconnect.present?{kind:'login-restored',sourceRpc:'player_login',raw:parsed.raw,deskinfo:parsed.reconnect.value}:{kind:'login-waiting',sourceRpc:'player_login',raw:parsed.raw});
|
||||
}
|
||||
|
||||
handleCreateRoom(data: unknown): void {
|
||||
@@ -106,14 +109,14 @@ export class RuntimeSession {
|
||||
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);
|
||||
// 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');
|
||||
this.openCommittedRoom(committed, {present: false}, 'create room open',{kind:'created',sourceRpc:'create_room',raw:parsed.raw});
|
||||
}
|
||||
|
||||
handleSelfJoin(data: unknown): void {
|
||||
handleSelfJoin(data: unknown, message?:RoomStartMessage): void {
|
||||
this.requireOperational();
|
||||
const parsed = parseSelfJoinRoomResponse(data);
|
||||
this.options.roomHistory?.joined(parsed.raw);
|
||||
@@ -124,10 +127,21 @@ export class RuntimeSession {
|
||||
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');
|
||||
const started=parsed.raw.deskwar===1;
|
||||
const reconnect=started?{present:false} as const:parsed.reconnect;
|
||||
this.openCommittedRoom(committed,reconnect,'self join room open',started?{kind:'joined-started',sourceRpc:'self_join_room',raw:parsed.raw}:reconnect.present?{kind:'joined-snapshot',sourceRpc:'self_join_room',raw:parsed.raw,deskinfo:reconnect.value}:{kind:'joined-waiting',sourceRpc:'self_join_room',raw:parsed.raw});
|
||||
if(started)this.options.gameSession.publish({type:'room.started',sourceRpc:'self_join_room',message:message??{route:'agent',rpc:'self_join_room',data:parsed.raw}});
|
||||
this.lastJoinedRoomcode = parsed.room!.roomcode;
|
||||
}
|
||||
|
||||
handleBreakRoom(payload:unknown):void {
|
||||
this.requireOperational();
|
||||
if(this.options.store.getState().room.kind==='outside')return;
|
||||
this.options.roomHistory?.broken(requireRecord(payload,'$.data'));
|
||||
this.options.store.clearRoom();
|
||||
this.releaseOrFault('break room cleanup');
|
||||
}
|
||||
|
||||
handleSelfExit(parsed: ParsedSelfExitPayload): void {
|
||||
this.requireOperational();
|
||||
const state = this.options.store.getState();
|
||||
@@ -172,6 +186,29 @@ export class RuntimeSession {
|
||||
if (errors.length > 0) this.throwFault('server switch', errors);
|
||||
}
|
||||
|
||||
/** The server returns free_room only after this switch; settlement still owns the game lease. */
|
||||
handleDissolutionServerSwitch(switchConnection:()=>void):void {
|
||||
this.requireOperational();
|
||||
if(this.options.store.getState().room.kind==='outside'&&this.options.gameSession.state==='idle'&&this.activeLease===null){
|
||||
switchConnection();
|
||||
return;
|
||||
}
|
||||
if(this.options.gameSession.state!=='active')throw Error('Dissolution server switch requires an active game session');
|
||||
switchConnection();
|
||||
}
|
||||
|
||||
handleDissolutionResult(result:DissolutionResult):void {
|
||||
this.requireOperational();
|
||||
if(!result.passed)throw Error('Dissolution settlement requires a server-approved result');
|
||||
if(result.deskfree!==undefined){
|
||||
this.options.gameSession.publish({type:'room.dissolution-confirmed',freeNow:result.freeNow,deskfree:result.deskfree,
|
||||
...(result.tips!==undefined?{tips:result.tips}:{}),...(result.time!==undefined?{time:result.time}:{})});
|
||||
return;
|
||||
}
|
||||
this.options.store.clearRoom();
|
||||
this.releaseOrFault('dissolution without settlement cleanup');
|
||||
}
|
||||
|
||||
handleKick(_data: unknown, stopConnection: () => void): void {
|
||||
if (this.wasKicked) return;
|
||||
this.requireOperational();
|
||||
@@ -191,6 +228,7 @@ export class RuntimeSession {
|
||||
state: PlatformState,
|
||||
reconnect: ReconnectSnapshot,
|
||||
operation: string,
|
||||
entry: RoomEntryInput,
|
||||
): void {
|
||||
if (state.room.kind !== 'inside') {
|
||||
this.throwFault(operation, [new Error('Cannot open a game session without committed room state')]);
|
||||
@@ -217,6 +255,7 @@ export class RuntimeSession {
|
||||
this.options.gameSession.publish({
|
||||
type: 'room.entered',
|
||||
roomtype: state.room.roomtype,
|
||||
entry,
|
||||
});
|
||||
if (this.gameWasDisposed()) return;
|
||||
if (reconnect.present) this.options.gameSession.restore(reconnect.value);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RuntimeConfig } from '../config/runtime-config.ts';
|
||||
import { LOGIN_GUARD_MS } from '../core/constants.ts';
|
||||
import { STARTUP_CONFIG } from '../config/profiles.ts';
|
||||
import type { OutboundEnvelope } from '../core/types/envelope.ts';
|
||||
import type { InboundMessage, OutboundEnvelope } from '../core/types/envelope.ts';
|
||||
import { realClock, type Clock } from '../net/heartbeat.ts';
|
||||
import type { WireClient, WireEvent } from '../net/wire-client.ts';
|
||||
import {
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
} from '../protocol/contracts/index.ts';
|
||||
import { PlatformHandlers } from '../protocol/platform-handlers.ts';
|
||||
import { Router } from '../protocol/router.ts';
|
||||
import { isLoginResponse } from '../protocol/first-slice-routes.ts';
|
||||
import { FIRST_SLICE_RPC_ROUTE, isLoginResponse } from '../protocol/first-slice-routes.ts';
|
||||
import type { GameEntry, GameHost } from '../sdk/contracts/index.ts';
|
||||
import { GameSessionHost } from '../sdk/runtime/game-session-host.ts';
|
||||
import {SceneOrderedGameSessionHost} from './room/scene-game-session.ts';
|
||||
import {ROOM_LIFECYCLE_CONFIG} from '../config/room-lifecycle.ts';
|
||||
import { PlatformCommands, type JoinRoomCommand, type CreateRoomCommand } from './commands.ts';
|
||||
import type { ConnectionIntent } from './connection-intent.ts';
|
||||
import {
|
||||
createGameHostAdapter,
|
||||
type GameHostLease,
|
||||
type GameHostLease,
|
||||
type NumericToolsPort,
|
||||
} from './game-host-adapter.ts';
|
||||
import { RuntimeSession } from './runtime-session.ts';
|
||||
import type { ScenePort } from './scene-port.ts';
|
||||
@@ -31,7 +33,9 @@ export type PlatformWireClient = Pick<
|
||||
'subscribe' | 'start' | 'send' | 'switchServer' | 'reconnectCurrent' | 'stop'
|
||||
>;
|
||||
|
||||
export interface PlatformRuntimeOptions {
|
||||
export interface PlatformRuntimeOptions {
|
||||
readonly feedback?:import('../sdk/contracts/game-capabilities.ts').FeedbackOwnerPort;
|
||||
readonly numericTools?:NumericToolsPort;
|
||||
readonly gameEntry: GameEntry;
|
||||
readonly resolveRuntimeConfig: () => Promise<RuntimeConfig>;
|
||||
readonly createWireClient: (config: RuntimeConfig) => PlatformWireClient;
|
||||
@@ -45,51 +49,6 @@ export interface PlatformRuntimeOptions {
|
||||
|
||||
type RuntimeLifecycle = 'idle' | 'starting' | 'running' | 'kicked' | 'fatal' | 'stopped';
|
||||
|
||||
/** Keeps the scene transition between room activation and optional snapshot restore. */
|
||||
class SceneOrderedGameSessionHost extends GameSessionHost {
|
||||
private roomSceneShown = false;
|
||||
|
||||
constructor(entry: GameEntry, private readonly scene: ScenePort) {
|
||||
super(entry);
|
||||
}
|
||||
|
||||
open(host: GameHost): void {
|
||||
this.roomSceneShown = false;
|
||||
super.open(host);
|
||||
}
|
||||
|
||||
restore(deskinfo: unknown): void {
|
||||
this.showRoomAfterOpen();
|
||||
if (this.state === 'disposed') return;
|
||||
super.restore(deskinfo);
|
||||
}
|
||||
|
||||
showRoomAfterOpen(): void {
|
||||
if (this.roomSceneShown) return;
|
||||
if (this.state !== 'active') {
|
||||
throw new Error('Cannot show the room without an active game session');
|
||||
}
|
||||
this.roomSceneShown = true;
|
||||
this.scene.showRoom();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
try {
|
||||
super.close();
|
||||
} finally {
|
||||
this.roomSceneShown = false;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
try {
|
||||
super.dispose();
|
||||
} finally {
|
||||
this.roomSceneShown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PlatformRuntimeCleanupError extends Error {
|
||||
readonly errors: readonly unknown[];
|
||||
|
||||
@@ -221,17 +180,22 @@ export class PlatformRuntime {
|
||||
this.requireActiveStartup(generation);
|
||||
const gameSession = new SceneOrderedGameSessionHost(
|
||||
this.options.gameEntry,
|
||||
this.options.scene,
|
||||
this.options.scene,
|
||||
{...ROOM_LIFECYCLE_CONFIG,clock:this.clock,onFailure:failure=>{
|
||||
if(!this.isTerminal())this.showFatalOnce(failure);
|
||||
}},
|
||||
host=>{if(this.activeGameHostLease?.host===host)this.activeGameHostLease.invalidate();},
|
||||
);
|
||||
const send = Object.freeze({
|
||||
send: (envelope: OutboundEnvelope): void => {
|
||||
this.requireOpenWire().send(envelope);
|
||||
},
|
||||
});
|
||||
const commands = new PlatformCommands({
|
||||
runtimeConfig: config,
|
||||
store: this.store,
|
||||
send,
|
||||
const commands = new PlatformCommands({
|
||||
runtimeConfig: config,
|
||||
store: this.store,
|
||||
send,
|
||||
getRoomProjection:()=>gameSession.roomProjection?.getSnapshot()??null,
|
||||
});
|
||||
const session = new RuntimeSession({
|
||||
roomHistory:this.options.roomHistory,
|
||||
@@ -318,6 +282,26 @@ export class PlatformRuntime {
|
||||
this.requireCommands().prepare();
|
||||
}
|
||||
|
||||
sendRoomText(text:string,announcement=false):void {this.requireCommands().sendRoomText(text,announcement);}
|
||||
sendRoomPhrase(text:string):void {this.requireCommands().sendRoomPhrase(text);}
|
||||
sendRoomVoice(voiceurl:string,time:string|number):void {this.requireCommands().sendRoomVoice(voiceurl,time);}
|
||||
sendRoomExpression(id:number):void {this.requireCommands().sendRoomExpression(id);}
|
||||
breakRoom():void {this.requireCommands().breakRoom();}
|
||||
surrenderRoom(count:number):void {this.requireCommands().surrenderRoom(count);}
|
||||
changeRoom():void {this.requireCommands().changeRoom();}
|
||||
applyDissolution():void {this.requireCommands().applyDissolution();}
|
||||
agreeDissolution():void {this.requireCommands().agreeDissolution();}
|
||||
refuseDissolution():void {this.requireCommands().refuseDissolution();}
|
||||
acknowledgeDissolution():void {
|
||||
const result=this.state.dissolution;
|
||||
if(result?.kind!=='result')throw Error('dissolution acknowledgement requires a result');
|
||||
if(result.passed&&!result.freeNow){
|
||||
this.requireSession().handleDissolutionResult(result);
|
||||
if(this.state.room.kind==='outside'){this.options.scene.showLobby();return;}
|
||||
}
|
||||
if(this.state.dissolution===result)this.store.acknowledgeDissolution();
|
||||
}
|
||||
|
||||
getVipRooms(codes:readonly (string|number)[]):void {this.requireCommands().getVipRooms(codes);}
|
||||
getVipRankList(limit:number):void {this.requireCommands().getVipRankList(limit);}
|
||||
joinOwnedRoom(code:number,location:unknown):void {this.requireCommands().joinOwnedRoom(code,location);}
|
||||
@@ -425,16 +409,8 @@ export class PlatformRuntime {
|
||||
if (isLoginResponse(event.message)) {
|
||||
this.clearLoginGuard();
|
||||
}
|
||||
const stateBeforeDispatch = this.store.getState();
|
||||
const dispatchResult = this.requireRouter().dispatch(event.message);
|
||||
this.throwObserverDispatchError();
|
||||
if (
|
||||
dispatchResult === 'handled-platform'
|
||||
&& this.lifecycle === 'running'
|
||||
&& this.store.getState() !== stateBeforeDispatch
|
||||
) {
|
||||
this.updateSceneAfterMessage(event.message.route, event.message.rpc);
|
||||
}
|
||||
if(this.shouldQueueRoomPacket(event.message))this.gameSession!.queueRoomPacket(()=>this.dispatchMessage(event.message));
|
||||
else this.dispatchMessage(event.message);
|
||||
break;
|
||||
case 'slow':
|
||||
case 'reconnecting':
|
||||
@@ -442,10 +418,12 @@ export class PlatformRuntime {
|
||||
this.store.setConnectionPhase(event.type);
|
||||
this.throwObserverDispatchError();
|
||||
if (this.isTerminal()) return;
|
||||
this.gameSession?.cancelPendingMount();
|
||||
this.options.scene.showReconnect();
|
||||
break;
|
||||
case 'close':
|
||||
this.wireIsOpen = false;
|
||||
case 'close':
|
||||
this.wireIsOpen = false;
|
||||
this.gameSession?.cancelPendingMount();
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -455,9 +433,33 @@ export class PlatformRuntime {
|
||||
if (this.isStopped()) throw failure;
|
||||
this.showFatalOnce(failure);
|
||||
}
|
||||
}
|
||||
|
||||
private isStopped(): boolean {
|
||||
}
|
||||
|
||||
private shouldQueueRoomPacket(message:InboundMessage):boolean {
|
||||
// Preserve Router's authentication barrier at receipt time, before a later mount can finish.
|
||||
if(this.session?.loginPending||!this.gameSession?.isRoomScopeAvailable)return false;
|
||||
if(message.route===this.options.gameEntry.route)return true;
|
||||
if(message.route==='agent'&&message.rpc==='free_room')return true;
|
||||
if(message.route!=='room'||!Object.prototype.hasOwnProperty.call(FIRST_SLICE_RPC_ROUTE,message.rpc))return false;
|
||||
// Entry and connection control packets may invalidate the pending scope immediately.
|
||||
return !['player_login','create_room','self_join_room','connect_roomserver','connect_agentserver','kick_server','kick_offline'].includes(message.rpc);
|
||||
}
|
||||
|
||||
private dispatchMessage(message:InboundMessage):void {
|
||||
try {
|
||||
const stateBeforeDispatch=this.store.getState();
|
||||
const dispatchResult=this.requireRouter().dispatch(message);
|
||||
this.throwObserverDispatchError();
|
||||
if(dispatchResult==='handled-platform'&&this.lifecycle==='running'&&this.store.getState()!==stateBeforeDispatch){
|
||||
this.updateSceneAfterMessage(message.route,message.rpc);
|
||||
}
|
||||
} catch(error) {
|
||||
// Deferred packet processing keeps observer errors in the same failure chain.
|
||||
throw this.withObserverDispatchError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private isStopped(): boolean {
|
||||
return this.lifecycle === 'stopped';
|
||||
}
|
||||
|
||||
@@ -509,7 +511,9 @@ export class PlatformRuntime {
|
||||
commands: PlatformCommands,
|
||||
send: { send(envelope: OutboundEnvelope): void },
|
||||
): GameHostLease {
|
||||
const lease = createGameHostAdapter({
|
||||
const lease = createGameHostAdapter({
|
||||
feedback:this.options.feedback,
|
||||
numericTools:this.options.numericTools,
|
||||
gameEntry: this.options.gameEntry,
|
||||
store: this.store,
|
||||
commands,
|
||||
@@ -549,6 +553,7 @@ export class PlatformRuntime {
|
||||
return;
|
||||
}
|
||||
if (rpc === 'self_exit_room') this.options.scene.showLobby();
|
||||
if (rpc === 'free_room'&&this.state.room.kind==='outside')this.options.scene.showLobby();
|
||||
}
|
||||
|
||||
private stopForKick(data: unknown): void {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ServerDenial } from './runtime-session.ts';
|
||||
import type { ServerDenial } from './runtime-session.ts';
|
||||
import type {RoomScope} from './room/room-scope.ts';
|
||||
|
||||
/** Semantic scene intents; the headless runtime never imports Cocos. */
|
||||
export interface ScenePort {
|
||||
showLoading(): void;
|
||||
showLogin(): void;
|
||||
showLobby(): void;
|
||||
showRoom(): void;
|
||||
showRoom(scope:RoomScope): void|PromiseLike<void>;
|
||||
showReconnect(): void;
|
||||
showKicked(data: unknown): void;
|
||||
/** Required when the host supports the server's generic message UI. */
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import {requireArray,requireInteger,requireNumber,requireRecord,requireString} from '../../protocol/contracts/validation.ts';
|
||||
import type {ActiveDissolution,DissolutionParticipant,DissolutionResult,DissolutionState,InsideRoomState,PlatformState} from './platform-types.ts';
|
||||
|
||||
export type DissolutionRpc = 'self_apply_free_room'|'other_apply_free_room'|'self_agree_free_room'|'other_agree_free_room'|'self_refuse_free_room'|'other_refuse_free_room'|'free_room';
|
||||
|
||||
function inside(state:PlatformState):InsideRoomState {
|
||||
if(state.room.kind!=='inside')throw Error('dissolution requires an occupied room');
|
||||
return state.room;
|
||||
}
|
||||
|
||||
function participants(state:PlatformState,value:unknown):readonly DissolutionParticipant[]{
|
||||
const room=inside(state),values=requireArray(value,'agreefree.state');
|
||||
if(values.length!==room.seatPlayerIds.length)throw Error('agreefree.state length must match server room seats');
|
||||
const result:DissolutionParticipant[]=[];
|
||||
values.forEach((value,seat)=>{
|
||||
const playerid=room.seatPlayerIds[seat];
|
||||
if(playerid===null){if(value!==null)throw Error(`agreefree.state[${seat}] requires null for empty seat`);return;}
|
||||
const player=state.players.entities[playerid];
|
||||
if(!player)throw Error(`players.entities missing occupied seat ${seat}`);
|
||||
const vote=requireInteger(value,`agreefree.state[${seat}]`);
|
||||
if(vote!==-1&&vote!==0&&vote!==1&&vote!==2)throw Error('agreefree.state requires -1, 0, 1, or 2');
|
||||
result.push(Object.freeze({seat,playerid,nickname:player.nickname,state:vote}));
|
||||
});
|
||||
return Object.freeze(result);
|
||||
}
|
||||
|
||||
function initiator(players:readonly DissolutionParticipant[]):DissolutionParticipant {
|
||||
const matches=players.filter(player=>player.state===0);
|
||||
if(matches.length!==1)throw Error('agreefree.state requires exactly one initiator');
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
export function restoreDissolution(state:PlatformState,payload:unknown,now:number):ActiveDissolution {
|
||||
const room=inside(state),raw=requireRecord(payload,'agreefree');
|
||||
const players=participants(state,raw.state),applicant=initiator(players);
|
||||
const countdown=requireNumber(raw.countdown,'agreefree.countdown');
|
||||
if(countdown<0)throw Error('agreefree.countdown requires non-negative seconds');
|
||||
return Object.freeze({kind:'active',roomcode:room.roomcode,selfSeat:room.selfSeat,participants:players,initiator:applicant,deadline:now+countdown*1000});
|
||||
}
|
||||
|
||||
/** No quorum or timeout calculation is performed here. Every transition is a server packet. */
|
||||
export function reduceDissolution(state:PlatformState,rpc:DissolutionRpc,payload:unknown,now:number):DissolutionState {
|
||||
const room=inside(state),raw=requireRecord(payload,'$.data');
|
||||
if(rpc==='self_apply_free_room'||rpc==='other_apply_free_room'){
|
||||
const vote=restoreDissolution(state,raw.agreefree,now);
|
||||
const seat=rpc==='self_apply_free_room'?room.selfSeat:requireInteger(raw.seat,'$.data.seat');
|
||||
if(vote.initiator.seat!==seat)throw Error('apply seat conflicts with agreefree initiator');
|
||||
return vote;
|
||||
}
|
||||
if(rpc==='free_room'){
|
||||
// Legacy/actual server omits freeNow for ordinary voting, and emits 1 for immediate dissolution.
|
||||
if(raw.freeNow!==undefined&&raw.freeNow!==false&&raw.freeNow!==true&&raw.freeNow!==0&&raw.freeNow!==1)throw Error('freeNow requires boolean or protocol flag');
|
||||
const freeNow=raw.freeNow===true||raw.freeNow===1;
|
||||
const players=freeNow&&raw.seats===undefined
|
||||
? state.dissolution?.participants ?? Object.freeze([])
|
||||
: participants(state,raw.seats);
|
||||
const applicant=freeNow&&raw.seats===undefined?state.dissolution?.initiator??null:initiator(players);
|
||||
const result:DissolutionResult={kind:'result',roomcode:room.roomcode,selfSeat:room.selfSeat,participants:players,initiator:applicant,passed:true,refusedSeat:null,freeNow,
|
||||
...(raw.deskfree!==undefined&&raw.deskfree!==null?{deskfree:requireRecord(raw.deskfree,'$.data.deskfree')}:{}),
|
||||
...(raw.tips!==undefined?{tips:requireString(raw.tips,'$.data.tips')}:{}),
|
||||
...(raw.time!==undefined?{time:requireNumber(raw.time,'$.data.time')}:{})};
|
||||
return Object.freeze(result);
|
||||
}
|
||||
const vote=state.dissolution;
|
||||
if(!vote||vote.kind!=='active')throw Error(`${rpc} requires an active dissolution vote`);
|
||||
const seat=rpc.startsWith('self_')?room.selfSeat:requireInteger(raw.seat,'$.data.seat');
|
||||
const voter=vote.participants.find(player=>player.seat===seat);
|
||||
if(!voter)throw Error(`${rpc} requires a participating seat`);
|
||||
const refused=rpc==='self_refuse_free_room'||rpc==='other_refuse_free_room';
|
||||
const players=Object.freeze(vote.participants.map(player=>player.seat===seat?Object.freeze({...player,state:refused?2 as const:1 as const}):player));
|
||||
if(refused)return Object.freeze({kind:'result',roomcode:vote.roomcode,selfSeat:vote.selfSeat,participants:players,initiator:vote.initiator,passed:false,refusedSeat:seat,freeNow:false});
|
||||
return Object.freeze({...vote,participants:players});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "660abd9a-8986-48fb-94f6-e5718b9715b8",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
+127
-13
@@ -1,13 +1,16 @@
|
||||
import type {Roomtype} from '../../sdk/contracts/roomtype.ts';
|
||||
import type {RoomSocialMessage,RoomSocialState} from './platform-types.ts';
|
||||
import {reduceDissolution,restoreDissolution,type DissolutionRpc} from './dissolution.ts';
|
||||
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';
|
||||
import type { TaskData } from '../../core/types/task.ts';
|
||||
import type {LoginAccountIdentity} from '../../protocol/contracts/login-contract.ts';
|
||||
import {requireStringOrFiniteNumber} from '../../protocol/contracts/validation.ts';
|
||||
import {parseResponseRoomcode} from '../../protocol/contracts/room-contracts.ts';
|
||||
import {
|
||||
hasOwn,
|
||||
requireArray,
|
||||
requireArray,
|
||||
requireRoomtype,
|
||||
requireBoolean,
|
||||
requireInteger,
|
||||
@@ -154,7 +157,7 @@ function verifyRoomtypeSourceGraph(graph: readonly RoomtypeObjectSnapshot[]): vo
|
||||
}
|
||||
}
|
||||
|
||||
export function cloneAndFreezeRoomtype(value: Roomtype): Roomtype {
|
||||
export function cloneAndFreezeRoomtype(value: unknown): Roomtype {
|
||||
const copies = new WeakMap<object, object>();
|
||||
const active = new WeakSet<object>();
|
||||
const ownedGraph: object[] = [];
|
||||
@@ -207,6 +210,7 @@ export function cloneAndFreezeRoomtype(value: Roomtype): Roomtype {
|
||||
}
|
||||
arrayLength = requireInteger(lengthProperty.descriptor.value, `${path}.length`);
|
||||
if (arrayLength < 0) throw new RangeError(`${path}.length: expected non-negative integer`);
|
||||
if(properties.length!==arrayLength+1)throw new TypeError(path+': sparse array is not JSON data');
|
||||
target = new Array(arrayLength);
|
||||
} else {
|
||||
target = prototype === null ? Object.create(null) as Record<string, unknown> : {};
|
||||
@@ -291,7 +295,7 @@ function buildRoom(
|
||||
const selfSeat = requireInteger(room.seat, '$.room.seat');
|
||||
const roomtype = requireRoomtype(room.roomtype, '$.room.roomtype');
|
||||
const sourcePlayers = requireArray(room.players, '$.room.players');
|
||||
if (requireString(raw.roomcode, '$.raw.roomcode') !== roomcode) {
|
||||
if (parseResponseRoomcode(raw.roomcode, '$.raw.roomcode') !== roomcode) {
|
||||
throw new RangeError('$.room.roomcode: expected value parsed from raw roomcode');
|
||||
}
|
||||
if (requireInteger(raw.seat, '$.raw.seat') !== selfSeat) {
|
||||
@@ -366,6 +370,7 @@ function buildRoom(
|
||||
stage,
|
||||
needprepare,
|
||||
infinite,
|
||||
...roomMenuMetadata(raw,infinite),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -428,16 +433,88 @@ interface PlatformTransition {
|
||||
readonly listeners: readonly PlatformStateListener[];
|
||||
}
|
||||
|
||||
export class PlatformStore {
|
||||
export class PlatformStore {
|
||||
private nextSocialId=1;
|
||||
private state: PlatformState = INITIAL_STATE;
|
||||
private readonly listeners = new Set<PlatformStateListener>();
|
||||
private readonly transitions: PlatformTransition[] = [];
|
||||
private publishing = false;
|
||||
|
||||
/** Legacy addBroadcast discards marquee receipts outside the main room scene. */
|
||||
addRoomBroadcast(text:string):void {
|
||||
requireString(text,'$.data.msgcontent');
|
||||
if(this.state.room.kind!=='inside')return;
|
||||
const prior=this.state.roomBroadcasts;
|
||||
this.commit(Object.freeze({...this.state,roomBroadcasts:Object.freeze(prior===undefined?[text]:[...prior,text])}));
|
||||
}
|
||||
|
||||
applyRoomSocial(rpc:'send_text'|'send_voice'|'send_phiz',payload:unknown):void {
|
||||
const room=requireInsideRoom(this.state),raw=requireRecord(payload,'$.data');
|
||||
// Legacy missing type means ordinary player chat, normalized only at ingress.
|
||||
const type=raw.type===undefined?0:requireTaskNumber(raw.type,'$.data.type');
|
||||
const prior=this.state.roomSocial;
|
||||
const history=prior===undefined?Object.freeze([]):prior.history;
|
||||
const announcements=prior===undefined?Object.freeze([]):prior.announcements;
|
||||
if(rpc==='send_text'&&type===1){
|
||||
const text=requireString(raw.text,'$.data.text');
|
||||
this.commit(Object.freeze({...this.state,roomSocial:Object.freeze({history,announcements:Object.freeze([...announcements,text])})}));return;
|
||||
}
|
||||
if(type!==0&&type!==2&&!(rpc==='send_text'&&type===3))throw Error('Unsupported room social type');
|
||||
const seat=requireInteger(requireTaskNumber(raw.seat,'$.data.seat'),'$.data.seat');
|
||||
const playerid=requireOccupiedSeat(room,seat),player=this.state.players.entities[playerid];
|
||||
if(!player)throw Error('room social sender missing from player directory');
|
||||
const sender=Object.freeze({...player,seat});
|
||||
const info=type===2?(cloneAndFreezeRoomtype([requireRecord(raw.info,'$.data.info')]) as readonly Readonly<Record<string,unknown>>[])[0]:undefined;
|
||||
let message:Omit<RoomSocialMessage,'id'>;
|
||||
if(rpc==='send_voice'){
|
||||
const time=requireTaskNumber(raw.time,'$.data.time');if(time<=0)throw Error('voice time must be positive');
|
||||
message={kind:'voice',sender,voiceurl:requireString(raw.voiceurl,'$.data.voiceurl'),time,autoPlay:type===0};
|
||||
}else if(rpc==='send_phiz'||type===3){
|
||||
const index=requirePositiveInteger(requireTaskNumber(raw.text,'$.data.text'),'$.data.text');
|
||||
message={kind:rpc==='send_phiz'?'expression':'phrase',sender,index};
|
||||
}else message={kind:'text',sender,text:requireString(raw.text,'$.data.text')};
|
||||
const social:RoomSocialState=Object.freeze({announcements,history:Object.freeze([...history,Object.freeze({...message,id:this.nextSocialId++,...(info===undefined?{}:{info})})])});
|
||||
this.commit(Object.freeze({...this.state,roomSocial:social}));
|
||||
}
|
||||
|
||||
changeSeats(payload:unknown):void {
|
||||
const room=requireInsideRoom(this.state),raw=requireRecord(payload,'$.data');
|
||||
const seat1=requireInteger(raw.seat1,'$.data.seat1'),seat2=requireInteger(raw.seat2,'$.data.seat2');
|
||||
for(const seat of [seat1,seat2])if(seat<0||seat>=room.seatPlayerIds.length)throw Error('change_seat outside seat range');
|
||||
const ids=[...room.seatPlayerIds];[ids[seat1],ids[seat2]]=[ids[seat2],ids[seat1]];
|
||||
const swap=(seat:number)=>seat===seat1?seat2:seat===seat2?seat1:seat;
|
||||
this.commit(Object.freeze({...this.state,room:Object.freeze({...room,selfSeat:swap(room.selfSeat),seatPlayerIds:Object.freeze(ids),readySeats:Object.freeze(room.readySeats.map(swap).sort((a,b)=>a-b)),offlineSeats:Object.freeze(room.offlineSeats.map(swap).sort((a,b)=>a-b))})}));
|
||||
}
|
||||
|
||||
getState(): PlatformState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
applyDissolution(rpc:DissolutionRpc,payload:unknown,now=Date.now()):void {
|
||||
// server_room/rpc.js deletes data on self agree/refuse acknowledgements.
|
||||
// Only these two explicit empty-ACK contracts permit an absent payload.
|
||||
const raw=payload===undefined&&(rpc==='self_agree_free_room'||rpc==='self_refuse_free_room')
|
||||
? {} : requireRecord(payload,'$.data');
|
||||
let dissolution=reduceDissolution(this.state,rpc,raw,now);
|
||||
if(dissolution.kind==='result'&&dissolution.deskfree!==undefined){
|
||||
dissolution=Object.freeze({...dissolution,deskfree:(cloneAndFreezeRoomtype([dissolution.deskfree]) as readonly unknown[])[0] as Readonly<Record<string,unknown>>});
|
||||
}
|
||||
let players=this.state.players;
|
||||
if(rpc==='free_room'&&hasOwn(raw,'roomcard')){
|
||||
const roomcard=requireNumber(raw.roomcard,'$.data.roomcard');
|
||||
const playerid=players.selfPlayerId;
|
||||
if(playerid===null||!players.entities[playerid])throw Error('free_room requires authenticated player');
|
||||
players=Object.freeze({...players,entities:Object.freeze({...players.entities,[playerid]:Object.freeze({...players.entities[playerid],roomcard})})});
|
||||
}
|
||||
this.commit(Object.freeze({...this.state,players,dissolution,
|
||||
...(rpc==='free_room'?{app:Object.freeze({phase:'logged-in' as const})}:{})}));
|
||||
}
|
||||
|
||||
acknowledgeDissolution():void {
|
||||
if(this.state.dissolution?.kind!=='result')throw Error('dissolution acknowledgement requires a result');
|
||||
this.commit(Object.freeze({...this.state,dissolution:null}));
|
||||
}
|
||||
|
||||
applyTaskList(payload:unknown):void {
|
||||
const raw=requireRecord(payload,'$.data');
|
||||
const ids=new Set<string>();
|
||||
@@ -571,7 +648,7 @@ export class PlatformStore {
|
||||
this.transitions.push({
|
||||
state,
|
||||
previous,
|
||||
listeners: [...this.listeners],
|
||||
listeners: [...this.listeners],
|
||||
});
|
||||
if (this.publishing) return state;
|
||||
|
||||
@@ -644,11 +721,9 @@ export class PlatformStore {
|
||||
player,
|
||||
requireProtocolFlag(raw.isbattle, '$.login.raw.isbattle'),
|
||||
);
|
||||
return this.commit(Object.freeze({
|
||||
app: Object.freeze({ phase: 'logged-in' }),
|
||||
players: roomResult.players,
|
||||
room: roomResult.room,
|
||||
}));
|
||||
const next:PlatformState={app:Object.freeze({phase:'logged-in'}),players:roomResult.players,room:roomResult.room};
|
||||
const dissolution=raw.agreefree===undefined||raw.agreefree===null?undefined:restoreDissolution(next,raw.agreefree,Date.now());
|
||||
return this.commit(Object.freeze({...next,...(dissolution?{dissolution}:{})}));
|
||||
}
|
||||
|
||||
createRoom(parsed: ParsedCreateRoomResponse, seatCount: number): PlatformState {
|
||||
@@ -673,6 +748,8 @@ export class PlatformStore {
|
||||
seatPlayerIds: Object.freeze(seatPlayerIds), readySeats: Object.freeze([]),
|
||||
offlineSeats: Object.freeze([]), roomtype: cloneAndFreezeRoomtype(roomtype),
|
||||
stage: 0, needprepare: parsed.needprepare, infinite: parsed.infinite,
|
||||
...roomMenuMetadata(parsed.raw,parsed.infinite),
|
||||
status:1,canexit:1,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
@@ -719,7 +796,9 @@ export class PlatformStore {
|
||||
if (reconnectPresent && stage !== 1) {
|
||||
throw new RangeError('$.response.stage: reconnect room must be in battle stage');
|
||||
}
|
||||
const roomResult = buildRoom(input.room, raw, selfPlayer, stage);
|
||||
const builtRoom = buildRoom(input.room, raw, selfPlayer, stage);
|
||||
// self_join_room sets ChangeExit(1), then deskwar locks finite rooms.
|
||||
const roomResult = {...builtRoom,room:Object.freeze({...builtRoom.room,canexit:(builtRoom.room.infinite===1||!raw.deskwar?1:0) as 0|1})};
|
||||
return this.commit(Object.freeze({
|
||||
// A switch authenticates with connect_*server, then self_join_room instead of player_login.
|
||||
app: previous.app.phase === 'logged-in'
|
||||
@@ -742,6 +821,10 @@ export class PlatformStore {
|
||||
throw new Error('players.entities: missing authenticated player');
|
||||
}
|
||||
this.commit(Object.freeze({
|
||||
...previous,
|
||||
dissolution:null,
|
||||
roomSocial:undefined,
|
||||
roomBroadcasts:undefined,
|
||||
app: previous.app,
|
||||
players: Object.freeze({
|
||||
selfPlayerId,
|
||||
@@ -782,6 +865,7 @@ export class PlatformStore {
|
||||
: [...room.offlineSeats];
|
||||
|
||||
this.commit(Object.freeze({
|
||||
...previous,
|
||||
app: previous.app,
|
||||
players: Object.freeze({
|
||||
selfPlayerId: previous.players.selfPlayerId,
|
||||
@@ -793,10 +877,24 @@ export class PlatformStore {
|
||||
readySeats: Object.freeze(readySeats),
|
||||
offlineSeats: Object.freeze(offlineSeats),
|
||||
stage,
|
||||
...(event.raw.deskwar?{canexit:(room.infinite===1?1:0) as 0|1}:{}),
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
/** Desk.self_makewar / Desk.makewar mark seat zero ready before StartWar. */
|
||||
startRoom(sourceRpc:'self_makewar'|'other_makewar'):void {
|
||||
const previous=this.getState();const room=requireInsideRoom(previous);
|
||||
const ownerId=requireOccupiedSeat(room,0);const owner=previous.players.entities[ownerId];
|
||||
if(owner===undefined)throw new Error('Start requires seat zero player');
|
||||
const readySeats=room.readySeats.includes(0)?room.readySeats:Object.freeze([...room.readySeats,0]);
|
||||
const selfReady=sourceRpc==='self_makewar'||readySeats.includes(room.selfSeat);
|
||||
const canexit=room.infinite===1?1:(room.needprepare!==1||selfReady?0:room.canexit);
|
||||
this.commit(Object.freeze({...previous,
|
||||
players:Object.freeze({...previous.players,entities:Object.freeze({...previous.players.entities,[ownerId]:Object.freeze({...owner,isprepare:1})})}),
|
||||
room:Object.freeze({...room,stage:1,readySeats,canexit})}));
|
||||
}
|
||||
|
||||
playerReady(parsed: ParsedSeatPayload): void {
|
||||
const event = parseSeatAction(parsed);
|
||||
const previous = this.getState();
|
||||
@@ -816,12 +914,13 @@ export class PlatformStore {
|
||||
});
|
||||
const readySeats = Object.freeze([...room.readySeats, event.seat]);
|
||||
this.commit(Object.freeze({
|
||||
...previous,
|
||||
app: previous.app,
|
||||
players: Object.freeze({
|
||||
selfPlayerId: previous.players.selfPlayerId,
|
||||
entities,
|
||||
}),
|
||||
room: Object.freeze({ ...room, readySeats, stage }),
|
||||
room: Object.freeze({ ...room, readySeats, stage,...(event.raw.deskwar?{canexit:(room.infinite===1?1:0) as 0|1}:{}) }),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -843,6 +942,7 @@ export class PlatformStore {
|
||||
});
|
||||
const offlineSeats = Object.freeze([...room.offlineSeats, event.seat]);
|
||||
this.commit(Object.freeze({
|
||||
...previous,
|
||||
app: previous.app,
|
||||
players: Object.freeze({
|
||||
selfPlayerId: previous.players.selfPlayerId,
|
||||
@@ -877,6 +977,7 @@ export class PlatformStore {
|
||||
room.offlineSeats.filter((seat) => seat !== event.seat),
|
||||
);
|
||||
this.commit(Object.freeze({
|
||||
...previous,
|
||||
app: previous.app,
|
||||
players: Object.freeze({
|
||||
selfPlayerId: previous.players.selfPlayerId,
|
||||
@@ -905,6 +1006,7 @@ export class PlatformStore {
|
||||
const offlineSeats = room.offlineSeats.filter((seat) => seat !== event.seat);
|
||||
|
||||
this.commit(Object.freeze({
|
||||
...previous,
|
||||
app: previous.app,
|
||||
players: Object.freeze({
|
||||
selfPlayerId: previous.players.selfPlayerId,
|
||||
@@ -919,3 +1021,15 @@ export class PlatformStore {
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/** Missing optional wire metadata stays absent; legacy derivations live only here. */
|
||||
function roomMenuMetadata(raw:Readonly<Record<string,unknown>>,infinite:number){
|
||||
return {
|
||||
ownerNotice:raw.ownerNotice===undefined||raw.ownerNotice===null||raw.ownerNotice===''?null:requireString(raw.ownerNotice,'ownerNotice'),
|
||||
...(raw.roommode===undefined?{}:{roomMode:requireInteger(requireTaskNumber(raw.roommode,'roommode'),'roommode')}),
|
||||
...(raw.isowner===undefined?{}:{status:(requireProtocolFlag(raw.isowner,'isowner')===1?1:2) as 1|2}),
|
||||
...(infinite===1?{canexit:1 as const}:raw.isbet===undefined?{}:{canexit:(requireTaskNumber(raw.isbet,'isbet')===0?1:0) as 0|1}),
|
||||
shortcode:raw.shortcode===undefined||raw.shortcode===null?null:requireStringOrFiniteNumber(raw.shortcode,'shortcode'),
|
||||
matchId:raw.matchid===undefined||raw.matchid===null?null:requireStringOrFiniteNumber(raw.matchid,'matchid'),
|
||||
};
|
||||
}
|
||||
|
||||
+60
-1
@@ -40,7 +40,14 @@ export interface OutsideRoomState {
|
||||
readonly kind: 'outside';
|
||||
}
|
||||
|
||||
export interface InsideRoomState {
|
||||
export interface InsideRoomState {
|
||||
/** Desk.setOwnerNotice accepts nonempty source text; absent/empty means no notice. */
|
||||
readonly ownerNotice?:string|null;
|
||||
readonly roomMode?:number;
|
||||
readonly status?:1|2;
|
||||
readonly canexit?:0|1;
|
||||
readonly shortcode?:string|number|null;
|
||||
readonly matchId?:string|number|null;
|
||||
readonly kind: 'inside';
|
||||
readonly roomcode: string;
|
||||
readonly selfSeat: number;
|
||||
@@ -55,7 +62,59 @@ export interface InsideRoomState {
|
||||
|
||||
export type RoomState = OutsideRoomState | InsideRoomState;
|
||||
|
||||
/** Server agreefree/seats encoding: null is an empty seat, 0 is the initiator. */
|
||||
export interface DissolutionParticipant {
|
||||
readonly seat:number;
|
||||
readonly playerid:number;
|
||||
readonly nickname:string;
|
||||
readonly state:-1|0|1|2;
|
||||
}
|
||||
interface DissolutionBase {
|
||||
readonly roomcode:string;
|
||||
readonly selfSeat:number;
|
||||
readonly participants:readonly DissolutionParticipant[];
|
||||
}
|
||||
export interface ActiveDissolution extends DissolutionBase {
|
||||
readonly kind:'active';
|
||||
readonly initiator:DissolutionParticipant;
|
||||
/** Epoch milliseconds for display only; the server owns timeout/default votes. */
|
||||
readonly deadline:number;
|
||||
}
|
||||
export interface DissolutionResult extends DissolutionBase {
|
||||
readonly kind:'result';
|
||||
readonly initiator:DissolutionParticipant|null;
|
||||
readonly passed:boolean;
|
||||
readonly refusedSeat:number|null;
|
||||
readonly freeNow:boolean;
|
||||
readonly deskfree?:Readonly<Record<string,unknown>>;
|
||||
readonly tips?:string;
|
||||
readonly time?:number;
|
||||
}
|
||||
export type DissolutionState = ActiveDissolution|DissolutionResult;
|
||||
|
||||
export interface RoomSocialMessage {
|
||||
/** Client-local immutable identity; never transmitted to the server. */
|
||||
readonly id:number;
|
||||
readonly kind:'text'|'phrase'|'voice'|'expression';
|
||||
readonly sender:Readonly<PlatformPlayer & {seat:number}>;
|
||||
readonly text?:string;
|
||||
readonly index?:number;
|
||||
/** Defined for voice: ordinary messages auto-play; robot messages are history-only. */
|
||||
readonly autoPlay?:boolean;
|
||||
readonly voiceurl?:string;
|
||||
readonly time?:number;
|
||||
readonly info?:Readonly<Record<string,unknown>>;
|
||||
}
|
||||
export interface RoomSocialState {
|
||||
readonly history:readonly RoomSocialMessage[];
|
||||
readonly announcements:readonly string[];
|
||||
}
|
||||
export interface PlatformState {
|
||||
/** Ordered broadcast/msgtype=1 receipts; absent means no room broadcasts received. */
|
||||
readonly roomBroadcasts?:readonly string[];
|
||||
readonly roomSocial?:RoomSocialState;
|
||||
/** Absent before any vote; null after local result acknowledgement. */
|
||||
readonly dissolution?:DissolutionState|null;
|
||||
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. */
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import type {NumericToolsPort} from '../sdk/contracts/game-capabilities.ts';
|
||||
|
||||
type InputRequest=Parameters<NumericToolsPort['openNumberInput']>[0];
|
||||
type MultiplierRequest=Parameters<NumericToolsPort['showMultiplier']>[0];
|
||||
type PendingRequest={kind:'input';request:InputRequest}|{kind:'multiplier';request:MultiplierRequest};
|
||||
|
||||
/** Defers game attach/restore requests until the destination room Canvas is ready. */
|
||||
export class DeferredNumericToolsPort implements NumericToolsPort {
|
||||
private target:NumericToolsPort|null=null;
|
||||
private readonly pending=new Map<PendingRequest['kind'],PendingRequest>();
|
||||
|
||||
bind(target:NumericToolsPort):void {
|
||||
this.target=target;
|
||||
while(this.target===target&&this.pending.size>0){
|
||||
const next=this.pending.values().next().value as PendingRequest;
|
||||
this.pending.delete(next.kind);
|
||||
if(next.kind==='input')target.openNumberInput(next.request);
|
||||
else target.showMultiplier(next.request);
|
||||
}
|
||||
}
|
||||
/** A scene transition drops the old view, retaining requests queued for its replacement. */
|
||||
unbind():void {this.target=null;}
|
||||
openNumberInput(request:InputRequest):void {
|
||||
if(this.target!==null){this.target.openNumberInput(request);return;}
|
||||
this.enqueue({kind:'input',request});
|
||||
}
|
||||
closeNumberInput():void {
|
||||
const pending=this.pending.get('input');this.pending.delete('input');pending?.request.onCancel?.();this.target?.closeNumberInput();
|
||||
}
|
||||
showMultiplier(request:MultiplierRequest):void {
|
||||
if(this.target!==null){this.target.showMultiplier(request);return;}
|
||||
this.enqueue({kind:'multiplier',request});
|
||||
}
|
||||
hideMultiplier():void {
|
||||
const pending=this.pending.get('multiplier');this.pending.delete('multiplier');pending?.request.onCancel?.();this.target?.hideMultiplier();
|
||||
}
|
||||
/** Lease invalidation also releases queued callbacks and prevents new work reaching an old view. */
|
||||
close():void {
|
||||
const pending=[...this.pending.values()];this.pending.clear();
|
||||
const target=this.target;this.unbind();const errors:unknown[]=[];
|
||||
for(const entry of pending){try{entry.request.onCancel?.();}catch(error){errors.push(error);}}
|
||||
try{target?.close();}catch(error){errors.push(error);}
|
||||
if(errors.length===1)throw errors[0];
|
||||
if(errors.length>1)throw Object.assign(new Error('Numeric tools cleanup failed'),{errors:Object.freeze(errors)});
|
||||
}
|
||||
private enqueue(pending:PendingRequest):void {
|
||||
// Replacing a request moves it to the end, preserving which tool should be on top.
|
||||
const previous=this.pending.get(pending.kind);this.pending.delete(pending.kind);previous?.request.onCancel?.();this.pending.set(pending.kind,pending);
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "0c3a8ee5-c1b7-4694-a926-26fae90f5e55",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import type {PlatformGameSnapshot} from '../sdk/contracts/snapshots.ts';
|
||||
import type {GameRoomProjection} from '../sdk/contracts/game-room-projection.ts';
|
||||
|
||||
/** Keep authority and game facts separate; composition never mutates either owner. */
|
||||
export function composeGameRoomPresentation(platform:PlatformGameSnapshot,game:GameRoomProjection|null){
|
||||
return Object.freeze({platform,game});
|
||||
}
|
||||
|
||||
export {gameAllowsRoomAction} from '../sdk/room-action-policy.ts';
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "d30b033d-a7fc-4e44-8d16-b342d0b3e3af",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export interface KeyedRowPort<T,R>{key(item:T):number;create(item:T,index:number):R;update(row:R,item:T,index:number):void;remove(row:R):void;}
|
||||
/** Stable identity belongs to the source; this reconciler never derives keys from content. */
|
||||
export class KeyedRows<T,R>{
|
||||
private readonly rows=new Map<number,{item:T;row:R;index:number}>();
|
||||
constructor(private readonly port:KeyedRowPort<T,R>){}
|
||||
get(key:number):R|undefined{return this.rows.get(key)?.row;}
|
||||
render(items:readonly T[]):void{
|
||||
const keys=new Set<number>();
|
||||
for(const item of items){const key=this.port.key(item);if(keys.has(key))throw Error('Duplicate row ID '+key);keys.add(key);}
|
||||
for(const [key,value]of this.rows)if(!keys.has(key)){this.rows.delete(key);this.port.remove(value.row);}
|
||||
items.forEach((item,index)=>{const key=this.port.key(item),prior=this.rows.get(key);
|
||||
if(!prior)this.rows.set(key,{item,index,row:this.port.create(item,index)});
|
||||
else if(prior.item!==item||prior.index!==index){this.port.update(prior.row,item,index);prior.item=item;prior.index=index;}
|
||||
});
|
||||
}
|
||||
dispose():void{const errors:unknown[]=[];for(const value of this.rows.values())try{this.port.remove(value.row);}catch(error){errors.push(error);}this.rows.clear();if(errors.length)throw Object.assign(Error('Row cleanup failed'),{errors});}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "d565fcd4-07ea-4269-a7d3-6fae0c3f4266",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
export interface MultiplierOptions {
|
||||
readonly originalValues:readonly number[];
|
||||
readonly onConfirm:(values:number[])=>void;
|
||||
readonly storage:Pick<Storage,'getItem'|'setItem'>;
|
||||
readonly gameid:string|number;
|
||||
readonly agentid:string|number;
|
||||
}
|
||||
|
||||
/** Layer607 calculator owns the legacy saved preference's default/repair semantics. */
|
||||
export class MultiplierModel {
|
||||
private value:string;
|
||||
private readonly originalValues:readonly number[];
|
||||
private readonly key:string;
|
||||
constructor(private readonly options:MultiplierOptions){
|
||||
if(!Array.isArray(options.originalValues)||!options.originalValues.every(Number.isFinite))throw TypeError('Calculator requires finite original values');
|
||||
if(typeof options.onConfirm!=='function')throw TypeError('Calculator requires a result callback');
|
||||
this.originalValues=[...options.originalValues];
|
||||
this.key=`tsgame_multiple_${options.gameid}${options.agentid}`;
|
||||
const saved=options.storage.getItem(this.key);
|
||||
if(saved!==null&&/^\d+(?:\.\d*)?$/.test(saved)&&Number.isFinite(Number(saved))&&Number(saved)>0){
|
||||
this.value=String(Number(saved)).replace('.','b');
|
||||
}else{
|
||||
this.value='1';options.storage.setItem(this.key,'1');
|
||||
}
|
||||
}
|
||||
get text():string{return this.value;}
|
||||
get multiplier():number{return Number(this.value.replace('b','.'));}
|
||||
inputDigit(digit:number):string|null {
|
||||
if(!Number.isInteger(digit)||digit<0||digit>9)throw RangeError('Invalid multiplier digit');
|
||||
return this.append(String(digit));
|
||||
}
|
||||
inputDecimal():string|null{return this.append('b');}
|
||||
backspace():void{this.value=this.value.length>1?this.value.slice(0,-1):'0';}
|
||||
confirm():number[] {
|
||||
const multiplier=this.multiplier===0?1:this.multiplier;
|
||||
const result=this.originalValues.map(value=>decimalMultiply(value,multiplier));
|
||||
this.options.storage.setItem(this.key,String(multiplier));
|
||||
this.options.onConfirm(result);
|
||||
this.value=String(multiplier).replace('.','b');
|
||||
return result;
|
||||
}
|
||||
private append(input:string):string|null {
|
||||
const next=this.value==='0'&&input!=='b'?input:this.value+input;
|
||||
if(next.length>8)return '输入长度超过限制!';
|
||||
if(!/^\d+(?:b\d*)?$/.test(next))return '输入不正确!';
|
||||
this.value=next;return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Integer coefficient arithmetic retains Func.accMul's decimal precision, including exponent notation. */
|
||||
function decimalMultiply(left:number,right:number):number {
|
||||
const parts=(value:number):{coefficient:number;exponent:number}=>{
|
||||
const [mantissa,exponentText]=String(value).split('e');
|
||||
const decimals=mantissa.includes('.')?mantissa.length-mantissa.indexOf('.')-1:0;
|
||||
return {coefficient:Number(mantissa.replace('.','')),exponent:(exponentText===undefined?0:Number(exponentText))-decimals};
|
||||
};
|
||||
const a=parts(left),b=parts(right);
|
||||
const result=Number(`${a.coefficient*b.coefficient}e${a.exponent+b.exponent}`);
|
||||
if(!Number.isFinite(result))throw RangeError('Calculator result exceeds finite number range');
|
||||
return result;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "104f8058-384f-4221-bfa0-aff820f7ebe6",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export interface NumericInputOptions {
|
||||
readonly message:string;
|
||||
/** Legacy openInputPanel omitting begin0 selects numeric mode. */
|
||||
readonly begin0?:boolean;
|
||||
readonly onConfirm:(value:string|number)=>void;
|
||||
}
|
||||
|
||||
/** Layer602 input state. Confirmation intentionally leaves dismissal to its caller. */
|
||||
export class NumericInputModel {
|
||||
private value:string;
|
||||
constructor(private readonly options:NumericInputOptions){
|
||||
if(typeof options.message!=='string'||typeof options.onConfirm!=='function')throw TypeError('Invalid numeric input request');
|
||||
this.value=options.begin0?'':'0';
|
||||
}
|
||||
get message():string{return this.options.message;}
|
||||
get text():string{return this.value;}
|
||||
inputDigit(digit:number):string|null {
|
||||
if(!Number.isInteger(digit)||digit<0||digit>9)throw RangeError('Invalid input digit');
|
||||
if(this.value.length>=18)return '输入超出范围';
|
||||
this.value=!this.options.begin0&&this.value==='0'?String(digit):this.value+digit;
|
||||
return null;
|
||||
}
|
||||
backspace():void {
|
||||
this.value=this.value.length===1?(this.options.begin0?'':'0'):this.value.slice(0,-1);
|
||||
}
|
||||
paste(text:string):string|null {
|
||||
if(!text)return null;
|
||||
const digits=text.replace(/[^0-9]/g,'');
|
||||
if(digits.length>18)return '输入超出范围';
|
||||
this.value=digits;return null;
|
||||
}
|
||||
confirm():string|number {
|
||||
const result=this.options.begin0?this.value:Number(this.value);
|
||||
this.options.onConfirm(result);return result;
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "70c4afff-ffe3-4598-899f-f07c4d80e1b8",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type {FeedbackOwnerPort,OwnerHandle} from '../sdk/contracts/game-capabilities.ts';
|
||||
|
||||
/** One arbiter for the real loading/tip surface, including existing platform callers. */
|
||||
export class OwnedFeedback implements FeedbackOwnerPort {
|
||||
private disposed=false;
|
||||
private platformLoading=false;
|
||||
private readonly loadingOwners=new Set<object>();
|
||||
private tipOwner:object|null=null;
|
||||
constructor(private readonly render:{loading(show:boolean):void;tip(message:string,time:number):void;closeTip():void}){}
|
||||
setPlatformLoading(show:boolean):void{this.active();this.platformLoading=show;this.render.loading(show||this.loadingOwners.size>0);}
|
||||
beginLoading():OwnerHandle {
|
||||
this.active();const owner={};this.loadingOwners.add(owner);
|
||||
try{this.render.loading(true);}catch(error){this.loadingOwners.delete(owner);throw error;}
|
||||
return Object.freeze({close:()=>{if(!this.loadingOwners.delete(owner)||this.disposed)return;this.render.loading(this.platformLoading||this.loadingOwners.size>0);}});
|
||||
}
|
||||
showBusinessTip(message:string,time:number):OwnerHandle {
|
||||
if(!Number.isFinite(time))throw TypeError('Business tip requires finite display time');
|
||||
return this.showPlatformTip(message,time);
|
||||
}
|
||||
/** Legacy show_message uses Number(time): non-positive/NaN means no automatic dismissal. */
|
||||
showPlatformTip(message:string,time:number):OwnerHandle {
|
||||
this.active();if(typeof message!=='string'||message.length===0||typeof time!=='number')throw TypeError('Business tip requires text and display time');
|
||||
const owner={};this.tipOwner=owner;
|
||||
try{this.render.tip(message,time);}catch(error){if(this.tipOwner===owner)this.tipOwner=null;throw error;}
|
||||
return Object.freeze({close:()=>{if(this.disposed||this.tipOwner!==owner)return;this.tipOwner=null;this.render.closeTip();}});
|
||||
}
|
||||
dismissTip():void{if(this.disposed)return;this.tipOwner=null;this.render.closeTip();}
|
||||
clear():void {
|
||||
this.platformLoading=false;this.loadingOwners.clear();this.tipOwner=null;const errors:unknown[]=[];
|
||||
try{this.render.loading(false);}catch(error){errors.push(error);}
|
||||
try{this.render.closeTip();}catch(error){errors.push(error);}
|
||||
if(errors.length)throw Object.assign(new Error('Feedback cleanup failed'),{errors:Object.freeze(errors)});
|
||||
}
|
||||
dispose():void{if(this.disposed)return;this.disposed=true;this.clear();}
|
||||
private active():void{if(this.disposed)throw Error('Feedback owner is disposed');}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "4579cc6c-b016-4b55-902c-35887b3dce31",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import {callPlatformSetting,type PlatformNativeHost} from '../adapters/native/platform-native.ts';
|
||||
|
||||
export type PlatformSetting='bgm'|'sound'|'shake'|'voice';
|
||||
|
||||
/** Utl.Config + Logic setting actions. The source defines absent preferences as on. */
|
||||
export class PlatformSettings {
|
||||
private readonly keys:Record<PlatformSetting,string>;
|
||||
constructor(private readonly storage:Pick<Storage,'getItem'|'setItem'>,gameid:string|number,agentid:string|number,
|
||||
private readonly host:PlatformNativeHost){
|
||||
this.keys={bgm:`tsgame_bgm_${gameid}`,sound:`tsgame_sound_${gameid}${agentid}`,
|
||||
shake:`tsgame_shake_${gameid}${agentid}`,voice:`tsgame_voice_${gameid}${agentid}`};
|
||||
}
|
||||
read(setting:PlatformSetting):boolean {
|
||||
const key=this.keys[setting],raw=this.storage.getItem(key);
|
||||
if(raw===null){this.storage.setItem(key,'1');return true;}
|
||||
if(raw!=='0'&&raw!=='1')throw Error(`Invalid ${key}`);
|
||||
return raw==='1';
|
||||
}
|
||||
/** Logic initialization reapplies the saved voice preference to each native session. */
|
||||
synchronizeVoice():void {callPlatformSetting(this.host,'voicePlaying',this.read('voice')?1:0);}
|
||||
toggle(setting:PlatformSetting):boolean {
|
||||
const on=!this.read(setting);
|
||||
this.storage.setItem(this.keys[setting],on?'1':'0');
|
||||
if(setting==='voice')callPlatformSetting(this.host,'voicePlaying',on?1:0);
|
||||
if(setting==='sound')callPlatformSetting(this.host,'SwitchShake',on?1:0);
|
||||
if(setting==='shake'){
|
||||
// The lobby has no active game shake action (legacy shakeID == nil).
|
||||
if(on)callPlatformSetting(this.host,'SwitchShake',this.read('sound')?1:0);
|
||||
else callPlatformSetting(this.host,'stopshake');
|
||||
}
|
||||
return on;
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "26177acb-fa14-427a-abfe-716837c7bba0",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
export interface ResourceLease<T>{readonly value:Promise<T>;release():void;}
|
||||
interface Entry<T>{promise:Promise<T>;refs:number;value?:T;loaded:boolean;}
|
||||
export class ResourceLeaseCache<T>{
|
||||
private readonly entries=new Map<string,Entry<T>>();
|
||||
constructor(private readonly options:{idleCapacity:number;load:(key:string)=>Promise<T>;destroy:(value:T)=>void}){
|
||||
if(!Number.isSafeInteger(options.idleCapacity)||options.idleCapacity<0)throw Error('Invalid idle resource capacity');
|
||||
}
|
||||
get size():number{return this.entries.size;}
|
||||
get leases():number{let count=0;for(const e of this.entries.values())count+=e.refs;return count;}
|
||||
acquire(key:string):ResourceLease<T>{
|
||||
let entry=this.entries.get(key);
|
||||
if(!entry){
|
||||
const fresh:Entry<T>={refs:0,loaded:false,promise:undefined as unknown as Promise<T>};entry=fresh;this.entries.set(key,fresh);
|
||||
try{fresh.promise=this.options.load(key).then(value=>{fresh.loaded=true;fresh.value=value;this.trim();return value;},error=>{if(this.entries.get(key)===fresh)this.entries.delete(key);throw error;});}
|
||||
catch(error){this.entries.delete(key);throw error;}
|
||||
}
|
||||
entry.refs++;let released=false;const owned=entry;
|
||||
return {value:entry.promise,release:()=>{if(released)return;released=true;owned.refs--;this.trim();}};
|
||||
}
|
||||
clear():void{for(const [key,entry]of this.entries)if(entry.refs===0&&entry.loaded){this.entries.delete(key);this.options.destroy(entry.value as T);}}
|
||||
private trim():void{
|
||||
const idle=[...this.entries].filter(([,e])=>e.refs===0&&e.loaded);
|
||||
for(let i=0;i<idle.length-this.options.idleCapacity;i++){const [key,entry]=idle[i];this.entries.delete(key);this.options.destroy(entry.value as T);}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "01f41cce-b7ad-4441-b4fa-128811f7f70f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import {roomChatByteLength} from './room-chat-config.ts';
|
||||
|
||||
/** Game_Surface_3 02_Const.Broadcast: pixels per byte and pixels per millisecond. */
|
||||
export const ROOM_BROADCAST = Object.freeze({textWidth:18,speed:100});
|
||||
export interface RoomBroadcastFrame {
|
||||
readonly text:string;
|
||||
readonly textWidth:number;
|
||||
readonly offsetX:number;
|
||||
readonly durationSeconds:number;
|
||||
readonly elapsedSeconds:number;
|
||||
}
|
||||
|
||||
/** Layer402/390 marquee. Its owner admits messages only while in the room. */
|
||||
export class RoomBroadcastQueue {
|
||||
private readonly pending:string[]=[];
|
||||
private frame:RoomBroadcastFrame|null=null;
|
||||
constructor(private readonly clipWidth:number){
|
||||
if(!Number.isFinite(clipWidth)||clipWidth<=0)throw Error('Broadcast clip width must be positive');
|
||||
}
|
||||
get current():RoomBroadcastFrame|null{return this.frame;}
|
||||
enqueue(text:string):void {
|
||||
if(typeof text!=='string')throw Error('Broadcast text must be a string');
|
||||
this.pending.push(text);
|
||||
if(this.frame===null)this.startNext();
|
||||
}
|
||||
advance(deltaSeconds:number):void {
|
||||
if(!Number.isFinite(deltaSeconds)||deltaSeconds<0)throw Error('Broadcast elapsed time must be nonnegative');
|
||||
while(this.frame!==null){
|
||||
const remaining=this.frame.durationSeconds-this.frame.elapsedSeconds;
|
||||
if(deltaSeconds<remaining){
|
||||
const elapsedSeconds=this.frame.elapsedSeconds+deltaSeconds;
|
||||
this.frame=Object.freeze({...this.frame,elapsedSeconds,offsetX:this.clipWidth-elapsedSeconds*ROOM_BROADCAST.speed});
|
||||
return;
|
||||
}
|
||||
deltaSeconds-=remaining;
|
||||
this.startNext();
|
||||
}
|
||||
}
|
||||
clear():void {this.pending.length=0;this.frame=null;}
|
||||
private startNext():void {
|
||||
const text=this.pending.shift();
|
||||
if(text===undefined){this.frame=null;return;}
|
||||
const textWidth=roomChatByteLength(text)*ROOM_BROADCAST.textWidth;
|
||||
this.frame=Object.freeze({text,textWidth,offsetX:this.clipWidth,durationSeconds:(textWidth+this.clipWidth)/ROOM_BROADCAST.speed,elapsedSeconds:0});
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c22a2ea0-b39e-4b44-b771-70700994da31",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type {RemoteConfig} from '../config/remote-config.ts';
|
||||
import type {RoomChatConfig,RoomChatPhrase} from '../sdk/contracts/room-chat.ts';
|
||||
export type {RoomChatConfig,RoomChatPhrase} from '../sdk/contracts/room-chat.ts';
|
||||
/** Incoming send_text type 3 uses a 1-based phrase index; outgoing quick chat uses text. */
|
||||
export function resolveRoomChatPhrase(config:RoomChatConfig,index:number):RoomChatPhrase {
|
||||
if(!Number.isInteger(index)||index<1||config.phrases.length===0)throw Error(`Invalid room chat phrase index: ${index}`);
|
||||
return config.phrases[(index-1)%config.phrases.length];
|
||||
}
|
||||
|
||||
/** Game_Surface_3 Func.gblen, for visual text measurement only. */
|
||||
export function roomChatByteLength(text:string):number {
|
||||
let length=0;
|
||||
for(let index=0;index<text.length;index++)length+=text.charCodeAt(index)>127||text.charCodeAt(index)===94?2:1;
|
||||
return length;
|
||||
}
|
||||
export function validateRoomChatText(config:RoomChatConfig,text:string,announcement=false):void {
|
||||
if(typeof text!=='string')throw Error('Invalid chat text');
|
||||
if(text===''||/^[ ]+$/.test(text))throw Error('请输入聊天内容');
|
||||
const limit=announcement?config.announcementLimit:config.limitLength;
|
||||
if(text.length>limit)throw Error(`聊天内容不能超过${limit}个字符`);
|
||||
if(config.bannedWords.some(word=>word!==''&&text.indexOf(word)>=0))throw Error('内容含有敏感词汇请重新输入!');
|
||||
}
|
||||
/** Source defaults from 12_Logic.getConfig_Succ, not consumer fallbacks. */
|
||||
export function resolveRoomChatPolicy(remote:RemoteConfig):{noticeLimit:number;noticeCost:number} {
|
||||
const limit=remote.getValue('noticelimit'),cost=remote.getValue('noticecost');
|
||||
const noticeLimit=limit==null?-1:limit,noticeCost=cost?cost:0;
|
||||
if(typeof noticeLimit!=='number'||!Number.isFinite(noticeLimit))throw Error('Invalid noticelimit');
|
||||
if(typeof noticeCost!=='number'||!Number.isFinite(noticeCost))throw Error('Invalid noticecost');
|
||||
return {noticeLimit,noticeCost};
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "86b1833e-8e0d-490d-9431-aa99238ecb78",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {roomExitAction} from '../sdk/room-exit-action.ts';
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c70435c2-745c-48aa-9615-8bc7bfc6ee3d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type {InsideRoomState} from '../platform/stores/platform-types.ts';
|
||||
|
||||
export function roomHudCycles(room:Pick<InsideRoomState,'infinite'|'matchId'>):boolean {
|
||||
return room.infinite!==1&&!room.matchId;
|
||||
}
|
||||
export function roomHudTime(now:Date):{hours:string;minutes:string} {
|
||||
if(!Number.isFinite(now.getTime()))throw Error('Invalid room HUD clock date');
|
||||
return {hours:String(now.getHours()).padStart(2,'0'),minutes:String(now.getMinutes()).padStart(2,'0')};
|
||||
}
|
||||
/** 11_GameUI playTopInfoAnimation and timer 3, driven by the Cocos update clock. */
|
||||
export class RoomHudClock {
|
||||
showRoomCode=false;
|
||||
colonOpacity:255|100=255;
|
||||
private seconds=0;
|
||||
private cycleSeconds=0;
|
||||
reset():void{this.seconds=0;this.cycleSeconds=0;this.showRoomCode=false;this.colonOpacity=255;}
|
||||
update(dt:number,visible:boolean,cycles:boolean):void {
|
||||
if(!Number.isFinite(dt)||dt<0)throw Error('Invalid room HUD elapsed time');
|
||||
if(!visible)return;
|
||||
this.seconds+=dt;
|
||||
const ticks=Math.floor(this.seconds);this.seconds-=ticks;
|
||||
if(ticks%2!==0)this.colonOpacity=this.colonOpacity===255?100:255;
|
||||
if(!cycles){this.showRoomCode=false;this.cycleSeconds=0;return;}
|
||||
this.cycleSeconds+=dt;
|
||||
const cycleTicks=Math.floor(this.cycleSeconds/5);this.cycleSeconds-=cycleTicks*5;
|
||||
if(cycleTicks%2!==0)this.showRoomCode=!this.showRoomCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "15fa0855-2809-472f-b634-b8daada8ac90",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type {RuntimeConfig} from '../config/runtime-config.ts';
|
||||
import type {PlatformState} from '../platform/stores/platform-types.ts';
|
||||
import {selectSelfPlayer} from '../platform/stores/selectors.ts';
|
||||
import {resolveLobbyPolicy} from './lobby-model.ts';
|
||||
import {resolveSharePolicy} from './share-model.ts';
|
||||
import {buildRoomMenuModel,type RoomMenuContext} from './room-menu-model.ts';
|
||||
|
||||
import type {GameDefinition} from '../sdk/contracts/game-definition.ts';
|
||||
|
||||
/** Call when opening Layer413; missing protocol metadata must not stop the HUD. */
|
||||
export function resolveRoomMenuContext(state:PlatformState,config:RuntimeConfig,policy:GameDefinition['roomMenu'],
|
||||
host:{h5Version:number;versionState:number}):RoomMenuContext {
|
||||
if(state.room.kind!=='inside')throw Error('Room menu requires inside room');
|
||||
if(!policy||typeof policy.mainSceneButton!=='boolean'||typeof policy.vipInfinite!=='boolean')throw Error('Invalid game room menu policy');
|
||||
const room=state.room,self=selectSelfPlayer(state);
|
||||
if(!self)throw Error('Room menu requires self player');
|
||||
for(const key of ['status','canexit','roomMode','shortcode','matchId'] as const){
|
||||
if(room[key]===undefined)throw Error(`Missing authoritative room source: ${key}`);
|
||||
}
|
||||
const rawHideEscape=config.remoteConfig.getValue('hideEscape');
|
||||
// 04_Data.sysConfig.hideEscape starts at zero; only non-null remote overrides.
|
||||
const hideEscape=rawHideEscape==null?false:Boolean(rawHideEscape);
|
||||
const lobby=resolveLobbyPolicy(config.remoteConfig),share=resolveSharePolicy(config);
|
||||
const context:RoomMenuContext={
|
||||
status:room.status!,canexit:room.canexit!,roomMode:room.roomMode!,shortcode:room.shortcode!,matchId:room.matchId!,
|
||||
stage:room.stage,infinite:room.infinite,playercnt:room.seatPlayerIds.filter(id=>id!==null).length,
|
||||
...host,appdownload:share.url,hideEscape,...policy,
|
||||
roomCardName:lobby.roomCardName,starName:lobby.starName,bean:self.bean,
|
||||
};
|
||||
buildRoomMenuModel(context);
|
||||
return context;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "9e2e7f5f-9a36-42ff-9ba1-7e25e62f06f7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/** Pure projection of Game_Surface_3/11_GameUI.js openMainSceneBtnPanel.
|
||||
* All legacy source fields must be resolved before opening the panel. */
|
||||
export interface RoomMenuContext {
|
||||
readonly status:number; readonly canexit:number; readonly stage:number;
|
||||
readonly playercnt:number; readonly roomMode:number; readonly infinite:number;
|
||||
readonly shortcode:string|number|null; readonly matchId:string|number|null;
|
||||
readonly h5Version:number; readonly appdownload:string;
|
||||
readonly hideEscape:boolean; readonly mainSceneButton:boolean; readonly vipInfinite:boolean;
|
||||
readonly versionState:number; readonly roomCardName:string; readonly bean:number; readonly starName:string;
|
||||
}
|
||||
export type RoomMenuSpiritId=151|150|185|233|3014|513|656;
|
||||
export interface RoomMenuButton {readonly spiritId:RoomMenuSpiritId;readonly label:string;readonly y:number;}
|
||||
export type RoomMenuAction =
|
||||
| {readonly kind:'command';readonly command:'room.exit'|'room.apply-dissolution'|'room.change';readonly message?:never}
|
||||
| {readonly kind:'confirm';readonly checkType:1|2|3;readonly message:string;readonly command:'room.exit'|'room.break'|'room.escape';readonly count?:number}
|
||||
| {readonly kind:'input'|'tip';readonly message:string}
|
||||
| {readonly kind:'panel';readonly panel:'settings'|'help';readonly message?:never}
|
||||
| {readonly kind:'download';readonly url:string;readonly message?:never};
|
||||
|
||||
export function buildRoomMenuModel(context:RoomMenuContext):readonly RoomMenuButton[] {
|
||||
for(const field of ['status','canexit','stage','playercnt','roomMode','infinite','h5Version','versionState','bean'] as const){
|
||||
if(!Number.isFinite(context[field]))throw Error(`Missing or invalid room menu source: ${field}`);
|
||||
}
|
||||
for(const field of ['hideEscape','mainSceneButton','vipInfinite'] as const){
|
||||
if(typeof context[field]!=='boolean')throw Error(`Missing or invalid room menu source: ${field}`);
|
||||
}
|
||||
for(const field of ['appdownload','roomCardName','starName'] as const){
|
||||
if(typeof context[field]!=='string')throw Error(`Missing or invalid room menu source: ${field}`);
|
||||
}
|
||||
for(const field of ['shortcode','matchId'] as const){
|
||||
if(context[field]!==null&&typeof context[field]!=='string'&&typeof context[field]!=='number')throw Error(`Missing or invalid room menu source: ${field}`);
|
||||
}
|
||||
if(context.canexit!==0&&context.canexit!==1)throw Error('Invalid room menu source: canexit');
|
||||
// The original first hides dissolution for matchId/infinite, then overwrites
|
||||
// it in the stage branch; infinite is the final override. Preserve that order.
|
||||
const exit=context.infinite===1||context.stage!==1||context.canexit===1;
|
||||
const dissolve=context.infinite!==1&&(context.stage===1?context.canexit===0:context.status===1);
|
||||
const escape=context.status===1&&context.playercnt===2&&context.roomMode===1&&context.infinite===0&&context.stage===0&&!context.mainSceneButton&&!context.hideEscape;
|
||||
const buttons:readonly [RoomMenuSpiritId,string,boolean][]=[
|
||||
[151,'设置',true],[150,'帮助',true],[185,'解散',dissolve],[233,'退出',exit],
|
||||
[3014,'换房',context.infinite===1&&!context.shortcode&&!context.vipInfinite],
|
||||
[513,'逃跑',escape],[656,'下载',context.h5Version===1&&Boolean(context.appdownload)],
|
||||
];
|
||||
return buttons.filter(([, ,visible])=>visible).map(([spiritId,label],index)=>({spiritId,label,y:75+93*index}));
|
||||
}
|
||||
|
||||
export function roomMenuAction(spiritId:RoomMenuSpiritId,context:RoomMenuContext):RoomMenuAction {
|
||||
if(!buildRoomMenuModel(context).some(button=>button.spiritId===spiritId))throw Error(`Room menu button ${spiritId} is not visible`);
|
||||
switch(spiritId){
|
||||
case 151:return {kind:'panel',panel:'settings'};
|
||||
case 150:return {kind:'panel',panel:'help'};
|
||||
case 233:return context.infinite===0&&context.status===1
|
||||
?{kind:'confirm',checkType:2,message:'房间仍存在,是否退出?',command:'room.exit'}
|
||||
:{kind:'command',command:'room.exit'};
|
||||
case 185:return context.stage===0
|
||||
?{kind:'confirm',checkType:1,message:context.versionState===1?' 是否解散房间?':`解散房间不扣${context.roomCardName},是否解散?`,command:'room.break'}
|
||||
:{kind:'command',command:'room.apply-dissolution'};
|
||||
case 3014:return {kind:'command',command:'room.change'};
|
||||
case 513:return {kind:'input',message:'请输入投降扣除数量(底翻)'};
|
||||
case 656:return {kind:'download',url:context.appdownload};
|
||||
}
|
||||
}
|
||||
|
||||
export function roomEscapeConfirmation(context:RoomMenuContext,count:number):RoomMenuAction {
|
||||
if(!Number.isFinite(count)||count<0)throw Error('Invalid surrender count');
|
||||
if(count>context.bean)return {kind:'tip',message:`输入大于您拥有的${count}${context.starName}数:${context.bean}!`};
|
||||
return {kind:'confirm',checkType:3,message:`将扣除${count}${context.starName},确定逃跑?`,command:'room.escape',count};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "087800ef-8c3f-4a0f-b81f-630fcbd69a4c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/** Logic.getVisibleNote + Utl.Config.ownerNoteTime from Game_Surface_3.
|
||||
* Call only for a nonempty room owner notice. Manual opening bypasses this check. */
|
||||
export function shouldShowOwnerNotice(
|
||||
storage:Pick<Storage,'getItem'|'setItem'>,gameid:string,agentid:string,now:number,
|
||||
):boolean {
|
||||
if(!Number.isSafeInteger(now)||now<0)throw Error('Invalid owner notice now');
|
||||
if(typeof gameid!=='string'||gameid===''||typeof agentid!=='string'||agentid==='')throw Error('Invalid owner notice identity');
|
||||
const key=`tsgame_ownerNoteTime_${gameid}${agentid}`;
|
||||
const stored=storage.getItem(key);
|
||||
let expiry=0;
|
||||
// Missing/empty storage and zero have the source's explicit first-show meaning.
|
||||
if(stored!==null&&stored!==''){
|
||||
if(!/^\d+$/.test(stored))throw Error('Invalid owner notice expiry');
|
||||
expiry=Number(stored);
|
||||
if(!Number.isSafeInteger(expiry))throw Error('Invalid owner notice expiry');
|
||||
}
|
||||
if(expiry!==0&&now<expiry)return false;
|
||||
const next=now+24*60*60*1000;
|
||||
if(!Number.isSafeInteger(next))throw Error('Invalid owner notice next expiry');
|
||||
storage.setItem(key,String(next));
|
||||
return true;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8ff34190-44a3-4c8f-8638-8fca68bdc940",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
+18
-4
@@ -39,8 +39,19 @@ export function reconnectFrom(raw: JsonRecord): ReconnectSnapshot {
|
||||
return { present: true, value: raw.deskinfo };
|
||||
}
|
||||
|
||||
export function parseRoomEntryFields(raw: JsonRecord): ParsedRoomSnapshot {
|
||||
const roomcode = requireString(raw.roomcode, '$.data.roomcode');
|
||||
/** Wire room IDs may be numeric; internal identity remains a string. Never mutate raw packets. */
|
||||
export function parseResponseRoomcode(value: unknown, path = '$.data.roomcode'): string {
|
||||
if (typeof value === 'number') {
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw new TypeError(`${path}: expected string or non-negative safe integer`);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
return requireString(value, path);
|
||||
}
|
||||
|
||||
export function parseRoomEntryFields(raw: JsonRecord): ParsedRoomSnapshot {
|
||||
const roomcode = parseResponseRoomcode(raw.roomcode);
|
||||
const seat = requireInteger(raw.seat, '$.data.seat');
|
||||
const roomtype = requireRoomtype(raw.roomtype, '$.data.roomtype');
|
||||
const players = requireArray(raw.players, '$.data.players');
|
||||
@@ -229,9 +240,12 @@ export interface ParsedAgentserverPayload {
|
||||
|
||||
export function parseConnectAgentserverPayload(value: unknown): ParsedAgentserverPayload {
|
||||
const raw = requireRecord(value, '$.data');
|
||||
// Server room/rpc.js emits the switch operation under data.data; older flat packets
|
||||
// carry opt directly. Parse the explicit envelope variant once at this boundary.
|
||||
const operation=raw.data===undefined?raw:requireRecord(raw.data,'$.data.data');
|
||||
return {
|
||||
agentserver: requireString(raw.agentserver, '$.data.agentserver'),
|
||||
opt: requireString(raw.opt, '$.data.opt'),
|
||||
opt: requireString(operation.opt, raw.data===undefined?'$.data.opt':'$.data.data.opt'),
|
||||
raw,
|
||||
};
|
||||
}
|
||||
@@ -296,7 +310,7 @@ export function parseCreateRoomResponse(value: unknown): ParsedCreateRoomRespons
|
||||
};
|
||||
return {
|
||||
state,
|
||||
room: {roomcode: requireString(raw.roomcode, '$.data.roomcode'),
|
||||
room: {roomcode: parseResponseRoomcode(raw.roomcode),
|
||||
seat: requireInteger(raw.seat, '$.data.seat'),
|
||||
roomtype: requireRoomtype(raw.roomtype, '$.data.roomtype')},
|
||||
needprepare: flag('needprepare'), infinite: flag('infinite'), raw,
|
||||
|
||||
@@ -70,6 +70,27 @@ export function optionalField<T>(
|
||||
|
||||
/** 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);
|
||||
const active=new WeakSet<object>();
|
||||
const visit=(item:unknown,at:string):void=>{
|
||||
if(item===null||typeof item==='string'||typeof item==='boolean')return;
|
||||
if(typeof item==='number'&&Number.isFinite(item))return;
|
||||
if(typeof item!=='object'||item===null)return invalid(at,'finite JSON value',item);
|
||||
if(active.has(item))throw new TypeError(at+': cyclic JSON value');
|
||||
const array=Array.isArray(item);
|
||||
if(!array&&Object.getPrototypeOf(item)!==Object.prototype&&Object.getPrototypeOf(item)!==null)return invalid(at,'plain JSON object',item);
|
||||
active.add(item);
|
||||
if(array&&Object.keys(item).length!==item.length)throw new TypeError(at+': sparse or extended array');
|
||||
const keys=Reflect.ownKeys(item);
|
||||
for(const key of keys){
|
||||
if(array&&key==='length')continue;
|
||||
const descriptor=Object.getOwnPropertyDescriptor(item,key)!;
|
||||
if(typeof key!=='string'||!descriptor.enumerable||!('value' in descriptor))throw new TypeError(at+': expected JSON data property');
|
||||
if(array&&(!/^(0|[1-9][0-9]*)$/.test(key)||Number(key)>=item.length))throw new TypeError(at+': expected array index');
|
||||
visit(descriptor.value,at+'.'+key);
|
||||
}
|
||||
const finalKeys=Reflect.ownKeys(item);
|
||||
if(keys.length!==finalKeys.length||keys.some((key,index)=>key!==finalKeys[index]))throw new TypeError(at+': unstable JSON own keys');
|
||||
active.delete(item);
|
||||
};
|
||||
visit(value,path);return value as Roomtype;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function parseVipRooms(payload:unknown):readonly VipRoom[]{
|
||||
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 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 roomcodes=codes?Object.freeze({roomtype:rawType,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');
|
||||
|
||||
@@ -7,6 +7,13 @@ export function isLoginResponse(message:{route:string;rpc:string}):boolean{
|
||||
}
|
||||
|
||||
export type FirstSliceRpc =
|
||||
| 'self_makewar' | 'other_makewar'
|
||||
| 'broadcast'
|
||||
| 'beanroom_surrender'
|
||||
| 'send_text' | 'send_voice' | 'send_phiz' | 'self_break_room' | 'other_break_room' | 'change_room' | 'change_seat'
|
||||
| 'self_apply_free_room' | 'other_apply_free_room'
|
||||
| 'self_agree_free_room' | 'other_agree_free_room'
|
||||
| 'self_refuse_free_room' | 'other_refuse_free_room' | 'free_room'
|
||||
| 'getVipRankList'
|
||||
| 'get_treasurelist'
|
||||
| 'topup_card'
|
||||
@@ -37,6 +44,12 @@ export type FirstSliceRoute = 'platform' | 'agent' | 'room';
|
||||
|
||||
/** 第一纵向切片 rpc → route 的唯一权威映射。 */
|
||||
export const FIRST_SLICE_RPC_ROUTE = {
|
||||
self_makewar:'room',other_makewar:'room',
|
||||
broadcast:'agent',
|
||||
beanroom_surrender:'room',send_text:'room',send_voice:'room',send_phiz:'room',self_break_room:'room',other_break_room:'room',change_room:'room',change_seat:'room',
|
||||
self_apply_free_room:'room',other_apply_free_room:'room',
|
||||
self_agree_free_room:'room',other_agree_free_room:'room',
|
||||
self_refuse_free_room:'room',other_refuse_free_room:'room',free_room:'room',
|
||||
getVipRankList: 'agent',
|
||||
get_treasurelist: 'agent',
|
||||
topup_card: 'agent',
|
||||
|
||||
@@ -50,13 +50,69 @@ export class PlatformHandlers {
|
||||
}
|
||||
const rpc = message.rpc as FirstSliceRpc;
|
||||
const expectedRoute = routeForFirstSlice(rpc);
|
||||
if (message.route !== expectedRoute && !isLoginResponse(message) && !(rpc==='update_bean'&&message.route==='room')) {
|
||||
if (message.route !== expectedRoute && !isLoginResponse(message)
|
||||
&& !(rpc==='update_bean'&&message.route==='room')
|
||||
&& !(rpc==='broadcast'&&message.route==='room')
|
||||
&& !(rpc==='connect_agentserver'&&message.route==='room')
|
||||
&& !(rpc==='free_room'&&message.route==='agent')) {
|
||||
throw new Error(
|
||||
`Platform rpc route mismatch: rpc=${rpc} expected=${expectedRoute} received=${message.route}`,
|
||||
);
|
||||
}
|
||||
|
||||
switch (rpc) {
|
||||
case 'broadcast':{
|
||||
const raw=requireRecord(message.data,'$.data');
|
||||
// Protocol source explicitly defines omitted msgtype as immediate message (0).
|
||||
const type=raw.msgtype===undefined?0:raw.msgtype;
|
||||
const text=requireString(raw.msgcontent,'$.data.msgcontent');
|
||||
if(type===1)this.options.store.addRoomBroadcast(text);
|
||||
else if(type===0){
|
||||
// ShowiMessage remains until dismissed; reuse the platform immediate popup.
|
||||
if(!this.options.showMessage)throw Error('broadcast requires a message UI');
|
||||
this.options.showMessage(text,0,'show_message');
|
||||
}else throw Error('Unsupported broadcast msgtype');
|
||||
break;
|
||||
}
|
||||
case 'beanroom_surrender':{
|
||||
this.requireActiveGame(rpc);
|
||||
const raw=requireRecord(message.data,'$.data');
|
||||
const state=Number(raw.state);
|
||||
if(!Number.isInteger(state)||raw.state===null||raw.state==='')throw Error('beanroom_surrender requires server state');
|
||||
if(state===0){
|
||||
if(!this.options.showMessage)throw Error('beanroom_surrender settlement requires subgame support');
|
||||
this.options.showMessage('服务器已确认投降,当前子游戏尚未接入投降结算。',0,'show_message');
|
||||
}else if(Number(raw.showerror)===1){
|
||||
if(!this.options.showMessage)throw Error('beanroom_surrender error requires message UI');
|
||||
this.options.showMessage(requireString(raw.error,'$.data.error'),0,'show_message');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'send_text':case 'send_voice':case 'send_phiz':
|
||||
this.requireActiveGame(rpc);this.options.store.applyRoomSocial(rpc,message.data);break;
|
||||
case 'change_seat':this.requireActiveGame(rpc);this.options.store.changeSeats(message.data);break;
|
||||
case 'self_break_room':case 'other_break_room':
|
||||
this.options.session.handleBreakRoom(message.data);
|
||||
if(rpc==='other_break_room'){
|
||||
if(!this.options.showMessage)throw Error('other_break_room requires a message UI');
|
||||
this.options.showMessage('房主已解散房间!',3000,'show_message');
|
||||
}
|
||||
break;
|
||||
case 'self_apply_free_room':case 'other_apply_free_room':
|
||||
case 'self_agree_free_room':case 'other_agree_free_room':
|
||||
case 'self_refuse_free_room':case 'other_refuse_free_room':case 'free_room':{
|
||||
this.requireActiveGame(rpc);
|
||||
this.options.store.applyDissolution(rpc,message.data);
|
||||
const result=this.options.store.getState().dissolution;
|
||||
if(result?.kind==='result'&&result.freeNow){
|
||||
this.options.session.handleDissolutionResult(result);
|
||||
if(result.tips!==undefined){
|
||||
if(!this.options.showMessage)throw Error('freeNow tips require a message UI');
|
||||
this.options.showMessage(result.tips,Number(result.time),'show_message');
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
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.
|
||||
@@ -97,13 +153,19 @@ export class PlatformHandlers {
|
||||
this.options.session.handleCreateRoom(message.data);
|
||||
break;
|
||||
case 'self_join_room':
|
||||
this.options.session.handleSelfJoin(message.data);
|
||||
this.options.session.handleSelfJoin(message.data,message);
|
||||
break;
|
||||
case 'self_makewar':case 'other_makewar':{
|
||||
this.requireActiveGame(rpc);requireRecord(message.data,'$.data');
|
||||
this.options.store.startRoom(rpc);
|
||||
this.options.gameSession.publish({type:'room.started',sourceRpc:rpc,message});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 });
|
||||
if(parsed.raw.deskwar===1)this.options.gameSession.publish({type:'room.started',sourceRpc:rpc,message});
|
||||
break;
|
||||
}
|
||||
case 'self_exit_room': {
|
||||
@@ -116,6 +178,7 @@ export class PlatformHandlers {
|
||||
this.requireActiveGame(rpc);
|
||||
this.options.store.playerJoin(parsed);
|
||||
this.options.gameSession.publish({ type: 'room.player-joined', seat: parsed.seat });
|
||||
if(parsed.raw.deskwar===1){this.options.store.startRoom('other_makewar');this.options.gameSession.publish({type:'room.started',sourceRpc:rpc,message});}
|
||||
break;
|
||||
}
|
||||
case 'other_exit_room': {
|
||||
@@ -152,9 +215,11 @@ export class PlatformHandlers {
|
||||
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),
|
||||
);
|
||||
const raw=parsed.raw;
|
||||
const nested=raw.data===undefined?undefined:requireRecord(raw.data,'$.data.data');
|
||||
const switchConnection=()=>this.options.connection.switchServer(target,resend);
|
||||
if(raw.opt==='free_room'||nested?.opt==='free_room')this.options.session.handleDissolutionServerSwitch(switchConnection);
|
||||
else this.options.session.handleServerSwitch(switchConnection);
|
||||
break;
|
||||
}
|
||||
case 'show_message': {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type {GameSeatMapper} from './game-seat-mapper.ts';
|
||||
import type {PlatformGameSnapshot} from './snapshots.ts';
|
||||
import type {JsonValue} from './roomtype.ts';
|
||||
|
||||
export type InputCancelReason = 'user' | 'scope-ended' | 'replaced';
|
||||
export type InputResult<T> = {readonly kind:'confirmed';readonly value:T}
|
||||
| {readonly kind:'cancelled';readonly reason:InputCancelReason};
|
||||
export interface OwnerHandle {close():void;}
|
||||
export interface FeedbackOwnerPort {
|
||||
beginLoading():OwnerHandle;
|
||||
/** Player-facing business text only; diagnostics have no route through this port. */
|
||||
showBusinessTip(message:string,time:number):OwnerHandle;
|
||||
}
|
||||
|
||||
export interface RoomCapabilities {
|
||||
readonly room: {
|
||||
readonly seat:GameSeatMapper;
|
||||
getSnapshot():PlatformGameSnapshot;
|
||||
subscribe(listener:(snapshot:PlatformGameSnapshot)=>void):()=>void;
|
||||
prepare():void;
|
||||
requestExit():void;
|
||||
applyDissolution():void;
|
||||
};
|
||||
readonly messages:{send(rpc:string,data:JsonValue):void};
|
||||
readonly ui:{
|
||||
beginLoading():OwnerHandle;
|
||||
showBusinessTip(message:string,time:number):OwnerHandle;
|
||||
requestNumber(request:{readonly message:string}):Promise<InputResult<number>>;
|
||||
requestDigitText(request:{readonly message:string}):Promise<InputResult<string>>;
|
||||
requestMultiplier(request:{readonly values:readonly number[]}):Promise<InputResult<readonly number[]>>;
|
||||
cancelNumber():void;
|
||||
cancelMultiplier():void;
|
||||
};
|
||||
readonly scope:{
|
||||
readonly active:boolean;
|
||||
/** Register cleanup; returned function unregisters it without executing it. */
|
||||
defer(cleanup:()=>void):()=>void;
|
||||
};
|
||||
// Audio is absent until a real room-owned audio adapter is installed.
|
||||
}
|
||||
|
||||
/** Presentation-only bridge. Cancellation callbacks are optional for legacy callers. */
|
||||
export interface NumericToolsPort {
|
||||
openNumberInput(request:{readonly message:string;readonly begin0?:boolean;readonly onConfirm:(value:number|string)=>void;readonly onCancel?:()=>void}):void;
|
||||
closeNumberInput():void;
|
||||
showMultiplier(request:{readonly values:readonly number[];readonly onConfirm:(values:readonly number[])=>void;readonly onCancel?:()=>void}):void;
|
||||
hideMultiplier():void;
|
||||
close():void;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "59d9a08e-2863-4aa6-93fa-75eae1905267",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type {GameEntry} from './game-entry.ts';
|
||||
import type {GameRoomProjection,GameRoomProjectionSource} from './game-room-projection.ts';
|
||||
import type {PlatformGameSnapshot} from './snapshots.ts';
|
||||
import type {RoomChatConfig} from './room-chat.ts';
|
||||
|
||||
export interface RoomPresentationActions {prepare():void;exit():void;}
|
||||
export interface RoomPresentation {
|
||||
render(snapshot:PlatformGameSnapshot,actions:RoomPresentationActions,projection?:GameRoomProjection|null):void;
|
||||
close():void;
|
||||
}
|
||||
export interface GameBinding {
|
||||
readonly entry:GameEntry;
|
||||
mount(view:RoomPresentation):void;
|
||||
readonly restoredSnapshot:unknown;
|
||||
readonly roomProjection:GameRoomProjectionSource|null;
|
||||
}
|
||||
/** Application-owned registration. The platform consumes the selected definition only. */
|
||||
export interface GameDefinition {
|
||||
readonly config:Readonly<{versionResource:string;assetRoot:string}>;
|
||||
readonly roomMenu:Readonly<{mainSceneButton:boolean;vipInfinite:boolean}>;
|
||||
readonly key:string;
|
||||
readonly route:string;
|
||||
readonly createGame:(gameId:string|number)=>GameBinding;
|
||||
readonly chat:RoomChatConfig;
|
||||
readonly createPageClass:string;
|
||||
readonly roomViewClass:string;
|
||||
readonly resources:{readonly createRoom:string;readonly room:string;readonly roomScene:string};
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "9c55d95a-dd90-4464-a021-65cb90c08155",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,15 +1,29 @@
|
||||
import type { GameSeatMapper } from './game-seat-mapper.ts';
|
||||
import type { PlatformGameSnapshot } from './snapshots.ts';
|
||||
import type {RoomCapabilities} from './game-capabilities.ts';
|
||||
|
||||
export type GameHostCommand =
|
||||
| {readonly type:'ui.open-number-input';readonly message:string;readonly begin0?:boolean;readonly onConfirm:(value:number|string)=>void}
|
||||
| {readonly type:'ui.close-number-input'}
|
||||
| {readonly type:'ui.show-multiplier';readonly values:readonly number[];readonly onConfirm:(values:readonly number[])=>void}
|
||||
| {readonly type:'ui.hide-multiplier'}
|
||||
| { readonly type:'room.apply-dissolution' }
|
||||
| { readonly type: 'room.prepare' }
|
||||
| { readonly type: 'room.exit' };
|
||||
|
||||
/** The only platform capability surface available to a game module. */
|
||||
export interface GameHost {
|
||||
/** Optional only for legacy test/host adapters; production hosts always provide it. */
|
||||
readonly capabilities?:RoomCapabilities;
|
||||
readonly seat: GameSeatMapper;
|
||||
getSnapshot(): PlatformGameSnapshot;
|
||||
subscribe(listener: (snapshot: PlatformGameSnapshot) => void): () => void;
|
||||
sendGameMessage(rpc: string, data: unknown): void;
|
||||
execute(command: GameHostCommand): void;
|
||||
}
|
||||
|
||||
/** Explicit migration boundary; absence is an integration error, never a fallback. */
|
||||
export function requireRoomCapabilities(host:GameHost):RoomCapabilities {
|
||||
if(!host.capabilities)throw new Error('Game host must provide room capabilities');
|
||||
return host.capabilities;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { GameHost } from './game-host.ts';
|
||||
import type {GameRoomProjectionSource} from './game-room-projection.ts';
|
||||
import type { GameServerMessage, PlatformToGameEvent } from './platform-events.ts';
|
||||
|
||||
/** Per-room game implementation with an explicit lifecycle. */
|
||||
export interface GameModule {
|
||||
/** Absent/null explicitly means this game has no public gameplay projection. */
|
||||
readonly roomProjection?:GameRoomProjectionSource|null;
|
||||
attach(host: GameHost): void;
|
||||
handlePlatformEvent(event: PlatformToGameEvent): void;
|
||||
handleGameMessage(message: GameServerMessage): void;
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/** Owned by the game model. These facts never overwrite platform readiness or assets. */
|
||||
export interface GameRoomFacts {
|
||||
readonly roundPhase:string;
|
||||
readonly selfParticipation:'playing'|'waiting'|'spectating';
|
||||
readonly scoresBySeat:Readonly<Record<number,number>>;
|
||||
/** A game may only add restrictions, never enable a platform-forbidden command. */
|
||||
readonly restrictions:Readonly<Partial<Record<'prepare'|'start'|'exit',false>>>;
|
||||
}
|
||||
export interface GameRoomProjection extends GameRoomFacts {readonly revision:number;}
|
||||
export interface GameRoomProjectionSource {
|
||||
getSnapshot():GameRoomProjection;
|
||||
subscribe(listener:(projection:GameRoomProjection)=>void):()=>void;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "6a623110-c503-4e7e-9b26-692ec40256b7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
export * from './game-entry.ts';
|
||||
export * from './create-room-page.ts';
|
||||
export * from './game-host.ts';
|
||||
export * from './game-host.ts';
|
||||
export * from './game-capabilities.ts';
|
||||
export * from './game-room-projection.ts';
|
||||
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';
|
||||
export type {JsonValue,Roomtype} from './roomtype.ts';
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
export type RoomEntryInput =
|
||||
| {readonly kind:'created';readonly sourceRpc:'create_room';readonly raw:Readonly<Record<string,unknown>>}
|
||||
| {readonly kind:'login-waiting';readonly sourceRpc:'player_login';readonly raw:Readonly<Record<string,unknown>>}
|
||||
| {readonly kind:'login-restored';readonly sourceRpc:'player_login';readonly raw:Readonly<Record<string,unknown>>;readonly deskinfo:unknown}
|
||||
| {readonly kind:'joined-waiting'|'joined-started';readonly sourceRpc:'self_join_room';readonly raw:Readonly<Record<string,unknown>>}
|
||||
| {readonly kind:'joined-snapshot';readonly sourceRpc:'self_join_room';readonly raw:Readonly<Record<string,unknown>>;readonly deskinfo:unknown};
|
||||
export interface RoomStartMessage {readonly route:string;readonly rpc:string;readonly data:unknown;}
|
||||
import type {Roomtype} from './roomtype.ts';
|
||||
/** Platform lifecycle events delivered to the current game module. */
|
||||
export type PlatformToGameEvent =
|
||||
| { readonly type: 'room.entered'; readonly roomtype: Roomtype }
|
||||
/** Explicit settlement integration point. The module must interpret deskfree;
|
||||
* platform preserves the active session and does not invent a result screen. */
|
||||
| {readonly type:'room.dissolution-confirmed';readonly freeNow:boolean;readonly deskfree?:Readonly<Record<string,unknown>>;readonly tips?:string;readonly time?:number}
|
||||
| { readonly type: 'room.entered'; readonly roomtype: Roomtype;readonly entry?:RoomEntryInput }
|
||||
| {readonly type:'room.started';readonly sourceRpc:'self_join_room'|'other_join_room'|'player_prepare'|'self_makewar'|'other_makewar';readonly message:RoomStartMessage}
|
||||
| { 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,10 @@
|
||||
export interface RoomChatPhrase {readonly text:string;readonly audio:string;}
|
||||
export interface RoomChatConfig {
|
||||
readonly phrases:readonly RoomChatPhrase[];
|
||||
/** Legacy maxLength counts JavaScript UTF-16 code units. */
|
||||
readonly limitLength:number;
|
||||
readonly showChatMs:number;
|
||||
readonly announcementLimit:number;
|
||||
readonly voiceHistoryTag:number;
|
||||
readonly bannedWords:readonly string[];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "c8f3ca02-72aa-44e5-a249-c9d88fe9b5cd",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
/** Opaque subgame wire configuration. The game defines its contents. */
|
||||
export type Roomtype = string | readonly unknown[];
|
||||
/** Opaque game-owned JSON transport value. */
|
||||
export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {readonly [key:string]:JsonValue};
|
||||
export type Roomtype = JsonValue;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type {GameRoomFacts,GameRoomProjection,GameRoomProjectionSource} from './contracts/game-room-projection.ts';
|
||||
|
||||
function snapshot(facts:GameRoomFacts,revision:number):GameRoomProjection {
|
||||
if(typeof facts.roundPhase!=='string'||facts.roundPhase.length===0)throw TypeError('Game round phase must be explicit');
|
||||
if(!['playing','waiting','spectating'].includes(facts.selfParticipation))throw TypeError('Invalid game participation');
|
||||
const scores:Record<number,number>={};
|
||||
for(const [seat,score] of Object.entries(facts.scoresBySeat)){
|
||||
if(!/^(0|[1-9][0-9]*)$/.test(seat)||!Number.isSafeInteger(Number(seat))||!Number.isFinite(score))throw TypeError('Game score requires a server seat and finite score');
|
||||
scores[Number(seat)]=score;
|
||||
}
|
||||
const restrictions:Partial<Record<'prepare'|'start'|'exit',false>>={};
|
||||
for(const [action,value] of Object.entries(facts.restrictions)){
|
||||
if(!['prepare','start','exit'].includes(action)||value!==false)throw TypeError('Game restrictions may only disable known commands');
|
||||
restrictions[action as keyof typeof restrictions]=false;
|
||||
}
|
||||
return Object.freeze({revision,roundPhase:facts.roundPhase,selfParticipation:facts.selfParticipation,
|
||||
scoresBySeat:Object.freeze(scores),restrictions:Object.freeze(restrictions)});
|
||||
}
|
||||
|
||||
/** The owner keeps commit/dispose; consumers receive only source. Each commit is a complete version. */
|
||||
export function createGameRoomProjection(initial:GameRoomFacts):{
|
||||
readonly source:GameRoomProjectionSource;commit(facts:GameRoomFacts):void;dispose():void;
|
||||
}{
|
||||
let current=snapshot(initial,0);let disposed=false;let notifying=false;
|
||||
const listeners=new Set<(projection:GameRoomProjection)=>void>();
|
||||
const queue:GameRoomProjection[]=[];
|
||||
const active=()=>{if(disposed)throw Error('Game projection is disposed');};
|
||||
const source:GameRoomProjectionSource=Object.freeze({getSnapshot:()=>{active();return current;},subscribe:(listener:(projection:GameRoomProjection)=>void)=>{
|
||||
active();if(typeof listener!=='function')throw TypeError('Projection listener must be a function');listeners.add(listener);
|
||||
try{listener(current);}catch(error){listeners.delete(listener);throw error;}
|
||||
return ()=>{listeners.delete(listener);};
|
||||
}});
|
||||
return Object.freeze({source,commit:(facts:GameRoomFacts)=>{
|
||||
active();const next=snapshot(facts,current.revision+1);current=next;queue.push(next);
|
||||
if(notifying)return;notifying=true;const errors:unknown[]=[];
|
||||
try{while(queue.length&&!disposed){const version=queue.shift()!;for(const listener of [...listeners]){
|
||||
if(disposed)break;if(!listeners.has(listener))continue;
|
||||
try{listener(version);}catch(error){errors.push(error);}
|
||||
}}}finally{notifying=false;}
|
||||
if(errors.length)throw Object.assign(new Error('Game projection notification failed'),{errors:Object.freeze(errors)});
|
||||
},dispose:()=>{disposed=true;queue.length=0;listeners.clear();}});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5d4c6bdf-207c-4da3-9f77-6c80aa78dd76",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type {GameRoomProjection} from './contracts/game-room-projection.ts';
|
||||
|
||||
/** Missing game projection means no additional restrictions, never extra permission. */
|
||||
export function gameAllowsRoomAction(platformAllowed:boolean,game:GameRoomProjection|null,action:'prepare'|'start'|'exit'):boolean {
|
||||
return platformAllowed&&(game===null||game.restrictions[action]!==false);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "378da623-0982-4ab4-97ef-6ebbac2a1d06",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type {PlatformGameSnapshot} from './contracts/snapshots.ts';
|
||||
/** GameUI's in-room exit/application action; unlimited rooms retain ordinary exit. */
|
||||
export function roomExitAction(room:PlatformGameSnapshot['room']):{command:'room.exit'|'room.apply-dissolution';label:string}{
|
||||
return room.stage===1&&room.infinite!==1?{command:'room.apply-dissolution',label:'申请解散'}:{command:'room.exit',label:'退出房间'};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "2fb0e986-b14a-481d-baff-e88b670ff0c0",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export function isGameSessionOpenFault(value: unknown): value is GameSessionOpen
|
||||
return extractGameSessionOpenFaultErrors(value) !== null;
|
||||
}
|
||||
|
||||
/** Validates the compile-time game identity before it reaches the runtime. */
|
||||
/** Validates declarations only; module contracts are checked on the real instance in open(). */
|
||||
export function assertGameEntry(entry: GameEntry): void {
|
||||
assertNonEmptyText(entry.key, 'key');
|
||||
assertGameId(entry.gameId);
|
||||
@@ -88,12 +88,6 @@ export function assertGameEntry(entry: GameEntry): void {
|
||||
if (typeof entry.createModule !== 'function') {
|
||||
throw new Error('GameEntry createModule must be a function');
|
||||
}
|
||||
const module = entry.createModule();
|
||||
try {
|
||||
assertGameModule(module);
|
||||
} finally {
|
||||
disposeModuleIfPresent(module);
|
||||
}
|
||||
}
|
||||
|
||||
/** The single GameEntry route admission check shared by runtime adapters. */
|
||||
@@ -107,6 +101,10 @@ export function assertGameRoute(route: unknown): asserts route is string {
|
||||
/** Owns one isolated game module for one room at a time. */
|
||||
export class GameSessionHost {
|
||||
private module: GameModule | undefined;
|
||||
/** Null explicitly means the current game does not provide public gameplay facts. */
|
||||
get roomProjection():import('../contracts/game-room-projection.ts').GameRoomProjectionSource|null {
|
||||
return this.module?.roomProjection??null;
|
||||
}
|
||||
private currentState: GameSessionState = 'idle';
|
||||
|
||||
constructor(readonly entry: GameEntry) {
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
"height": 86,
|
||||
"rawWidth": 137,
|
||||
"rawHeight": 86,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
"borderLeft": 0,
|
||||
"borderRight": 0,
|
||||
"borderTop": 19,
|
||||
"borderBottom": 17,
|
||||
"borderLeft": 12,
|
||||
"borderRight": 17,
|
||||
"packable": true,
|
||||
"pixelsToUnit": 100,
|
||||
"pivotX": 0.5,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.4 MiB |
@@ -45,9 +45,9 @@
|
||||
"offsetY": 0,
|
||||
"trimX": 0,
|
||||
"trimY": 0,
|
||||
"width": 1280,
|
||||
"width": 1600,
|
||||
"height": 720,
|
||||
"rawWidth": 1280,
|
||||
"rawWidth": 1600,
|
||||
"rawHeight": 720,
|
||||
"borderTop": 0,
|
||||
"borderBottom": 0,
|
||||
@@ -60,16 +60,16 @@
|
||||
"meshType": 0,
|
||||
"vertices": {
|
||||
"rawPosition": [
|
||||
-640,
|
||||
-800,
|
||||
-360,
|
||||
0,
|
||||
640,
|
||||
800,
|
||||
-360,
|
||||
0,
|
||||
-640,
|
||||
-800,
|
||||
360,
|
||||
0,
|
||||
640,
|
||||
800,
|
||||
360,
|
||||
0
|
||||
],
|
||||
@@ -84,11 +84,11 @@
|
||||
"uv": [
|
||||
0,
|
||||
720,
|
||||
1280,
|
||||
1600,
|
||||
720,
|
||||
0,
|
||||
0,
|
||||
1280,
|
||||
1600,
|
||||
0
|
||||
],
|
||||
"nuv": [
|
||||
@@ -102,12 +102,12 @@
|
||||
1
|
||||
],
|
||||
"minPos": [
|
||||
-640,
|
||||
-800,
|
||||
-360,
|
||||
0
|
||||
],
|
||||
"maxPos": [
|
||||
640,
|
||||
800,
|
||||
360,
|
||||
0
|
||||
]
|
||||
|
||||
+1003
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -2,12 +2,12 @@
|
||||
"ver": "1.1.50",
|
||||
"importer": "prefab",
|
||||
"imported": true,
|
||||
"uuid": "584dbb85-4acc-4bea-9d9b-e0cbeb8741da",
|
||||
"uuid": "988efea2-ee6f-4555-93aa-7696aed38a20",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"syncNodeName": "Layer27_CreateRoom"
|
||||
"syncNodeName": "ChatHistoryItem"
|
||||
}
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
[
|
||||
{
|
||||
"__type__": "cc.Prefab",
|
||||
"_name": "ChatPhraseItem",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_native": "",
|
||||
"data": {
|
||||
"__id__": 1
|
||||
},
|
||||
"optimizationPolicy": 0,
|
||||
"persistent": false
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "ChatPhraseItem",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": null,
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 2
|
||||
},
|
||||
{
|
||||
"__id__": 8
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 14
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 16
|
||||
},
|
||||
"_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": "Background",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 3
|
||||
},
|
||||
{
|
||||
"__id__": 5
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 7
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 250,
|
||||
"y": -30,
|
||||
"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__": 2
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 4
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 500,
|
||||
"height": 60
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "TNB63XlRCaHQZYwS1X3s3w"
|
||||
},
|
||||
{
|
||||
"__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__": "8c4a282d-467c-4e53-be42-d65cec714689@f9941",
|
||||
"__expectedType__": "cc.SpriteFrame"
|
||||
},
|
||||
"_type": 1,
|
||||
"_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": "UoBDtXkjih9BPjlQWc/sIQ"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "cxdsfQ0D2ZvHkhRarFW1TA",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Text",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 9
|
||||
},
|
||||
{
|
||||
"__id__": 11
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 13
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 250,
|
||||
"y": -30,
|
||||
"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__": 8
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 10
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 460,
|
||||
"height": 50
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "KQxGHBiaqmxDxyCrp56YSw"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
"_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": 241,
|
||||
"b": 204,
|
||||
"a": 255
|
||||
},
|
||||
"_string": "常用语",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 23.191666666666666,
|
||||
"_fontSize": 22,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 26,
|
||||
"_overflow": 1,
|
||||
"_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": "8UWF8nsKi0nMNXIDva6hbA"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "Sm70WG5ZN1YH4+urQklQ6g",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 1
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 15
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 500,
|
||||
"height": 60
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
"y": 1
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "fVvPKJaLU3S/EmLAWyGJKA"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "In1Ks9l4OyAaCSNIHjSSAg",
|
||||
"instance": null,
|
||||
"targetOverrides": null
|
||||
}
|
||||
]
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"ver": "1.1.50",
|
||||
"importer": "prefab",
|
||||
"imported": true,
|
||||
"uuid": "1033dc35-3112-4c07-8b38-a69bf5e24d98",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {
|
||||
"syncNodeName": "ChatPhraseItem"
|
||||
}
|
||||
}
|
||||
-4628
File diff suppressed because it is too large
Load Diff
+206
-80
@@ -25,14 +25,14 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 24
|
||||
"__id__": 30
|
||||
},
|
||||
{
|
||||
"__id__": 26
|
||||
"__id__": 32
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 28
|
||||
"__id__": 34
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -61,7 +61,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "97ML+NinVM/5R4WwGAZw6x"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
@@ -82,14 +82,14 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 19
|
||||
"__id__": 25
|
||||
},
|
||||
{
|
||||
"__id__": 21
|
||||
"__id__": 27
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 23
|
||||
"__id__": 29
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -118,7 +118,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "8d8YkrrDVLQLIAUr+6eoir"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
@@ -171,7 +171,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "521uXpZ2xFNJEH6Qv3/hKH"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -195,7 +195,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "4aKJHb6clH9YVfHdBjKmoM"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -229,7 +229,7 @@
|
||||
},
|
||||
"_type": 1,
|
||||
"_fillType": 0,
|
||||
"_sizeMode": 1,
|
||||
"_sizeMode": 0,
|
||||
"_fillCenter": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
@@ -240,7 +240,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "1aeqPkGRdLWKGd48CoF4dh"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -276,7 +276,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "e2gWMI8TtHhYtyRnb+fuzR"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -297,32 +297,36 @@
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Spirit390",
|
||||
"_name": "Viewport",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_children": [],
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 12
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 12
|
||||
"__id__": 18
|
||||
},
|
||||
{
|
||||
"__id__": 14
|
||||
"__id__": 20
|
||||
},
|
||||
{
|
||||
"__id__": 16
|
||||
"__id__": 22
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 18
|
||||
"__id__": 24
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -65.5,
|
||||
"y": 264.5,
|
||||
"x": 0.5,
|
||||
"y": 262,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -346,7 +350,57 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "9acfeEVWlFmbIAcZBmJKnv"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "Spirit390",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 11
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 13
|
||||
},
|
||||
{
|
||||
"__id__": 15
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 17
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": 314.5,
|
||||
"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",
|
||||
@@ -354,23 +408,23 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 11
|
||||
"__id__": 12
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 13
|
||||
"__id__": 14
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 27,
|
||||
"height": 35
|
||||
"width": 19.996825206892293,
|
||||
"height": 50.400000000000006
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"x": 0,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "43gYISu3RNJ5lVxTQ9pc9/"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -382,11 +436,11 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 11
|
||||
"__id__": 12
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 15
|
||||
"__id__": 16
|
||||
},
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
@@ -399,14 +453,14 @@
|
||||
"a": 255
|
||||
},
|
||||
"_string": "的",
|
||||
"_horizontalAlign": 1,
|
||||
"_horizontalAlign": 0,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 0,
|
||||
"_actualFontSize": 21.083333333333336,
|
||||
"_fontSize": 20,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 40,
|
||||
"_overflow": 0,
|
||||
"_enableWrapText": true,
|
||||
"_enableWrapText": false,
|
||||
"_font": null,
|
||||
"_isSystemFontUsed": true,
|
||||
"_spacingX": 0,
|
||||
@@ -438,48 +492,12 @@
|
||||
"y": 2
|
||||
},
|
||||
"_shadowBlur": 2,
|
||||
"_id": "eatna/BnxB5pi4q0wnRzyW"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "kILN1BaXpCdvFsoWPHjn6g"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Widget",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 11
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 17
|
||||
},
|
||||
"_alignFlags": 16,
|
||||
"_target": null,
|
||||
"_left": 0,
|
||||
"_right": 0,
|
||||
"_top": 0,
|
||||
"_bottom": 0,
|
||||
"_horizontalCenter": -65.5,
|
||||
"_verticalCenter": 0,
|
||||
"_isAbsLeft": true,
|
||||
"_isAbsRight": true,
|
||||
"_isAbsTop": true,
|
||||
"_isAbsBottom": true,
|
||||
"_isAbsHorizontalCenter": true,
|
||||
"_isAbsVerticalCenter": true,
|
||||
"_originalWidth": 0,
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "d3QN/fFlBIOrZ/fJhhqfLB"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "ZpnFtUECRCTHrqbESEi65A"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
@@ -493,6 +511,115 @@
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 11
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 19
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 629,
|
||||
"height": 40
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "f2KihUHONHE6LRmXf+OTrE"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Mask",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 11
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 21
|
||||
},
|
||||
"_type": 0,
|
||||
"_inverted": false,
|
||||
"_segments": 64,
|
||||
"_alphaThreshold": 0.1,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "ccD8rp4idLV6azRu/pMYxR"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Graphics",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 11
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 23
|
||||
},
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
"_dstBlendFactor": 4,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 255
|
||||
},
|
||||
"_lineWidth": 1,
|
||||
"_strokeColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 0,
|
||||
"g": 0,
|
||||
"b": 0,
|
||||
"a": 255
|
||||
},
|
||||
"_lineJoin": 2,
|
||||
"_lineCap": 0,
|
||||
"_fillColor": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 255,
|
||||
"b": 255,
|
||||
"a": 0
|
||||
},
|
||||
"_miterLimit": 10,
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "738x91XUhDDY5EHN2oJIWZ"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "3czKOcfOVBAIY73mqYRxwZ",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
"_name": "",
|
||||
@@ -503,7 +630,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 20
|
||||
"__id__": 26
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
@@ -515,7 +642,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "79vw3+/o5Nw7NOdTSRxZDg"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -531,7 +658,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 22
|
||||
"__id__": 28
|
||||
},
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
@@ -551,7 +678,7 @@
|
||||
"_originalHeight": 720,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "59c+tPLVNJK66xOSQurrlY"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -580,7 +707,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 25
|
||||
"__id__": 31
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
@@ -592,7 +719,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "f69mhdfLNNeYaU3UcNxl75"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -608,7 +735,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 27
|
||||
"__id__": 33
|
||||
},
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
@@ -628,7 +755,7 @@
|
||||
"_originalHeight": 720,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "23aDKevDNNBoqU9XM6lZw4"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -644,7 +771,6 @@
|
||||
},
|
||||
"fileId": "BFV63a9Ssi4rM4blQmfjcA",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
"targetOverrides": null
|
||||
}
|
||||
]
|
||||
+251
-89
@@ -25,14 +25,14 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 56
|
||||
"__id__": 62
|
||||
},
|
||||
{
|
||||
"__id__": 58
|
||||
"__id__": 64
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 60
|
||||
"__id__": 66
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -61,7 +61,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "8fCRfDPiFNfr4d1IaC10ku"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
@@ -82,26 +82,26 @@
|
||||
"__id__": 19
|
||||
},
|
||||
{
|
||||
"__id__": 27
|
||||
"__id__": 33
|
||||
},
|
||||
{
|
||||
"__id__": 35
|
||||
"__id__": 41
|
||||
},
|
||||
{
|
||||
"__id__": 43
|
||||
"__id__": 49
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 51
|
||||
"__id__": 57
|
||||
},
|
||||
{
|
||||
"__id__": 53
|
||||
"__id__": 59
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 55
|
||||
"__id__": 61
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -130,7 +130,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "d3L6zm04lNVIOH/oSUFU8G"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
@@ -183,7 +183,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "bd5sQjCo9KWou+XQMoaZsw"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -207,7 +207,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "daORHoXbhFF7Rg82Ac5Pzx"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -252,7 +252,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "3fg8vDOttPS6ke65z9ECQm"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -288,7 +288,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "19K+4LG7xBrZ9ZKmoFaJdj"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -358,7 +358,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "66sTAEvfRGCZFrQ+GcGuQE"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -382,7 +382,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "4ean6FQW5I67ALCIZ5sBKc"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -427,7 +427,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "deP3Q0NaJP2LTPF0wa4v8S"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -463,7 +463,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "52Z9ijFVBKs6vgD94SgJxU"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -490,21 +490,25 @@
|
||||
"_parent": {
|
||||
"__id__": 2
|
||||
},
|
||||
"_children": [],
|
||||
"_children": [
|
||||
{
|
||||
"__id__": 20
|
||||
}
|
||||
],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 20
|
||||
"__id__": 26
|
||||
},
|
||||
{
|
||||
"__id__": 22
|
||||
"__id__": 28
|
||||
},
|
||||
{
|
||||
"__id__": 24
|
||||
"__id__": 30
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 26
|
||||
"__id__": 32
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -533,7 +537,166 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "20HB+b2i1LF4V9EghXYItY"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
"_name": "NoticeText",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"_parent": {
|
||||
"__id__": 19
|
||||
},
|
||||
"_children": [],
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 21
|
||||
},
|
||||
{
|
||||
"__id__": 23
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 25
|
||||
},
|
||||
"_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.UITransform",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 20
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 22
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 90,
|
||||
"height": 58
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "aamvPdxzFGXqcE3T3pqcNq"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Label",
|
||||
"_name": "",
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 20
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 24
|
||||
},
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
"_dstBlendFactor": 4,
|
||||
"_color": {
|
||||
"__type__": "cc.Color",
|
||||
"r": 255,
|
||||
"g": 241,
|
||||
"b": 204,
|
||||
"a": 255
|
||||
},
|
||||
"_string": "",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 18.975,
|
||||
"_fontSize": 18,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 23,
|
||||
"_overflow": 1,
|
||||
"_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": "038PeJMgxJZ6pJGMeV1qdL"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
"__id__": 1
|
||||
},
|
||||
"asset": {
|
||||
"__id__": 0
|
||||
},
|
||||
"fileId": "41aQak0pVO17V8HDmpqQ1D",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -545,7 +708,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 21
|
||||
"__id__": 27
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
@@ -557,7 +720,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "3bnIT70VhIaaL7EXopecbC"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -573,7 +736,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 23
|
||||
"__id__": 29
|
||||
},
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
@@ -602,7 +765,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "b7TCW9Pp1PDZbAlGbnTxxQ"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -618,7 +781,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 25
|
||||
"__id__": 31
|
||||
},
|
||||
"_alignFlags": 16,
|
||||
"_target": null,
|
||||
@@ -638,7 +801,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "80pHnSLENKRaIXVSBmvlqk"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -669,17 +832,17 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 28
|
||||
"__id__": 34
|
||||
},
|
||||
{
|
||||
"__id__": 30
|
||||
"__id__": 36
|
||||
},
|
||||
{
|
||||
"__id__": 32
|
||||
"__id__": 38
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 34
|
||||
"__id__": 40
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -708,7 +871,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "6eAg0flUtMHafXvOJPhsRK"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -716,11 +879,11 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 27
|
||||
"__id__": 33
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 29
|
||||
"__id__": 35
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
@@ -732,7 +895,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "ebLaHjbERPd5mJ2gRESa82"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -744,11 +907,11 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 27
|
||||
"__id__": 33
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 31
|
||||
"__id__": 37
|
||||
},
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
@@ -777,7 +940,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "4cQk7oI2hD5pDovTg7mCLw"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -789,11 +952,11 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 27
|
||||
"__id__": 33
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 33
|
||||
"__id__": 39
|
||||
},
|
||||
"_alignFlags": 16,
|
||||
"_target": null,
|
||||
@@ -813,7 +976,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "a1JvIHhZdBF4432eXWt5a1"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -844,17 +1007,17 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 36
|
||||
"__id__": 42
|
||||
},
|
||||
{
|
||||
"__id__": 38
|
||||
"__id__": 44
|
||||
},
|
||||
{
|
||||
"__id__": 40
|
||||
"__id__": 46
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 42
|
||||
"__id__": 48
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -883,7 +1046,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "b50FA3cEJFUYnZW1JIxvId"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -891,23 +1054,23 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 35
|
||||
"__id__": 41
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 37
|
||||
"__id__": 43
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 72,
|
||||
"height": 21
|
||||
"width": 53.144487825778164,
|
||||
"height": 50.39999999999999
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "0dxVhUVG9FX5o30O/mMvl2"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -919,11 +1082,11 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 35
|
||||
"__id__": 41
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 39
|
||||
"__id__": 45
|
||||
},
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
@@ -938,7 +1101,7 @@
|
||||
"_string": "Spirit3116",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 0,
|
||||
"_actualFontSize": 12.65,
|
||||
"_fontSize": 12,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 40,
|
||||
@@ -975,7 +1138,7 @@
|
||||
"y": 2
|
||||
},
|
||||
"_shadowBlur": 2,
|
||||
"_id": "c18UgDxNhEBaotRA2XOpHe"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -987,11 +1150,11 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 35
|
||||
"__id__": 41
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 41
|
||||
"__id__": 47
|
||||
},
|
||||
"_alignFlags": 16,
|
||||
"_target": null,
|
||||
@@ -1011,7 +1174,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "d0CN9s8yRPWJ+Ys2tVx1rM"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1042,17 +1205,17 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 44
|
||||
"__id__": 50
|
||||
},
|
||||
{
|
||||
"__id__": 46
|
||||
"__id__": 52
|
||||
},
|
||||
{
|
||||
"__id__": 48
|
||||
"__id__": 54
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 50
|
||||
"__id__": 56
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -1081,7 +1244,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "3bABLKExJDD6iez+YLejMW"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -1089,23 +1252,23 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 43
|
||||
"__id__": 49
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 45
|
||||
"__id__": 51
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 18,
|
||||
"height": 21
|
||||
"width": 13.347650460103754,
|
||||
"height": 50.39999999999999
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "15SekI8pxEQYE6p4UV+hOh"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1117,11 +1280,11 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 43
|
||||
"__id__": 49
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 47
|
||||
"__id__": 53
|
||||
},
|
||||
"_customMaterial": null,
|
||||
"_srcBlendFactor": 2,
|
||||
@@ -1136,7 +1299,7 @@
|
||||
"_string": "12",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 0,
|
||||
"_actualFontSize": 12.65,
|
||||
"_fontSize": 12,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 40,
|
||||
@@ -1173,7 +1336,7 @@
|
||||
"y": 2
|
||||
},
|
||||
"_shadowBlur": 2,
|
||||
"_id": "17OjKsDYJOXINs3bAHPTLZ"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1185,11 +1348,11 @@
|
||||
"_objFlags": 0,
|
||||
"__editorExtras__": {},
|
||||
"node": {
|
||||
"__id__": 43
|
||||
"__id__": 49
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 49
|
||||
"__id__": 55
|
||||
},
|
||||
"_alignFlags": 16,
|
||||
"_target": null,
|
||||
@@ -1209,7 +1372,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "a8RDR4EwxJqb/el10QzlGy"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1238,7 +1401,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 52
|
||||
"__id__": 58
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
@@ -1250,7 +1413,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "dbQbBQjuVKd69uUvMzg2M7"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1266,7 +1429,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 54
|
||||
"__id__": 60
|
||||
},
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
@@ -1286,7 +1449,7 @@
|
||||
"_originalHeight": 720,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "56w6c5MalDsKM8vZTLw8Od"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1315,7 +1478,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 57
|
||||
"__id__": 63
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
@@ -1327,7 +1490,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "06fcPX5F5L/7Ej4v2rtAxV"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1343,7 +1506,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 59
|
||||
"__id__": 65
|
||||
},
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
@@ -1363,7 +1526,7 @@
|
||||
"_originalHeight": 720,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "4b3Kk4qzJIAo0PAdeotxXj"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1379,7 +1542,6 @@
|
||||
},
|
||||
"fileId": "l6/v7vbmQfCHSHw4Wy1EkA",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
"targetOverrides": null
|
||||
}
|
||||
]
|
||||
+1569
-1710
File diff suppressed because it is too large
Load Diff
+7238
-7412
File diff suppressed because it is too large
Load Diff
+54
-94
@@ -25,14 +25,14 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 64
|
||||
"__id__": 62
|
||||
},
|
||||
{
|
||||
"__id__": 66
|
||||
"__id__": 64
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 68
|
||||
"__id__": 66
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -61,7 +61,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "98Yiqdqx1FeoEhHRZTwS13"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
@@ -97,14 +97,14 @@
|
||||
"_active": true,
|
||||
"_components": [
|
||||
{
|
||||
"__id__": 59
|
||||
"__id__": 57
|
||||
},
|
||||
{
|
||||
"__id__": 61
|
||||
"__id__": 59
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 63
|
||||
"__id__": 61
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
@@ -133,7 +133,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "1a3QFfUoNMZJ9jfKJjDCdW"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.Node",
|
||||
@@ -186,7 +186,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "4b8CVpep1LJ4o8i+M13cxA"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -210,7 +210,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "4c75phiahObL4r7rv11iVf"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -244,7 +244,7 @@
|
||||
},
|
||||
"_type": 0,
|
||||
"_fillType": 0,
|
||||
"_sizeMode": 1,
|
||||
"_sizeMode": 0,
|
||||
"_fillCenter": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
@@ -255,7 +255,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "65Sm3zsP9AAJnmJYIDtUrV"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -291,7 +291,7 @@
|
||||
"_originalHeight": 720,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "40wijJWEtLWpvkCN4qrScm"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -361,7 +361,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "6aehj3nZ9IsZyW0RZzAXs3"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -385,7 +385,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "9apG9doCNNZqpKk8d6KkMz"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -419,7 +419,7 @@
|
||||
},
|
||||
"_type": 1,
|
||||
"_fillType": 0,
|
||||
"_sizeMode": 1,
|
||||
"_sizeMode": 0,
|
||||
"_fillCenter": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
@@ -430,7 +430,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "c8WIa2HyZAJpJKGzrVA0w4"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -466,7 +466,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "0eCrZJesFM2Kph7QVmKV3o"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -536,7 +536,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "6anaLwo3VFv7Dgv6a1deHl"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -560,7 +560,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "b4Vz6skmdEjrmsaB+q96+n"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -594,7 +594,7 @@
|
||||
},
|
||||
"_type": 1,
|
||||
"_fillType": 0,
|
||||
"_sizeMode": 1,
|
||||
"_sizeMode": 0,
|
||||
"_fillCenter": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0,
|
||||
@@ -605,7 +605,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "bbdM2FD+RJa7WmKWK3f1xL"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -641,7 +641,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "feHJzw659GVqRvJ5GxfutK"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -711,7 +711,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "da9HDBShFHXK5fN7FZmj4B"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -735,7 +735,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "42tMq0tbtP+JJOhY8iQBgb"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -780,7 +780,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "18o4UEuylCnrBx1JPf3aLa"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -816,7 +816,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "503ivcix5JAKL3Y9gKrDNp"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -886,7 +886,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "33cw3ASL1PqZv0YkyFkdLN"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -910,7 +910,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "6crkFpPL5EVZby1SM8pXpS"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -955,7 +955,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "0blC0nV+dBqpTuBo7qsLv6"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -991,7 +991,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "11fZeZZWJIloYFBf/LYera"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1061,7 +1061,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "f76um+urpJYbpqM/5sMfzz"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -1085,7 +1085,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "49VrwQXU1J2bl/O5huLFMb"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1130,7 +1130,7 @@
|
||||
"_isTrimmedMode": true,
|
||||
"_useGrayscale": false,
|
||||
"_atlas": null,
|
||||
"_id": "dc2Gds3v5JhpQG8vTLVkHp"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1166,7 +1166,7 @@
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "beSsQrCZhGZYiVgeUdPg3j"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1201,18 +1201,15 @@
|
||||
},
|
||||
{
|
||||
"__id__": 54
|
||||
},
|
||||
{
|
||||
"__id__": 56
|
||||
}
|
||||
],
|
||||
"_prefab": {
|
||||
"__id__": 58
|
||||
"__id__": 56
|
||||
},
|
||||
"_lpos": {
|
||||
"__type__": "cc.Vec3",
|
||||
"x": -228.5,
|
||||
"y": 54.5,
|
||||
"x": 0,
|
||||
"y": 55,
|
||||
"z": 0
|
||||
},
|
||||
"_lrot": {
|
||||
@@ -1236,7 +1233,7 @@
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
"_id": "7dQIMIPkZGBKuRAeWjIQb7"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.UITransform",
|
||||
@@ -1252,15 +1249,15 @@
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
"width": 15,
|
||||
"height": 35
|
||||
"width": 520,
|
||||
"height": 105
|
||||
},
|
||||
"_anchorPoint": {
|
||||
"__type__": "cc.Vec2",
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "ecTmKsitNEUoHzkAncHUKr"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1291,11 +1288,11 @@
|
||||
"_string": "1",
|
||||
"_horizontalAlign": 1,
|
||||
"_verticalAlign": 1,
|
||||
"_actualFontSize": 0,
|
||||
"_actualFontSize": 21.083333333333336,
|
||||
"_fontSize": 20,
|
||||
"_fontFamily": "Arial",
|
||||
"_lineHeight": 40,
|
||||
"_overflow": 0,
|
||||
"_overflow": 1,
|
||||
"_enableWrapText": true,
|
||||
"_font": null,
|
||||
"_isSystemFontUsed": true,
|
||||
@@ -1328,48 +1325,12 @@
|
||||
"y": 2
|
||||
},
|
||||
"_shadowBlur": 2,
|
||||
"_id": "400IAEPN1A3bKuNHKnGr+j"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "4zUsrME4uBgpisfm7zb/Bg"
|
||||
},
|
||||
{
|
||||
"__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": -228.5,
|
||||
"_verticalCenter": 0,
|
||||
"_isAbsLeft": true,
|
||||
"_isAbsRight": true,
|
||||
"_isAbsTop": true,
|
||||
"_isAbsBottom": true,
|
||||
"_isAbsHorizontalCenter": true,
|
||||
"_isAbsVerticalCenter": true,
|
||||
"_originalWidth": 0,
|
||||
"_originalHeight": 0,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "00xjA0jkpLCJkCB6XH/tWt"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
"fileId": "d5+8mViB2NvaNVcF9DyxZw"
|
||||
},
|
||||
{
|
||||
"__type__": "cc.PrefabInfo",
|
||||
"root": {
|
||||
@@ -1393,7 +1354,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 60
|
||||
"__id__": 58
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
@@ -1405,7 +1366,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "eccYgeSuVOZ6u+tpHh2kRy"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1421,7 +1382,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 62
|
||||
"__id__": 60
|
||||
},
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
@@ -1441,7 +1402,7 @@
|
||||
"_originalHeight": 720,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "506ovXgPVMh7/d/RBNP7ZQ"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1470,7 +1431,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 65
|
||||
"__id__": 63
|
||||
},
|
||||
"_contentSize": {
|
||||
"__type__": "cc.Size",
|
||||
@@ -1482,7 +1443,7 @@
|
||||
"x": 0.5,
|
||||
"y": 0.5
|
||||
},
|
||||
"_id": "315YfIJa1HSodKjD9G5oz8"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1498,7 +1459,7 @@
|
||||
},
|
||||
"_enabled": true,
|
||||
"__prefab": {
|
||||
"__id__": 67
|
||||
"__id__": 65
|
||||
},
|
||||
"_alignFlags": 45,
|
||||
"_target": null,
|
||||
@@ -1518,7 +1479,7 @@
|
||||
"_originalHeight": 720,
|
||||
"_alignMode": 2,
|
||||
"_lockFlags": 0,
|
||||
"_id": "582Mf9oMhA+69+onjsbZsp"
|
||||
"_id": ""
|
||||
},
|
||||
{
|
||||
"__type__": "cc.CompPrefabInfo",
|
||||
@@ -1534,7 +1495,6 @@
|
||||
},
|
||||
"fileId": "Q446YQvwRQYzhgcdOO5XcA",
|
||||
"instance": null,
|
||||
"targetOverrides": null,
|
||||
"nestedPrefabInstanceRoots": null
|
||||
"targetOverrides": null
|
||||
}
|
||||
]
|
||||
+277
-317
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user