Fix match dedupe

This commit is contained in:
Kalle
2026-09-25 19:43:21 +03:00
parent d99c2fb40a
commit ba55e0f41d
3 changed files with 46 additions and 14 deletions

View File

@@ -199,7 +199,9 @@ sequenceDiagram
order-free paint/K+A/deaths/specials lines), recording time within 20 min
of its play time — joins that match's sources instead of forming a new
one, so browsing the log after playing neither adds a card nor re-uploads
(the match was already sent). An event belongs to at most one match; deaths
(the match was already sent). Likewise a results screen read again with no
match opened since (a lost-connection dialog hid it and the detector
re-armed) joins the last match. An event belongs to at most one match; deaths
reveal enemy builds (`ability-harvest.ts`), the personal results screen
(`ScoreboardOwn`, seen within `OWN_RESULTS_WINDOW_SECONDS` of a closed
match's scoreboard) completes the POV player's full build, and minimap

View File

@@ -43,7 +43,10 @@ import {
} from "./detectors/objective/strip-weapons";
import { QUICK_SCOREBOARD_BATTLE_LOG_EVENT_TYPE } from "./detectors/quick-scoreboard-battle-log/index";
import { SCOREBOARD_EVENT_TYPES } from "./detectors/registry";
import type { ScoreboardData } from "./detectors/scoreboard/index";
import {
SCOREBOARD_EVENT_TYPE,
type ScoreboardData,
} from "./detectors/scoreboard/index";
import {
SCOREBOARD_BATTLE_LOG_EVENT_TYPE,
type ScoreboardBattleLogData,
@@ -175,7 +178,8 @@ export interface BuiltMatch<E extends DetectedEvent> {
* player's build on the match whose results screen it follows. A battle
* history screen showing an already built game (same scoreboard fingerprint,
* recording time not contradicting it) joins that match's `sources` instead of
* forming a new one. Every input event ends up in at most one match's `sources`.
* forming a new one, as does a results screen read again with no match
* opened since. Every input event ends up in at most one match's `sources`.
*/
export function buildScannerMatches<E extends DetectedEvent>(
events: readonly E[],
@@ -212,7 +216,9 @@ export function buildScannerMatches<E extends DetectedEvent>(
orphanStripWeapons = [];
orphanKills = [];
} else if (SCOREBOARD_EVENT_TYPES.includes(event.type)) {
const revisited = revisitedMatch(built, event);
const revisited =
revisitedMatch(built, event) ??
(open ? undefined : reshownResultsMatch(built, event));
if (revisited) {
// the game already has its match, and the one being played (if
// any) keeps gathering events
@@ -1381,15 +1387,7 @@ function revisitedMatch<E extends DetectedEvent>(
const recordedAt = playedAt(event);
return built.findLast((candidate) => {
const board = candidate.sources.find((source) =>
SCOREBOARD_EVENT_TYPES.includes(source.type),
);
if (
!board ||
scoreboardFingerprint(board.data as ScoreboardData) !== fingerprint
) {
return false;
}
if (closingBoardFingerprint(candidate) !== fingerprint) return false;
return (
recordedAt === null ||
candidate.match.playedAt === null ||
@@ -1399,6 +1397,31 @@ function revisitedMatch<E extends DetectedEvent>(
});
}
/**
* The last match again when its results screen is read a second time with no
* match opened since: an overlay (e.g. a lost-connection dialog) hid the screen
* long enough for the detector to re-arm.
*/
function reshownResultsMatch<E extends DetectedEvent>(
built: readonly BuiltMatch<E>[],
event: E,
): BuiltMatch<E> | undefined {
if (event.type !== SCOREBOARD_EVENT_TYPE) return undefined;
const last = built.at(-1);
const fingerprint = scoreboardFingerprint(event.data as ScoreboardData);
if (!last || fingerprint === null) return undefined;
return closingBoardFingerprint(last) === fingerprint ? last : undefined;
}
function closingBoardFingerprint<E extends DetectedEvent>(
built: BuiltMatch<E>,
): string | null {
const board = built.sources.find((source) =>
SCOREBOARD_EVENT_TYPES.includes(source.type),
);
return board ? scoreboardFingerprint(board.data as ScoreboardData) : null;
}
/**
* A game's identity off its board: each team's stat lines (paint, K+A, deaths,
* specials) as an order-free multiset, teams order-free too (a history screen

View File

@@ -97,7 +97,7 @@ function scoreboard(
players: weaponIds.map((weaponId, i) => ({
name: NAMES[i] ?? `p${i}`,
weaponId,
paint: paints.length > 0 ? (paints[i] ?? null) : 1000,
paint: paints.length > 0 ? (paints[i] ?? null) : 1000 + t,
ka: 10,
d: 5,
s: 2,
@@ -816,6 +816,13 @@ test("a battle log view does not close the match still gathering events", () =>
);
});
test("a results screen read again with no match opened since joins its match", () => {
const reread = scoreboard(345, { paints: GAME_PAINTS });
const built = buildScannerMatches([...playedGame(), reread]);
assert.equal(built.length, 1);
assert.equal(built[0]!.sources.at(-1), reread);
});
test("a results screen repeating an earlier board is a new game", () => {
const built = buildScannerMatches([
...playedGame(),