fix(platform): apply room action battle flags
This commit is contained in:
+14
-1
@@ -369,6 +369,16 @@ function parseSeatAction(value: ParsedSeatPayload): {
|
|||||||
return { seat, raw };
|
return { seat, raw };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stageAfterOptionalDeskwar(
|
||||||
|
raw: Readonly<Record<string, unknown>>,
|
||||||
|
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 {
|
function requireOccupiedSeat(room: InsideRoomState, seat: number): number {
|
||||||
if (seat < 0 || seat >= room.seatPlayerIds.length) {
|
if (seat < 0 || seat >= room.seatPlayerIds.length) {
|
||||||
throw new RangeError('$.event.seat: expected room seat range');
|
throw new RangeError('$.event.seat: expected room seat range');
|
||||||
@@ -572,6 +582,7 @@ export class PlatformStore {
|
|||||||
if (room.seatPlayerIds.includes(joined.player.playerid)) {
|
if (room.seatPlayerIds.includes(joined.player.playerid)) {
|
||||||
throw new RangeError('$.event.raw.playerid: duplicate player seat');
|
throw new RangeError('$.event.raw.playerid: duplicate player seat');
|
||||||
}
|
}
|
||||||
|
const stage = stageAfterOptionalDeskwar(event.raw, room.stage);
|
||||||
|
|
||||||
const player = Object.freeze(joined.player);
|
const player = Object.freeze(joined.player);
|
||||||
const entities = Object.freeze({
|
const entities = Object.freeze({
|
||||||
@@ -598,6 +609,7 @@ export class PlatformStore {
|
|||||||
seatPlayerIds: Object.freeze(seatPlayerIds),
|
seatPlayerIds: Object.freeze(seatPlayerIds),
|
||||||
readySeats: Object.freeze(readySeats),
|
readySeats: Object.freeze(readySeats),
|
||||||
offlineSeats: Object.freeze(offlineSeats),
|
offlineSeats: Object.freeze(offlineSeats),
|
||||||
|
stage,
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -614,6 +626,7 @@ export class PlatformStore {
|
|||||||
if (player === undefined) {
|
if (player === undefined) {
|
||||||
throw new RangeError('players.entities: occupied seat has no canonical player');
|
throw new RangeError('players.entities: occupied seat has no canonical player');
|
||||||
}
|
}
|
||||||
|
const stage = stageAfterOptionalDeskwar(event.raw, room.stage);
|
||||||
const entities = Object.freeze({
|
const entities = Object.freeze({
|
||||||
...previous.players.entities,
|
...previous.players.entities,
|
||||||
[playerid]: Object.freeze({ ...player, isprepare: 1 }),
|
[playerid]: Object.freeze({ ...player, isprepare: 1 }),
|
||||||
@@ -625,7 +638,7 @@ export class PlatformStore {
|
|||||||
selfPlayerId: previous.players.selfPlayerId,
|
selfPlayerId: previous.players.selfPlayerId,
|
||||||
entities,
|
entities,
|
||||||
}),
|
}),
|
||||||
room: Object.freeze({ ...room, readySeats }),
|
room: Object.freeze({ ...room, readySeats, stage }),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,106 @@ test('playerJoin atomically adds one canonical entity and its seat id', () => {
|
|||||||
assert.equal(Object.isFrozen(state.players.entities[77]), true);
|
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<string, unknown> = {
|
||||||
|
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<string, unknown> = {
|
||||||
|
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', () => {
|
test('playerReady replaces the affected player and room subtrees without prior mutation', () => {
|
||||||
const store = new PlatformStore();
|
const store = new PlatformStore();
|
||||||
store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data));
|
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);
|
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<string, unknown> = { 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<string, unknown> = { 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', () => {
|
test('playerOffline records protocol and room status with one atomic commit', () => {
|
||||||
const store = new PlatformStore();
|
const store = new PlatformStore();
|
||||||
store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data));
|
store.applyLoginSuccess(parseLoginResponse(fixture('player-login-room.json').data));
|
||||||
|
|||||||
Reference in New Issue
Block a user