From 72227efd05a6c2e0662c8a803912349cc42f98bd Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sat, 5 Sep 2026 03:44:32 +0800 Subject: [PATCH] fix(platform): apply room action battle flags --- .../platform/stores/platform-store.ts | 15 +- .../platform/platform-store.test.ts | 180 ++++++++++++++++++ 2 files changed, 194 insertions(+), 1 deletion(-) diff --git a/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts index 9f8e3dd..536be36 100644 --- a/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts +++ b/cocoscreator_projects/YouleNexus/assets/framework/platform/stores/platform-store.ts @@ -369,6 +369,16 @@ function parseSeatAction(value: ParsedSeatPayload): { return { seat, raw }; } +function stageAfterOptionalDeskwar( + raw: Readonly>, + currentStage: number, +): number { + if (!hasOwn(raw, 'deskwar')) return currentStage; + return requireProtocolFlag(raw.deskwar, '$.event.raw.deskwar') === 1 + ? 1 + : currentStage; +} + function requireOccupiedSeat(room: InsideRoomState, seat: number): number { if (seat < 0 || seat >= room.seatPlayerIds.length) { throw new RangeError('$.event.seat: expected room seat range'); @@ -572,6 +582,7 @@ export class PlatformStore { if (room.seatPlayerIds.includes(joined.player.playerid)) { throw new RangeError('$.event.raw.playerid: duplicate player seat'); } + const stage = stageAfterOptionalDeskwar(event.raw, room.stage); const player = Object.freeze(joined.player); const entities = Object.freeze({ @@ -598,6 +609,7 @@ export class PlatformStore { seatPlayerIds: Object.freeze(seatPlayerIds), readySeats: Object.freeze(readySeats), offlineSeats: Object.freeze(offlineSeats), + stage, }), })); } @@ -614,6 +626,7 @@ export class PlatformStore { if (player === undefined) { throw new RangeError('players.entities: occupied seat has no canonical player'); } + const stage = stageAfterOptionalDeskwar(event.raw, room.stage); const entities = Object.freeze({ ...previous.players.entities, [playerid]: Object.freeze({ ...player, isprepare: 1 }), @@ -625,7 +638,7 @@ export class PlatformStore { selfPlayerId: previous.players.selfPlayerId, entities, }), - room: Object.freeze({ ...room, readySeats }), + room: Object.freeze({ ...room, readySeats, stage }), })); } diff --git a/cocoscreator_projects/framework-tests/platform/platform-store.test.ts b/cocoscreator_projects/framework-tests/platform/platform-store.test.ts index 81ff8bc..4299043 100644 --- a/cocoscreator_projects/framework-tests/platform/platform-store.test.ts +++ b/cocoscreator_projects/framework-tests/platform/platform-store.test.ts @@ -120,6 +120,106 @@ test('playerJoin atomically adds one canonical entity and its seat id', () => { assert.equal(Object.isFrozen(state.players.entities[77]), true); }); +test('playerJoin applies deskwar battle start in the same root commit', () => { + const store = new PlatformStore(); + const loginData = fixture('player-login-room.json').data; + loginData.isbattle = 0; + store.applyLoginSuccess(parseLoginResponse(loginData)); + const before = store.getState(); + assert.equal(before.room.kind, 'inside'); + if (before.room.kind !== 'inside') return; + assert.equal(before.room.stage, 0); + const transitions: PlatformState[] = []; + store.subscribe((state) => { transitions.push(state); }); + + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, + playerid: 77, + nickname: '对家', + avatar: 'http://b', + sex: 1, + ip: '127.0.0.2', + onstate: 0, + bean: 90, + isprepare: 0, + deskwar: 1, + })); + + const after = store.getState(); + assert.equal(transitions.length, 1); + assert.equal(transitions[0], after); + assert.equal(after.room.kind, 'inside'); + if (after.room.kind !== 'inside') return; + assert.equal(after.room.stage, 1); + assert.equal(after.room.seatPlayerIds[2], 77); + assert.equal(after.players.entities[77]?.playerid, 77); +}); + +test('playerJoin preserves the current stage when deskwar is absent or zero', () => { + for (const initialStage of [0, 1] as const) { + for (const deskwar of [undefined, 0] as const) { + const store = new PlatformStore(); + const loginData = fixture('player-login-room.json').data; + loginData.isbattle = initialStage; + store.applyLoginSuccess(parseLoginResponse(loginData)); + const joinData: Record = { + seat: 2, + playerid: 77, + nickname: '对家', + avatar: 'http://b', + sex: 1, + ip: '127.0.0.2', + onstate: 0, + bean: 90, + isprepare: 0, + }; + if (deskwar !== undefined) joinData.deskwar = deskwar; + + store.playerJoin(parseOtherJoinRoomPayload(joinData)); + + const state = store.getState(); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') continue; + assert.equal( + state.room.stage, + initialStage, + `initial stage ${initialStage}, deskwar ${String(deskwar)}`, + ); + } + } +}); + +test('playerJoin rejects an invalid own deskwar atomically without changing input', () => { + for (const deskwar of [2, -1, true, '1', null, undefined]) { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + const joinData: Record = { + seat: 2, + playerid: 77, + nickname: '对家', + avatar: 'http://b', + sex: 1, + ip: '127.0.0.2', + onstate: 0, + bean: 90, + isprepare: 0, + deskwar, + }; + const originalInput = { ...joinData }; + const parsed = parseOtherJoinRoomPayload(joinData); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.playerJoin(parsed), /deskwar/i); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); + assert.deepEqual(joinData, originalInput); + assert.equal(Object.prototype.hasOwnProperty.call(joinData, 'deskwar'), true); + assert.equal(Object.isFrozen(joinData), false); + } +}); + test('playerReady replaces the affected player and room subtrees without prior mutation', () => { const store = new PlatformStore(); store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); @@ -147,6 +247,86 @@ test('playerReady replaces the affected player and room subtrees without prior m assert.equal(Object.isFrozen(after.room.readySeats), true); }); +test('playerReady applies deskwar battle start in the same root commit', () => { + const store = new PlatformStore(); + const loginData = fixture('player-login-room.json').data; + loginData.isbattle = 0; + store.applyLoginSuccess(parseLoginResponse(loginData)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, + })); + const before = store.getState(); + assert.equal(before.room.kind, 'inside'); + if (before.room.kind !== 'inside') return; + assert.equal(before.room.stage, 0); + const transitions: PlatformState[] = []; + store.subscribe((state) => { transitions.push(state); }); + + store.playerReady(parsePlayerPreparePayload({ seat: 2, deskwar: 1 })); + + const after = store.getState(); + assert.equal(transitions.length, 1); + assert.equal(transitions[0], after); + assert.equal(after.room.kind, 'inside'); + if (after.room.kind !== 'inside') return; + assert.equal(after.room.stage, 1); + assert.deepEqual(after.room.readySeats, [1, 2]); + assert.equal(after.players.entities[77]?.isprepare, 1); +}); + +test('playerReady preserves the current stage when deskwar is absent or zero', () => { + for (const initialStage of [0, 1] as const) { + for (const deskwar of [undefined, 0] as const) { + const store = new PlatformStore(); + const loginData = fixture('player-login-room.json').data; + loginData.isbattle = initialStage; + store.applyLoginSuccess(parseLoginResponse(loginData)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, + })); + const prepareData: Record = { seat: 2 }; + if (deskwar !== undefined) prepareData.deskwar = deskwar; + + store.playerReady(parsePlayerPreparePayload(prepareData)); + + const state = store.getState(); + assert.equal(state.room.kind, 'inside'); + if (state.room.kind !== 'inside') continue; + assert.equal( + state.room.stage, + initialStage, + `initial stage ${initialStage}, deskwar ${String(deskwar)}`, + ); + } + } +}); + +test('playerReady rejects an invalid own deskwar atomically without changing input', () => { + for (const deskwar of [2, -1, true, '1', null, undefined]) { + const store = new PlatformStore(); + store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data)); + store.playerJoin(parseOtherJoinRoomPayload({ + seat: 2, playerid: 77, nickname: '对家', avatar: 'http://b', + sex: 1, bean: 90, ip: '127.0.0.2', onstate: 0, isprepare: 0, + })); + const prepareData: Record = { seat: 2, deskwar }; + const originalInput = { ...prepareData }; + const parsed = parsePlayerPreparePayload(prepareData); + const before = store.getState(); + let notifications = 0; + store.subscribe(() => { notifications += 1; }); + + assert.throws(() => store.playerReady(parsed), /deskwar/i); + assert.equal(store.getState(), before); + assert.equal(notifications, 0); + assert.deepEqual(prepareData, originalInput); + assert.equal(Object.prototype.hasOwnProperty.call(prepareData, 'deskwar'), true); + assert.equal(Object.isFrozen(prepareData), false); + } +}); + test('playerOffline records protocol and room status with one atomic commit', () => { const store = new PlatformStore(); store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data));