mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-28 14:20:46 -05:00
Fix match dedupe
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user