From 3674a7dfe7089d6637e93d75c1842ba3ea7fa8dd Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:01:03 +0300 Subject: [PATCH] Fix scripts typecheck --- .github/workflows/main.yml | 2 + docs/dev/scripts.md | 1 + package.json | 2 +- .../add-leaderboard-teams-to-tournament.ts | 5 +- scripts/benchmark-db/cases.ts | 9 +- scripts/check-season-status.ts | 2 +- scripts/scanner/overlay-rois.ts | 3 +- scripts/scanner/status-audit.ts | 19 ++- scripts/splat3-types.ts | 127 ++++++++++++++++++ scripts/utils.ts | 38 +++--- 10 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 scripts/splat3-types.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index db353ae80..8370f1aa1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,8 @@ jobs: run: pnpm run biome:check - name: Typecheck run: pnpm run typecheck + - name: Typecheck scripts + run: pnpm run typecheck:scripts - name: Unit tests run: pnpm run test:unit:browser - name: Knip unused check diff --git a/docs/dev/scripts.md b/docs/dev/scripts.md index ac439baee..ed0539602 100644 --- a/docs/dev/scripts.md +++ b/docs/dev/scripts.md @@ -41,6 +41,7 @@ Note: it only works with Node 16. ## Doing monthly update 1. Drop the whole [splat3 repository](https://github.com/Leanny/splat3) into `/scripts/dicts/splat3` (pull before). +1. If the dump's shape changed, update `/scripts/splat3-types.ts` to match (the dump is gitignored, so these types are hand-maintained rather than inferred from the JSON). 1. Update all `CURRENT_SEASON` constants 1. Update `CURRENT_PATCH` constants 1. Update `PATCHES` constant with the late patch + remove the oldest diff --git a/package.json b/package.json index 2443348ef..f4fc86ef3 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "test:e2e": "playwright test", "test:e2e:flaky-detect": "playwright test --repeat-each=10 --max-failures=1", "check-plural-collapse": "node scripts/collapse-single-plural-keys.ts --check", - "checks": "pnpm run biome:fix && pnpm run test:unit:browser && pnpm run check-translation-jsons && pnpm run check-plural-collapse && pnpm run typecheck && pnpm run knip", + "checks": "pnpm run biome:fix && pnpm run test:unit:browser && pnpm run check-translation-jsons && pnpm run check-plural-collapse && pnpm run typecheck && pnpm run typecheck:scripts && pnpm run knip", "checks:scanner": "pnpm checks && pnpm test:scanner", "seed": "cross-env DB_PATH=db.sqlite3 vite-node ./scripts/seed.ts", "setup": "cross-env DB_PATH=db.sqlite3 vite-node ./scripts/setup.ts", diff --git a/scripts/add-leaderboard-teams-to-tournament.ts b/scripts/add-leaderboard-teams-to-tournament.ts index 2ecb39536..b62370707 100644 --- a/scripts/add-leaderboard-teams-to-tournament.ts +++ b/scripts/add-leaderboard-teams-to-tournament.ts @@ -30,10 +30,7 @@ for (const p of placements) { async function loadTournament() { try { - return await tournamentFromDB({ - user: undefined, - tournamentId, - }); + return await tournamentFromDB(tournamentId); } catch { throw new Error(`Tournament with id ${tournamentId} not found`); } diff --git a/scripts/benchmark-db/cases.ts b/scripts/benchmark-db/cases.ts index 65a7d47ae..9ea677627 100644 --- a/scripts/benchmark-db/cases.ts +++ b/scripts/benchmark-db/cases.ts @@ -1133,9 +1133,12 @@ export function buildCases(fx: Fixtures): { ); add( "TournamentTeamRepository.findRecentlyPlayedMapsByIds", - fx.tournamentTeamPair, - (teamIds) => - TournamentTeamRepository.findRecentlyPlayedMapsByIds({ teamIds }), + both(fx.tournamentTeamPair, fx.heavyTournamentMatchId), + ([teamIds, excludeMatchId]) => + TournamentTeamRepository.findRecentlyPlayedMapsByIds({ + teamIds, + excludeMatchId, + }), ); add( "TournamentTeamRepository.findMapPoolsByTeamIds", diff --git a/scripts/check-season-status.ts b/scripts/check-season-status.ts index f06252bd9..00fca38ae 100644 --- a/scripts/check-season-status.ts +++ b/scripts/check-season-status.ts @@ -71,7 +71,7 @@ if (unfinalizedTournaments.length > 0) { `Unfinalized ranked tournaments (${unfinalizedTournaments.length}):`, ); for (const tournament of unfinalizedTournaments) { - const date = new Date(tournament.startTime * 1000).toISOString(); + const date = new Date(tournament.startsAt * 1000).toISOString(); logger.info(` - ID ${tournament.id}: "${tournament.name}" (${date})`); } } else { diff --git a/scripts/scanner/overlay-rois.ts b/scripts/scanner/overlay-rois.ts index 67da3ea5e..c324f5b1c 100644 --- a/scripts/scanner/overlay-rois.ts +++ b/scripts/scanner/overlay-rois.ts @@ -128,7 +128,8 @@ if (detector === "scoreboard") { rect(frame, minimap.enemyCrossRoi(cy), [255, 0, 255]); } for (const roi of [ - minimap.GATE_CLOSE_BRIGHT, + ...minimap.GATE_CLOSE_X_BRIGHT, + ...minimap.GATE_CLOSE_X_DARK, minimap.GATE_SPAWN_BRIGHT, ...minimap.GATE_CLOSE_DARK_PROBES, ...minimap.GATE_SPAWN_DARK_PROBES, diff --git a/scripts/scanner/status-audit.ts b/scripts/scanner/status-audit.ts index 9fea2b826..d17a0768d 100644 --- a/scripts/scanner/status-audit.ts +++ b/scripts/scanner/status-audit.ts @@ -19,7 +19,10 @@ * Usage: pnpm scanner:status-audit [--all] */ import { readFileSync } from "node:fs"; -import { statusSpans } from "../../app/components/PlayerStatusTimeline"; +import { + type PlayerStatusTimelineSample, + statusSpans, +} from "../../app/components/PlayerStatusTimeline"; import { formatTime } from "../../app/features/scanner/components/format"; import { lobbyLabel, @@ -284,6 +287,9 @@ function parsePlayerStatusCell(cell: string): PlayerStatusData { special: [a.special, b.special], dead: [a.dead, b.dead], layout: layout as PlayerStatusLayout, + // the CSV carries no camera-badge evidence, so cast orientation + // degrades to the `cast-mirror` layout fallback + castProven: false, }; } @@ -457,9 +463,12 @@ interface FixtureCandidate { reads: number[]; } -interface StatusSpan { +interface TimeWindow { start: number; end: number; +} + +interface StatusSpan extends TimeWindow { /** sample timestamps that read the flag true inside the span */ confirmingReads: number[]; /** @@ -484,7 +493,7 @@ interface SlotAnalysis { function annotatedSpans( samples: readonly ScannerMatchPlayerStatusSample[], - flagOf: (sample: ScannerMatchPlayerStatusSample) => boolean, + flagOf: (sample: PlayerStatusTimelineSample) => boolean, ): StatusSpan[] { return statusSpans(samples, flagOf).map((span) => { const confirmingReads = samples @@ -552,8 +561,8 @@ function analyzeSlot( function observationGaps( samples: readonly ScannerMatchPlayerStatusSample[], -): StatusSpan[] { - const gaps: StatusSpan[] = []; +): TimeWindow[] { + const gaps: TimeWindow[] = []; for (let i = 1; i < samples.length; i++) { const dt = samples[i]!.t - samples[i - 1]!.t; if (dt > OBSERVATION_GAP_SECONDS) { diff --git a/scripts/splat3-types.ts b/scripts/splat3-types.ts new file mode 100644 index 000000000..877491c8b --- /dev/null +++ b/scripts/splat3-types.ts @@ -0,0 +1,127 @@ +/** + * Hand-maintained types for the [Leanny/splat3](https://github.com/Leanny/splat3) game data dump. + * + * The dump itself is not checked in (see `.gitignore`), so these types are declared here rather + * than inferred from the JSON files. That keeps `pnpm run typecheck:scripts` passing whether or + * not the dump is currently sitting in `scripts/dicts/splat3`. + * + * Only the fields the scripts actually read are declared for the deeply nested parameter dumps. + * When a monthly update changes the dump's shape, update these types alongside the scripts. + */ + +/** `data/language/.json` — message category -> message key -> localized (URI encoded) text. */ +export type LangDict = Record>; + +interface WeaponInfoEntry { + DefaultDamageRateInfoRow: string; + DefaultHitEffectorType: string; + ExtraDamageRateInfoRowSet: Array<{ + DamageRateInfoRow: string; + ExtraInfo: string; + }>; + ExtraHitEffectorInfoSet: Array<{ + ExtraInfo: string; + HitEffectorType: string; + }>; + Id: number; + Label: string; + NpcActor: string; + SpecActor: string; + Type: string; + __RowId: string; +} + +/** `data/mush//WeaponInfoMain.json` */ +export interface WeaponInfoMainEntry extends WeaponInfoEntry { + DebugDispColumn: number; + DebugDispOrder: number; + GameActor: string; + IsCoopRare: boolean; + LObjParam: string; + LockerContentInfo: string[]; + LockerModelColor: { A: number; B: number; G: number; R: number }; + MatchingId: number; + Range: number; + RewardLv2: string; + RewardLv3: string; + Season: number; + ShopPrice: number; + ShopUnlockRank: number; + SpecialPoint: number; + SpecialWeapon: string; + SubWeapon: string; + UIParam: Array<{ Type: string; Value: number }>; + WeaponInfoForCoop: string; +} + +/** `data/mush//WeaponInfoSub.json` */ +export interface WeaponInfoSubEntry extends WeaponInfoEntry { + LockerGoodsSubWeaponInfo: string; +} + +/** `data/mush//WeaponInfoSpecial.json` */ +export interface WeaponInfoSpecialEntry extends WeaponInfoEntry { + StandAlone: boolean; +} + +/** `data/mush//GearInfo{Head,Clothes,Shoes}.json` — all three share one shape. */ +export interface GearInfoEntry { + AlphaMaskF: string; + AlphaMaskM: string; + AlphaMaskV1: string; + Brand: string; + CallSign: number; + CallSignPriority: number; + CaptureModelType: string; + Genre0: string; + Genre1: string; + HarnessType: string; + HeadParamSetPath: string; + HowToGet: string; + Id: number; + IsHideHarness: boolean; + IsThinHarness: boolean; + IsUnisex: boolean; + LObjParam: string; + LObjParamM: string; + Label: string; + Material: string; + Price: number; + Rarity: number; + Season: number; + Skill: string; + UrokoPrice: { + BronzeUrokoNum: number; + GoldUrokoNum: number; + SilverUrokoNum: number; + }; + UrokoUnlockLevel: number; + VariationNum: number; + __RowId: string; +} + +/** `data/parameter//misc/SplPlayer.game__GameParameterTable.json` */ +export interface SplPlayerParams { + GameParameters: { + spl__PlayerBeaconSubSpecUpParam: { + SubSpecUpParam: { High: number; Mid: number }; + }; + }; +} + +/** + * `data/parameter//misc/spl__DamageRateInfoConfig.pp__CombinationDataTableData.json` + * + * `DamageRate` is absent for cells that take no damage rate modifier. + */ +export interface DamageRateInfoConfig { + CellList: Record< + string, + { + ColumnKey: string; + DamageRate?: number; + RowKey: string; + } + >; + TableType: string; +} diff --git a/scripts/utils.ts b/scripts/utils.ts index c08068ce6..8423c05c0 100644 --- a/scripts/utils.ts +++ b/scripts/utils.ts @@ -1,17 +1,15 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import type euEn from "./dicts/splat3/data/language/EUen.json"; -// The splat3 dump exposes a `latest` symlink to the newest version folder, so these loaders never -// need to hard-code a version. -import type gearInfoClothes from "./dicts/splat3/data/mush/latest/GearInfoClothes.json"; -import type gearInfoHead from "./dicts/splat3/data/mush/latest/GearInfoHead.json"; -import type gearInfoShoes from "./dicts/splat3/data/mush/latest/GearInfoShoes.json"; -import type weaponInfoMain from "./dicts/splat3/data/mush/latest/WeaponInfoMain.json"; -import type weaponInfoSpecial from "./dicts/splat3/data/mush/latest/WeaponInfoSpecial.json"; -import type weaponInfoSub from "./dicts/splat3/data/mush/latest/WeaponInfoSub.json"; -import type splPlayer from "./dicts/splat3/data/parameter/latest/misc/SplPlayer.game__GameParameterTable.json"; -import type damageRateInfo from "./dicts/splat3/data/parameter/latest/misc/spl__DamageRateInfoConfig.pp__CombinationDataTableData.json"; +import type { + DamageRateInfoConfig, + GearInfoEntry, + LangDict, + SplPlayerParams, + WeaponInfoMainEntry, + WeaponInfoSpecialEntry, + WeaponInfoSubEntry, +} from "./splat3-types"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -41,7 +39,7 @@ export const LANG_JSONS_TO_CREATE = [ ]; export async function loadLangDicts() { - const result: Array<[langCode: string, translations: typeof euEn]> = []; + const result: Array<[langCode: string, translations: LangDict]> = []; const files = await fs.promises.readdir(LANG_DICTS_PATH); for (const file of files) { @@ -71,23 +69,23 @@ export function weaponParamsDir() { } export const loadWeaponInfoMain = () => - loadLatestMushJson("WeaponInfoMain"); + loadLatestMushJson("WeaponInfoMain"); export const loadWeaponInfoSub = () => - loadLatestMushJson("WeaponInfoSub"); + loadLatestMushJson("WeaponInfoSub"); export const loadWeaponInfoSpecial = () => - loadLatestMushJson("WeaponInfoSpecial"); + loadLatestMushJson("WeaponInfoSpecial"); export const loadGearInfoClothes = () => - loadLatestMushJson("GearInfoClothes"); + loadLatestMushJson("GearInfoClothes"); export const loadGearInfoHead = () => - loadLatestMushJson("GearInfoHead"); + loadLatestMushJson("GearInfoHead"); export const loadGearInfoShoes = () => - loadLatestMushJson("GearInfoShoes"); + loadLatestMushJson("GearInfoShoes"); export const loadSplPlayerParams = () => - loadLatestParameterMiscJson( + loadLatestParameterMiscJson( "SplPlayer.game__GameParameterTable", ); export const loadDamageRateInfo = () => - loadLatestParameterMiscJson( + loadLatestParameterMiscJson( "spl__DamageRateInfoConfig.pp__CombinationDataTableData", );