Order ranked emotes against delayed status responses
This commit is contained in:
@@ -28,6 +28,7 @@ interface TerminalMatch {
|
||||
interface QueueEntry { playerId: string; gameSessionId: string; expiresAt: number }
|
||||
interface RankedMatch {
|
||||
emotes: ReturnType<typeof createEmotes>;
|
||||
emoteVersion: number;
|
||||
id: string; playerIds: Pair<string>; appearances: Pair<FighterAppearance>; engine: MatchEngine;
|
||||
actions: Pair<ActionId | null>; departed: Set<string>; timer?: ReturnType<Clock['setTimeout']>;
|
||||
terminal?: TerminalMatch; saved: boolean; finishedExpiresAt?: Pair<number>;
|
||||
@@ -222,7 +223,7 @@ export async function createRankedService(db: Db, clock: Clock = systemClock, en
|
||||
state.selfIndex = selfIndex; state.ownAction = match.actions[selfIndex];
|
||||
// Own submissions change this participant's version without revealing whether the
|
||||
// opponent has locked a choice. Engine transitions always move past either value.
|
||||
state.version = state.version * 2 + (state.ownAction === null ? 0 : 1);
|
||||
state.version = (state.version + match.emoteVersion) * 2 + (state.ownAction === null ? 0 : 1);
|
||||
state.ranked = true; state.appearances = structuredClone(match.appearances);
|
||||
state.emotes = match.emotes.view(clock.now());
|
||||
state.opponentLeft = match.departed.has(match.playerIds[1 - selfIndex]);
|
||||
@@ -325,7 +326,7 @@ export async function createRankedService(db: Db, clock: Clock = systemClock, en
|
||||
}
|
||||
const first = await readProfile(candidate.playerId), second = await readProfile(playerId);
|
||||
const id = randomUUID(), engine = createMatchEngine({ matchId: id, mode: 'friend', selfIndex: 0, clock, settings: await createGlobalSettingsStore(db).get() });
|
||||
const match: RankedMatch = { id, engine, emotes: createEmotes(), playerIds: [candidate.playerId, playerId],
|
||||
const match: RankedMatch = { id, engine, emotes: createEmotes(), emoteVersion: 0, playerIds: [candidate.playerId, playerId],
|
||||
appearances: [first.appearance, second.appearance], actions: [null, null], departed: new Set(), saved: false };
|
||||
engine.start(clock.now() + matchIntroductionDuration(engine.view().settings, true)); matches.add(match);
|
||||
for (const id of match.playerIds) { queue.delete(id); byPlayer.set(id, match); }
|
||||
@@ -348,6 +349,7 @@ export async function createRankedService(db: Db, clock: Clock = systemClock, en
|
||||
if (match.engine.view().phase === 'finished' || match.departed.size) throw new ApiError(409, 'NO_ACTIVE_MATCH');
|
||||
const slot = match.playerIds[0] === playerId ? 0 : 1;
|
||||
if (!match.emotes.send(slot, id, clock.now())) throw new ApiError(400, 'INVALID_REQUEST');
|
||||
match.emoteVersion++;
|
||||
return statusView(playerId);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -25,9 +25,11 @@ test('ranked emotes authenticate membership, reach both players and preserve hid
|
||||
const matchId = paired.match!.matchId;
|
||||
await f.clock.advance(paired.match!.startsAt - f.clock.now());
|
||||
await f.service.action('player-a', f.a.gameSessionId, matchId, 1, 'charge');
|
||||
const before = await f.service.status('player-b', f.b.gameSessionId);
|
||||
const sent = await f.service.emote('player-a', f.a.gameSessionId, matchId, 'greet');
|
||||
const received = await f.service.status('player-b', f.b.gameSessionId);
|
||||
assert.equal(received.match!.emotes?.[0]?.id, 'greet');
|
||||
assert.ok(received.match!.version > before.match!.version, 'an older poll cannot overwrite this emote');
|
||||
assert.deepEqual(received.match!.emotes, sent.match!.emotes);
|
||||
assert.equal(received.match!.ownAction, null);
|
||||
assert.deepEqual(received.match!.energy, [0, 0]);
|
||||
|
||||
Reference in New Issue
Block a user