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:
2026-09-09 03:13:18 +08:00
parent a7ec8aa6a7
commit 35216f75e7
1028 changed files with 835556 additions and 21415 deletions
@@ -0,0 +1,36 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {startPlatformLocation,callPlatformSetting} from '../../YouleNexus/assets/framework/adapters/native/platform-native.ts';
test('browser location enters the legacy callback as JSON, with a fresh payload each time',()=>{
const results:string[]=[];
startPlatformLocation({kind:'browser'},value=>results.push(value as string));
startPlatformLocation({kind:'browser'},value=>results.push(value as string));
assert.equal(results.length,2);
assert.deepEqual(JSON.parse(results[0]),{
address:'江西省南昌市青云谱区施尧路靠近上海浦东发展银行(长天支行)',city:'南昌市',cityCode:'0791',country:'中国',
district:'青云谱区',latitude:28.623546,longitude:115.900333,province:'江西省',street:'施尧路',errorCode:0,
});
assert.equal(results[0],results[1]);
});
test('settings and WVJB preserve scalar and no-argument conventions without browser callbacks',()=>{
const calls:unknown[]=[];
const settings={startlocation:(...args:unknown[])=>calls.push(['location',...args]),
stopshake:(...args:unknown[])=>calls.push(['stop',...args]),voicePlaying:(n:number)=>calls.push(['voice',n]),
SwitchShake:(n:number)=>calls.push(['sound',n])};
const host={kind:'settings' as const,settings};
startPlatformLocation(host,()=>assert.fail('native must own its callback'));
callPlatformSetting(host,'stopshake');callPlatformSetting(host,'voicePlaying',0);callPlatformSetting(host,'SwitchShake',1);
const bridge={kind:'bridge' as const,call:(name:string,data:unknown)=>calls.push([name,data])};
startPlatformLocation(bridge,()=>assert.fail('bridge must own its callback'));
callPlatformSetting(bridge,'stopshake');callPlatformSetting(bridge,'voicePlaying',1);
assert.deepEqual(calls,[['location',2],['stop'],['voice',0],['sound',1],['startlocation',2],['stopshake',''],['voicePlaying',1]]);
});
test('browser setting commands complete, but malformed native hosts and native failures stay visible',()=>{
callPlatformSetting({kind:'browser'},'voicePlaying',0);
callPlatformSetting({kind:'browser'},'stopshake');
assert.throws(()=>callPlatformSetting({kind:'settings',settings:{}},'voicePlaying',1),/voicePlaying/);
assert.throws(()=>startPlatformLocation({kind:'settings',settings:{startlocation(){throw Error('native failed');}}},()=>assert.fail()),/native failed/);
});
@@ -0,0 +1,79 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {RoomDeviceStatusAdapter,createRoomDeviceStatusSubscriptions} from '../../YouleNexus/assets/framework/adapters/native/room-device-status.ts';
import type {PlatformNativeHost} from '../../YouleNexus/assets/framework/adapters/native/platform-native.ts';
import type {NativeHandler} from '../../YouleNexus/assets/framework/adapters/native/native-contracts.ts';
function setup(host:PlatformNativeHost) {
const handlers=new Map<string,NativeHandler>();
const adapter=new RoomDeviceStatusAdapter({host,subscribe:(name,handler)=>{handlers.set(name,handler);return()=>{handlers.delete(name);};}});
return {adapter,handlers};
}
test('browser owns explicit legacy test defaults',()=>{
const {adapter}=setup({kind:'browser'});
assert.deepEqual(adapter.snapshot(),{battery:1,network:1,wifiLevel:null,wifiFrame:4,batteryFrame:2});
adapter.refresh();assert.equal(adapter.snapshot().battery,1);
});
test('settings uses exact synchronous getters and wifi callback positional arguments',()=>{
const calls:string[]=[];
const {adapter,handlers}=setup({kind:'settings',settings:{getbattery(){calls.push('getbattery');return '0.2';},getnetwork(){calls.push('getnetwork');return '2';},getwifisignalLevel(){calls.push('getwifisignalLevel');}}});
assert.deepEqual(calls,['getnetwork','getwifisignalLevel','getbattery']);
assert.equal(adapter.snapshot().batteryFrame,3);assert.equal(adapter.snapshot().wifiFrame,3);
for(const [level,frame] of [[0,1],[1,2],[2,2],[3,3],[4,3]]){
handlers.get('getwifiLevel')!('real ssid',String(level));assert.equal(adapter.snapshot().wifiFrame,frame);
}
handlers.get('getBattery')!('0.21');assert.equal(adapter.snapshot().batteryFrame,2);
handlers.get('getnetwork')!('3');assert.equal(adapter.snapshot().wifiFrame,4);
handlers.get('getnetwork')!('1');assert.equal(adapter.snapshot().wifiFrame,4);
});
test('bridge waits for callbacks without inventing native getter calls',()=>{
const {adapter,handlers}=setup({kind:'bridge',call(){assert.fail('legacy status getters never call WVJB');}});
assert.deepEqual(adapter.snapshot(),{battery:null,network:null,wifiLevel:null,wifiFrame:null,batteryFrame:null});
handlers.get('getwifiLevel')!(JSON.stringify({ssidname:'real',signalLevel:'2'}));
handlers.get('getnetwork')!('2');handlers.get('getBattery')!('0.1');
assert.equal(adapter.snapshot().wifiFrame,3);assert.equal(adapter.snapshot().batteryFrame,3);
adapter.refresh();assert.equal(adapter.snapshot().wifiFrame,3);
});
test('invalid native data and absent settings methods expose source errors',()=>{
assert.throws(()=>setup({kind:'settings',settings:{}}),/getnetwork/);
const {adapter,handlers}=setup({kind:'bridge',call(){}});
for(const value of [null,undefined,'',true,'NaN',-0.1,1.1])assert.throws(()=>handlers.get('getBattery')!(value),/battery/i);
for(const value of [0,4,'',null])assert.throws(()=>handlers.get('getnetwork')!(value),/network/);
for(const signalLevel of [-1,5,1.2,'',null])assert.throws(()=>handlers.get('getwifiLevel')!({ssidname:'x',signalLevel}),/wifi/i);
assert.equal(adapter.snapshot().battery,null);
});
test('observable status unsubscribes and late callbacks remain inert',()=>{
const {adapter,handlers}=setup({kind:'bridge',call(){}});let count=0;
const off=adapter.subscribe(()=>count++);
handlers.get('getBattery')!(1);assert.equal(count,1);off();
handlers.get('getBattery')!(0.5);assert.equal(count,1);
const retained=handlers.get('getBattery')!;adapter.dispose();retained('bad');assert.equal(handlers.size,0);
assert.throws(()=>adapter.refresh(),/disposed/);
});
test('subscriptions restore settings globals and guard retained WVJB callbacks',()=>{
const original=()=>{};const globals:Record<string,unknown>={getBattery:original};let count=0;
const subscribe=createRoomDeviceStatusSubscriptions({kind:'settings',settings:{}},globals);
const off=subscribe('getBattery',()=>count++);const retained=globals.getBattery as NativeHandler;
retained(1);off();retained(1);assert.equal(count,1);assert.equal(globals.getBattery,original);
const offNew=subscribe('getnetwork',()=>{});offNew();assert.equal('getnetwork' in globals,false);
const newer=()=>{};const offOwned=subscribe('getwifiLevel',()=>{});globals.getwifiLevel=newer;offOwned();assert.equal(globals.getwifiLevel,newer);
const handlers=new Map<string,NativeHandler>();
const offBridge=createRoomDeviceStatusSubscriptions({kind:'bridge',call(){}},{},{register:(name,handler)=>{handlers.set(name,handler);}})('getBattery',()=>count++);
handlers.get('getBattery')!(1);offBridge();handlers.get('getBattery')!(1);assert.equal(count,2);
});
test('failed settings initialization releases every installed global callback',()=>{
const original=()=>{};const globals:Record<string,unknown>={getBattery:original};
const host:PlatformNativeHost={kind:'settings',settings:{getnetwork(){return 2;}}};
assert.throws(()=>new RoomDeviceStatusAdapter({host,subscribe:createRoomDeviceStatusSubscriptions(host,globals)}),/getwifisignalLevel/);
assert.deepEqual(globals,{getBattery:original});
});
test('settings wifi callback can synchronously respond to the exact no-argument request',()=>{
const globals:Record<string,unknown>={};
const settings={getnetwork(){return 2;},getbattery(){return 0;},getwifisignalLevel(...args:unknown[]){
assert.deepEqual(args,[]);assert.equal(this,settings);(globals.getwifiLevel as NativeHandler)('ssid',0);
}};
const host:PlatformNativeHost={kind:'settings',settings};
const adapter=new RoomDeviceStatusAdapter({host,subscribe:createRoomDeviceStatusSubscriptions(host,globals)});
assert.equal(adapter.snapshot().wifiFrame,1);assert.equal(adapter.snapshot().battery,0);
adapter.dispose();assert.deepEqual(globals,{});
});
@@ -0,0 +1,69 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {RoomVoiceAdapter,createRoomVoiceSubscriptions} from '../../YouleNexus/assets/framework/adapters/native/room-voice.ts';
import type {PlatformNativeHost} from '../../YouleNexus/assets/framework/adapters/native/platform-native.ts';
import type {NativeHandler} from '../../YouleNexus/assets/framework/adapters/native/native-contracts.ts';
function setup(host:PlatformNativeHost) {
const handlers=new Map<string,NativeHandler>();const sent:unknown[]=[];const played:unknown[]=[];
const adapter=new RoomVoiceAdapter({host,subscribe:(name,handler)=>{handlers.set(name,handler);return()=>{handlers.delete(name);};},
sendVoice:(url,time)=>sent.push([url,time]),onPlayback:(user,playing)=>played.push([user,playing])});
return {adapter,handlers,sent,played};
}
test('settings recording and history playback retain exact positional arguments',()=>{
const calls:unknown[]=[];
const {adapter,handlers,sent}=setup({kind:'settings',settings:{prepareaudio(...args:unknown[]){calls.push(args);},mediaTypeAudio(...args:unknown[]){calls.push(args);}}});
adapter.startRecording();adapter.stopRecording();
handlers.get('getaudiourl')!('https://audio/real.aac','3');
adapter.playVoice('https://audio/real.aac',1,10003);
assert.deepEqual(calls,[[],['https://audio/real.aac',1,10003]]);
assert.deepEqual(sent,[['https://audio/real.aac','3']]);
});
test('WVJB empty start payload, structured playback and JSON upload are unchanged',()=>{
const calls:unknown[]=[];
const {adapter,handlers,sent,played}=setup({kind:'bridge',call:(name,data)=>{calls.push([name,data]);}});
adapter.startRecording();adapter.stopRecording();
handlers.get('getaudiourl')!(JSON.stringify({audiourl:'actual.aac',time:2}));
adapter.playVoice('actual.aac',0,10001);
handlers.get('gameui_play_voice')!('10001');handlers.get('gameui_stop_voice')!(10001);
assert.deepEqual(calls,[['prepareaudio',''],['mediaTypeAudio',{audiourl:'actual.aac',type:0,user:10001}]]);
assert.deepEqual(sent,[['actual.aac',2]]);assert.deepEqual(played,[[10001,true],[10001,false]]);
});
test('browser reports unsupported without creating uploads or playback',()=>{
const {adapter,sent,played}=setup({kind:'browser'});
assert.throws(()=>adapter.startRecording(),/不支持/);assert.throws(()=>adapter.playVoice('x',0,1),/不支持/);
assert.deepEqual(sent,[]);assert.deepEqual(played,[]);
});
test('cancelled, zero duration and duplicate uploads never send; dispose removes callbacks',()=>{
const {adapter,handlers,sent}=setup({kind:'bridge',call(){}});
adapter.startRecording();adapter.cancelRecording();
assert.throws(()=>adapter.startRecording(),/pending/);
handlers.get('getaudiourl')!({audiourl:'cancelled',time:3});
adapter.startRecording();handlers.get('getaudiourl')!({audiourl:'',time:0});
adapter.startRecording();handlers.get('getaudiourl')!({audiourl:'once',time:1});
handlers.get('getaudiourl')!({audiourl:'duplicate',time:1});
assert.deepEqual(sent,[['once',1]]);
adapter.startRecording();const late=handlers.get('getaudiourl')!;adapter.dispose();late({audiourl:'late',time:3});
assert.equal(handlers.size,0);assert.equal(sent.length,1);
});
test('invalid upload source and missing native method are exposed',()=>{
const {adapter,handlers}=setup({kind:'bridge',call(){}});
adapter.startRecording();assert.throws(()=>handlers.get('getaudiourl')!({audiourl:'x'}),/time/);
assert.throws(()=>setup({kind:'settings',settings:{}}).adapter.startRecording(),/prepareaudio/);
});
test('settings subscriptions own globals and restore previous callbacks on dispose',()=>{
const original=()=>{};const globals:Record<string,unknown>={getaudiourl:original};let count=0;
const subscribe=createRoomVoiceSubscriptions({kind:'settings',settings:{}},globals);
const off=subscribe('getaudiourl',()=>{count++;});const callback=globals.getaudiourl as NativeHandler;
callback('actual',1);off();callback('late',1);
assert.equal(count,1);assert.equal(globals.getaudiourl,original);
const offNew=subscribe('gameui_play_voice',()=>{});offNew();assert.equal(Object.prototype.hasOwnProperty.call(globals,'gameui_play_voice'),false);
});
test('bridge subscriptions use legacy names, neutralize retained callbacks and preserve newer owners',()=>{
const handlers=new Map<string,NativeHandler>();let count=0;
const subscribe=createRoomVoiceSubscriptions({kind:'bridge',call(){}},{},{register:(name,handler)=>{handlers.set(name,handler);}});
const off=subscribe('gameui_play_voice',()=>{count++;});const retained=handlers.get('gameui_play_voice')!;
retained(1);off();retained(1);assert.equal(count,1);
const globals:Record<string,unknown>={};const settingsSubscribe=createRoomVoiceSubscriptions({kind:'settings',settings:{}},globals);
const release=settingsSubscribe('getaudiourl',()=>{});const newer=()=>{};globals.getaudiourl=newer;release();assert.equal(globals.getaudiourl,newer);
});