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:
@@ -13,8 +13,8 @@ import {
|
||||
GameSessionHost,
|
||||
GameSessionOpenFault,
|
||||
} from '../../YouleNexus/assets/framework/sdk/runtime/game-session-host.ts';
|
||||
import {OwnedRoomHistory} from '../../YouleNexus/assets/framework/adapters/native/owned-room-history.ts';
|
||||
import { RuntimeSession } from '../../YouleNexus/assets/framework/platform/runtime-session.ts';
|
||||
import {OwnedRoomHistory} from '../../YouleNexus/assets/framework/adapters/native/owned-room-history.ts';
|
||||
import { RuntimeSession, RuntimeSessionFault } from '../../YouleNexus/assets/framework/platform/runtime-session.ts';
|
||||
import { PlatformStore } from '../../YouleNexus/assets/framework/platform/stores/platform-store.ts';
|
||||
import { selectGameSnapshot } from '../../YouleNexus/assets/framework/platform/stores/selectors.ts';
|
||||
|
||||
@@ -22,6 +22,14 @@ function fixture(name: string): Record<string, unknown> {
|
||||
return JSON.parse(readFileSync(new URL(`../fixtures/contracts/${name}`, import.meta.url), 'utf8')).data;
|
||||
}
|
||||
|
||||
test('session fault message exposes the original cause and preserves error references',()=>{
|
||||
const cause=new RangeError('GameEntry seat count 3 does not match room seat count 4');
|
||||
const fault=new RuntimeSessionFault('login room open',[cause]);
|
||||
assert.match(fault.message,/login room open.*RangeError: GameEntry seat count 3 does not match room seat count 4/);
|
||||
assert.equal(fault.errors[0],cause);
|
||||
assert.ok(Object.isFrozen(fault.errors));
|
||||
});
|
||||
|
||||
function makeSession(roomHistory?:OwnedRoomHistory) {
|
||||
const store = new PlatformStore();
|
||||
const log: string[] = [];
|
||||
@@ -54,7 +62,7 @@ function makeSession(roomHistory?:OwnedRoomHistory) {
|
||||
};
|
||||
const gameSession = new GameSessionHost(entry);
|
||||
log.length = 0;
|
||||
const session = new RuntimeSession({
|
||||
const session = new RuntimeSession({
|
||||
roomHistory,
|
||||
store,
|
||||
gameSession,
|
||||
@@ -134,7 +142,7 @@ test('RuntimeSession self join commits, opens a fresh session, and restores the
|
||||
const setup = makeSession();
|
||||
setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
setup.log.length = 0;
|
||||
const join = fixture('self-join-room.json');
|
||||
const join:Record<string,unknown> = {...fixture('self-join-room.json'),deskwar:0};
|
||||
setup.store.subscribe(() => { setup.log.push('store'); });
|
||||
|
||||
setup.session.handleSelfJoin(join);
|
||||
@@ -173,8 +181,8 @@ test('RuntimeSession self join honors the parser-owned deskwar and deskinfo matr
|
||||
if (state.room.kind === 'inside') {
|
||||
assert.equal(state.room.stage, row.expectedStage, JSON.stringify(row));
|
||||
}
|
||||
assert.equal(setup.restored.length, row.deskinfo ? 1 : 0, JSON.stringify(row));
|
||||
if (row.deskinfo) assert.equal(setup.restored[0], deskinfo);
|
||||
assert.equal(setup.restored.length, row.deskinfo && row.deskwar !== 1 ? 1 : 0, JSON.stringify(row));
|
||||
if (row.deskinfo && row.deskwar !== 1) assert.equal(setup.restored[0], deskinfo);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -200,7 +208,6 @@ function makeLeaseSession() {
|
||||
const errors = Object.fromEntries([
|
||||
'lease-factory', 'module-create', 'attach', 'event', 'restore', 'dispose', 'invalidate',
|
||||
].map((point) => [point, new Error(`failure:${point}`)])) as Record<LeaseFailurePoint, Error>;
|
||||
let validatingEntry = true;
|
||||
let moduleDisposals = 0;
|
||||
|
||||
function host(): GameHost {
|
||||
@@ -219,21 +226,20 @@ function makeLeaseSession() {
|
||||
route: 'fixture-game',
|
||||
resolveSeatCount: () => 4,
|
||||
createModule: (): GameModule => {
|
||||
if (!validatingEntry && failures.has('module-create')) throw errors['module-create'];
|
||||
if (failures.has('module-create')) throw errors['module-create'];
|
||||
return {
|
||||
attach(attachedHost) {
|
||||
if (!validatingEntry) attachedHosts.push(attachedHost);
|
||||
if (!validatingEntry && failures.has('attach')) throw errors.attach;
|
||||
attachedHosts.push(attachedHost);
|
||||
if (failures.has('attach')) throw errors.attach;
|
||||
},
|
||||
handlePlatformEvent() {
|
||||
if (!validatingEntry && failures.has('event')) throw errors.event;
|
||||
if (failures.has('event')) throw errors.event;
|
||||
},
|
||||
handleGameMessage() {},
|
||||
restore() {
|
||||
if (!validatingEntry && failures.has('restore')) throw errors.restore;
|
||||
if (failures.has('restore')) throw errors.restore;
|
||||
},
|
||||
dispose() {
|
||||
if (validatingEntry) return;
|
||||
moduleDisposals += 1;
|
||||
if (failures.has('dispose')) throw errors.dispose;
|
||||
},
|
||||
@@ -241,7 +247,6 @@ function makeLeaseSession() {
|
||||
},
|
||||
};
|
||||
const gameSession = new GameSessionHost(entry);
|
||||
validatingEntry = false;
|
||||
const sessionOptions = {
|
||||
store,
|
||||
gameSession,
|
||||
@@ -544,72 +549,90 @@ test('RuntimeSession keeps ordinary aggregates and unbranded or accessor spoofs
|
||||
assert.equal(accessorReads, 0);
|
||||
assert.equal(descriptorReads, 2);
|
||||
});
|
||||
|
||||
test('server room outcomes update owned room cache at the session boundary',()=>{
|
||||
const values=new Map<string,string>();const history=new OwnedRoomHistory({getItem:k=>values.get(k)??null,setItem:(k,v)=>{values.set(k,v);}},'g','a');
|
||||
const setup=makeSession(history);setup.session.handleLogin({...fixture('player-login-room.json'),infinite:0});
|
||||
const room=setup.store.getState().room;if(room.kind!=='inside')throw Error('fixture room');
|
||||
setup.session.handleSelfExit({seat:room.selfSeat,raw:{isowner:1,roomcode:room.roomcode}});
|
||||
assert.deepEqual(history.read(),[Number(room.roomcode)]);
|
||||
setup.session.handleSelfJoin({state:99,roomcode:room.roomcode});assert.deepEqual(history.read(),[]);
|
||||
});
|
||||
|
||||
test('create success uses login profile and entry seats without wire players, opens once and exits', () => {
|
||||
const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
const data={state:0,roomcode:'789123',seat:2,roomtype:[1,4],makewar:0,asetcount:8};
|
||||
setup.store.subscribe(()=>setup.log.push('store'));
|
||||
setup.session.handleCreateRoom(data);
|
||||
const state=setup.store.getState();
|
||||
assert.equal(state.room.kind,'inside');
|
||||
if(state.room.kind!=='inside')throw Error('missing room');
|
||||
assert.deepEqual(state.room.seatPlayerIds,[null,null,430511,null]);
|
||||
assert.equal(state.room.selfSeat,2);assert.equal(state.room.stage,0);
|
||||
assert.equal(state.room.needprepare,0);assert.equal(state.room.infinite,0);
|
||||
assert.deepEqual(state.room.readySeats,[]);assert.deepEqual(state.room.offlineSeats,[]);
|
||||
assert.equal(state.players.entities[430511].isprepare,0);
|
||||
assert.equal('players' in data,false);
|
||||
assert.deepEqual(setup.log,['store','attach','event:room.entered']);
|
||||
setup.session.handleCreateRoom({...data});
|
||||
assert.equal(setup.store.getState(),state);
|
||||
assert.deepEqual(setup.log,['store','attach','event:room.entered']);
|
||||
setup.session.handleSelfExit({raw:{}});
|
||||
assert.equal(setup.store.getState().room.kind,'outside');
|
||||
assert.equal(setup.gameSession.state,'idle');assert.ok(setup.log.includes('dispose'));
|
||||
});
|
||||
|
||||
test('create denial never commits or instantiates a game; invalid seat fails before state mutation',()=>{
|
||||
const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
const before=setup.store.getState(), denial={state:2,showerror:1,error:'denied'};
|
||||
setup.session.handleCreateRoom(denial);
|
||||
assert.equal(setup.store.getState(),before);assert.deepEqual(setup.log,[]);
|
||||
assert.deepEqual(setup.denials,[{rpc:'create_room',state:2,data:denial}]);
|
||||
assert.throws(()=>setup.session.handleCreateRoom({state:0,roomcode:'123',seat:4,roomtype:[1]}),/seat/);
|
||||
assert.equal(setup.store.getState(),before);
|
||||
|
||||
test('server room outcomes update owned room cache at the session boundary',()=>{
|
||||
const values=new Map<string,string>();const history=new OwnedRoomHistory({getItem:k=>values.get(k)??null,setItem:(k,v)=>{values.set(k,v);}},'g','a');
|
||||
const setup=makeSession(history);setup.session.handleLogin({...fixture('player-login-room.json'),infinite:0});
|
||||
const room=setup.store.getState().room;if(room.kind!=='inside')throw Error('fixture room');
|
||||
setup.session.handleSelfExit({seat:room.selfSeat,raw:{isowner:1,roomcode:room.roomcode}});
|
||||
assert.deepEqual(history.read(),[Number(room.roomcode)]);
|
||||
setup.session.handleSelfJoin({state:99,roomcode:room.roomcode});assert.deepEqual(history.read(),[]);
|
||||
});
|
||||
|
||||
test('duplicate successful self join responses do not replace module or restore twice',()=>{
|
||||
const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
const data=fixture('self-join-room.json');setup.session.handleSelfJoin(data);
|
||||
const before=setup.store.getState(),log=[...setup.log];
|
||||
setup.session.handleSelfJoin({...data});
|
||||
assert.equal(setup.store.getState(),before);assert.deepEqual(setup.log,log);
|
||||
assert.equal(setup.restored.length,1);
|
||||
|
||||
test('create success uses login profile and entry seats without wire players, opens once and exits', () => {
|
||||
const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
const data={state:0,roomcode:'789123',seat:2,roomtype:[1,4],makewar:0,asetcount:8};
|
||||
setup.store.subscribe(()=>setup.log.push('store'));
|
||||
setup.session.handleCreateRoom(data);
|
||||
const state=setup.store.getState();
|
||||
assert.equal(state.room.kind,'inside');
|
||||
if(state.room.kind!=='inside')throw Error('missing room');
|
||||
assert.deepEqual(state.room.seatPlayerIds,[null,null,430511,null]);
|
||||
assert.equal(state.room.selfSeat,2);assert.equal(state.room.stage,0);
|
||||
assert.equal(state.room.needprepare,0);assert.equal(state.room.infinite,0);
|
||||
assert.deepEqual(state.room.readySeats,[]);assert.deepEqual(state.room.offlineSeats,[]);
|
||||
assert.equal(state.players.entities[430511].isprepare,0);
|
||||
assert.equal('players' in data,false);
|
||||
assert.deepEqual(setup.log,['store','attach','event:room.entered']);
|
||||
setup.session.handleCreateRoom({...data});
|
||||
assert.equal(setup.store.getState(),state);
|
||||
assert.deepEqual(setup.log,['store','attach','event:room.entered']);
|
||||
setup.session.handleSelfExit({raw:{}});
|
||||
assert.equal(setup.store.getState().room.kind,'outside');
|
||||
assert.equal(setup.gameSession.state,'idle');assert.ok(setup.log.includes('dispose'));
|
||||
});
|
||||
|
||||
for (const mutate of [
|
||||
(roomtype: readonly unknown[]) => { (roomtype as unknown[])[0] = 999; },
|
||||
(roomtype: readonly unknown[]) => { (roomtype[1] as {count: number}).count = 999; },
|
||||
]) test('create seat resolver cannot mutate raw roomtype or partially commit a room', () => {
|
||||
const setup = makeSession();
|
||||
setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
const before = setup.store.getState();
|
||||
const data = {state: 0, roomcode: '123456', seat: 0, roomtype: [1, {count: 4}]};
|
||||
setup.gameSession.entry.resolveSeatCount = roomtype => { assert.ok(Array.isArray(roomtype)); mutate(roomtype); return 4; };
|
||||
assert.throws(() => setup.session.handleCreateRoom(data), TypeError);
|
||||
assert.deepEqual(data.roomtype, [1, {count: 4}]);
|
||||
assert.equal(Object.isFrozen(data.roomtype), false);
|
||||
assert.equal(Object.isFrozen(data.roomtype[1]), false);
|
||||
assert.equal(setup.store.getState(), before);
|
||||
assert.equal(setup.gameSession.state, 'idle');
|
||||
assert.deepEqual(setup.log, []);
|
||||
|
||||
test('create denial never commits or instantiates a game; invalid seat fails before state mutation',()=>{
|
||||
const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
const before=setup.store.getState(), denial={state:2,showerror:1,error:'denied'};
|
||||
setup.session.handleCreateRoom(denial);
|
||||
assert.equal(setup.store.getState(),before);assert.deepEqual(setup.log,[]);
|
||||
assert.deepEqual(setup.denials,[{rpc:'create_room',state:2,data:denial}]);
|
||||
assert.throws(()=>setup.session.handleCreateRoom({state:0,roomcode:'123',seat:4,roomtype:[1]}),/seat/);
|
||||
assert.equal(setup.store.getState(),before);
|
||||
});
|
||||
|
||||
test('duplicate successful self join responses do not replace module or restore twice',()=>{
|
||||
const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
const data={...fixture('self-join-room.json'),deskwar:0};setup.session.handleSelfJoin(data);
|
||||
const before=setup.store.getState(),log=[...setup.log];
|
||||
setup.session.handleSelfJoin({...data});
|
||||
assert.equal(setup.store.getState(),before);assert.deepEqual(setup.log,log);
|
||||
assert.equal(setup.restored.length,1);
|
||||
});
|
||||
|
||||
for (const mutate of [
|
||||
(roomtype: readonly unknown[]) => { (roomtype as unknown[])[0] = 999; },
|
||||
(roomtype: readonly unknown[]) => { (roomtype[1] as {count: number}).count = 999; },
|
||||
]) test('create seat resolver cannot mutate raw roomtype or partially commit a room', () => {
|
||||
const setup = makeSession();
|
||||
setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
const before = setup.store.getState();
|
||||
const data = {state: 0, roomcode: '123456', seat: 0, roomtype: [1, {count: 4}]};
|
||||
setup.gameSession.entry.resolveSeatCount = roomtype => { assert.ok(Array.isArray(roomtype)); mutate(roomtype); return 4; };
|
||||
assert.throws(() => setup.session.handleCreateRoom(data), TypeError);
|
||||
assert.deepEqual(data.roomtype, [1, {count: 4}]);
|
||||
assert.equal(Object.isFrozen(data.roomtype), false);
|
||||
assert.equal(Object.isFrozen(data.roomtype[1]), false);
|
||||
assert.equal(setup.store.getState(), before);
|
||||
assert.equal(setup.gameSession.state, 'idle');
|
||||
assert.deepEqual(setup.log, []);
|
||||
});
|
||||
|
||||
test('entry inputs preserve create/login/join sources and deskwar takes priority over deskinfo',()=>{
|
||||
for(const kind of ['login-waiting','login-restored','joined-waiting','joined-snapshot','joined-started'] as const){
|
||||
const setup=makeSession();
|
||||
const login=kind.startsWith('login');const raw=fixture(login?'player-login-room.json':'self-join-room.json');
|
||||
const snapshot={unknownField:[1,'x']};
|
||||
if(kind==='login-restored'||kind==='joined-snapshot'||kind==='joined-started')raw.deskinfo=snapshot;else delete raw.deskinfo;
|
||||
raw.deskwar=kind==='joined-started'?1:0;
|
||||
if(login)setup.session.handleLogin(raw);else{setup.session.handleLogin(fixture('player-login-success.json'));setup.session.handleSelfJoin(raw);}
|
||||
const event=setup.events[0].event;assert.equal(event.type,'room.entered');if(event.type!=='room.entered')throw Error('entry');
|
||||
assert.equal(event.entry?.kind,kind);assert.equal(event.entry?.raw,raw);
|
||||
assert.equal(setup.restored.length,kind==='login-restored'||kind==='joined-snapshot'?1:0);
|
||||
assert.equal(setup.events.filter(x=>x.event.type==='room.started').length,kind==='joined-started'?1:0);
|
||||
}
|
||||
const setup=makeSession();setup.session.handleLogin(fixture('player-login-success.json'));
|
||||
const raw={state:0,roomcode:'123456',seat:0,roomtype:{mode:[true,null,3]},infinite:0};setup.session.handleCreateRoom(raw);
|
||||
const event=setup.events[0].event;if(event.type!=='room.entered')throw Error('entry');assert.equal(event.entry?.kind,'created');assert.equal(event.entry?.raw,raw);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user