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:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user