Fix scripts typecheck

This commit is contained in:
Kalle
2026-08-22 18:01:03 +03:00
parent f322faec4d
commit 3674a7dfe7
10 changed files with 173 additions and 35 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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`);
}

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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,

View File

@@ -19,7 +19,10 @@
* Usage: pnpm scanner:status-audit <events.csv> [--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) {

127
scripts/splat3-types.ts Normal file
View File

@@ -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/<Lang>.json` — message category -> message key -> localized (URI encoded) text. */
export type LangDict = Record<string, Record<string, string>>;
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/<version>/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/<version>/WeaponInfoSub.json` */
export interface WeaponInfoSubEntry extends WeaponInfoEntry {
LockerGoodsSubWeaponInfo: string;
}
/** `data/mush/<version>/WeaponInfoSpecial.json` */
export interface WeaponInfoSpecialEntry extends WeaponInfoEntry {
StandAlone: boolean;
}
/** `data/mush/<version>/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/<version>/misc/SplPlayer.game__GameParameterTable.json` */
export interface SplPlayerParams {
GameParameters: {
spl__PlayerBeaconSubSpecUpParam: {
SubSpecUpParam: { High: number; Mid: number };
};
};
}
/**
* `data/parameter/<version>/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;
}

View File

@@ -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<typeof weaponInfoMain>("WeaponInfoMain");
loadLatestMushJson<WeaponInfoMainEntry[]>("WeaponInfoMain");
export const loadWeaponInfoSub = () =>
loadLatestMushJson<typeof weaponInfoSub>("WeaponInfoSub");
loadLatestMushJson<WeaponInfoSubEntry[]>("WeaponInfoSub");
export const loadWeaponInfoSpecial = () =>
loadLatestMushJson<typeof weaponInfoSpecial>("WeaponInfoSpecial");
loadLatestMushJson<WeaponInfoSpecialEntry[]>("WeaponInfoSpecial");
export const loadGearInfoClothes = () =>
loadLatestMushJson<typeof gearInfoClothes>("GearInfoClothes");
loadLatestMushJson<GearInfoEntry[]>("GearInfoClothes");
export const loadGearInfoHead = () =>
loadLatestMushJson<typeof gearInfoHead>("GearInfoHead");
loadLatestMushJson<GearInfoEntry[]>("GearInfoHead");
export const loadGearInfoShoes = () =>
loadLatestMushJson<typeof gearInfoShoes>("GearInfoShoes");
loadLatestMushJson<GearInfoEntry[]>("GearInfoShoes");
export const loadSplPlayerParams = () =>
loadLatestParameterMiscJson<typeof splPlayer>(
loadLatestParameterMiscJson<SplPlayerParams>(
"SplPlayer.game__GameParameterTable",
);
export const loadDamageRateInfo = () =>
loadLatestParameterMiscJson<typeof damageRateInfo>(
loadLatestParameterMiscJson<DamageRateInfoConfig>(
"spl__DamageRateInfoConfig.pp__CombinationDataTableData",
);