From dd065bbb32d3528fbd0e93b9a2c8504e9475f2cf Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 27 Sep 2026 07:11:19 +0300 Subject: [PATCH] Adjust which lobbies get uploaded --- .../actions/scanner-ingest.server.ts | 51 ++-- .../scanner-ingest/core/Matches.test.ts | 9 + app/features/scanner-ingest/core/Matches.ts | 3 +- .../scanner-ingest/core/Scoreboards.ts | 10 +- .../tests/ingest-scenarios.test.ts | 40 ++- .../scanner/components/UploadStatus.tsx | 2 +- app/features/scanner/core/labels.ts | 5 + .../scanner/core/localized-entries.ts | 280 ++++++++++++++++++ app/features/scanner/core/match-builder.ts | 12 +- app/features/scanner/scanner-types.ts | 30 +- .../scanner/tests/logic/match-builder.test.ts | 18 +- scripts/scanner/build-localized-entries.ts | 12 +- 12 files changed, 425 insertions(+), 47 deletions(-) diff --git a/app/features/scanner-ingest/actions/scanner-ingest.server.ts b/app/features/scanner-ingest/actions/scanner-ingest.server.ts index ff6681f32..6e50dcd9c 100644 --- a/app/features/scanner-ingest/actions/scanner-ingest.server.ts +++ b/app/features/scanner-ingest/actions/scanner-ingest.server.ts @@ -2,6 +2,10 @@ import { subDays } from "date-fns"; import type { ActionFunction } from "react-router"; import { requireUser } from "~/features/auth/core/user.server"; import type { ScannerMatch } from "~/features/scanner/core/scanner-match"; +import { + isLinkableLobby, + isUploadedLobby, +} from "~/features/scanner/scanner-types"; import { dateToDatabaseTimestamp } from "~/utils/dates"; import { logger } from "~/utils/logger"; import { parseBody } from "~/utils/remix.server"; @@ -23,14 +27,15 @@ export const action: ActionFunction = async ({ request }) => { const povUserId = user.id; - const indexedMatches = data.matches - .map((match, requestIndex) => ({ - match: withoutDisprovenCast(match), - requestIndex, - })) - .filter(({ match }) => match.lobby === null || match.lobby === "PRIVATE"); + const requestMatches = data.matches.map(withoutDisprovenCast); + const indexedMatches = requestMatches + .map((match, requestIndex) => ({ match, requestIndex })) + .filter(({ match }) => isLinkableLobby(match.lobby)); const matches = indexedMatches.map(({ match }) => match); - if (matches.length === 0) { + const storeOnlyMatches = requestMatches.filter( + (match) => isUploadedLobby(match.lobby) && !isLinkableLobby(match.lobby), + ); + if (matches.length === 0 && storeOnlyMatches.length === 0) { return { storedMatchesCount: 0, mergedMatchesCount: 0, @@ -40,19 +45,31 @@ export const action: ActionFunction = async ({ request }) => { } satisfies IngestResponse; } - const resolved = await resolveIngestContext({ - matches, + const storedOnly = await ScannerIngestRepository.addOrMergeMatches({ povUserId, - casterUserId: user.id, + submitterUserId: user.id, + matches: storeOnlyMatches, + context: null, }); - const { insertedCount, mergedCount, effectiveMatches } = - await ScannerIngestRepository.addOrMergeMatches({ - povUserId, - submitterUserId: user.id, - matches, - context: resolved?.context ?? null, - }); + const resolved = + matches.length > 0 + ? await resolveIngestContext({ + matches, + povUserId, + casterUserId: user.id, + }) + : null; + + const linkable = await ScannerIngestRepository.addOrMergeMatches({ + povUserId, + submitterUserId: user.id, + matches, + context: resolved?.context ?? null, + }); + const { effectiveMatches } = linkable; + const insertedCount = storedOnly.insertedCount + linkable.insertedCount; + const mergedCount = storedOnly.mergedCount + linkable.mergedCount; let linkedGamesCount = 0; let linkedMatches: IngestResponse["linkedMatches"] = []; diff --git a/app/features/scanner-ingest/core/Matches.test.ts b/app/features/scanner-ingest/core/Matches.test.ts index a1653acc6..b693e514e 100644 --- a/app/features/scanner-ingest/core/Matches.test.ts +++ b/app/features/scanner-ingest/core/Matches.test.ts @@ -113,6 +113,15 @@ describe("isSameMatch", () => { ).toBe(false); }); + test("differing lobbies contradict identity", () => { + expect( + Matches.isSameMatch( + scannerMatch({ lobby: "PRIVATE" }), + scannerMatch({ lobby: "X" }), + ), + ).toBe(false); + }); + test("a null mode does not contradict a read one", () => { expect( Matches.isSameMatch( diff --git a/app/features/scanner-ingest/core/Matches.ts b/app/features/scanner-ingest/core/Matches.ts index 6be5cecc5..c9781fd58 100644 --- a/app/features/scanner-ingest/core/Matches.ts +++ b/app/features/scanner-ingest/core/Matches.ts @@ -65,10 +65,11 @@ export function canonicalMatch(match: ScannerMatch): ScannerMatch { /** * Whether two (possibly partial) matches describe the same game; callers pre-scope to the same - * tournament + POV user. Contradicting mode/stage/replay-code/play-time rules identity out; then a + * tournament + POV user. Contradicting lobby/mode/stage/replay-code/play-time rules identity out; then a * matching replay code, close play times, or an aligning roster (names, else weapons) rules it in. */ export function isSameMatch(a: ScannerMatch, b: ScannerMatch): boolean { + if (a.lobby !== null && b.lobby !== null && a.lobby !== b.lobby) return false; if (a.mode !== null && b.mode !== null && a.mode !== b.mode) return false; if (a.stage !== null && b.stage !== null && a.stage !== b.stage) return false; diff --git a/app/features/scanner-ingest/core/Scoreboards.ts b/app/features/scanner-ingest/core/Scoreboards.ts index 45e2d726d..feddc0d34 100644 --- a/app/features/scanner-ingest/core/Scoreboards.ts +++ b/app/features/scanner-ingest/core/Scoreboards.ts @@ -3,7 +3,10 @@ import type { ScannerMatchObjective, ScannerMatchPlayerStatus, } from "~/features/scanner/core/scanner-match"; -import type { ScannerLobby } from "~/features/scanner/scanner-types"; +import { + isLinkableLobby, + type ScannerLobby, +} from "~/features/scanner/scanner-types"; import type { AbilityWithUnknown, MainWeaponId, @@ -13,9 +16,6 @@ import type { import { databaseTimestampToJavascriptTimestamp } from "~/utils/dates"; import * as Matches from "./Matches"; -/** Lobby header value scoreboards of tournament/SendouQ games are expected to have. */ -const TOURNAMENT_LOBBY = "PRIVATE"; - /** Of 8 player rows, how many must share name and position for a match to count as a re-detection (tolerates a couple of OCR misreads). */ const MIN_LINKED_DUPLICATE_NAME_MATCHES = 6; @@ -158,7 +158,7 @@ export function matchedGames({ return view ? { ...view, matchIndex } : null; }) .filter((view): view is IndexedView => view !== null) - .filter((view) => !view.lobby || view.lobby === TOURNAMENT_LOBBY) + .filter((view) => isLinkableLobby(view.lobby)) .sort((a, b) => a.order - b.order), ); const orderedGames = games.toSorted( diff --git a/app/features/scanner-ingest/tests/ingest-scenarios.test.ts b/app/features/scanner-ingest/tests/ingest-scenarios.test.ts index 722a15f9d..e158312fc 100644 --- a/app/features/scanner-ingest/tests/ingest-scenarios.test.ts +++ b/app/features/scanner-ingest/tests/ingest-scenarios.test.ts @@ -22,12 +22,13 @@ import { } from "./harness"; describe("gating & request filtering", () => { - test("G2 non-private lobby: only X-battle matches in the request → skipped entirely", async () => { + test("G2 other lobbies: anarchy and casual matches are skipped entirely", async () => { const w = await sendouqWorld(); await w.conclude(); const res = await ingest(w.povUser, [ - w.scanned(w.maps[0]!, { lobby: "X" }), + w.scanned(w.maps[0]!, { lobby: "SERIES" }), + w.scanned(w.maps[1]!, { lobby: "REGULAR" }), ]); expect(res).toEqual({ @@ -40,22 +41,47 @@ describe("gating & request filtering", () => { expect(await fetchIngestedMatches()).toHaveLength(0); }); - test("G3 mixed request keeps indices: only the private match is stored and linked", async () => { + test("G3 X battle: stored without a context hint and never linked", async () => { + const w = await sendouqWorld(); + await w.conclude(); + + const res = await ingest(w.povUser, [ + w.scanned(w.maps[0]!, { lobby: "X" }), + ]); + + expect(res).toEqual({ + storedMatchesCount: 1, + mergedMatchesCount: 0, + linkedGamesCount: 0, + linkedMatches: [], + contextResolved: false, + }); + const rows = await fetchIngestedMatches(); + expect(rows).toHaveLength(1); + expect(rows[0]!.data.lobby).toBe("X"); + expect(rows[0]!.groupMatchIdHint).toBeNull(); + expect(await fetchLinks()).toHaveLength(0); + }); + + test("G4 mixed request keeps indices: X battle stored, private match stored and linked", async () => { const w = await sendouqWorld(); await w.conclude(); const res = await ingest(w.povUser, [ w.scanned(w.maps[1]!, { lobby: "X" }), + w.scanned(w.maps[0]!, { lobby: "REGULAR" }), w.scanned(w.maps[0]!), ]); - expect(res.storedMatchesCount).toBe(1); + expect(res.storedMatchesCount).toBe(2); expect(res.linkedMatches).toEqual([ - { matchIndex: 1, link: { type: "sendouq", groupMatchId: w.match.id } }, + { matchIndex: 2, link: { type: "sendouq", groupMatchId: w.match.id } }, ]); const rows = await fetchIngestedMatches(); - expect(rows).toHaveLength(1); - expect(rows[0]!.data.lobby).toBe("PRIVATE"); + expect(new Set(rows.map((row) => row.data.lobby))).toEqual( + new Set(["PRIVATE", "X"]), + ); + expect(await fetchLinks()).toHaveLength(1); }); }); diff --git a/app/features/scanner/components/UploadStatus.tsx b/app/features/scanner/components/UploadStatus.tsx index 283d97a19..a7d6b820d 100644 --- a/app/features/scanner/components/UploadStatus.tsx +++ b/app/features/scanner/components/UploadStatus.tsx @@ -186,7 +186,7 @@ function viewOf(state: UploadState): UploadView { detail: state.reason === "disconnect" ? "A disconnect ended this game before it was decided, so there is no result to upload." - : `Only Private Battle games are uploaded. This one was ${lobbyLabel(state.lobby) ?? "not a private battle"}.`, + : `Only Private Battle and X Battle games are uploaded. This one was ${lobbyLabel(state.lobby) ?? "neither"}.`, }; } } diff --git a/app/features/scanner/core/labels.ts b/app/features/scanner/core/labels.ts index ccac5d83b..cdfbe9f65 100644 --- a/app/features/scanner/core/labels.ts +++ b/app/features/scanner/core/labels.ts @@ -51,6 +51,11 @@ const LOBBY_LABELS: Record = { SERIES: "Anarchy Battle (Series)", OPEN: "Anarchy Battle (Open)", PRIVATE: "Private Battle", + REGULAR: "Regular Battle", + CHALLENGE: "Challenge", + SPLATFEST_OPEN: "Splatfest Battle (Open)", + SPLATFEST_PRO: "Splatfest Battle (Pro)", + TRICOLOR: "Tricolor Battle", }; export function lobbyLabel(lobby: ScannerLobby | null): string | null { diff --git a/app/features/scanner/core/localized-entries.ts b/app/features/scanner/core/localized-entries.ts index 0c9b07242..68af995a1 100644 --- a/app/features/scanner/core/localized-entries.ts +++ b/app/features/scanner/core/localized-entries.ts @@ -60,6 +60,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "私人比赛", lobby: "PRIVATE", }, + { + text: "一般比赛", + lobby: "REGULAR", + }, + { + text: "活动比赛", + lobby: "CHALLENGE", + }, + { + text: "祭典比赛(开放)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "祭典比赛(挑战)", + lobby: "SPLATFEST_PRO", + }, + { + text: "三色夺宝比赛", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -209,6 +229,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Privater Kampf", lobby: "PRIVATE", }, + { + text: "Standardkampf", + lobby: "REGULAR", + }, + { + text: "Event-Kampf", + lobby: "CHALLENGE", + }, + { + text: "Splatfestkampf (Offen)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Splatfestkampf (Profi)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Dreifarb-Match", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -367,6 +407,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Private Battle", lobby: "PRIVATE", }, + { + text: "Regular Battle", + lobby: "REGULAR", + }, + { + text: "Challenge", + lobby: "CHALLENGE", + }, + { + text: "Splatfest Battle (Open)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Splatfest Battle (Pro)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Tricolor Battle", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -516,6 +576,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Combate privado", lobby: "PRIVATE", }, + { + text: "Combate amistoso", + lobby: "REGULAR", + }, + { + text: "Combate evento", + lobby: "CHALLENGE", + }, + { + text: "Combate temático (informal)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Combate temático (formal)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Pugna tricolor", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -674,6 +754,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Match privé", lobby: "PRIVATE", }, + { + text: "Match classique", + lobby: "REGULAR", + }, + { + text: "Match Challenge", + lobby: "CHALLENGE", + }, + { + text: "Festimatch (ouvert)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Festimatch (défi)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Match tricolore", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -823,6 +923,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Partita privata", lobby: "PRIVATE", }, + { + text: "Partita amichevole", + lobby: "REGULAR", + }, + { + text: "Partita evento", + lobby: "CHALLENGE", + }, + { + text: "Partita tematica (Aperta)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Partita tematica (Sfida)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Scontro tricolore", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -972,6 +1092,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Privégevecht", lobby: "PRIVATE", }, + { + text: "Standaardgevecht", + lobby: "REGULAR", + }, + { + text: "Eventgevecht", + lobby: "CHALLENGE", + }, + { + text: "Splatfest-gevecht (open)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Splatfest-gevecht (pro)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Driekleurengevecht", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -1142,6 +1282,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Частный бой", lobby: "PRIVATE", }, + { + text: "Бой салаг", + lobby: "REGULAR", + }, + { + text: "Бой-событие", + lobby: "CHALLENGE", + }, + { + text: "Сплатфест-бой (открытый)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Сплатфест-бой (профи)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Трехцветная стычка", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -1296,6 +1456,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "プライベートマッチ", lobby: "PRIVATE", }, + { + text: "レギュラーマッチ", + lobby: "REGULAR", + }, + { + text: "イベントマッチ", + lobby: "CHALLENGE", + }, + { + text: "フェスマッチ(オープン)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "フェスマッチ(チャレンジ)", + lobby: "SPLATFEST_PRO", + }, + { + text: "トリカラマッチ", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -1445,6 +1625,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "프라이빗 매치", lobby: "PRIVATE", }, + { + text: "레귤러 매치", + lobby: "REGULAR", + }, + { + text: "이벤트 매치", + lobby: "CHALLENGE", + }, + { + text: "페스티벌 매치(오픈)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "페스티벌 매치(챌린지)", + lobby: "SPLATFEST_PRO", + }, + { + text: "트리컬러 매치", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -1594,6 +1794,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "私人比賽", lobby: "PRIVATE", }, + { + text: "一般比賽", + lobby: "REGULAR", + }, + { + text: "活動比賽", + lobby: "CHALLENGE", + }, + { + text: "祭典比賽(開放)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "祭典比賽(挑戰)", + lobby: "SPLATFEST_PRO", + }, + { + text: "三色奪寶比賽", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -1743,6 +1963,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Private Battle", lobby: "PRIVATE", }, + { + text: "Regular Battle", + lobby: "REGULAR", + }, + { + text: "Challenge", + lobby: "CHALLENGE", + }, + { + text: "Splatfest Battle (Open)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Splatfest Battle (Pro)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Tricolor Battle", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -1892,6 +2132,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Combate privado", lobby: "PRIVATE", }, + { + text: "Combate amistoso", + lobby: "REGULAR", + }, + { + text: "Combate especial", + lobby: "CHALLENGE", + }, + { + text: "Combate temático (informal)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Combate temático (formal)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Combate tricolor", + lobby: "TRICOLOR", + }, ], modes: [ { @@ -2041,6 +2301,26 @@ export const LANGUAGE_ENTRIES: readonly LanguageEntries[] = [ text: "Match privé", lobby: "PRIVATE", }, + { + text: "Match classique", + lobby: "REGULAR", + }, + { + text: "Match épreuve", + lobby: "CHALLENGE", + }, + { + text: "Festimatch (ouvert)", + lobby: "SPLATFEST_OPEN", + }, + { + text: "Festimatch (défi)", + lobby: "SPLATFEST_PRO", + }, + { + text: "Match tricolore", + lobby: "TRICOLOR", + }, ], modes: [ { diff --git a/app/features/scanner/core/match-builder.ts b/app/features/scanner/core/match-builder.ts index 950f78012..7046d9fd7 100644 --- a/app/features/scanner/core/match-builder.ts +++ b/app/features/scanner/core/match-builder.ts @@ -14,6 +14,7 @@ import type { ModeShort, StageId, } from "~/modules/in-game-lists/types"; +import { isUploadedLobby } from "../scanner-types"; import { type GearMains, harvestAbilities, @@ -82,9 +83,6 @@ import { } from "./slot-row-assignment"; import { editDistance, matchKey } from "./text"; -/** The lobby header value private battles (tournament games) carry. */ -const TOURNAMENT_LOBBY = "PRIVATE"; - /** How far back a scoreboard with no MapStart claims deaths: matches run well under 8 min. */ const FALLBACK_WINDOW_SECONDS = 480; @@ -312,14 +310,14 @@ function attachOwnResults( /** Why a built match is held back from /ingest; absent = it is sent. */ export type IngestSkipReason = - /** not a tournament (Private Battle) game */ + /** not a Private Battle or X Battle game */ | "lobby" /** a disconnect ended it before it could be decided */ | "disconnect"; /** - * Which built matches are not worth sending to /ingest, and why: non-tournament - * lobbies (unread lobbies get the benefit of the doubt), and games a disconnect + * Which built matches are not worth sending to /ingest, and why: lobbies other + * than Private and X Battle (unread lobbies get the benefit of the doubt), and games a disconnect * cut short — counter reads show the game couldn't have ended on its own * (`endedEarly`), or the same map/mode was replayed right after with a score. * Replay evidence only arrives after the fact, so a live scan may already have @@ -331,7 +329,7 @@ export function ingestSkipReasons( const reasons = new Map, IngestSkipReason>(); for (const [index, candidate] of built.entries()) { const { match } = candidate; - if (match.lobby !== null && match.lobby !== TOURNAMENT_LOBBY) { + if (!isUploadedLobby(match.lobby)) { reasons.set(candidate, "lobby"); } else if (endedEarly(match) || wasReplayed(built, index)) { reasons.set(candidate, "disconnect"); diff --git a/app/features/scanner/scanner-types.ts b/app/features/scanner/scanner-types.ts index c4cbdf9ca..e4d179712 100644 --- a/app/features/scanner/scanner-types.ts +++ b/app/features/scanner/scanner-types.ts @@ -10,10 +10,36 @@ import type { } from "~/modules/in-game-lists/types"; import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids"; -/** The scoreboard header's lobby tag. PRIVATE marks tournament games. */ -export const SCANNER_LOBBIES = ["X", "SERIES", "OPEN", "PRIVATE"] as const; +/** + * The scoreboard header's lobby tag. PRIVATE marks tournament games. The casual + * lobbies are in the closed set so their headers snap to themselves instead of + * reading as unknown (or as a lookalike ranked lobby). + */ +export const SCANNER_LOBBIES = [ + "X", + "SERIES", + "OPEN", + "PRIVATE", + "REGULAR", + "CHALLENGE", + "SPLATFEST_OPEN", + "SPLATFEST_PRO", + "TRICOLOR", +] as const; export type ScannerLobby = (typeof SCANNER_LOBBIES)[number]; +const UPLOADED_LOBBIES: ReadonlySet = new Set(["PRIVATE", "X"]); + +/** Whether /ingest stores games of this lobby; an unread lobby gets the benefit of the doubt. */ +export function isUploadedLobby(lobby: ScannerLobby | null): boolean { + return lobby === null || UPLOADED_LOBBIES.has(lobby); +} + +/** Whether games of this lobby link to reported tournament/SendouQ games (private battles); an unread lobby gets the benefit of the doubt. */ +export function isLinkableLobby(lobby: ScannerLobby | null): boolean { + return lobby === null || lobby === "PRIVATE"; +} + const MAIN_WEAPON_ID_SET: ReadonlySet = new Set(mainWeaponIds); const ABILITY_SET: ReadonlySet = new Set(abilities.map((a) => a.name)); diff --git a/app/features/scanner/tests/logic/match-builder.test.ts b/app/features/scanner/tests/logic/match-builder.test.ts index c6e703e7a..c78beeb0a 100644 --- a/app/features/scanner/tests/logic/match-builder.test.ts +++ b/app/features/scanner/tests/logic/match-builder.test.ts @@ -752,18 +752,24 @@ test("the fallback window does not reach past the previous scoreboard", () => { ); }); -test("non-private lobbies are recorded and skipped on ingest", () => { +test("lobbies other than private and X battle are recorded and skipped on ingest", () => { const built = buildScannerMatches([ mapStart(0), - scoreboard(300, { lobby: "X" }), + scoreboard(300, { lobby: "REGULAR" }), mapStart(400), - scoreboard(700), + scoreboard(700, { lobby: "SERIES" }), + mapStart(800), + scoreboard(1100, { lobby: "X" }), + mapStart(1200), + scoreboard(1500), ]); const skipped = ingestSkipReasons(built); - assert.equal(built.length, 2); - assert.equal(built[0]!.match.lobby, "X"); + assert.equal(built.length, 4); + assert.equal(built[0]!.match.lobby, "REGULAR"); assert.equal(skipped.get(built[0]!), "lobby"); - assert.equal(skipped.get(built[1]!), undefined); + assert.equal(skipped.get(built[1]!), "lobby"); + assert.equal(skipped.get(built[2]!), undefined); + assert.equal(skipped.get(built[3]!), undefined); }); test("a scoreless match whose counters had no time to run out is a disconnect", () => { diff --git a/scripts/scanner/build-localized-entries.ts b/scripts/scanner/build-localized-entries.ts index ebd87f593..9ad63d9ce 100644 --- a/scripts/scanner/build-localized-entries.ts +++ b/scripts/scanner/build-localized-entries.ts @@ -8,7 +8,7 @@ * Sources per language: * CommonMsg/VS/VSRuleName modes (+ _2L two-line intro-splash wraps, "Muschel-\nchaos") * CommonMsg/VS/VSStageName stages (keyed via the USen values) - * CommonMsg/MatchMode lobby tags (XMatch / Private) + * CommonMsg/MatchMode lobby tags (XMatch / Private / casual lobbies) * LayoutMsg/Lobby_MenuMode_00 the intro splash's "MODE" label * LayoutMsg/Mng_Result_00 replay-browser VICTORY / DEFEAT tags * LayoutMsg/VS_Beaten_00 (999) death-burst message; the weapon placeholder sits on @@ -64,6 +64,11 @@ const LOBBY_KEYS: Record = { Bankara: "SERIES", BankaraOpen: "OPEN", Private: "PRIVATE", + Regular: "REGULAR", + League: "CHALLENGE", + FestRegular: "SPLATFEST_OPEN", + FestChallenge: "SPLATFEST_PRO", + FestTriColor: "TRICOLOR", }; /** the English lobby tags as the game shows them, for USen validation */ @@ -72,6 +77,11 @@ const LOBBY_ENGLISH: Record = { SERIES: "Anarchy Battle (Series)", OPEN: "Anarchy Battle (Open)", PRIVATE: "Private Battle", + REGULAR: "Regular Battle", + CHALLENGE: "Challenge", + SPLATFEST_OPEN: "Splatfest Battle (Open)", + SPLATFEST_PRO: "Splatfest Battle (Pro)", + TRICOLOR: "Tricolor Battle", }; type LangDump = Record>;