* checkpoint

* server compiles again

* fmt

* checkpoint

* last one out, get the lights

* euston station

* like, mostly good. I'm under the ocean right now.

* fix: some nonsense AI stuff

* fix: new eslint rules, and cleanup imports across the board

* feat: folder slug rewrite, email logic change, too

* fix: register must use txn not db

* checkpoint
This commit is contained in:
zk
2026-04-20 23:11:16 +01:00
committed by GitHub
parent edda03e916
commit 2bc4c3a628
1073 changed files with 52259 additions and 56015 deletions
+1 -1
View File
@@ -13,7 +13,7 @@
},
"scripts": {
"test": "vitest",
"bench": "vitest bench --passWithNoTests",
"bench": "vitest bench",
"build": "tsgo -p tsconfig.build.json",
"prepublishOnly": "tsgo -p tsconfig.build.json",
"lint": "eslint .",
+11 -2
View File
@@ -1,6 +1,15 @@
import { describe, expect, it } from "vitest";
import { allSupportedGameGroups, GetGameGroupConfig, GetGamePTConfig } from "./config";
import { IsValidGame } from "../utils/util";
import { allSupportedGameGroups, GetGameGroupConfig, LEGACY_GetGamePTConfig } from "./config";
describe("#IsValidGame", () => {
it("accepts games and rejects unknown strings", () => {
expect(IsValidGame("iidx-sp")).toBe(true);
expect(IsValidGame("not-a-real-game")).toBe(false);
expect(IsValidGame("")).toBe(false);
});
});
describe("#GetGameGroupConfig", () => {
it("defines configs for every supported game group with valid IDs", () => {
@@ -34,7 +43,7 @@ describe("#GetGamePTConfig", () => {
const gameConfig = GetGameGroupConfig(game);
for (const playtype of gameConfig.playtypes) {
const conf = GetGamePTConfig(game, playtype);
const conf = LEGACY_GetGamePTConfig(game, playtype);
expect(conf, `'${game}:${playtype}' should have a config defined.`).toBeDefined();
if (!conf) {
+127 -106
View File
@@ -1,37 +1,47 @@
import { p } from "prudence";
import type {
GameConfig,
GameGroup,
GameGroupConfig,
GamePTConfig,
GPTString,
Playtype,
Playtypes,
LEGACY_GPTString,
LEGACY_Playtype,
LEGACY_Playtypes,
SpecificGameConfig,
V3Game,
} from "../types/game-config";
import type {
INTERNAL_GAME_CONFIG as INTERNAL_GAME_GROUP_CONFIG,
INTERNAL_GAME_PT_CONFIG,
} from "../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../types/internals";
import type { ConfEnumScoreMetric, ConfScoreMetric } from "../types/metrics";
import { ARCAEA_CONF, ARCAEA_TOUCH_CONF } from "./game-support/arcaea";
import { BMS_7K_CONF, BMS_14K_CONF, BMS_CONF } from "./game-support/bms";
import { CHUNITHM_CONF, CHUNITHM_SINGLE_CONF } from "./game-support/chunithm";
import { DDR_CONF, DDR_DP_CONF, DDR_SP_CONF } from "./game-support/ddr";
import { GITADORA_CONF, GITADORA_DORA_CONF, GITADORA_GITA_CONF } from "./game-support/gitadora";
import { IIDX_CONF, IIDX_DP_CONF, IIDX_SP_CONF } from "./game-support/iidx";
import { ITG_CONF, ITG_STAMINA_CONF } from "./game-support/itg";
import { JUBEAT_CONF, JUBEAT_SINGLE_CONF } from "./game-support/jubeat";
import { MAIMAI_CONF, MAIMAI_SINGLE_CONF } from "./game-support/maimai";
import { MAIMAI_DX_CONF, MAIMAI_DX_SINGLE_CONF } from "./game-support/maimai-dx";
import { MUSECA_CONF, MUSECA_SINGLE_CONF } from "./game-support/museca";
import { ONGEKI_CONF, ONGEKI_SINGLE_CONF } from "./game-support/ongeki";
import { PMS_CONF, PMS_CONTROLLER_CONF, PMS_KEYBOARD_CONF } from "./game-support/pms";
import { POPN_9B_CONF, POPN_CONF } from "./game-support/popn";
import { SDVX_CONF, SDVX_SINGLE_CONF } from "./game-support/sdvx";
import { USC_CONF, USC_CONTROLLER_CONF, USC_KEYBOARD_CONF } from "./game-support/usc";
import { WACCA_CONF, WACCA_SINGLE_CONF } from "./game-support/wacca";
import { GAME_ARCAEA_CONF, GAME_GROUP_ARCAEA_CONF } from "./game-support/arcaea";
import { GAME_BMS_7K_CONF, GAME_BMS_14K_CONF, GAME_GROUP_BMS_CONF } from "./game-support/bms";
import { GAME_CHUNITHM_CONF, GAME_GROUP_CHUNITHM_CONF } from "./game-support/chunithm";
import { GAME_DDR_DP_CONF, GAME_DDR_SP_CONF, GAME_GROUP_DDR_CONF } from "./game-support/ddr";
import {
GAME_GITADORA_DORA_CONF,
GAME_GITADORA_GITA_CONF,
GAME_GROUP_GITADORA_CONF,
} from "./game-support/gitadora";
import { GAME_GROUP_IIDX_CONF, GAME_IIDX_DP_CONF, GAME_IIDX_SP_CONF } from "./game-support/iidx";
import { GAME_GROUP_ITG_CONF, GAME_ITG_STAMINA_CONF } from "./game-support/itg";
import { GAME_GROUP_JUBEAT_CONF, GAME_JUBEAT_SINGLE_CONF } from "./game-support/jubeat";
import { GAME_GROUP_MAIMAI_CONF, GAME_MAIMAI_CONF } from "./game-support/maimai";
import { GAME_GROUP_MAIMAI_DX_CONF, GAME_MAIMAI_DX_CONF } from "./game-support/maimai-dx";
import { GAME_GROUP_MUSECA_CONF, GAME_MUSECA_CONF } from "./game-support/museca";
import { GAME_GROUP_ONGEKI_CONF, GAME_ONGEKI_CONF } from "./game-support/ongeki";
import {
GAME_PMS_CONF,
GAME_PMS_CONTROLLER_CONF,
GAME_PMS_KEYBOARD_CONF,
} from "./game-support/pms";
import { GAME_GROUP_POPN_CONF, GAME_POPN_CONF } from "./game-support/popn";
import { GAME_GROUP_SDVX_CONF, GAME_SDVX_CONF } from "./game-support/sdvx";
import {
GAME_GROUP_USC_CONF,
GAME_USC_CONTROLLER_CONF,
GAME_USC_KEYBOARD_CONF,
} from "./game-support/usc";
import { GAME_GROUP_WACCA_CONF, GAME_WACCA_CONF } from "./game-support/wacca";
/**
* All game groups that Tachi supports.
@@ -39,23 +49,23 @@ import { WACCA_CONF, WACCA_SINGLE_CONF } from "./game-support/wacca";
* @warn DO NOT ACCESS THIS DIRECTLY! Use @see {GetGameGroupConfig} for better type safety.
*/
export const GAME_GROUP_CONFIGS = {
iidx: IIDX_CONF,
museca: MUSECA_CONF,
chunithm: CHUNITHM_CONF,
bms: BMS_CONF,
gitadora: GITADORA_CONF,
jubeat: JUBEAT_CONF,
maimai: MAIMAI_CONF,
maimaidx: MAIMAI_DX_CONF,
popn: POPN_CONF,
sdvx: SDVX_CONF,
usc: USC_CONF,
wacca: WACCA_CONF,
pms: PMS_CONF,
itg: ITG_CONF,
arcaea: ARCAEA_CONF,
ongeki: ONGEKI_CONF,
ddr: DDR_CONF,
iidx: GAME_GROUP_IIDX_CONF,
museca: GAME_GROUP_MUSECA_CONF,
chunithm: GAME_GROUP_CHUNITHM_CONF,
bms: GAME_GROUP_BMS_CONF,
gitadora: GAME_GROUP_GITADORA_CONF,
jubeat: GAME_GROUP_JUBEAT_CONF,
maimai: GAME_GROUP_MAIMAI_CONF,
maimaidx: GAME_GROUP_MAIMAI_DX_CONF,
popn: GAME_GROUP_POPN_CONF,
sdvx: GAME_GROUP_SDVX_CONF,
usc: GAME_GROUP_USC_CONF,
wacca: GAME_GROUP_WACCA_CONF,
pms: GAME_PMS_CONF,
itg: GAME_GROUP_ITG_CONF,
arcaea: GAME_GROUP_ARCAEA_CONF,
ongeki: GAME_GROUP_ONGEKI_CONF,
ddr: GAME_GROUP_DDR_CONF,
} as const satisfies Record<string, INTERNAL_GAME_GROUP_CONFIG>;
/**
@@ -70,45 +80,45 @@ export function GetGameGroupConfig<G extends GameGroup>(game: G): GameGroupConfi
/**
* Given a game and playtype, combine them into a GPTString.
*/
export function GetGPTString(game: GameGroup, playtype: Playtype): GPTString {
return `${game}:${playtype}` as GPTString;
export function LEGACY_GetGPTString(game: GameGroup, playtype: LEGACY_Playtype): LEGACY_GPTString {
return `${game}:${playtype}` as LEGACY_GPTString;
}
export function SplitGPT(gpt: GPTString) {
return gpt.split(":") as [GameGroup, Playtype];
export function LEGACY_SplitGPT(gpt: LEGACY_GPTString) {
return gpt.split(":") as [GameGroup, LEGACY_Playtype];
}
/**
* Based on every declared playtype for every declared game, they all need a GPT
* config. This controls almost everything about each GPT.
*/
export const GAME_PT_CONFIGS = {
"iidx:SP": IIDX_SP_CONF,
"iidx:DP": IIDX_DP_CONF,
"museca:Single": MUSECA_SINGLE_CONF,
"sdvx:Single": SDVX_SINGLE_CONF,
"bms:14K": BMS_14K_CONF,
"bms:7K": BMS_7K_CONF,
"gitadora:Dora": GITADORA_DORA_CONF,
"gitadora:Gita": GITADORA_GITA_CONF,
"chunithm:Single": CHUNITHM_SINGLE_CONF,
"wacca:Single": WACCA_SINGLE_CONF,
"jubeat:Single": JUBEAT_SINGLE_CONF,
"popn:9B": POPN_9B_CONF,
"maimai:Single": MAIMAI_SINGLE_CONF,
"maimaidx:Single": MAIMAI_DX_SINGLE_CONF,
"pms:Controller": PMS_CONTROLLER_CONF,
"pms:Keyboard": PMS_KEYBOARD_CONF,
"usc:Controller": USC_CONTROLLER_CONF,
"usc:Keyboard": USC_KEYBOARD_CONF,
"itg:Stamina": ITG_STAMINA_CONF,
"arcaea:Touch": ARCAEA_TOUCH_CONF,
"ongeki:Single": ONGEKI_SINGLE_CONF,
"ddr:SP": DDR_SP_CONF,
"ddr:DP": DDR_DP_CONF,
} as const satisfies Record<GPTString, INTERNAL_GAME_PT_CONFIG>;
export const GAME_CONFIGS = {
"iidx-sp": GAME_IIDX_SP_CONF,
"iidx-dp": GAME_IIDX_DP_CONF,
museca: GAME_MUSECA_CONF,
sdvx: GAME_SDVX_CONF,
"bms-14k": GAME_BMS_14K_CONF,
"bms-7k": GAME_BMS_7K_CONF,
"gitadora-dora": GAME_GITADORA_DORA_CONF,
"gitadora-gita": GAME_GITADORA_GITA_CONF,
chunithm: GAME_CHUNITHM_CONF,
wacca: GAME_WACCA_CONF,
jubeat: GAME_JUBEAT_SINGLE_CONF,
popn: GAME_POPN_CONF,
maimai: GAME_MAIMAI_CONF,
maimaidx: GAME_MAIMAI_DX_CONF,
"pms-controller": GAME_PMS_CONTROLLER_CONF,
"pms-keyboard": GAME_PMS_KEYBOARD_CONF,
"usc-controller": GAME_USC_CONTROLLER_CONF,
"usc-keyboard": GAME_USC_KEYBOARD_CONF,
"itg-stamina": GAME_ITG_STAMINA_CONF,
arcaea: GAME_ARCAEA_CONF,
ongeki: GAME_ONGEKI_CONF,
"ddr-sp": GAME_DDR_SP_CONF,
"ddr-dp": GAME_DDR_DP_CONF,
} as const satisfies Record<V3Game, INTERNAL_GAME_CONFIG>;
const v3GameMappings: Record<GPTString, V3Game> = {
const v3GameMappings: Record<LEGACY_GPTString, V3Game> = {
"iidx:SP": "iidx-sp",
"iidx:DP": "iidx-dp",
"museca:Single": "museca",
@@ -134,16 +144,16 @@ const v3GameMappings: Record<GPTString, V3Game> = {
"ddr:DP": "ddr-dp",
};
export function GamePTToV3(game: GameGroup, playtype: Playtype): V3Game {
return GPTStringToV3(GetGPTString(game, playtype));
export function LEGACY_GameGroupPTToGame(game: GameGroup, playtype: LEGACY_Playtype): V3Game {
return GPTStringToGame(LEGACY_GetGPTString(game, playtype));
}
export function GPTStringToV3(gptString: GPTString): V3Game {
export function GPTStringToGame(gptString: LEGACY_GPTString): V3Game {
return v3GameMappings[gptString];
}
export function V3ToGPTString(v3Game: V3Game): GPTString {
const mapping: Record<V3Game, GPTString> = {
export function LEGACY_GameToGPTString(game: V3Game): LEGACY_GPTString {
const mapping: Record<V3Game, LEGACY_GPTString> = {
"iidx-sp": "iidx:SP",
"iidx-dp": "iidx:DP",
museca: "museca:Single",
@@ -169,12 +179,12 @@ export function V3ToGPTString(v3Game: V3Game): GPTString {
"ddr-sp": "ddr:SP",
};
return mapping[v3Game];
return mapping[game];
}
export const v3AllGames = Object.values(v3GameMappings);
export const ALL_GAMES = Object.keys(GAME_CONFIGS) as Array<V3Game>;
export function V3ToGameGroup(v3Game: V3Game): GameGroup {
export function GameToGameGroup(game: V3Game): GameGroup {
const mapping: Record<V3Game, GameGroup> = {
"iidx-sp": "iidx",
"iidx-dp": "iidx",
@@ -201,51 +211,62 @@ export function V3ToGameGroup(v3Game: V3Game): GameGroup {
"ddr-dp": "ddr",
};
return mapping[v3Game];
return mapping[game];
}
export function V3ToGamePT(v3Game: V3Game): { game: GameGroup; playtype: Playtype } {
const gptString = V3ToGPTString(v3Game);
const [game, playtype] = SplitGPT(gptString);
return { game, playtype };
export function LEGACY_GameToGameGroupPT(v3Game: V3Game): {
gameGroup: GameGroup;
playtype: LEGACY_Playtype;
} {
const gptString = LEGACY_GameToGPTString(v3Game);
const [gameGroup, playtype] = LEGACY_SplitGPT(gptString);
return { gameGroup, playtype };
}
export function LEGACY_GameToPlaytypeFn(v3Game: V3Game): LEGACY_Playtype {
return LEGACY_GameToGameGroupPT(v3Game).playtype;
}
/**
* Returns the configuration for this Game + Playtype. The type here is expanded to
* its most generic form, for easiest interaction.
*/
export function GetGamePTConfig(game: GameGroup, playtype: Playtypes[GameGroup]): GamePTConfig {
const gptString = GetGPTString(game, playtype);
export function LEGACY_GetGamePTConfig(
gameGroup: GameGroup,
playtype: LEGACY_Playtypes[GameGroup],
): GameConfig {
const game = LEGACY_GameGroupPTToGame(gameGroup, playtype);
return GAME_PT_CONFIGS[gptString] as unknown as GamePTConfig;
return GAME_CONFIGS[game] as unknown as GameConfig;
}
export function GetGPTConfig(gptString: GPTString): GamePTConfig {
return GAME_PT_CONFIGS[gptString] as unknown as GamePTConfig;
export function LEGACY_GetGPTConfig(gptString: LEGACY_GPTString): GameConfig {
const game = GPTStringToGame(gptString);
return GAME_CONFIGS[game] as unknown as GameConfig;
}
export function V3GetGameConfig(game: V3Game): GamePTConfig {
return GAME_PT_CONFIGS[V3ToGPTString(game)] as unknown as GamePTConfig;
export function GetGameConfig(game: V3Game): GameConfig {
return GAME_CONFIGS[game] as unknown as GameConfig;
}
/**
* Returns the configuration for this specific Game + Playtype. This type is narrowed
* down to its least generic form, and is instead for gpt-specific use cases.
*/
export function GetSpecificGPTConfig<GPT extends GPTString>(gpt: GPT) {
return GAME_PT_CONFIGS[gpt];
export function GetSpecificGameConfig<TGame extends V3Game>(game: TGame) {
return GAME_CONFIGS[game] as unknown as SpecificGameConfig<TGame>;
}
export const allSupportedGameGroups = Object.keys(GAME_GROUP_CONFIGS) as Array<GameGroup>;
export const allGPTStrings = Object.keys(GAME_PT_CONFIGS) as Array<GPTString>;
export const allGPTStrings = Object.keys(GAME_CONFIGS) as Array<LEGACY_GPTString>;
export function GetScoreMetrics(
gptConfig: GamePTConfig,
gameConfig: GameConfig,
type?: Array<ConfScoreMetric["type"]> | ConfScoreMetric["type"],
) {
let metrics = [
...Object.entries(gptConfig.providedMetrics),
...Object.entries(gptConfig.derivedMetrics),
...Object.entries(gameConfig.providedMetrics),
...Object.entries(gameConfig.derivedMetrics),
];
if (Array.isArray(type)) {
@@ -257,10 +278,10 @@ export function GetScoreMetrics(
return metrics.map((e) => e[0]);
}
export function GetScoreEnumConfs(gptConfig: GamePTConfig) {
export function GetScoreEnumConfs(gameConfig: GameConfig) {
const scoreMetrics = {
...gptConfig.providedMetrics,
...gptConfig.derivedMetrics,
...gameConfig.providedMetrics,
...gameConfig.derivedMetrics,
};
const enumMetrics: Record<string, ConfEnumScoreMetric<string>> = {};
@@ -282,10 +303,10 @@ export function GetScoreEnumConfs(gptConfig: GamePTConfig) {
*
* @note GRAPH and NULLABLE_GRAPH types are never valid here.
*/
export function ValidateMetric(gptConfig: GamePTConfig, metricName: string, metricValue: number) {
const scoreMetrics = GetScoreMetrics(gptConfig, ["DECIMAL", "INTEGER", "ENUM"]);
export function ValidateMetric(gameConfig: GameConfig, metricName: string, metricValue: number) {
const scoreMetrics = GetScoreMetrics(gameConfig, ["DECIMAL", "INTEGER", "ENUM"]);
const conf = gptConfig.providedMetrics[metricName] ?? gptConfig.derivedMetrics[metricName];
const conf = gameConfig.providedMetrics[metricName] ?? gameConfig.derivedMetrics[metricName];
if (!conf || !scoreMetrics.includes(metricName)) {
return `Invalid metric ${metricName}, Expected any of ${scoreMetrics.join(", ")}.`;
@@ -306,6 +327,6 @@ export function ValidateMetric(gptConfig: GamePTConfig, metricName: string, metr
return conf.validate(metricValue);
}
export function GetScoreMetricConf(gptConfig: GamePTConfig, metric: string) {
return gptConfig.providedMetrics[metric] ?? gptConfig.derivedMetrics[metric];
export function GetScoreMetricConf(gameConfig: GameConfig, metric: string) {
return gameConfig.providedMetrics[metric] ?? gameConfig.derivedMetrics[metric];
}
@@ -1,20 +1,21 @@
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum } from "../../utils/util";
import { ClassValue, ToDecimalPlaces, zodNonNegativeInt } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const ARCAEA_CONF = {
export const GAME_GROUP_ARCAEA_CONF = {
name: "Arcaea",
games: ["arcaea"],
// Potential future controller playtype support?
playtypes: ["Touch"],
songData: z.strictObject({
displayVersion: z.string(),
songPack: z.string(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
const ArcaeaBadges = [
ClassValue("BLUE", "Blue", "0.00 - 3.49 Potential"),
@@ -41,7 +42,7 @@ const ArcaeaClasses = [
ClassValue("PHASE_11", "Phase 11", "Radiant Genesis"),
];
export const ARCAEA_TOUCH_CONF = {
export const GAME_ARCAEA_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -100,7 +101,7 @@ export const ARCAEA_TOUCH_CONF = {
difficulties: {
type: "FIXED",
order: ["Past", "Present", "Future", "Beyond"],
shorthand: {
format: {
Past: "PST",
Present: "PRS",
Future: "FTR",
@@ -136,4 +137,4 @@ export const ARCAEA_TOUCH_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["inGameStrID", "songTitle", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,14 +1,15 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtPercent, FmtScoreNoCommas } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const BMS_CONF = {
export const GAME_GROUP_BMS_CONF = {
name: "BMS",
games: ["bms-7k", "bms-14k"],
playtypes: ["7K", "14K"],
songData: z.strictObject({
genre: z.nullable(z.string()),
@@ -16,7 +17,7 @@ export const BMS_CONF = {
subartist: z.nullable(z.string()),
tableString: z.nullable(z.string()),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export function FormatSieglindeBMS(sgl: number): string {
if (sgl < 13) {
@@ -161,7 +162,7 @@ export const BMSScratchDans = [
const RANDOM_SCHEMA = z.enum(["MIRROR", "NONRAN", "R-RANDOM", "RANDOM", "S-RANDOM"]);
export const BMS_7K_CONF = {
export const GAME_BMS_7K_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -351,7 +352,7 @@ export const BMS_7K_CONF = {
// "CHART" isn't a very creative name, but this is in a similar vein to how
// games with no playtypes use the value "Single".
order: ["CHART"],
shorthand: {},
format: {},
default: "CHART",
},
@@ -389,10 +390,10 @@ export const BMS_7K_CONF = {
}),
supportedMatchTypes: ["bmsChartHash", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
export const BMS_14K_CONF = {
...BMS_7K_CONF,
export const GAME_BMS_14K_CONF = {
...GAME_BMS_7K_CONF,
classes: {
genocideDan: { type: "PROVIDED", values: BMSGenocideDans },
@@ -405,4 +406,4 @@ export const BMS_14K_CONF = {
client: z.enum(["lr2oraja", "LR2"]).optional().nullable(),
gauge: z.enum(["EASY", "NORMAL", "HARD", "EX-HARD"]).optional().nullable(),
}),
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,21 +1,22 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum } from "../../utils/util";
import { ClassValue, ToDecimalPlaces, zodNonNegativeInt } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const CHUNITHM_CONF = {
export const GAME_GROUP_CHUNITHM_CONF = {
name: "CHUNITHM",
games: ["chunithm"],
playtypes: ["Single"],
songData: z.strictObject({
genre: z.string(),
displayVersion: z.string(),
duration: z.number().optional(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export const CHUNITHMColours = [
ClassValue("BLUE", "青", "Blue: 0 - 1.99 Rating"),
@@ -45,7 +46,7 @@ export const CHUNITHMClasses = [
ClassValue("DAN_INFINITE", "∞", "Infinite Class"),
];
export const CHUNITHM_SINGLE_CONF = {
export const GAME_CHUNITHM_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -139,7 +140,7 @@ export const CHUNITHM_SINGLE_CONF = {
difficulties: {
type: "FIXED",
order: ["BASIC", "ADVANCED", "EXPERT", "MASTER", "ULTIMA"],
shorthand: {
format: {
BASIC: "B",
ADVANCED: "A",
EXPERT: "E",
@@ -203,4 +204,4 @@ export const CHUNITHM_SINGLE_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["inGameID", "songTitle", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,7 +1,7 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum, FmtScoreNoCommas } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
@@ -55,8 +55,9 @@ export const DDRFlare = [
ClassValue("WORLD", "世界 / WORLD"),
];
export const DDR_CONF = {
export const GAME_GROUP_DDR_CONF = {
name: "DDR",
games: ["ddr-sp", "ddr-dp"],
playtypes: ["SP", "DP"],
songData: z.strictObject({
inGameID: zodNonNegativeInt,
@@ -64,9 +65,9 @@ export const DDR_CONF = {
basename: z.string().optional(),
ddrSongHash: z.string().optional(), // optional because konaste-only songs have no hashes
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export const DDR_SP_CONF = {
export const GAME_DDR_SP_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -174,7 +175,7 @@ export const DDR_SP_CONF = {
difficulties: {
type: "FIXED",
order: DDR_DIFFICULTIES,
shorthand: {
format: {
BEGINNER: "BEG",
BASIC: "BAS",
DIFFICULT: "DIF",
@@ -212,8 +213,8 @@ export const DDR_SP_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["inGameID", "songTitle", "tachiSongID", "ddrSongHash"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
export const DDR_DP_CONF = {
...DDR_SP_CONF,
} as const satisfies INTERNAL_GAME_PT_CONFIG;
export const GAME_DDR_DP_CONF = {
...GAME_DDR_SP_CONF,
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,17 +1,18 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtPercent } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const GITADORA_CONF = {
export const GAME_GROUP_GITADORA_CONF = {
name: "GITADORA",
games: ["gitadora-gita", "gitadora-dora"],
playtypes: ["Gita", "Dora"],
songData: z.strictObject({}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
const GitadoraColours = [
ClassValue("WHITE", "白", "White"),
@@ -33,7 +34,7 @@ const GitadoraColours = [
ClassValue("RAINBOW", "虹", "Rainbow"),
];
export const GITADORA_GITA_CONF = {
export const GAME_GITADORA_GITA_CONF = {
providedMetrics: {
percent: {
type: "DECIMAL",
@@ -94,7 +95,7 @@ export const GITADORA_GITA_CONF = {
"BASS EXTREME",
"BASS MASTER",
],
shorthand: {
format: {
BASIC: "G-BSC",
ADVANCED: "G-ADV",
EXTREME: "G-EXT",
@@ -131,15 +132,15 @@ export const GITADORA_GITA_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["inGameID", "songTitle", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
export const GITADORA_DORA_CONF = {
...GITADORA_GITA_CONF,
export const GAME_GITADORA_DORA_CONF = {
...GAME_GITADORA_GITA_CONF,
difficulties: {
type: "FIXED",
order: ["BASIC", "ADVANCED", "EXTREME", "MASTER"],
shorthand: {
format: {
BASIC: "D-BSC",
ADVANCED: "D-ADV",
EXTREME: "D-EXT",
@@ -147,4 +148,4 @@ export const GITADORA_DORA_CONF = {
},
default: "EXTREME",
},
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,13 +1,14 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum, FmtPercent, FmtScoreNoCommas } from "../../utils/util";
import { ClassValue, zodNonNegativeInt, zodTierlistData } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const IIDX_CONF = {
export const GAME_GROUP_IIDX_CONF = {
games: ["iidx-sp", "iidx-dp"],
name: "beatmania IIDX",
playtypes: ["SP", "DP"],
songData: z.strictObject({
@@ -19,7 +20,7 @@ export const IIDX_CONF = {
eamusementCsvArtist: z.optional(z.string()),
eamusementCsvGenre: z.optional(z.string()),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export const IIDXDans = [
ClassValue("KYU_7", "七級", "7th Kyu"),
@@ -55,7 +56,7 @@ const BASE_IIDX_CHART_DATA = {
const RANDOM_SCHEMA = z.enum(["NONRAN", "MIRROR", "R-RANDOM", "RANDOM", "S-RANDOM"]);
export const IIDX_SP_CONF = {
export const GAME_IIDX_SP_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -209,23 +210,23 @@ export const IIDX_SP_CONF = {
"Kiraku ANOTHER",
"Kiraku LEGGENDARIA",
],
shorthand: {
NORMAL: "N",
HYPER: "H",
ANOTHER: "A",
LEGGENDARIA: "L",
"All Scratch NORMAL": "N (Scr.)",
"All Scratch HYPER": "H (Scr.)",
"All Scratch ANOTHER": "A (Scr.)",
"All Scratch LEGGENDARIA": "L (Scr.)",
"Kichiku NORMAL": "N (Kc.)",
"Kichiku HYPER": "H (Kc.)",
"Kichiku ANOTHER": "A (Kc.)",
"Kichiku LEGGENDARIA": "L (Kc.)",
"Kiraku NORMAL": "N (Kr.)",
"Kiraku HYPER": "H (Kr.)",
"Kiraku ANOTHER": "A (Kr.)",
"Kiraku LEGGENDARIA": "L (Kr.)",
format: {
NORMAL: "SPN",
HYPER: "SPH",
ANOTHER: "SPA",
LEGGENDARIA: "SPL",
"All Scratch NORMAL": "SPN (Scr.)",
"All Scratch HYPER": "SPH (Scr.)",
"All Scratch ANOTHER": "SPA (Scr.)",
"All Scratch LEGGENDARIA": "SPL (Scr.)",
"Kichiku NORMAL": "SPN (Kc.)",
"Kichiku HYPER": "SPH (Kc.)",
"Kichiku ANOTHER": "SPA (Kc.)",
"Kichiku LEGGENDARIA": "SPL (Kc.)",
"Kiraku NORMAL": "SPN (Kr.)",
"Kiraku HYPER": "SPH (Kr.)",
"Kiraku ANOTHER": "SPA (Kr.)",
"Kiraku LEGGENDARIA": "SPL (Kr.)",
},
default: "ANOTHER",
},
@@ -305,10 +306,10 @@ export const IIDX_SP_CONF = {
}),
supportedMatchTypes: ["inGameID", "tachiSongID", "songTitle"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
export const IIDX_DP_CONF = {
...IIDX_SP_CONF,
export const GAME_IIDX_DP_CONF = {
...GAME_IIDX_SP_CONF,
chartData: z.strictObject({
...BASE_IIDX_CHART_DATA,
@@ -322,4 +323,45 @@ export const IIDX_DP_CONF = {
range: z.enum(["NONE", "HIDDEN+", "SUDDEN+", "LIFT", "LIFT SUD+", "SUD+ HID+"]).optional(),
gauge: z.enum(["ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD"]).optional(),
}),
} as const satisfies INTERNAL_GAME_PT_CONFIG;
difficulties: {
type: "FIXED",
order: [
"NORMAL",
"HYPER",
"ANOTHER",
"LEGGENDARIA",
"All Scratch NORMAL",
"All Scratch HYPER",
"All Scratch ANOTHER",
"All Scratch LEGGENDARIA",
"Kichiku NORMAL",
"Kichiku HYPER",
"Kichiku ANOTHER",
"Kichiku LEGGENDARIA",
"Kiraku NORMAL",
"Kiraku HYPER",
"Kiraku ANOTHER",
"Kiraku LEGGENDARIA",
],
format: {
NORMAL: "DPN",
HYPER: "DPH",
ANOTHER: "DPA",
LEGGENDARIA: "DPL",
"All Scratch NORMAL": "DPN (Scr.)",
"All Scratch HYPER": "DPH (Scr.)",
"All Scratch ANOTHER": "DPA (Scr.)",
"All Scratch LEGGENDARIA": "DPL (Scr.)",
"Kichiku NORMAL": "DPN (Kc.)",
"Kichiku HYPER": "DPH (Kc.)",
"Kichiku ANOTHER": "DPA (Kc.)",
"Kichiku LEGGENDARIA": "DPL (Kc.)",
"Kiraku NORMAL": "DPN (Kr.)",
"Kiraku HYPER": "DPH (Kr.)",
"Kiraku ANOTHER": "DPA (Kr.)",
"Kiraku LEGGENDARIA": "DPL (Kr.)",
},
default: "ANOTHER",
},
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,20 +1,21 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtPercent } from "../../utils/util";
import { NoDecimalPlace, zodNonNegativeInt } from "../config-utils";
import { NoDecimalPlace } from "../config-utils";
export const ITG_CONF = {
export const GAME_GROUP_ITG_CONF = {
name: "ITG",
games: ["itg-stamina"],
playtypes: ["Stamina"],
songData: z.strictObject({
subtitle: z.string().nullable(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export const ITG_STAMINA_CONF = {
export const GAME_ITG_STAMINA_CONF = {
providedMetrics: {
scorePercent: {
type: "DECIMAL",
@@ -192,4 +193,4 @@ export const ITG_STAMINA_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["itgChartHash"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,19 +1,20 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum, FmtPercent } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const JUBEAT_CONF = {
export const GAME_GROUP_JUBEAT_CONF = {
name: "jubeat",
games: ["jubeat"],
playtypes: ["Single"],
songData: z.strictObject({
displayVersion: z.string(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
const JubeatColours = [
ClassValue("BLACK", "Black"),
@@ -28,7 +29,7 @@ const JubeatColours = [
ClassValue("GOLD", "Gold"),
];
export const JUBEAT_SINGLE_CONF = {
export const GAME_JUBEAT_SINGLE_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -97,7 +98,7 @@ export const JUBEAT_SINGLE_CONF = {
difficulties: {
type: "FIXED",
order: ["BSC", "ADV", "EXT", "HARD BSC", "HARD ADV", "HARD EXT"],
shorthand: {
format: {
BSC: "BSC",
ADV: "ADV",
EXT: "EXT",
@@ -139,4 +140,4 @@ export const JUBEAT_SINGLE_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["inGameID", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,20 +1,21 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtPercent } from "../../utils/util";
import { ClassValue, NoDecimalPlace } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const MAIMAI_DX_CONF = {
export const GAME_GROUP_MAIMAI_DX_CONF = {
name: "maimai DX",
games: ["maimaidx"],
playtypes: ["Single"],
songData: z.strictObject({
genre: z.string(),
duration: z.number().optional(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
const MaimaiDXDans = [
ClassValue("DAN_1", "初段", "1st Dan"),
@@ -65,7 +66,6 @@ const MaimaiDXMatchingClasses = [
ClassValue("B2", "B2"),
ClassValue("B1", "B1"),
ClassValue("A5", "A5"),
ClassValue("A5", "A5"),
ClassValue("A4", "A4"),
ClassValue("A3", "A3"),
@@ -93,7 +93,7 @@ const MaimaiDXMatchingClasses = [
ClassValue("LEGEND", "Legend"),
];
export const MAIMAI_DX_SINGLE_CONF = {
export const GAME_MAIMAI_DX_CONF = {
providedMetrics: {
percent: {
type: "DECIMAL",
@@ -187,7 +187,7 @@ export const MAIMAI_DX_SINGLE_CONF = {
"DX Master",
"DX Re:Master",
],
shorthand: {
format: {
Basic: "BAS",
Advanced: "ADV",
Expert: "EXP",
@@ -244,4 +244,4 @@ export const MAIMAI_DX_SINGLE_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["songTitle", "tachiSongID", "inGameID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,20 +1,21 @@
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtPercent } from "../../utils/util";
import { ClassValue, ToDecimalPlaces } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const MAIMAI_CONF = {
export const GAME_GROUP_MAIMAI_CONF = {
name: "maimai",
games: ["maimai"],
playtypes: ["Single"],
songData: z.strictObject({
titleJP: z.string(),
artistJP: z.string(),
displayVersion: z.string(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
const MaimaiDans = [
ClassValue("DAN_1", "初段", "1st Dan"),
@@ -55,7 +56,7 @@ const MaimaiColours = [
ClassValue("RAINBOW", "Rainbow", ">=15 Rating"),
];
export const MAIMAI_SINGLE_CONF = {
export const GAME_MAIMAI_CONF = {
providedMetrics: {
percent: {
type: "DECIMAL",
@@ -130,7 +131,7 @@ export const MAIMAI_SINGLE_CONF = {
difficulties: {
type: "FIXED",
order: ["Easy", "Basic", "Advanced", "Expert", "Master", "Re:Master"],
shorthand: {
format: {
Easy: "ESY",
Basic: "BAS",
Advanced: "ADV",
@@ -169,4 +170,4 @@ export const MAIMAI_SINGLE_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["songTitle", "tachiSongID", "inGameID", "inGameStrID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,23 +1,24 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum } from "../../utils/util";
import { NoDecimalPlace, zodNonNegativeInt } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const MUSECA_CONF = {
export const GAME_GROUP_MUSECA_CONF = {
name: "MÚSECA",
games: ["museca"],
playtypes: ["Single"],
songData: z.strictObject({
titleJP: z.string(),
artistJP: z.string(),
displayVersion: z.string(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export const MUSECA_SINGLE_CONF = {
export const GAME_MUSECA_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -84,7 +85,7 @@ export const MUSECA_SINGLE_CONF = {
difficulties: {
type: "FIXED",
order: ["Green", "Yellow", "Red"],
shorthand: { Green: "G", Yellow: "Y", Red: "R" },
format: { Green: "G", Yellow: "Y", Red: "R" },
default: "Red",
},
@@ -106,4 +107,4 @@ export const MUSECA_SINGLE_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["songTitle", "tachiSongID", "inGameID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,14 +1,15 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum, FmtStars } from "../../utils/util";
import { ClassValue, ToDecimalPlaces } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const ONGEKI_CONF = {
export const GAME_GROUP_ONGEKI_CONF = {
name: "O.N.G.E.K.I.",
games: ["ongeki"],
playtypes: ["Single"],
songData: z.strictObject({
genre: z.enum([
@@ -23,7 +24,7 @@ export const ONGEKI_CONF = {
]),
duration: z.number(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export const OngekiColours = [
ClassValue("BLUE", "水", "Blue: 0.000~3.999 Rating"),
@@ -40,7 +41,7 @@ export const OngekiColours = [
ClassValue("RAINBOW_EX", "虹(極)", "Rainbow Extreme: 21.000~ Rating"),
];
export const ONGEKI_SINGLE_CONF = {
export const GAME_ONGEKI_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -183,7 +184,7 @@ export const ONGEKI_SINGLE_CONF = {
difficulties: {
type: "FIXED",
order: ["BASIC", "ADVANCED", "EXPERT", "MASTER", "LUNATIC"],
shorthand: {
format: {
BASIC: "BAS",
ADVANCED: "ADV",
EXPERT: "EXP",
@@ -236,4 +237,4 @@ export const ONGEKI_SINGLE_CONF = {
scoreMeta: z.strictObject({}),
supportedMatchTypes: ["songTitle", "tachiSongID", "inGameID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,14 +1,15 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtPercent, FmtScoreNoCommas } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { BMS_7K_CONF } from "./bms";
import { GAME_BMS_7K_CONF } from "./bms";
export const PMS_CONF = {
export const GAME_PMS_CONF = {
name: "PMS",
games: ["pms-controller", "pms-keyboard"],
playtypes: ["Controller", "Keyboard"],
songData: z.strictObject({
genre: z.nullable(z.string()),
@@ -16,7 +17,7 @@ export const PMS_CONF = {
subartist: z.nullable(z.string()),
tableString: z.nullable(z.string()),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
const PMSDans = [
ClassValue("INSANE_1", "●1", "Insane 1st Dan"),
@@ -42,7 +43,7 @@ function FormatSieglindePMS(sgl: number): string {
return `●${(sgl - 12).toFixed(2)}`;
}
export const PMS_CONTROLLER_CONF = {
export const GAME_PMS_CONTROLLER_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -93,7 +94,7 @@ export const PMS_CONTROLLER_CONF = {
preferredDefaultEnum: "lamp",
optionalMetrics: {
...BMS_7K_CONF.optionalMetrics,
...GAME_BMS_7K_CONF.optionalMetrics,
},
scoreRatingAlgs: {
@@ -125,7 +126,7 @@ export const PMS_CONTROLLER_CONF = {
difficulties: {
type: "FIXED",
order: ["CHART"],
shorthand: {},
format: {},
default: "CHART",
},
@@ -154,6 +155,6 @@ export const PMS_CONTROLLER_CONF = {
}),
supportedMatchTypes: ["bmsChartHash", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
export const PMS_KEYBOARD_CONF = PMS_CONTROLLER_CONF;
export const GAME_PMS_KEYBOARD_CONF = GAME_PMS_CONTROLLER_CONF;
@@ -1,21 +1,22 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum, FmtPercent } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const POPN_CONF = {
export const GAME_GROUP_POPN_CONF = {
name: "pop'n music",
games: ["popn"],
playtypes: ["9B"],
songData: z.strictObject({
displayVersion: z.nullable(z.string()),
genre: z.string(),
genreEN: z.nullable(z.string()),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
const PopnClasses = [
ClassValue("KITTY", "にゃんこ", "Kitty"),
@@ -28,7 +29,7 @@ const PopnClasses = [
ClassValue("GOD", "神", "God"),
];
export const POPN_9B_CONF = {
export const GAME_POPN_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -118,7 +119,7 @@ export const POPN_9B_CONF = {
difficulties: {
type: "FIXED",
order: ["Easy", "Normal", "Hyper", "EX"],
shorthand: {
format: {
Easy: "E",
Normal: "N",
Hyper: "H",
@@ -160,4 +161,4 @@ export const POPN_9B_CONF = {
preferences: z.strictObject({}),
supportedMatchTypes: ["inGameID", "tachiSongID", "popnChartHash"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,19 +1,20 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum, FmtPercent } from "../../utils/util";
import { ClassValue, ToDecimalPlaces, zodNonNegativeInt, zodTierlistData } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const SDVX_CONF = {
export const GAME_GROUP_SDVX_CONF = {
name: "SOUND VOLTEX",
games: ["sdvx"],
playtypes: ["Single"],
songData: z.strictObject({
displayVersion: z.string(),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export const SDVXDans = [
ClassValue("DAN_1", "LV.01", "1st Dan"),
@@ -73,7 +74,7 @@ export const SDVXVFClasses = [
ClassValue("IMPERIAL_IV", "Imperial IV", ">23VF"),
];
export const SDVX_SINGLE_CONF = {
export const GAME_SDVX_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -158,7 +159,7 @@ export const SDVX_SINGLE_CONF = {
difficulties: {
type: "FIXED",
order: ["NOV", "ADV", "EXH", "INF", "GRV", "HVN", "VVD", "XCD", "MXM", "ULT"],
shorthand: {}, // they're all already short enough
format: {}, // they're all already short enough
default: "EXH",
},
@@ -199,4 +200,4 @@ export const SDVX_SINGLE_CONF = {
}),
supportedMatchTypes: ["sdvxInGameID", "songTitle", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -1,20 +1,21 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum, FmtPercent } from "../../utils/util";
import { ToDecimalPlaces } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
import { SDVXVFClasses } from "./sdvx";
export const USC_CONF = {
export const GAME_GROUP_USC_CONF = {
name: "USC",
games: ["usc-controller", "usc-keyboard"],
playtypes: ["Controller", "Keyboard"],
songData: z.strictObject({}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export const USC_CONTROLLER_CONF = {
export const GAME_USC_CONTROLLER_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -87,7 +88,9 @@ export const USC_CONTROLLER_CONF = {
difficulties: {
type: "FIXED",
order: ["NOV", "ADV", "EXH", "INF"],
shorthand: {}, // they're all already short enough.
// deliberately not ported to v3 - formatting of charts as
// "CON EXH" etc.
format: {},
default: "EXH",
},
@@ -117,6 +120,6 @@ export const USC_CONTROLLER_CONF = {
}),
supportedMatchTypes: ["uscChartHash", "tachiSongID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
export const USC_KEYBOARD_CONF = USC_CONTROLLER_CONF;
export const GAME_USC_KEYBOARD_CONF = GAME_USC_CONTROLLER_CONF;
@@ -1,20 +1,21 @@
import { p } from "prudence";
import { z } from "zod";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals";
import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_GROUP_CONFIG } from "../../types/internals";
import { FmtNum } from "../../utils/util";
import { ClassValue, zodNonNegativeInt } from "../config-utils";
import { FAST_SLOW_MAXCOMBO } from "./_common";
export const WACCA_CONF = {
export const GAME_GROUP_WACCA_CONF = {
name: "WACCA",
games: ["wacca"],
playtypes: ["Single"],
songData: z.strictObject({
genre: z.string(),
displayVersion: z.nullable(z.string()),
}),
} as const satisfies INTERNAL_GAME_CONFIG;
} as const satisfies INTERNAL_GAME_GROUP_CONFIG;
export const WaccaStageUps = [
ClassValue("I", "I"),
@@ -45,7 +46,7 @@ export const WaccaColours = [
ClassValue("RAINBOW", "Rainbow"),
];
export const WACCA_SINGLE_CONF = {
export const GAME_WACCA_CONF = {
providedMetrics: {
score: {
type: "INTEGER",
@@ -113,7 +114,7 @@ export const WACCA_SINGLE_CONF = {
difficulties: {
type: "FIXED",
order: ["NORMAL", "HARD", "EXPERT", "INFERNO"],
shorthand: {
format: {
NORMAL: "NRM",
HARD: "HRD",
EXPERT: "EXP",
@@ -149,4 +150,4 @@ export const WACCA_SINGLE_CONF = {
scoreMeta: z.strictObject({ mirror: z.boolean().optional() }),
supportedMatchTypes: ["songTitle", "tachiSongID", "inGameID"],
} as const satisfies INTERNAL_GAME_PT_CONFIG;
} as const satisfies INTERNAL_GAME_CONFIG;
@@ -3,8 +3,8 @@ import { describe, expect, it } from "vitest";
import { BMS_TABLES } from "./bms-tables";
describe("BMS_TABLES", () => {
it("should have unique playtype-prefix keys", () => {
const allKeys = BMS_TABLES.map((e) => `${e.playtype}-${e.prefix}`);
it("should have unique game-prefix keys", () => {
const allKeys = BMS_TABLES.map((e) => `${e.game}-${e.prefix}`);
expect(allKeys).toStrictEqual([...new Set(allKeys)]);
});
+82 -31
View File
@@ -1,4 +1,4 @@
import type { Playtypes } from "../types";
import type { BMSGames } from "../types";
import { COLOUR_SET } from "./colour-set";
@@ -6,13 +6,14 @@ export interface BMSTableInfo {
prefix: string;
/**
* Used to generate tableIDs. This stops us from having characters like "◎" in tableIDs
* which would undoubtedly break some URLs.
* Used to generate tableIDs and folder slugs. This stops us from having characters like "◎" in tableIDs
* which would undoubtedly break some URLs. (Also referred to as an ASCII-friendly identifier for the
* `prefix`.)
*/
asciiPrefix: string;
name: string;
description: string;
playtype: Playtypes["bms"];
game: BMSGames;
url: string;
// Should this table be hidden in the UI by default?
@@ -29,7 +30,7 @@ export interface BMSTableInfo {
export const BMS_TABLES: Array<BMSTableInfo> = [
{
name: "Insane",
playtype: "7K",
game: "bms-7k",
description: "The 7K GENOSIDE insane table.",
url: "https://darksabun.github.io/table/archive/insane1/",
prefix: "★",
@@ -38,7 +39,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
},
{
name: "Normal",
playtype: "7K",
game: "bms-7k",
description: "The 7K GENOSIDE normal table.",
url: "https://darksabun.github.io/table/archive/normal1/",
prefix: "☆",
@@ -47,7 +48,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
},
{
name: "Stella",
playtype: "7K",
game: "bms-7k",
description: "The stella table, from Stellaverse.",
url: "https://stellabms.xyz/st/table.html",
prefix: "st",
@@ -56,7 +57,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
},
{
name: "Satellite",
playtype: "7K",
game: "bms-7k",
description: "The satellite table, from Stellaverse.",
url: "https://stellabms.xyz/sl/table.html",
prefix: "sl",
@@ -65,7 +66,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
},
{
name: "Insane2",
playtype: "7K",
game: "bms-7k",
description: "A successor to the original insane table, but is generally less consistent.",
url: "https://rattoto10.github.io/second_table/insane_header.json",
prefix: "▼",
@@ -74,7 +75,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
},
{
name: "Normal2",
playtype: "7K",
game: "bms-7k",
description: "A successor to the original normal table.",
url: "https://rattoto10.github.io/second_table/header.json",
prefix: "▽",
@@ -84,7 +85,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
{
name: "Overjoy",
description: "The overjoy table. Level 1 is roughly equivalent to Insane 20.",
playtype: "7K",
game: "bms-7k",
url: "http://rattoto10.jounin.jp/table_overjoy.html",
prefix: "★★",
asciiPrefix: "overjoy",
@@ -93,7 +94,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
{
name: "DP Insane",
description: "The 14K Insane table.",
playtype: "14K",
game: "bms-14k",
prefix: "★",
asciiPrefix: "dpInsane",
url: "http://dpbmsdelta.web.fc2.com/table/insane.html",
@@ -102,7 +103,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
{
name: "DP Normal",
description: "The 14K Normal table, Sometimes called the delta table.",
playtype: "14K",
game: "bms-14k",
prefix: "δ",
asciiPrefix: "dpNormal",
url: "http://dpbmsdelta.web.fc2.com/table/dpdelta.html",
@@ -111,7 +112,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
{
name: "DP Satellite",
description: "The 14K Satellite table.",
playtype: "14K",
game: "bms-14k",
prefix: "sl",
asciiPrefix: "dpSatellite",
url: "https://stellabms.xyz/dp/table.html",
@@ -122,7 +123,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
description: "The 7K Sara 3 table.",
prefix: "h◎",
asciiPrefix: "scratch",
playtype: "7K",
game: "bms-7k",
url: "http://minddnim.web.fc2.com/sara/3rd_hard/bms_sara_3rd_hard.html",
colour: COLOUR_SET.vibrantRed,
},
@@ -131,7 +132,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
description: "The 7K LN table.",
prefix: "◆",
asciiPrefix: "ln",
playtype: "7K",
game: "bms-7k",
url: "http://flowermaster.web.fc2.com/lrnanido/gla/LN.html",
colour: COLOUR_SET.pink,
},
@@ -139,7 +140,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Stardust",
prefix: "ξ",
asciiPrefix: "stardust",
playtype: "7K",
game: "bms-7k",
url: "https://mqppppp.neocities.org/StardustTable.html",
description: "The 7K Stardust table. This table covers ☆1 to ☆7.",
colour: COLOUR_SET.paleBlue,
@@ -148,7 +149,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Starlight",
prefix: "sr",
asciiPrefix: "starlight",
playtype: "7K",
game: "bms-7k",
url: "https://djkuroakari.github.io/starlighttable.html",
description: "The 7K Starlight table. This table covers ☆7 to ☆12.",
colour: COLOUR_SET.blue,
@@ -157,7 +158,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "LN Overjoy",
prefix: "◆◆",
asciiPrefix: "lnOverjoy",
playtype: "7K",
game: "bms-7k",
url: "https://notepara.com/glassist/lnoj",
description: "The 7K LN Overjoy table.",
notDefault: true,
@@ -167,7 +168,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Luminous",
prefix: "ln",
asciiPrefix: "luminous",
playtype: "7K",
game: "bms-7k",
url: "https://ladymade-star.github.io/luminous/",
description: "The 7K Luminous Table. This is an alternative to the mainline LN tables.",
notDefault: true,
@@ -177,7 +178,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Gachimjoy",
prefix: "双",
asciiPrefix: "gachimjoy",
playtype: "7K",
game: "bms-7k",
url: "http://su565fx.web.fc2.com/Gachimijoy/gachimijoy.html",
notDefault: true,
description:
@@ -189,7 +190,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
// these trailing slashes are important. keep them.
url: "https://lets-go-time-hell.github.io/Delay-joy-table/",
playtype: "7K",
game: "bms-7k",
prefix: "dl",
notDefault: true,
description:
@@ -199,7 +200,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Arm Shougakkou",
asciiPrefix: "armShougakkou",
url: "https://lets-go-time-hell.github.io/Arm-Shougakkou-table/",
playtype: "7K",
game: "bms-7k",
prefix: "Ude",
notDefault: true,
description:
@@ -209,7 +210,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Exoplanet",
asciiPrefix: "exoplanet",
url: "https://stellabms.xyz/fr/table.html",
playtype: "7K",
game: "bms-7k",
prefix: "fr",
notDefault: true,
description:
@@ -219,14 +220,14 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "DP Library",
asciiPrefix: "dpLibrary",
url: "https://yaruki0.net/DPlibrary/",
playtype: "14K",
game: "bms-14k",
prefix: "☆",
notDefault: true,
description: "A huge collection of Normal-Scale 14K charts.",
},
{
name: "Dystopia",
playtype: "7K",
game: "bms-7k",
description: "The dystopia table, dy0-dy7 == st5-st12.",
url: "https://monibms.github.io/Dystopia/dystopia.html",
prefix: "dy",
@@ -237,7 +238,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Scramble",
prefix: "SB",
asciiPrefix: "Scramble",
playtype: "7K",
game: "bms-7k",
description: "The Scramble table, a 7k scratch table",
url: "https://egret9.github.io/Scramble/",
colour: COLOUR_SET.vibrantOrange,
@@ -246,7 +247,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Supernova",
asciiPrefix: "supernova",
url: "https://stellabms.xyz/sn/table.html",
playtype: "7K",
game: "bms-7k",
prefix: "sn",
description:
"Supernova, Stellaverse's alternative table for Insane1-esque charts. Follows the Stella table in difficulty.",
@@ -255,7 +256,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Solar",
asciiPrefix: "solar",
url: "https://stellabms.xyz/so/table.html",
playtype: "7K",
game: "bms-7k",
prefix: "so",
description:
"Solar, Stellaverse's alternative table for Insane1-esque charts. Follows the Satellite table in difficulty.",
@@ -264,7 +265,7 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Code Stream (st)",
asciiPrefix: "csst",
url: "https://air-afother.github.io/chordstream-table-split/chordstreamST.html",
playtype: "7K",
game: "bms-7k",
prefix: "重発狂",
description: "16th Chordstream table, st0+ difficulty",
},
@@ -272,8 +273,58 @@ export const BMS_TABLES: Array<BMSTableInfo> = [
name: "Code Stream (sl)",
asciiPrefix: "cssl",
url: "https://air-afother.github.io/chordstream-table-split/chordstream.html",
playtype: "7K",
game: "bms-7k",
prefix: "乱打",
description: "16th Chordstream table, sl0-sl12 difficulty",
},
// PMS `tableFolders` keys (seeds/folders.json); no public table URL.
{
name: "PMS ● (controller)",
game: "pms-controller",
prefix: "●",
asciiPrefix: "pmsCircle",
description: "PMS tableFolders key ● (dan / level charts).",
url: "https://github.com/TachiServices/Tachi",
},
{
name: "PMS P● (controller)",
game: "pms-controller",
prefix: "P●",
asciiPrefix: "pmsPStar",
description: "PMS tableFolders key P●.",
url: "https://github.com/TachiServices/Tachi",
},
{
name: "PMS PLv (controller)",
game: "pms-controller",
prefix: "PLv",
asciiPrefix: "pmsPLv",
description: "PMS tableFolders key PLv.",
url: "https://github.com/TachiServices/Tachi",
},
{
name: "PMS ● (keyboard)",
game: "pms-keyboard",
prefix: "●",
asciiPrefix: "pmsCircle",
description: "PMS tableFolders key ● (dan / level charts).",
url: "https://github.com/TachiServices/Tachi",
},
{
name: "PMS P● (keyboard)",
game: "pms-keyboard",
prefix: "P●",
asciiPrefix: "pmsPStar",
description: "PMS tableFolders key P●.",
url: "https://github.com/TachiServices/Tachi",
},
{
name: "PMS PLv (keyboard)",
game: "pms-keyboard",
prefix: "PLv",
asciiPrefix: "pmsPLv",
description: "PMS tableFolders key PLv.",
url: "https://github.com/TachiServices/Tachi",
},
];
@@ -1,4 +1,4 @@
import type { GPTStrings } from "../types/game-config";
import type { GamesForGroup } from "../types/game-config";
import type { GetEnumValue } from "../types/metrics";
export interface GradeBoundary<G extends string> {
@@ -20,7 +20,7 @@ export function MakeGradeBoundaries<G extends string>(rec: Record<G, number>) {
return out;
}
export type IIDXLikes = GPTStrings["bms" | "iidx" | "pms"];
export type IIDXLikes = GamesForGroup["bms" | "iidx" | "pms"];
/**
* @note - These are *also* grade boundaries for BMS and PMS.
@@ -47,7 +47,7 @@ export const IIDXLIKE_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<IIDXLikes,
MAX: (100 * 9) / 9,
});
export const CHUNITHM_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"chunithm:Single", "grade">>({
export const CHUNITHM_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"chunithm", "grade">>({
D: 0,
C: 500_000,
B: 600_000,
@@ -64,7 +64,7 @@ export const CHUNITHM_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"chunithm:S
"SSS+": 1_009_000,
});
export const WACCA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"wacca:Single", "grade">>({
export const WACCA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"wacca", "grade">>({
D: 0,
C: 1, // LOL
B: 300_100,
@@ -80,7 +80,7 @@ export const WACCA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"wacca:Single"
MASTER: 1_000_000,
});
export const JUBEAT_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"jubeat:Single", "grade">>({
export const JUBEAT_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"jubeat", "grade">>({
E: 0,
D: 500_000,
C: 700_000,
@@ -92,7 +92,7 @@ export const JUBEAT_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"jubeat:Singl
EXC: 1_000_000,
});
export const ITG_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"itg:Stamina", "grade">>({
export const ITG_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"itg-stamina", "grade">>({
// cannot be achieved. This can only be attained if the player failed the chart.
F: -Infinity,
D: 0, // The lowest score you can get in ITG is -400%. Scores "can" go negative.
@@ -109,7 +109,7 @@ export const ITG_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"itg:Stamina", "
});
export const GITADORA_GBOUNDARIES = MakeGradeBoundaries<
GetEnumValue<GPTStrings["gitadora"], "grade">
GetEnumValue<"gitadora-dora" | "gitadora-gita", "grade">
>({
C: 0,
B: 63,
@@ -119,7 +119,7 @@ export const GITADORA_GBOUNDARIES = MakeGradeBoundaries<
MAX: 100,
});
export const MUSECA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"museca:Single", "grade">>({
export const MUSECA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"museca", "grade">>({
没: 0,
拙: 600_000,
凡: 700_000,
@@ -135,7 +135,7 @@ export const MUSECA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"museca:Singl
* @note - The SSS+ rank boundary is chart-dependent - it is the maximum percent of a chart.
* 104 is just the upper limit of the maximum percent.
*/
export const MAIMAI_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"maimai:Single", "grade">>({
export const MAIMAI_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"maimai", "grade">>({
F: 0,
E: 10,
D: 20,
@@ -153,7 +153,7 @@ export const MAIMAI_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"maimai:Singl
"SSS+": 104,
});
export const MAIMAIDX_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"maimaidx:Single", "grade">>({
export const MAIMAIDX_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"maimaidx", "grade">>({
D: 0,
C: 50,
B: 60,
@@ -170,7 +170,7 @@ export const MAIMAIDX_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"maimaidx:S
"SSS+": 100.5,
});
export const POPN_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"popn:9B", "grade">>({
export const POPN_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"popn", "grade">>({
E: 0,
D: 50_000,
C: 62_000,
@@ -181,7 +181,7 @@ export const POPN_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"popn:9B", "gra
S: 98_000,
});
type SDVXLikes = GPTStrings["sdvx" | "usc"];
type SDVXLikes = GamesForGroup["sdvx" | "usc"];
export const SDVXLIKE_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<SDVXLikes, "grade">>({
D: 0,
@@ -197,7 +197,7 @@ export const SDVXLIKE_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<SDVXLikes,
PUC: 10_000_000,
});
export const ARCAEA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"arcaea:Touch", "grade">>({
export const ARCAEA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"arcaea", "grade">>({
D: 0,
C: 8_600_000,
B: 8_900_000,
@@ -207,7 +207,7 @@ export const ARCAEA_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"arcaea:Touch
"EX+": 9_900_000,
});
export const ONGEKI_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"ongeki:Single", "grade">>({
export const ONGEKI_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"ongeki", "grade">>({
D: 0,
C: 500_000,
B: 700_000,
@@ -222,7 +222,7 @@ export const ONGEKI_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"ongeki:Singl
"SSS+": 1_007_500,
});
export const DDR_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<GPTStrings["ddr"], "grade">>({
export const DDR_GBOUNDARIES = MakeGradeBoundaries<GetEnumValue<"ddr-dp" | "ddr-sp", "grade">>({
AAA: 990000,
"AA+": 950000,
AA: 900000,
+1 -1
View File
@@ -6,7 +6,7 @@ import * as tachiCommon from "./index";
describe("Property Checks", () => {
it("exports core APIs and constants", () => {
expect(typeof tachiCommon.GetGameGroupConfig).toBe("function");
expect(typeof tachiCommon.GetGamePTConfig).toBe("function");
expect(typeof tachiCommon.LEGACY_GetGamePTConfig).toBe("function");
expect(typeof tachiCommon.COLOUR_SET).toBe("object");
});
});
+1
View File
@@ -7,6 +7,7 @@ export * from "./constants/game";
export * from "./constants/grade-boundaries";
export * as StaticConfig from "./constants/import-types";
export * from "./constants/permissions";
export * from "./lib/folder-slug";
export * as Schemas from "./lib/schemas";
export * from "./lib/zod-schemas";
export * from "./seeds/types";
+475
View File
@@ -0,0 +1,475 @@
/**
* Computes URL-safe folder slugs for `db/seeds/folders.json` rows.
* Slugs are lowercase ASCII [a-z0-9-] only.
*/
import { BMS_TABLES } from "../constants/bms-tables";
export type SeedFolderRow = {
game: string;
id: string;
slug?: string;
title: string;
versionFilter?: Array<string>;
where: string;
};
const CHART_LEVEL_EQ_RE = /^chart\.level = '([^']+)'$/u;
const LEVEL_NUM_GE_LT_RE = /chart\.level_num >= ([0-9.]+) AND chart\.level_num < ([0-9.]+)/u;
const LEVEL_NUM_GE_LE_RE = /chart\.level_num >= ([0-9.]+) AND chart\.level_num <= ([0-9.]+)/u;
const CHART_LEVEL_IN_RE = /chart\.level IN \(([^)]+)\)/u;
/** IIDX: maps `versionFilter[0]` → slug segment (see add-iidx-folder-slugs history). */
export const IIDX_VERSION_SLUG: Record<string, string> = {
1: "1st",
substream: "substream",
2: "2nd",
3: "3rd",
4: "4th",
5: "5th",
6: "6th",
7: "7th",
8: "8th",
9: "9th",
10: "10th",
11: "red",
12: "happysky",
13: "distorted",
14: "gold",
15: "djtroopers",
16: "empress",
17: "sirius",
18: "resortanthem",
19: "lincle",
"3-cs": "3rd-cs",
"4-cs": "4th-cs",
"5-cs": "5th-cs",
"6-cs": "6th-cs",
"7-cs": "7th-cs",
"8-cs": "8th-cs",
"9-cs": "9th-cs",
"10-cs": "10th-cs",
"11-cs": "11th-cs",
"12-cs": "12th-cs",
"13-cs": "13th-cs",
"14-cs": "14th-cs",
"15-cs": "15th-cs",
"16-cs": "16th-cs",
20: "tricoro",
21: "spada",
22: "pendual",
23: "copula",
24: "sinobuz",
25: "cannonballers",
26: "rootage",
"26-omni": "rootage-omni",
27: "heroicverse",
"27-2dxtra": "heroicverse-2dxtra",
"27-omni": "heroicverse-omni",
28: "bistrover",
"28-omni": "bistrover-omni",
29: "casthour",
"29-omni": "casthour-omni",
30: "resident",
"30-omni": "resident-omni",
31: "epolis",
"31-omni": "epolis-omni",
32: "pinkycrush",
"32-omni": "pinkycrush-omni",
33: "sparkleshower",
bmus: "bmus",
inf: "inf",
};
const ASCII_SLUG_RE = /^[a-z0-9-]+$/u;
export function assertAsciiSlug(slug: string, ctx: string): void {
if (!ASCII_SLUG_RE.test(slug)) {
throw new Error(`${ctx}: slug must match [a-z0-9-], got ${JSON.stringify(slug)}`);
}
}
/** Collapses separators to single `-`; strips non-alphanumeric. */
export function asciiSlugSegment(input: string): string {
const stripped = input
.normalize("NFKD")
.replace(/[\u0300-\u036f]/gu, "")
.replace(/[^a-zA-Z0-9]+/gu, "-")
.replace(/^-+|-+$/gu, "")
.replace(/-+/gu, "-")
.toLowerCase();
return stripped.length > 0 ? stripped : "x";
}
export function slugLevelPart(level: string): string {
return level.replaceAll("+", "p").replaceAll(".", "p");
}
function expectSingleVersionFilter(folder: SeedFolderRow, label: string): string {
const vf = folder.versionFilter;
if (!vf || vf.length !== 1) {
throw new Error(
`${label} folder "${folder.title}" (${folder.id}): expected exactly one versionFilter entry, got ${JSON.stringify(vf)}`,
);
}
return vf[0];
}
function slugLevelFromWhere(where: string, label: string, id: string, title: string): string {
const eq = CHART_LEVEL_EQ_RE.exec(where);
if (eq !== null) {
return slugLevelPart(eq[1]);
}
const lt = LEVEL_NUM_GE_LT_RE.exec(where);
if (lt !== null) {
return `ge-${slugLevelPart(lt[1])}-lt-${slugLevelPart(lt[2])}`;
}
const le = LEVEL_NUM_GE_LE_RE.exec(where);
if (le !== null) {
return `ge-${slugLevelPart(le[1])}-le-${slugLevelPart(le[2])}`;
}
const mIn = CHART_LEVEL_IN_RE.exec(where);
if (mIn !== null) {
const vals = mIn[1].split(",").map((x) => x.trim().replace(/^'/u, "").replace(/'$/u, ""));
return vals.map((v) => slugLevelPart(v)).join("-");
}
throw new Error(
`${label} folder "${title}" (${id}): unhandled level predicate ${JSON.stringify(where)}`,
);
}
/** `{level}-{version}` with slugified version token. */
function slugLevelPlusVersion(folder: SeedFolderRow, label: string): string {
const rawVf = expectSingleVersionFilter(folder, label);
const levelSeg = slugLevelFromWhere(folder.where, label, folder.id, folder.title);
return `${levelSeg}-${asciiSlugSegment(rawVf)}`;
}
function slugIidx(folder: SeedFolderRow): string {
const rawKey = expectSingleVersionFilter(folder, "IIDX");
const versionSlug = IIDX_VERSION_SLUG[rawKey];
if (versionSlug === undefined) {
throw new Error(
`IIDX folder "${folder.title}" (${folder.id}): unmapped versionFilter ${JSON.stringify(rawKey)}`,
);
}
const match = CHART_LEVEL_EQ_RE.exec(folder.where);
if (match === null) {
throw new Error(
`IIDX folder "${folder.title}" (${folder.id}): expected chart.level = '…', got ${JSON.stringify(folder.where)}`,
);
}
return `${slugLevelPart(match[1])}-${versionSlug}`;
}
function slugChunithm(folder: SeedFolderRow): string {
const rawVf = expectSingleVersionFilter(folder, "chunithm");
const vfPart = asciiSlugSegment(rawVf);
const w = folder.where;
const mLv = CHART_LEVEL_EQ_RE.exec(w);
if (mLv !== null) {
return `${slugLevelPart(mLv[1])}-${vfPart}`;
}
const mDiff = /chart\.difficulty = '([^']+)'/u.exec(w);
if (mDiff !== null) {
return `${asciiSlugSegment(mDiff[1])}-${vfPart}`;
}
const mIn = /chart\.difficulty IN \(([^)]+)\)/u.exec(w);
if (mIn !== null) {
const parts = mIn[1].split(",").map((x) => x.trim().replace(/^'/u, "").replace(/'$/u, ""));
return `${asciiSlugSegment(parts.join("-"))}-${vfPart}`;
}
throw new Error(
`chunithm folder "${folder.title}" (${folder.id}): unhandled where ${JSON.stringify(w)}`,
);
}
function slugOngeki(folder: SeedFolderRow): string {
const w = folder.where;
if (w.includes("song.data->>'genre'") && w.includes("'LUNATIC'")) {
return "lunatic-only-songs";
}
if (
w ===
"(chart.data->>'inGameID')::numeric >= 7000 AND (chart.data->>'inGameID')::numeric < 8000"
) {
return "bonus-tracks";
}
const rawVf = expectSingleVersionFilter(folder, "ongeki");
const vfPart = asciiSlugSegment(rawVf);
const isOld = folder.title.includes("(Old)");
const mLv = /chart\.level = '([^']+)'/u.exec(w);
if (mLv !== null) {
let s = `${slugLevelPart(mLv[1])}-${vfPart}`;
if (isOld) {
s = `${s}-old`;
}
return s;
}
let remaster = "";
if (/isReMaster' = 'true'/u.test(w)) {
remaster = "-remaster";
}
const mD = /chart\.difficulty = '([^']+)'/u.exec(w);
if (mD !== null) {
return `${vfPart}-${asciiSlugSegment(mD[1])}${remaster}`;
}
const mIn = /chart\.difficulty IN \(([^)]+)\)/u.exec(w);
if (mIn !== null) {
const parts = mIn[1].split(",").map((x) => x.trim().replace(/^'/u, "").replace(/'$/u, ""));
return `${vfPart}-${asciiSlugSegment(parts.join("-"))}${remaster}`;
}
throw new Error(
`ongeki folder "${folder.title}" (${folder.id}): unhandled where ${JSON.stringify(w)}`,
);
}
function slugJubeat(folder: SeedFolderRow): string {
const rawVf = expectSingleVersionFilter(folder, "jubeat");
const vfPart = asciiSlugSegment(rawVf);
const w = folder.where;
const hard = w.includes("HARD BSC");
const mode = hard ? "hard" : "normal";
const mLv = /chart\.level = '([^']+)'/u.exec(w);
if (mLv !== null) {
return `${slugLevelPart(mLv[1])}-${mode}-${vfPart}`;
}
const mRg = /chart\.level_num >= ([0-9.]+) AND chart\.level_num < ([0-9.]+)/u.exec(w);
if (mRg !== null) {
return `ge-${slugLevelPart(mRg[1])}-lt-${slugLevelPart(mRg[2])}-${mode}-${vfPart}`;
}
throw new Error(
`jubeat folder "${folder.title}" (${folder.id}): unhandled where ${JSON.stringify(w)}`,
);
}
function slugGitadora(folder: SeedFolderRow): string {
const rawVf = expectSingleVersionFilter(folder, "gitadora");
const vfPart = asciiSlugSegment(rawVf);
const m = /chart\.level_num >= ([0-9.]+) AND chart\.level_num < ([0-9.]+)/u.exec(folder.where);
if (m === null) {
throw new Error(
`gitadora folder "${folder.title}" (${folder.id}): expected level_num range, got ${JSON.stringify(folder.where)}`,
);
}
const a = m[1].replaceAll(".", "p");
const b = m[2].replaceAll(".", "p");
return `${vfPart}-ge-${a}-lt-${b}`;
}
/** Non-official USC folders use the seed title as the slug (e.g. `os8`). */
function slugUscUnofficialTitle(title: string): string {
// Match level-style `+` handling so `us17` vs `us17+` do not collide; `*` → `s` for titles like `us0*`.
return asciiSlugSegment(title.replaceAll("+", "p").replaceAll("*", "s"));
}
function slugUsc(folder: SeedFolderRow): string {
const w = folder.where;
const mOfficial = /chart\.data->>'isOfficial' = 'true' AND chart\.level = '([^']+)'/u.exec(w);
if (mOfficial !== null) {
return `official-${slugLevelPart(mOfficial[1])}`;
}
return slugUscUnofficialTitle(folder.title);
}
function slugItgStamina(folder: SeedFolderRow): string {
return asciiSlugSegment(folder.title);
}
/**
* Maps a `tableFolders` JSON key to a slug segment using {@link BMS_TABLES} `asciiPrefix`
* (ASCII-friendly identifier for the table `prefix`).
*/
function encodeBmsTableKey(game: string, key: string): string {
const row = BMS_TABLES.find((e) => e.game === game && e.prefix === key);
if (row !== undefined) {
return asciiSlugSegment(row.asciiPrefix);
}
// USC and other non-BMS seeds: `os`, `us`, etc.
if (/^[a-zA-Z0-9_]+$/u.test(key)) {
return key.toLowerCase();
}
throw new Error(
`No BMS_TABLES row for game=${JSON.stringify(game)} prefix=${JSON.stringify(key)} — add a BMSTableInfo entry or use an ascii-only prefix.`,
);
}
function encodeBmsTableValue(value: string): string {
const parts: Array<string> = [];
for (const ch of value) {
if (/[a-z0-9]/iu.test(ch)) {
parts.push(ch.toLowerCase());
} else if (ch === "+") {
parts.push("p");
} else if (ch === "?") {
parts.push("q");
} else if (ch === "-") {
parts.push("dash");
} else if (ch === "!") {
parts.push("excl");
} else {
parts.push(`u${ch.codePointAt(0)?.toString(16) ?? "0"}`);
}
}
return parts.join("-");
}
function slugBmsLike(folder: SeedFolderRow, label: string): string {
const w = folder.where;
const mKv = /tableFolders'->>'((?:[^']|'')+)'\) = '((?:[^']|'')*)'/u.exec(w);
if (mKv !== null) {
const key = mKv[1].replaceAll("''", "'");
const val = mKv[2].replaceAll("''", "'");
return `tf-${encodeBmsTableKey(folder.game, key)}-${encodeBmsTableValue(val)}`;
}
const mPr = /tableFolders'\) \? '((?:[^']|'')+)'/u.exec(w);
if (mPr !== null) {
const key = mPr[1].replaceAll("''", "'");
return `tf-${encodeBmsTableKey(folder.game, key)}-present`;
}
const mRg =
/chart\.data->>'([a-zA-Z0-9_]+)'\)::numeric >= ([0-9.]+) AND \(chart\.data->>'\1'\)::numeric < ([0-9.]+)/u.exec(
w,
);
if (mRg !== null) {
return `${mRg[1].toLowerCase()}-ge-${mRg[2]}-lt-${mRg[3]}`;
}
throw new Error(
`${label} folder "${folder.title}" (${folder.id}): unhandled where ${JSON.stringify(w)}`,
);
}
export function computeFolderSlug(folder: SeedFolderRow): string {
let slug: string;
switch (folder.game) {
case "iidx-dp":
case "iidx-sp":
slug = slugIidx(folder);
break;
case "arcaea":
case "ddr-dp":
case "ddr-sp":
case "maimai":
case "maimaidx":
case "museca":
case "popn":
case "sdvx":
case "wacca":
slug = slugLevelPlusVersion(folder, folder.game);
break;
case "chunithm":
slug = slugChunithm(folder);
break;
case "ongeki":
slug = slugOngeki(folder);
break;
case "jubeat":
slug = slugJubeat(folder);
break;
case "gitadora-dora":
case "gitadora-gita":
slug = slugGitadora(folder);
break;
case "usc-controller":
case "usc-keyboard":
slug = slugUsc(folder);
break;
case "itg-stamina":
slug = slugItgStamina(folder);
break;
case "bms-14k":
case "bms-7k":
case "pms-controller":
case "pms-keyboard":
slug = slugBmsLike(folder, folder.game);
break;
default:
throw new Error(`computeFolderSlug: unsupported game ${JSON.stringify(folder.game)}`);
}
assertAsciiSlug(slug, `folder "${folder.title}" (${folder.id})`);
return slug;
}
+92 -90
View File
@@ -8,21 +8,21 @@ import {
type ValidationFunctionParentOptionsKeychain,
type ValidSchemaValue,
} from "prudence";
import { z, type ZodType } from "zod";
import type { GameGroup, Playtype, Playtypes, V3Game } from "../types/game-config";
import type { INTERNAL_GAME_PT_CONFIG } from "../types/internals";
import type { GameGroup, LEGACY_Playtypes, V3Game } from "../types/game-config";
import type { ConfScoreMetric } from "../types/metrics";
import type { NotificationBody } from "../types/notifications";
import {
allGPTStrings,
allSupportedGameGroups,
GameToGameGroup,
GetGameConfig,
GetGameGroupConfig,
GetGamePTConfig,
GetGPTString,
GetScoreMetrics,
v3AllGames,
LEGACY_GameGroupPTToGame,
LEGACY_GameToPlaytypeFn,
LEGACY_GetGamePTConfig,
} from "../config/config";
import { allImportTypes } from "../constants/import-types";
import { ALL_PERMISSIONS } from "../constants/permissions";
@@ -50,17 +50,15 @@ function prSchemaFnWrap(schema: PrudenceSchema) {
};
}
const PR_GAME_STATS = (
game: GameGroup,
playtype: Playtypes[GameGroup],
gptConfig: INTERNAL_GAME_PT_CONFIG,
) => ({
const PR_GAME_STATS = (game: V3Game) => ({
userID: p.isPositiveNonZeroInteger,
game: p.is(game),
playtype: p.is(playtype),
ratings: Object.fromEntries(Object.keys(gptConfig.scoreRatingAlgs).map((s) => [s, "*number"])),
game: p.is(GameToGameGroup(game)),
playtype: p.is(LEGACY_GameToPlaytypeFn(game)),
ratings: Object.fromEntries(
Object.keys(GetGameConfig(game).scoreRatingAlgs).map((s) => [s, "*number"]),
),
classes: Object.fromEntries(
Object.keys(gptConfig.classes).map((e) => [e, p.optional(p.isInteger)]),
Object.keys(GetGameConfig(game).classes).map((e) => [e, p.optional(p.isInteger)]),
),
});
@@ -86,7 +84,7 @@ const extractGPTIDString = (self: unknown) => {
// if there's no ":" in the string, this returns only one element.
const [game, playtype] = s.gptString.split(":") as [string, string | undefined];
if (!IsValidGame(game)) {
if (!IsValidGameGroup(game)) {
throw new Error(`Expected valid game -- got ${game} from gptString ${s.gptString}.`);
}
@@ -100,7 +98,7 @@ const extractGPTIDString = (self: unknown) => {
return { game, playtype };
};
const extractGPT = (self: unknown) => {
const extractGame = (self: unknown) => {
if (typeof self !== "object" || !self) {
throw new Error("Expected an object.");
}
@@ -115,7 +113,7 @@ const extractGPT = (self: unknown) => {
throw new Error(`Expected a string where self.playtype is. Got ${s.playtype}`);
}
if (!IsValidGame(s.game)) {
if (!IsValidGameGroup(s.game)) {
throw new Error(`Expected valid game -- got ${s.game}.`);
}
@@ -123,18 +121,22 @@ const extractGPT = (self: unknown) => {
throw new Error(`Expected valid playtype -- got ${s.playtype}.`);
}
return { game: s.game, playtype: s.playtype };
return {
v3Game: LEGACY_GameGroupPTToGame(s.game, s.playtype),
gameGroup: s.game,
playtype: s.playtype,
};
};
function IsValidPlaytype(game: GameGroup, str: string): str is Playtypes[GameGroup] {
return GetGameGroupConfig(game).playtypes.includes(str as Playtypes[GameGroup]);
function IsValidPlaytype(game: GameGroup, str: string): str is LEGACY_Playtypes[GameGroup] {
return GetGameGroupConfig(game).playtypes.includes(str as LEGACY_Playtypes[GameGroup]);
}
function IsValidGame(str: string): str is GameGroup {
function IsValidGameGroup(str: string): str is GameGroup {
return allSupportedGameGroups.includes(str as GameGroup);
}
const getPlaytype = (game: GameGroup, self: unknown): Playtypes[GameGroup] => {
const getPlaytype = (game: GameGroup, self: unknown): LEGACY_Playtypes[GameGroup] => {
if (self === null || typeof self !== "object") {
throw new Error("Expected an object.");
}
@@ -155,7 +157,7 @@ const getPlaytype = (game: GameGroup, self: unknown): Playtypes[GameGroup] => {
const games = allSupportedGameGroups;
const isValidPlaytype = (self: unknown, parent: Record<string, unknown>) => {
if (typeof parent.game !== "string" || !IsValidGame(parent.game)) {
if (typeof parent.game !== "string" || !IsValidGameGroup(parent.game)) {
throw new Error(`Invalid Schema, need game to base IsValidPlaytype off of.`);
}
@@ -171,7 +173,7 @@ const isValidPlaytype = (self: unknown, parent: Record<string, unknown>) => {
};
export const PR_GOAL_SCHEMA = (self: unknown) => {
const { game, playtype } = extractGPT(self);
const { gameGroup: game, playtype } = extractGame(self);
return prSchemaFnWrap({
game: p.isIn(games),
@@ -182,8 +184,8 @@ export const PR_GOAL_SCHEMA = (self: unknown) => {
{
mode: p.is("single"),
key: (self) => {
const gptConfig = GetGamePTConfig(game, playtype);
const metrics = GetScoreMetrics(gptConfig);
const gameConfig = LEGACY_GetGamePTConfig(game, playtype);
const metrics = GetScoreMetrics(gameConfig);
return p.isIn(metrics)(self);
},
@@ -193,8 +195,8 @@ export const PR_GOAL_SCHEMA = (self: unknown) => {
mode: p.isIn("absolute", "proportion"),
countNum: p.isPositive,
key: (self) => {
const gptConfig = GetGamePTConfig(game, playtype);
const metrics = GetScoreMetrics(gptConfig);
const gameConfig = LEGACY_GetGamePTConfig(game, playtype);
const metrics = GetScoreMetrics(gameConfig);
return p.isIn(metrics)(self);
},
@@ -234,7 +236,7 @@ const PR_SONG_DOCUMENT = (game: GameGroup): PrudenceSchema => {
const PR_CHART_DOCUMENT = (game: GameGroup) => (self: unknown) => {
const playtype = getPlaytype(game, self);
const gptConfig = GetGamePTConfig(game, playtype);
const gameConfig = LEGACY_GetGamePTConfig(game, playtype);
return prSchemaFnWrap({
songID: p.isPositiveInteger,
@@ -248,18 +250,18 @@ const PR_CHART_DOCUMENT = (game: GameGroup) => (self: unknown) => {
playtype: p.is(playtype),
difficulty:
gptConfig.difficulties.type === "DYNAMIC"
gameConfig.difficulties.type === "DYNAMIC"
? "string"
: p.isIn(gptConfig.difficulties.order),
: p.isIn(gameConfig.difficulties.order),
data: PrudenceZodShim(gptConfig.chartData),
data: PrudenceZodShim(gameConfig.chartData),
versions: (self) => {
if (!Array.isArray(self)) {
return "Expected an array";
}
const versions = Object.keys(gptConfig.versions);
const versions = Object.keys(gameConfig.versions);
if (self.find((k) => !versions.includes(k))) {
return "Array contained invalid versions";
@@ -330,14 +332,14 @@ const PRE_SCHEMAS = {
},
set: (self, pa) => {
const parent = pa as any;
const config = GetGamePTConfig("bms", parent.playtype);
const config = GetGameConfig(parent.game);
return p.isIn(Object.keys(config.classes))(self);
},
playtype: p.isIn("7K", "14K"),
game: p.isIn("bms-7k", "bms-14k"),
value: (self, pa) => {
const parent = pa as any;
const config = GetGamePTConfig("bms", parent.playtype);
const config = GetGameConfig(parent.game);
return p.isIn(config.classes[parent.set]?.values.map((e) => e.id) ?? [])(self);
},
@@ -429,9 +431,9 @@ const PRE_SCHEMAS = {
}),
goals: PR_GOAL_SCHEMA,
scores: (self: unknown) => {
const { game, playtype } = extractGPT(self);
const { gameGroup: game, playtype } = extractGame(self);
const gptConfig = GetGamePTConfig(game, playtype);
const gameConfig = LEGACY_GetGamePTConfig(game, playtype);
return prSchemaFnWrap({
service: "string",
@@ -448,27 +450,27 @@ const PRE_SCHEMAS = {
importType: p.nullable(p.isIn(allImportTypes)),
timeAchieved: p.nullable(p.isPositive),
scoreData: {
...PR_METRICS(gptConfig.providedMetrics),
...PR_METRICS(gptConfig.derivedMetrics),
...PR_METRICS(gameConfig.providedMetrics),
...PR_METRICS(gameConfig.derivedMetrics),
judgements: Object.fromEntries(
gptConfig.orderedJudgements.map((j) => [j, optNull(p.isInteger)]),
gameConfig.orderedJudgements.map((j) => [j, optNull(p.isInteger)]),
),
optional: PR_METRICS(gptConfig.optionalMetrics, true),
optional: PR_METRICS(gameConfig.optionalMetrics, true),
},
scoreMeta: PrudenceZodShim(gptConfig.scoreMeta),
scoreMeta: PrudenceZodShim(gameConfig.scoreMeta),
calculatedData: Object.fromEntries(
Object.keys(gptConfig.scoreRatingAlgs).map((a) => [a, "*?number"]),
Object.keys(gameConfig.scoreRatingAlgs).map((a) => [a, "*?number"]),
),
})(self);
},
"personal-bests": (self: unknown) => {
const { game, playtype } = extractGPT(self);
const { gameGroup: game, playtype } = extractGame(self);
const gptConfig = GetGamePTConfig(game, playtype);
const gameConfig = LEGACY_GetGamePTConfig(game, playtype);
return prSchemaFnWrap({
composedFrom: [{ name: "string", scoreID: "string" }],
@@ -485,18 +487,18 @@ const PRE_SCHEMAS = {
isPrimary: "boolean",
timeAchieved: p.nullable(p.isPositive),
scoreData: {
...PR_METRICS(gptConfig.providedMetrics),
...PR_METRICS(gptConfig.derivedMetrics),
...PR_METRICS(gameConfig.providedMetrics),
...PR_METRICS(gameConfig.derivedMetrics),
// TODO ENUMINDEXES
judgements: Object.fromEntries(
gptConfig.orderedJudgements.map((j) => [j, optNull(p.isInteger)]),
gameConfig.orderedJudgements.map((j) => [j, optNull(p.isInteger)]),
),
optional: PR_METRICS(gptConfig.optionalMetrics, true),
optional: PR_METRICS(gameConfig.optionalMetrics, true),
},
calculatedData: Object.fromEntries(
Object.keys(gptConfig.scoreRatingAlgs).map((a) => [a, "*?number"]),
Object.keys(gameConfig.scoreRatingAlgs).map((a) => [a, "*?number"]),
),
})(self);
},
@@ -512,9 +514,9 @@ const PRE_SCHEMAS = {
},
}),
sessions: (self: unknown) => {
const { game, playtype } = extractGPT(self);
const { gameGroup: game, playtype } = extractGame(self);
const gptConfig = GetGamePTConfig(game, playtype);
const gameConfig = LEGACY_GetGamePTConfig(game, playtype);
return prSchemaFnWrap({
userID: p.isPositiveNonZeroInteger,
@@ -528,7 +530,7 @@ const PRE_SCHEMAS = {
timeStarted: p.isPositiveInteger,
highlight: "boolean",
calculatedData: Object.fromEntries(
Object.keys(gptConfig.sessionRatingAlgs).map((k) => [k, "*?number"]),
Object.keys(gameConfig.sessionRatingAlgs).map((k) => [k, "*?number"]),
),
scoreIDs: ["string"],
})(self);
@@ -592,21 +594,17 @@ const PRE_SCHEMAS = {
score: p.any,
}),
"game-stats": (self: unknown) => {
const { game, playtype } = extractGPT(self);
const { v3Game } = extractGame(self);
const gptConfig = GetGamePTConfig(game, playtype);
return prSchemaFnWrap(PR_GAME_STATS(game, playtype, gptConfig))(self);
return prSchemaFnWrap(PR_GAME_STATS(v3Game))(self);
},
"game-stats-snapshots": (self: unknown) => {
const { game, playtype } = extractGPT(self);
const gptConfig = GetGamePTConfig(game, playtype);
const { v3Game } = extractGame(self);
return prSchemaFnWrap(
Object.assign(PR_GAME_STATS(game, playtype, gptConfig), {
Object.assign(PR_GAME_STATS(v3Game), {
rankings: Object.fromEntries(
Object.keys(gptConfig.profileRatingAlgs).map((e) => [
Object.keys(GetGameConfig(v3Game).profileRatingAlgs).map((e) => [
e,
{ outOf: p.isPositiveNonZeroInteger, ranking: p.isPositiveNonZeroInteger },
]),
@@ -617,19 +615,23 @@ const PRE_SCHEMAS = {
)(self);
},
"game-settings": (s: unknown) => {
const { game, playtype } = extractGPT(s);
const gptConfig = GetGamePTConfig(game, playtype);
const { v3Game } = extractGame(s);
return prSchemaFnWrap({
userID: p.isPositiveNonZeroInteger,
game: p.is(game),
playtype: p.is(playtype),
game: p.is(GameToGameGroup(v3Game)),
playtype: p.is(LEGACY_GameToPlaytypeFn(v3Game)),
rivals: [p.isPositiveNonZeroInteger],
preferences: {
preferredScoreAlg: p.nullable(p.isIn(Object.keys(gptConfig.scoreRatingAlgs))),
preferredSessionAlg: p.nullable(p.isIn(Object.keys(gptConfig.sessionRatingAlgs))),
preferredProfileAlg: p.nullable(p.isIn(Object.keys(gptConfig.profileRatingAlgs))),
preferredScoreAlg: p.nullable(
p.isIn(Object.keys(GetGameConfig(v3Game).scoreRatingAlgs)),
),
preferredSessionAlg: p.nullable(
p.isIn(Object.keys(GetGameConfig(v3Game).sessionRatingAlgs)),
),
preferredProfileAlg: p.nullable(
p.isIn(Object.keys(GetGameConfig(v3Game).profileRatingAlgs)),
),
// ouch
stats: p.and(
@@ -654,7 +656,7 @@ const PRE_SCHEMAS = {
defaultTable: p.nullable("string"),
preferredRanking: p.isIn("global", "rival", null),
gameSpecific: PrudenceZodShim(gptConfig.preferences),
gameSpecific: PrudenceZodShim(GetGameConfig(v3Game).preferences),
},
})(s);
},
@@ -666,12 +668,12 @@ const PRE_SCHEMAS = {
game: p.isIn(games),
playtype: isValidPlaytype,
classSet: (self, parent) => {
const gptConfig = GetGamePTConfig(
const gameConfig = LEGACY_GetGamePTConfig(
parent.game as GameGroup,
parent.playtype as Playtypes[GameGroup],
parent.playtype as LEGACY_Playtypes[GameGroup],
);
const keys = Object.keys(gptConfig.classes);
const keys = Object.keys(gameConfig.classes);
if (typeof self !== "string") {
return "Expected a string.";
@@ -864,11 +866,11 @@ const PRE_SCHEMAS = {
export const SCHEMAS: Record<keyof typeof PRE_SCHEMAS, SchemaValidatorFunction> = PRE_SCHEMAS;
const PR_BATCH_MANUAL_SCORE = (game: GameGroup, playtype: Playtype): PrudenceSchema => {
const gptConfig = GetGamePTConfig(game, playtype);
const PR_BATCH_MANUAL_SCORE = (game: V3Game): PrudenceSchema => {
const gameConfig = GetGameConfig(game);
return {
...PR_METRICS(gptConfig.providedMetrics),
...PR_METRICS(gameConfig.providedMetrics),
matchType: p.isIn(
"songTitle",
@@ -902,8 +904,8 @@ const PR_BATCH_MANUAL_SCORE = (game: GameGroup, playtype: Playtype): PrudenceSch
}
for (const [key, v] of Object.entries(self)) {
if (!gptConfig.orderedJudgements.includes(key)) {
return `Invalid Key ${key}. Expected any of ${gptConfig.orderedJudgements.join(
if (!gameConfig.orderedJudgements.includes(key)) {
return `Invalid Key ${key}. Expected any of ${gameConfig.orderedJudgements.join(
", ",
)}`;
}
@@ -915,9 +917,9 @@ const PR_BATCH_MANUAL_SCORE = (game: GameGroup, playtype: Playtype): PrudenceSch
return true;
}),
optional: optNull(PR_METRICS(gptConfig.optionalMetrics, true)),
hitMeta: optNull(PR_METRICS(gptConfig.optionalMetrics, true)),
scoreMeta: optNull(PrudenceZodShim(gptConfig.scoreMeta)),
optional: optNull(PR_METRICS(gameConfig.optionalMetrics, true)),
hitMeta: optNull(PR_METRICS(gameConfig.optionalMetrics, true)),
scoreMeta: optNull(PrudenceZodShim(gameConfig.scoreMeta)),
};
};
@@ -982,8 +984,8 @@ function PR_METRICS(metrics: Record<string, ConfScoreMetric>, shouldAllBeOptNull
return schema;
}
const PR_BATCH_MANUAL_CLASSES = (game: GameGroup, playtype: Playtype): PrudenceSchema => {
const config = GetGamePTConfig(game, playtype);
const PR_BATCH_MANUAL_CLASSES = (game: V3Game): PrudenceSchema => {
const config = GetGameConfig(game);
const schema: PrudenceSchema = {};
@@ -998,15 +1000,15 @@ const PR_BATCH_MANUAL_CLASSES = (game: GameGroup, playtype: Playtype): PrudenceS
return schema;
};
export const PR_BATCH_MANUAL = (game: GameGroup, playtype: Playtype): PrudenceSchema => ({
export const PR_BATCH_MANUAL = (game: V3Game): PrudenceSchema => ({
meta: {
service: p.isBoundedString(3, 60),
game: p.isIn(allSupportedGameGroups),
playtype: p.is(playtype),
playtype: p.is(LEGACY_GameToPlaytypeFn(game)),
version: "*?string",
},
scores: [PR_BATCH_MANUAL_SCORE(game, playtype)],
classes: optNull(PR_BATCH_MANUAL_CLASSES(game, playtype)),
scores: [PR_BATCH_MANUAL_SCORE(game)],
classes: optNull(PR_BATCH_MANUAL_CLASSES(game)),
});
export const PR_RESOLVER: PrudenceSchema = {
+17 -17
View File
@@ -1,14 +1,14 @@
import type {
BMSCourseDocument,
ChartDocument,
FolderDocument,
GameGroup,
GPTStrings,
MONGO_BMSCourseDocument,
MONGO_ChartDocument,
MONGO_FolderDocument,
MONGO_GoalDocument,
MONGO_QuestDocument,
MONGO_QuestlineDocument,
MONGO_SongDocument,
MONGO_TableDocument,
GoalDocument,
QuestDocument,
QuestlineDocument,
SongDocument,
TableDocument,
V3Game,
} from "../types";
import { allSupportedGameGroups } from "../config/config";
@@ -17,20 +17,20 @@ import { allSupportedGameGroups } from "../config/config";
// note that TS won't let you do this multiple times within an object
// so, we have to join them ourselves. Ah well, not that bad.
type ChartDBSeeds = {
[G in GameGroup as `charts-${G}.json`]: Array<MONGO_ChartDocument<GPTStrings[G]>>;
[TGame in V3Game as `charts-${TGame}.json`]: Array<ChartDocument<TGame>>;
};
type SongDBSeeds = {
[G in GameGroup as `songs-${G}.json`]: Array<MONGO_SongDocument<G>>;
[G in GameGroup as `songs-${G}.json`]: Array<SongDocument<G>>;
};
interface OtherDBSeeds {
"bms-course-lookup.json": Array<MONGO_BMSCourseDocument>;
"folders.json": Array<MONGO_FolderDocument>;
"goals.json": Array<MONGO_GoalDocument>;
"questlines.json": Array<MONGO_QuestlineDocument>;
"quests.json": Array<MONGO_QuestDocument>;
"tables.json": Array<MONGO_TableDocument>;
"bms-course-lookup.json": Array<BMSCourseDocument>;
"folders.json": Array<FolderDocument>;
"goals.json": Array<GoalDocument>;
"questlines.json": Array<QuestlineDocument>;
"quests.json": Array<QuestDocument>;
"tables.json": Array<TableDocument>;
}
export type AllDatabaseSeeds = ChartDBSeeds & OtherDBSeeds & SongDBSeeds;
+1 -1
View File
@@ -18,7 +18,7 @@ export enum UserAuthLevels {
* The config the server expects, and emits on /api/v1/config.
*/
export interface TachiServerCoreConfig {
GAMES: Array<GameGroup>;
GAME_GROUPS: Array<GameGroup>;
IMPORT_TYPES: Array<ImportTypes>;
NAME: string;
TYPE: "boku" | "kamai" | "omni";
+16 -21
View File
@@ -1,15 +1,11 @@
import type { integer } from "../types";
import type { GameGroupFromGame, integer, LEGACY_Playtypes, V3Game } from "../types";
import type {
ConfOptionalMetrics,
ConfProvidedMetrics,
Difficulties,
ExtractedClasses,
GameGroup,
GPTString,
GPTStringToGame,
GPTStringToPlaytype,
Judgements,
Playtype,
ScoreMeta,
Versions,
} from "./game-config";
@@ -31,9 +27,8 @@ type MatchTypesWithDifficulty =
export type MatchTypes = MatchTypesNoDifficulty | MatchTypesWithDifficulty;
interface MatchTypeBase {
game: GameGroup;
playtype: Playtype;
version: Versions[GPTString] | null;
game: V3Game;
version: Versions[V3Game] | null;
identifier: string;
artist?: string | null;
}
@@ -49,22 +44,22 @@ export type MatchTypeResolverNoDifficulty = {
export type MatchTypeResolver = MatchTypeResolverNoDifficulty | MatchTypeResolverWithDifficulty;
export type BatchManualScore<GPT extends GPTString = GPTString> = {
export type BatchManualScore<TGame extends V3Game = V3Game> = {
artist?: string | null;
comment?: string | null;
/**
* @deprecated Use `optional` instead.
*/
hitMeta?: AllFieldsNullableOptional<MongoExtractMetrics<ConfOptionalMetrics[GPT]>>;
hitMeta?: AllFieldsNullableOptional<MongoExtractMetrics<ConfOptionalMetrics[TGame]>>;
identifier: string;
judgements?: Record<Judgements[GPT], integer>;
optional?: AllFieldsNullableOptional<MongoExtractMetrics<ConfOptionalMetrics[GPT]>>;
judgements?: Record<Judgements[TGame], integer>;
optional?: AllFieldsNullableOptional<MongoExtractMetrics<ConfOptionalMetrics[TGame]>>;
scoreMeta?: Partial<ScoreMeta[GPT]>;
scoreMeta?: Partial<ScoreMeta[TGame]>;
timeAchieved?: number | null;
} & (
| {
difficulty: Difficulties[GPT];
difficulty: Difficulties[TGame];
matchType: MatchTypesWithDifficulty;
}
| {
@@ -72,15 +67,15 @@ export type BatchManualScore<GPT extends GPTString = GPTString> = {
matchType: MatchTypesNoDifficulty;
}
) &
MongoExtractMetrics<ConfProvidedMetrics[GPT]>;
MongoExtractMetrics<ConfProvidedMetrics[TGame]>;
export interface BatchManual<GPT extends GPTString = GPTString> {
export interface BatchManual<TGame extends V3Game = V3Game> {
meta: {
game: GPTStringToGame[GPT];
playtype: GPTStringToPlaytype[GPT];
game: GameGroupFromGame[TGame];
playtype: LEGACY_Playtypes[GameGroup];
service: string;
version?: Versions[GPT];
version?: Versions[TGame];
};
scores: Array<BatchManualScore<GPT>>;
classes?: ExtractedClasses[GPT] | null;
scores: Array<BatchManualScore<TGame>>;
classes?: ExtractedClasses[TGame] | null;
}
+148 -183
View File
@@ -2,26 +2,22 @@ import type { FilterQuery } from "mongodb";
import type {
AnyClasses,
ChartDocumentData,
Classes,
Difficulties,
ExtractedClasses,
GameGroup,
GPTString,
GPTStrings,
GPTStringToGame,
GPTStringToPlaytype,
GameGroupFromGame,
integer,
Judgements,
MONGO_ChartDocumentData,
MongoDerivedMetrics as MongoDerivedMetrics,
MongoOptionalMetrics as MongoOptionalMetrics,
MongoProvidedMetrics as MongoProvidedMetrics,
LEGACY_GPTStringToGameGroup,
MongoDerivedMetrics,
MongoOptionalMetrics,
MongoProvidedMetrics,
OptionalEnumIndexes,
PgDerivedMetrics,
PgOptionalMetrics,
PgProvidedMetrics,
Playtype,
Playtypes,
Preferences,
ProfileRatingAlgorithms,
ScoreEnumIndexes,
@@ -43,25 +39,14 @@ export interface IObjectID {
readonly toString: () => string;
}
export interface MONGO_CounterDocument {
counterName: string;
value: integer;
}
export interface MONGO_ChartFolderLookupDocument {
chartID: string;
folderID: string;
}
export interface MONGO_BaseGoalDocument<GPT extends GPTString> {
game: GameGroup;
playtype: Playtype;
export interface BaseGoalDocument<TGame extends V3Game = V3Game> {
game: TGame;
name: string;
goalID: string;
criteria: GoalCountCriteria<GPT> | GoalSingleCriteria<GPT>;
criteria: GoalCountCriteria<TGame> | GoalSingleCriteria<TGame>;
}
interface GoalCriteria<_GPT extends GPTString> {
interface GoalCriteria<_TGame extends V3Game = V3Game> {
// vvv this basically doesn't work as it starts thinking this might be a symbol
// causing a myriad of annoying errors.
// key: keyof DerivedMetrics[GPT] | keyof ProvidedMetrics[GPT];
@@ -72,7 +57,7 @@ interface GoalCriteria<_GPT extends GPTString> {
value: number;
}
export interface GoalSingleCriteria<GPT extends GPTString> extends GoalCriteria<GPT> {
export interface GoalSingleCriteria<TGame extends V3Game = V3Game> extends GoalCriteria<TGame> {
mode: "single";
}
@@ -80,7 +65,7 @@ export interface GoalSingleCriteria<GPT extends GPTString> extends GoalCriteria<
* Criteria for a score to match this criteria - this is a "count" mode, which means that
* atleast N scores have to match this criteria. This is for things like folders.
*/
export interface GoalCountCriteria<GPT extends GPTString> extends GoalCriteria<GPT> {
export interface GoalCountCriteria<TGame extends V3Game = V3Game> extends GoalCriteria<TGame> {
mode: "absolute" | "proportion";
countNum: number;
}
@@ -88,8 +73,7 @@ export interface GoalCountCriteria<GPT extends GPTString> extends GoalCriteria<G
/**
* Goal Document - Single. A goal document that is only for one specific chart.
*/
export interface MONGO_GoalDocumentSingle<GPT extends GPTString = GPTString>
extends MONGO_BaseGoalDocument<GPT> {
export interface GoalDocumentSingle<TGame extends V3Game = V3Game> extends BaseGoalDocument<TGame> {
charts: {
data: string;
type: "single";
@@ -100,8 +84,7 @@ export interface MONGO_GoalDocumentSingle<GPT extends GPTString = GPTString>
* Goal Document - Multi. A goal document whos set of charts is the array of
* chartIDs inside "charts".
*/
export interface MONGO_GoalDocumentMulti<GPT extends GPTString = GPTString>
extends MONGO_BaseGoalDocument<GPT> {
export interface GoalDocumentMulti<TGame extends V3Game = V3Game> extends BaseGoalDocument<TGame> {
charts: {
data: Array<string>;
type: "multi";
@@ -112,18 +95,20 @@ export interface MONGO_GoalDocumentMulti<GPT extends GPTString = GPTString>
* Goal Document - Folder. A goal document whos set of charts is derived from
* the folderID inside "charts".
*/
export interface MONGO_GoalDocumentFolder<GPT extends GPTString = GPTString>
extends MONGO_BaseGoalDocument<GPT> {
export interface GoalDocumentFolder<TGame extends V3Game = V3Game> extends BaseGoalDocument<TGame> {
charts: {
/** Internal Postgres `folder.id`; goal validation and storage use this. */
data: string;
/** Set on API responses so clients can link with {@link BaseFolderDocument.slug}. */
folderSlug?: string;
type: "folder";
};
}
export type MONGO_GoalDocument<GPT extends GPTString = GPTString> =
| MONGO_GoalDocumentFolder<GPT>
| MONGO_GoalDocumentMulti<GPT>
| MONGO_GoalDocumentSingle<GPT>;
export type GoalDocument<TGame extends V3Game = V3Game> =
| GoalDocumentFolder<TGame>
| GoalDocumentMulti<TGame>
| GoalDocumentSingle<TGame>;
interface BaseInviteCodeDocument {
createdBy: integer;
@@ -131,7 +116,7 @@ interface BaseInviteCodeDocument {
createdAt: number;
}
export type MONGO_InviteCodeDocument = (
export type InviteCodeDocument = (
| { consumed: false; consumedAt: null; consumedBy: null }
| { consumed: true; consumedAt: number; consumedBy: integer }
) &
@@ -157,19 +142,18 @@ interface SessionScoreNewInfo {
export type SessionScoreInfo = SessionScoreNewInfo | SessionScorePBInfo;
export interface MONGO_SessionDocument<GPT extends GPTString = GPTString> {
export interface SessionDocument<TGame extends V3Game = V3Game> {
userID: integer;
sessionID: string;
scoreIDs: Array<string>;
name: string;
desc: string | null;
game: GameGroup;
playtype: Playtype;
game: V3Game;
timeInserted: integer;
timeEnded: integer;
timeStarted: integer;
calculatedData: Partial<Record<SessionRatingAlgorithms[GPT], number | null>>;
calculatedData: Partial<Record<SessionRatingAlgorithms[TGame], number | null>>;
highlight: boolean;
}
@@ -178,17 +162,20 @@ interface ImportErrContent {
message: string;
}
export interface MONGO_ImportDocument {
export interface ImportDocument {
userID: integer;
timeStarted: number;
timeFinished: number;
// Contains an array of GPTString, which dictates what (game:playtype)s were involved in this import.
gptStrings: Array<GPTString>;
gameGroup: GameGroup;
// What games were involved in this import.
// this is just a subset of the games available in the gameGroup above
// the logic around whether imports can have more than one gameGroup is - uh - muddled
// at best.
games: Array<V3Game>;
importID: string;
scoreIDs: Array<string>;
game: GameGroup;
playtypes: Array<Playtypes[GameGroup]>;
errors: Array<ImportErrContent>;
// TODO(zk): Incoherent and wrong comment
@@ -207,7 +194,7 @@ export interface MONGO_ImportDocument {
userIntent: boolean;
}
export interface MONGO_ImportTimingsDocument {
export interface ImportTimingsDocument {
importID: string;
timestamp: number;
total: number;
@@ -236,7 +223,7 @@ interface ImportTimingSections {
}
export type GoalImportStat = Pick<
MONGO_GoalSubscriptionDocument,
GoalSubscriptionDocument,
"achieved" | "outOf" | "outOfHuman" | "progress" | "progressHuman"
>;
@@ -246,7 +233,7 @@ export interface GoalImportInfo {
new: GoalImportStat;
}
export type QuestImportStat = Pick<MONGO_QuestSubscriptionDocument, "achieved" | "progress">;
export type QuestImportStat = Pick<QuestSubscriptionDocument, "achieved" | "progress">;
export interface QuestImportInfo {
questID: string;
@@ -254,20 +241,18 @@ export interface QuestImportInfo {
new: QuestImportStat;
}
export type MONGO_GoalSubscriptionDocument = {
game: GameGroup;
export type GoalSubscriptionDocument = {
game: V3Game;
goalID: string;
lastInteraction: integer | null;
outOf: number;
outOfHuman: string;
playtype: Playtype;
progress: number | null;
progressHuman: string;
userID: integer;
// Was this goal assigned "standalone"? I.e. a user explicitly subscribed to this.
// instead of it being a result of a quest subscription.
wasAssignedStandalone: boolean;
wasInstantlyAchieved: boolean;
} & (
| {
@@ -291,27 +276,25 @@ export interface QuestSection {
goals: Array<QuestGoalReference>;
}
export interface MONGO_QuestDocument {
game: GameGroup;
playtype: Playtype;
export interface QuestDocument {
game: V3Game;
name: string;
desc: string;
questData: Array<QuestSection>;
questID: string;
}
export interface MONGO_QuestlineDocument {
export interface QuestlineDocument {
questlineID: string;
name: string;
desc: string;
game: GameGroup;
playtype: Playtype;
game: V3Game;
quests: Array<string>;
}
export type UserBadges = "alpha" | "beta" | "contributor" | "dev-team" | "significant-contributor";
export interface MONGO_UserDocument {
export interface UserDocument {
username: string;
usernameLowercase: string;
id: integer;
@@ -334,23 +317,21 @@ export interface MONGO_UserDocument {
isSupporter?: boolean;
}
export interface MONGO_SpecificUserGameStats<GPT extends GPTString> {
export interface SpecificUserGameStats<TGame extends V3Game> {
userID: integer;
game: GPTStringToGame[GPT];
playtype: GPTStringToPlaytype[GPT];
ratings: Partial<Record<ProfileRatingAlgorithms[GPT], number | null>>;
classes: Partial<ExtractedClasses[GPT]>;
game: V3Game;
ratings: Partial<Record<ProfileRatingAlgorithms[TGame], number | null>>;
classes: Partial<ExtractedClasses[TGame]>;
}
/**
* GPT agnostic stats for a game. This type is easier to work with than the
* specificUserGameStats one for general cases.
*/
export interface MONGO_UserGameStats {
export interface UserGameStats {
userID: integer;
game: GameGroup;
playtype: Playtype;
ratings: Partial<Record<ProfileRatingAlgorithms[GPTString], number | null>>;
game: V3Game;
ratings: Partial<Record<ProfileRatingAlgorithms[V3Game], number | null>>;
classes: AnyClasses;
}
@@ -360,20 +341,20 @@ export interface ChartTierlistInfo {
individualDifference?: boolean;
}
export interface MONGO_ChartDocument<GPT extends GPTString = GPTString> {
export interface ChartDocument<TGame extends V3Game = V3Game> {
game: TGame;
chartID: string;
songID: integer;
level: string;
levelNum: number;
isPrimary: boolean;
difficulty: Difficulties[GPT];
playtype: GPTStringToPlaytype[GPT];
data: MONGO_ChartDocumentData[GPT];
versions: Array<Versions[GPT]>;
difficulty: Difficulties[TGame];
data: ChartDocumentData[TGame];
versions: Array<Versions[TGame]>;
song: SongDocument<GameGroupFromGame[TGame]>;
}
export interface MONGO_SongDocument<G extends GameGroup = GameGroup> {
id: integer;
export interface SongDocument<G extends GameGroup = GameGroup> {
id: string;
title: string;
artist: string;
searchTerms: Array<string>;
@@ -381,10 +362,9 @@ export interface MONGO_SongDocument<G extends GameGroup = GameGroup> {
data: SongDocumentData[G];
}
export interface MONGO_TableDocument {
export interface TableDocument {
tableID: string;
game: GameGroup;
playtype: Playtype;
game: V3Game;
title: string;
description: string;
folders: Array<string>;
@@ -392,11 +372,11 @@ export interface MONGO_TableDocument {
default: boolean;
}
export interface MONGO_BaseFolderDocument {
title: string;
game: GameGroup;
playtype: Playtype;
export interface BaseFolderDocument {
folderID: string;
slug: string;
title: string;
game: V3Game;
/**
* This folder has been superceded by another folder,
@@ -406,35 +386,31 @@ export interface MONGO_BaseFolderDocument {
searchTerms: Array<string>;
}
export interface MONGO_FolderSongsDocument extends MONGO_BaseFolderDocument {
export interface FolderSongsDocument extends BaseFolderDocument {
type: "songs";
data: FilterQuery<MONGO_SongDocument>;
data: FilterQuery<SongDocument>;
}
export interface MONGO_FolderChartsDocument extends MONGO_BaseFolderDocument {
export interface FolderChartsDocument extends BaseFolderDocument {
type: "charts";
data: FilterQuery<MONGO_ChartDocument>;
data: FilterQuery<ChartDocument>;
}
export interface MONGO_FolderStaticDocument extends MONGO_BaseFolderDocument {
export interface FolderStaticDocument extends BaseFolderDocument {
type: "static";
data: Array<string>;
}
export type MONGO_FolderDocument =
| MONGO_FolderChartsDocument
| MONGO_FolderSongsDocument
| MONGO_FolderStaticDocument;
export type FolderDocument = FolderChartsDocument | FolderSongsDocument | FolderStaticDocument;
export interface FolderChartLookup {
chartID: string;
folderID: string;
}
export type MONGO_QuestSubscriptionDocument = {
game: GameGroup;
export type QuestSubscriptionDocument = {
game: V3Game;
lastInteraction: integer | null;
playtype: Playtype;
progress: integer;
questID: string;
userID: integer;
@@ -456,35 +432,33 @@ export type PgScoreData<Game extends V3Game = V3Game> = {
judgements: PgScoreJudgements<Game>;
};
export type PgScoreProvidedData<Game extends V3Game = V3Game> =
PgOptionalMetrics[V3GameToGPTString[Game]] & PgProvidedMetrics[V3GameToGPTString[Game]];
export type PgScoreProvidedData<TGame extends V3Game = V3Game> = PgOptionalMetrics[TGame] &
PgProvidedMetrics[TGame];
export type PgScoreDerivedData<Game extends V3Game = V3Game> =
PgDerivedMetrics[V3GameToGPTString[Game]];
export type PgScoreDerivedData<TGame extends V3Game = V3Game> = PgDerivedMetrics[TGame];
export type PgScoreJudgements<Game extends V3Game = V3Game> = Partial<
Record<Judgements[V3GameToGPTString[Game]], integer | null>
export type PgScoreJudgements<TGame extends V3Game = V3Game> = Partial<
Record<Judgements[TGame], integer | null>
>;
export type MONGO_ScoreData<GPT extends GPTString = GPTString> = {
enumIndexes: ScoreEnumIndexes<GPT>;
judgements: Partial<Record<Judgements[GPT], integer | null>>;
export type ScoreData<TGame extends V3Game = V3Game> = {
enumIndexes: ScoreEnumIndexes<TGame>;
judgements: Partial<Record<Judgements[TGame], integer | null>>;
optional: {
enumIndexes: OptionalEnumIndexes<GPT>;
} & MongoOptionalMetrics[GPT];
} & MongoDerivedMetrics[GPT] &
MongoProvidedMetrics[GPT];
enumIndexes: OptionalEnumIndexes<TGame>;
} & MongoOptionalMetrics[TGame];
} & MongoDerivedMetrics[TGame] &
MongoProvidedMetrics[TGame];
export interface MONGO_ScoreDocument<GPT extends GPTString = GPTString> {
export interface ScoreDocument<TGame extends V3Game = V3Game> {
service: string;
game: GPTStringToGame[GPT];
playtype: GPTStringToPlaytype[GPT];
game: TGame;
userID: integer;
scoreData: MONGO_ScoreData<GPT>;
scoreMeta: Partial<ScoreMeta[GPT]>;
calculatedData: Partial<Record<ScoreRatingAlgorithms[GPT], number | null>>;
scoreData: ScoreData<TGame>;
scoreMeta: Partial<ScoreMeta[TGame]>;
calculatedData: Partial<Record<ScoreRatingAlgorithms[TGame], number | null>>;
timeAchieved: integer | null;
songID: integer;
songID: string;
chartID: string;
isPrimary: boolean;
highlight: boolean;
@@ -499,7 +473,7 @@ export interface PBReference {
scoreID: string;
}
export interface MONGO_PBScoreDocument<GPT extends GPTString = GPTString> {
export interface PBScoreDocument<TGame extends V3Game = V3Game> {
// guaranteed to atleast have one element.
composedFrom: [PBReference, ...Array<PBReference>];
rankingData: {
@@ -514,14 +488,13 @@ export interface MONGO_PBScoreDocument<GPT extends GPTString = GPTString> {
};
userID: integer;
chartID: string;
game: GameGroup;
playtype: Playtype;
songID: integer;
game: TGame;
songID: string;
highlight: boolean;
isPrimary: boolean;
timeAchieved: number | null;
scoreData: MONGO_ScoreData<GPT>;
calculatedData: Partial<Record<ScoreRatingAlgorithms[GPT], number | null>>;
scoreData: ScoreData<TGame>;
calculatedData: Partial<Record<ScoreRatingAlgorithms[TGame], number | null>>;
}
export interface ImportProcessInfoOrphanExists {
@@ -549,12 +522,12 @@ export interface ImportProcessInfoAmbiguousTitle {
};
}
export interface ImportProcessInfoScoreImported<GPT extends GPTString = GPTString> {
export interface ImportProcessInfoScoreImported<TGame extends V3Game = V3Game> {
success: true;
type: "ScoreImported";
message: string;
content: {
score: MONGO_ScoreDocument<GPT>;
score: ScoreDocument<TGame>;
};
}
@@ -577,12 +550,12 @@ export interface ImportProcessInfoSongOrChartNotFound {
};
}
export type ImportProcessingInfo<GPT extends GPTString = GPTString> =
export type ImportProcessingInfo<TGame extends V3Game = V3Game> =
| ImportProcessInfoAmbiguousTitle
| ImportProcessInfoInternalError
| ImportProcessInfoInvalidDatapoint
| ImportProcessInfoOrphanExists
| ImportProcessInfoScoreImported<GPT>
| ImportProcessInfoScoreImported<TGame>
| ImportProcessInfoSongOrChartNotFound;
export interface ImportStatistics {
@@ -594,14 +567,14 @@ export interface ImportStatistics {
importID: string;
}
export interface MONGO_KaiAuthDocument {
export interface KaiAuthDocument {
userID: integer;
token: string;
refreshToken: string;
service: "EAG" | "FLO" | "MIN";
}
export interface MONGO_CGCardInfo {
export interface CGCardInfo {
userID: integer;
service: "dev" | "gan" | "nag";
cardID: string;
@@ -610,22 +583,25 @@ export interface MONGO_CGCardInfo {
pin: string;
}
export interface MONGO_MytCardInfo {
export interface MytCardInfo {
userID: integer;
cardAccessCode: string; // matches /^[0-9]{20}$/
}
interface BMSCourseInner<GPT extends GPTStrings["bms"], Set extends keyof ExtractedClasses[GPT]> {
interface BMSCourseInner<TGame extends V3Game, Set extends keyof ExtractedClasses[TGame]> {
set: Set;
playtype: GPTStringToPlaytype[GPT];
value: ExtractedClasses[GPT][Set];
game: TGame;
value: ExtractedClasses[TGame][Set];
}
/**
* Used to resolve beatoraja IR courses.
*/
export interface MONGO_BMSCourseDocument
extends BMSCourseInner<GPTStrings["bms"], keyof ExtractedClasses[GPTStrings["bms"]]> {
export interface BMSCourseDocument
extends BMSCourseInner<
"bms-7k" | "bms-14k" | "pms-controller" | "pms-keyboard",
keyof ExtractedClasses["bms-7k" | "bms-14k" | "pms-controller" | "pms-keyboard"]
> {
title: string;
md5sums: string;
}
@@ -633,7 +609,7 @@ export interface MONGO_BMSCourseDocument
/**
* Information about the API Token used to make this request.
*/
export interface MONGO_APITokenDocument {
export interface APITokenDocument {
userID: integer | null;
token: string | null;
identifier: string;
@@ -644,15 +620,13 @@ export interface MONGO_APITokenDocument {
fromAPIClient: string | null;
}
export interface MONGO_ImportLockDocument {
userID: integer;
}
export type ShowcaseStatDetails = ShowcaseStatChart | ShowcaseStatFolder;
export interface ShowcaseStatFolder {
mode: "folder";
folderID: string;
// TODO(zk): This should be folderID, not slug
/** {@link BaseFolderDocument.slug} for this game. */
slug: string;
// should be a valid metric for the showcase this game is for
// this is not checked by the typesystem though. sorry!
@@ -663,37 +637,32 @@ export interface ShowcaseStatFolder {
export interface ShowcaseStatChart {
mode: "chart";
chartID: string;
// should be a valid metric for the showcase this game is for
// this is not checked by the typesystem though. sorry!
metric: string;
}
export interface MONGO_UGPTSettingsDocument<GPT extends GPTString = GPTString> {
export interface UGPTSettingsDocument<TGame extends V3Game = V3Game> {
userID: integer;
game: GPTStringToGame[GPT];
playtype: GPTStringToPlaytype[GPT];
game: TGame;
preferences: {
defaultTable: string | null;
gameSpecific: Preferences[GPT];
gameSpecific: Preferences[TGame];
preferredDefaultEnum: string | null;
preferredProfileAlg: ProfileRatingAlgorithms[GPT] | null;
preferredProfileAlg: ProfileRatingAlgorithms[TGame] | null;
preferredRanking: "global" | "rival" | null;
preferredScoreAlg: ScoreRatingAlgorithms[GPT] | null;
preferredSessionAlg: SessionRatingAlgorithms[GPT] | null;
preferredScoreAlg: ScoreRatingAlgorithms[TGame] | null;
preferredSessionAlg: SessionRatingAlgorithms[TGame] | null;
stats: Array<ShowcaseStatDetails>;
};
rivals: Array<integer>;
}
export interface MONGO_UserGameStatsSnapshotDocument<GPT extends GPTString = GPTString>
extends MONGO_SpecificUserGameStats<GPT> {
rankings: Record<ProfileRatingAlgorithms[GPT], { outOf: integer; ranking: integer | null }>;
export interface UserGameStatsSnapshotDocument<TGame extends V3Game = V3Game>
extends SpecificUserGameStats<TGame> {
rankings: Record<ProfileRatingAlgorithms[TGame], { outOf: integer; ranking: integer | null }>;
playcount: integer;
timestamp: integer;
}
export interface MONGO_UserSettingsDocument {
export interface UserSettingsDocument {
userID: integer;
following: Array<integer>;
preferences: {
@@ -705,7 +674,7 @@ export interface MONGO_UserSettingsDocument {
};
}
export interface MONGO_TachiAPIClientDocument {
export interface TachiAPIClientDocument {
clientID: string;
clientSecret: string;
name: string;
@@ -717,62 +686,58 @@ export interface MONGO_TachiAPIClientDocument {
apiKeyFilename: string | null;
}
export interface MONGO_FervidexSettingsDocument {
export interface FervidexSettingsDocument {
userID: integer;
cards: Array<string> | null;
forceStaticImport: boolean;
}
export interface MONGO_KsHookSettingsDocument {
export interface KsHookSettingsDocument {
userID: integer;
forceStaticImport: boolean;
}
export interface MONGO_OrphanChartDocument<GPT extends GPTString = GPTString> {
gptString: GPT;
chartDoc: MONGO_ChartDocument<GPT>;
songDoc: MONGO_SongDocument<GPTStringToGame[GPT]>;
export interface OrphanChartDocument<TGame extends V3Game = V3Game> {
game: TGame;
chartDoc: ChartDocument<TGame>;
songDoc: SongDocument<LEGACY_GPTStringToGameGroup[V3GameToGPTString[TGame]]>;
userIDs: Array<integer>;
}
export interface ClassDelta {
game: GameGroup;
set: Classes[GPTString];
playtype: Playtype;
game: V3Game;
set: Classes[V3Game];
old: string | null;
new: string;
}
export interface MONGO_ClassAchievementDocument<GPT extends GPTString = GPTString> {
game: GPTStringToGame[GPT];
playtype: GPTStringToPlaytype[GPT];
classSet: Classes[GPT];
export interface ClassAchievementDocument<TGame extends V3Game = V3Game> {
game: TGame;
classSet: Classes[TGame];
classOldValue: string | null;
classValue: string;
timeAchieved: number;
userID: integer;
}
export interface MONGO_RecentlyViewedFolderDocument {
export interface RecentlyViewedFolderDocument {
userID: integer;
game: GameGroup;
playtype: Playtypes[GameGroup];
folderID: string;
game: V3Game;
// TODO(zk): should be folderID
/** Folder {@link BaseFolderDocument.slug} for this game. */
slug: string;
lastViewed: number;
}
export type MONGO_NotificationDocument = {
export type NotificationDocument = {
body: NotificationBody;
} & BaseNotification;
export interface MONGO_ChallengeSubscriptionDocument {
export interface ChallengeSubscriptionDocument {
chartID: string;
authorID: integer;
type: "lamp" | "score";
game: GameGroup;
playtype: Playtype;
game: V3Game;
userID: integer;
achieved: boolean;
achievedAt: number | null;
@@ -799,11 +764,11 @@ export interface ImportTrackerFailed extends BaseImportTracker {
* Tracks the status of an import while it goes through the pipeline or if it fails.
*
* Successful imports are removed from the tracking database, and their existence
* is kept track of via { @see MONGO_ImportDocument }.
* is kept track of via { @see ImportDocument }.
*/
export type MONGO_ImportTrackerDocument = ImportTrackerFailed | ImportTrackerOngoing;
export type ImportTrackerDocument = ImportTrackerFailed | ImportTrackerOngoing;
export interface MONGO_UserNameChangeDocument {
export interface UserNameChangeDocument {
userID: integer;
username: string;
timestamp: integer;
@@ -128,7 +128,7 @@ export interface FixedDifficulties<Difficulty extends string> {
*
* For example, in IIDX "ANOTHER" is generally shortened to "A".
*/
shorthand: Partial<Record<Difficulty, string>>;
format: Partial<Record<Difficulty, string>>;
default: Difficulty;
}
+150 -85
View File
@@ -1,6 +1,6 @@
import type { z, ZodObject } from "zod";
import type { GAME_GROUP_CONFIGS, GAME_PT_CONFIGS } from "../config/config";
import type { GAME_CONFIGS, GAME_GROUP_CONFIGS } from "../config/config";
import type { integer } from "../types";
import type { MatchTypes } from "./batch-manual";
import type {
@@ -9,7 +9,7 @@ import type {
ProfileRatingAlgorithmConfig,
RatingAlgorithmConfig,
} from "./game-config-utils";
import type { INTERNAL_GAME_PT_CONFIG } from "./internals";
import type { INTERNAL_GAME_CONFIG } from "./internals";
import type {
ExtractEnumMetricNames,
MongoExtractMetrics as MongoExtractMetrics,
@@ -18,11 +18,15 @@ import type {
import type { AllFieldsNullableOptional, ExtractArrayElementType } from "./utils";
/**
* All the games Tachi supports.
* All the game groups Tachi supports.
*/
export type GameGroup = keyof typeof GAME_GROUP_CONFIGS;
/**
* LEGACY. Game + Playtype was the v2 way of identifying games.
*
* ---
*
* What game + playtypes does Tachi support? We typically shorten this concept
* to a "GPT", or Game+Playtype.
*
@@ -36,15 +40,19 @@ export type GameGroup = keyof typeof GAME_GROUP_CONFIGS;
*
* For games that don't really have a meaningful concept of "playtypes", "Single"
* is the go-to.
*
* @deprecated
*/
export type Playtypes = {
export type LEGACY_Playtypes = {
[G in GameGroup]: (typeof GAME_GROUP_CONFIGS)[G]["playtypes"][number];
};
/**
* Expresses any playtype (for any game). Alias for Playtypes[Game].
* Expresses any playtype (for any game). Alias for LEGACY_Playtypes[Game].
*
* @deprecated
*/
export type Playtype = Playtypes[GameGroup];
export type LEGACY_Playtype = LEGACY_Playtypes[GameGroup];
export type SongDocumentData = {
[G in GameGroup]: z.infer<(typeof GAME_GROUP_CONFIGS)[G]["songData"]>;
@@ -55,27 +63,43 @@ export type SongDocumentData = {
* and what playtypes it supports.
*/
export interface GameGroupConfig<G extends GameGroup = GameGroup> {
readonly name: string;
readonly playtypes: ReadonlyArray<Playtypes[G]>;
/**
* Song documents get their own game-specific record that they use for whatever
* they want. IIDX song documents store genres, etc.
* A pretty name for this game group.
*/
readonly name: string;
/**
* In tachi 2, games were identified by a GameGroup + playtype. In Tachi 3,
* games are just identified by the Game.
*
* This is for backwards compatibility with the old system, and the v1 api.
*
* @deprecated Use `games` instead.
*/
readonly playtypes: ReadonlyArray<LEGACY_Playtypes[G]>;
/**
* What games are in this group?
*/
readonly games: ReadonlyArray<V3Game>;
/**
* Songs are the only thing that are meaningfully part of a "game group" but not a game
* itself - basically, for sharing song data between games.
*/
readonly songData: ZodObject;
}
/**
* GPTStrings are an internal (ish) identifier used to identify Game + Playtype combos.
* GPTStrings are an legacy identifier used to identify GameGroup + Playtype combos.
*
* These are used in places where we want to switch over all supported game + playtype
* combos.
*
* The below type magic automatically creates all combinations like iidx:SP, iidx:DP...
* using the `Playtypes` thing above.
*
* @deprecated Use V3Game instead.
*/
export type GPTString = keyof {
[G in GameGroup as `${G}:${Playtypes[G]}`]: never;
export type LEGACY_GPTString = keyof {
[G in GameGroup as `${G}:${LEGACY_Playtypes[G]}`]: never;
};
export type V3Game =
@@ -103,6 +127,32 @@ export type V3Game =
| "usc-keyboard"
| "wacca";
export interface GameGroupFromGame {
arcaea: "arcaea";
"bms-7k": "bms";
"bms-14k": "bms";
chunithm: "chunithm";
"ddr-dp": "ddr";
"ddr-sp": "ddr";
"gitadora-dora": "gitadora";
"gitadora-gita": "gitadora";
"iidx-dp": "iidx";
"iidx-sp": "iidx";
"itg-stamina": "itg";
jubeat: "jubeat";
maimai: "maimai";
maimaidx: "maimaidx";
museca: "museca";
ongeki: "ongeki";
"pms-controller": "pms";
"pms-keyboard": "pms";
popn: "popn";
sdvx: "sdvx";
"usc-controller": "usc";
"usc-keyboard": "usc";
wacca: "wacca";
}
export type V3GameToGPTString = {
arcaea: "arcaea:Touch";
"bms-7k": "bms:7K";
@@ -155,138 +205,150 @@ export type GPTStringToV3Game = {
"wacca:Single": "wacca";
};
export type GPTStrings = {
[G in GameGroup]: `${G}:${Playtypes[G]}`;
/// Get all the games that exist for this game group.
export type GamesForGroup = {
[G in GameGroup]: (typeof GAME_GROUP_CONFIGS)[G]["games"][number];
};
export type GetGameFromGPTString<GPT extends GPTString> = GPT extends `${infer G}:${infer _}`
? G
: never;
export type GetPlaytypeFromGPTString<GPT extends GPTString> = GPT extends `${infer _}:${infer PT}`
? PT
: never;
/**
* @deprecated Use V3Game instead.
*/
export type LEGACY_GPTStrings = {
[G in GameGroup]: `${G}:${LEGACY_Playtypes[G]}`;
};
export type GetGameGroupFromGPTString<GPT extends LEGACY_GPTString> =
GPT extends `${infer G}:${infer _}` ? G : never;
export type GetPlaytypeFromGPTString<GPT extends LEGACY_GPTString> =
GPT extends `${infer _}:${infer PT}` ? PT : never;
// Now that we've got GPTString defined, we can define "lookup types" for things about this GPT.
// For example, if we have a function that works with IIDX's difficulties, we want a type like
// Difficulties["iidx:SP"] which expresses those difficulties.
export type GPTStringToGame = {
[GPT in GPTString]: GetGameFromGPTString<GPT>;
export type LEGACY_GPTStringToGameGroup = {
[GPT in LEGACY_GPTString]: GetGameGroupFromGPTString<GPT>;
};
export type GPTStringToPlaytype = {
[GPT in GPTString]: GetPlaytypeFromGPTString<GPT>;
export type LEGACY_GPTStringToPlaytype = {
[GPT in LEGACY_GPTString]: GetPlaytypeFromGPTString<GPT>;
};
export type LEGACY_GameToPlaytype = {
[TGame in V3Game]: LEGACY_Playtypes[GetGameGroupFromGPTString<V3GameToGPTString[TGame]>];
};
export type Difficulties = {
// If this game has fixed difficulties, infer what they are
// otherwise, difficulties are an arbitrary string
[G in GPTString]: (typeof GAME_PT_CONFIGS)[G]["difficulties"] extends FixedDifficulties<infer D>
[TGame in V3Game]: (typeof GAME_CONFIGS)[TGame]["difficulties"] extends FixedDifficulties<
infer D
>
? D
: string;
};
export type DifficultyConfigs = {
[G in GPTString]: (typeof GAME_PT_CONFIGS)[G]["difficulties"];
[TGame in V3Game]: (typeof GAME_CONFIGS)[TGame]["difficulties"];
};
export type Judgements = {
[G in GPTString]: ExtractArrayElementType<(typeof GAME_PT_CONFIGS)[G]["orderedJudgements"]>;
[TGame in V3Game]: ExtractArrayElementType<(typeof GAME_CONFIGS)[TGame]["orderedJudgements"]>;
};
export type Versions = {
// https://stackoverflow.com/questions/51808160/keyof-inferring-string-number-when-key-is-only-a-string
[G in GPTString]: keyof (typeof GAME_PT_CONFIGS)[G]["versions"] & string;
[TGame in V3Game]: keyof (typeof GAME_CONFIGS)[TGame]["versions"] & string;
};
export type ScoreRatingAlgorithms = {
[G in GPTString]: keyof (typeof GAME_PT_CONFIGS)[G]["scoreRatingAlgs"] & string;
[TGame in V3Game]: keyof (typeof GAME_CONFIGS)[TGame]["scoreRatingAlgs"] & string;
};
export type SessionRatingAlgorithms = {
[G in GPTString]: keyof (typeof GAME_PT_CONFIGS)[G]["sessionRatingAlgs"] & string;
[TGame in V3Game]: keyof (typeof GAME_CONFIGS)[TGame]["sessionRatingAlgs"] & string;
};
export type ProfileRatingAlgorithms = {
[G in GPTString]: keyof (typeof GAME_PT_CONFIGS)[G]["profileRatingAlgs"] & string;
[TGame in V3Game]: keyof (typeof GAME_CONFIGS)[TGame]["profileRatingAlgs"] & string;
};
export type AnyScoreRatingAlg = ScoreRatingAlgorithms[GPTString];
export type AnySessionRatingAlg = SessionRatingAlgorithms[GPTString];
export type AnyProfileRatingAlg = ProfileRatingAlgorithms[GPTString];
export type AnyScoreRatingAlg = ScoreRatingAlgorithms[V3Game];
export type AnySessionRatingAlg = SessionRatingAlgorithms[V3Game];
export type AnyProfileRatingAlg = ProfileRatingAlgorithms[V3Game];
export type ConfProvidedMetrics = {
[G in GPTString]: (typeof GAME_PT_CONFIGS)[G]["providedMetrics"];
[TGame in V3Game]: (typeof GAME_CONFIGS)[TGame]["providedMetrics"];
};
export type ConfDerivedMetrics = {
[G in GPTString]: (typeof GAME_PT_CONFIGS)[G]["derivedMetrics"];
[TGame in V3Game]: (typeof GAME_CONFIGS)[TGame]["derivedMetrics"];
};
export type ConfOptionalMetrics = {
[G in GPTString]: (typeof GAME_PT_CONFIGS)[G]["optionalMetrics"];
[TGame in V3Game]: (typeof GAME_CONFIGS)[TGame]["optionalMetrics"];
};
export type ConfScoreMetrics = {
[G in GPTString]: ConfDerivedMetrics[G] & ConfProvidedMetrics[G];
[TGame in V3Game]: ConfDerivedMetrics[TGame] & ConfProvidedMetrics[TGame];
};
export type ExtractedClasses = {
[G in GPTString]: ExtractClassValues<(typeof GAME_PT_CONFIGS)[G]["classes"]>;
[TGame in V3Game]: ExtractClassValues<(typeof GAME_CONFIGS)[TGame]["classes"]>;
};
export type AnyClasses = {
[C in Classes[GPTString]]?: string | null;
[C in Classes[V3Game]]?: string | null;
};
export type Classes = {
[G in GPTString]: keyof (typeof GAME_PT_CONFIGS)[G]["classes"] & string;
[TGame in V3Game]: keyof (typeof GAME_CONFIGS)[TGame]["classes"] & string;
};
export type ClassConfigs = {
[G in GPTString]: (typeof GAME_PT_CONFIGS)[G]["classes"];
[TGame in V3Game]: (typeof GAME_CONFIGS)[TGame]["classes"];
};
export type MONGO_ChartDocumentData = {
[G in GPTString]: z.infer<(typeof GAME_PT_CONFIGS)[G]["chartData"]>;
export type ChartDocumentData = {
[TGame in V3Game]: z.infer<(typeof GAME_CONFIGS)[TGame]["chartData"]>;
};
export type Preferences = {
[G in GPTString]: z.infer<(typeof GAME_PT_CONFIGS)[G]["preferences"]>;
[TGame in V3Game]: z.infer<(typeof GAME_CONFIGS)[TGame]["preferences"]>;
};
export type ScoreMeta = {
[G in GPTString]: z.infer<(typeof GAME_PT_CONFIGS)[G]["scoreMeta"]>;
[TGame in V3Game]: z.infer<(typeof GAME_CONFIGS)[TGame]["scoreMeta"]>;
};
export type MongoProvidedMetrics = {
[G in GPTString]: MongoExtractMetrics<(typeof GAME_PT_CONFIGS)[G]["providedMetrics"]>;
[TGame in V3Game]: MongoExtractMetrics<(typeof GAME_CONFIGS)[TGame]["providedMetrics"]>;
};
export type MongoDerivedMetrics = {
[G in GPTString]: MongoExtractMetrics<(typeof GAME_PT_CONFIGS)[G]["derivedMetrics"]>;
[TGame in V3Game]: MongoExtractMetrics<(typeof GAME_CONFIGS)[TGame]["derivedMetrics"]>;
};
export type PgProvidedMetrics = {
[G in GPTString]: PgExtractMetrics<(typeof GAME_PT_CONFIGS)[G]["providedMetrics"]>;
[TGame in V3Game]: PgExtractMetrics<(typeof GAME_CONFIGS)[TGame]["providedMetrics"]>;
};
export type PgOptionalMetrics = {
[G in GPTString]: PgExtractMetrics<(typeof GAME_PT_CONFIGS)[G]["optionalMetrics"]>;
[TGame in V3Game]: PgExtractMetrics<(typeof GAME_CONFIGS)[TGame]["optionalMetrics"]>;
};
export type PgDerivedMetrics = {
[G in GPTString]: PgExtractMetrics<(typeof GAME_PT_CONFIGS)[G]["derivedMetrics"]>;
[TGame in V3Game]: PgExtractMetrics<(typeof GAME_CONFIGS)[TGame]["derivedMetrics"]>;
};
/**
* Top level metrics on a score.
*/
export type MongoScoreMetrics = {
[G in GPTString]: MongoDerivedMetrics[G] & MongoProvidedMetrics[G];
[TGame in V3Game]: MongoDerivedMetrics[TGame] & MongoProvidedMetrics[TGame];
};
export type MongoOptionalMetrics = {
[G in GPTString]: AllFieldsNullableOptional<
MongoExtractMetrics<(typeof GAME_PT_CONFIGS)[G]["optionalMetrics"]>
[TGame in V3Game]: AllFieldsNullableOptional<
MongoExtractMetrics<(typeof GAME_CONFIGS)[TGame]["optionalMetrics"]>
>;
};
@@ -294,37 +356,37 @@ export type MongoOptionalMetrics = {
* Alongside strings, we want to store integers to represent the integer value
* of enums.
*/
export type ScoreEnumIndexes<GPT extends GPTString> = Record<
ExtractEnumMetricNames<ConfDerivedMetrics[GPT] & ConfProvidedMetrics[GPT]>,
export type ScoreEnumIndexes<TGame extends V3Game> = Record<
ExtractEnumMetricNames<ConfDerivedMetrics[TGame] & ConfProvidedMetrics[TGame]>,
integer
>;
/**
* Same as ScoreEnumIndexes but for the optional properties on a score.
*/
export type OptionalEnumIndexes<GPT extends GPTString> = Partial<
Record<ExtractEnumMetricNames<ConfOptionalMetrics[GPT]>, integer>
export type OptionalEnumIndexes<TGame extends V3Game> = Partial<
Record<ExtractEnumMetricNames<ConfOptionalMetrics[TGame]>, integer>
>;
/**
* A generic GamePTConfig. This type is significantly less specific than the
* "SpecificGamePTConfig", which only really works as a type if you know *what*
* GamePTConfig you're working with.
* A generic GameConfig. This type is significantly less specific than the
* "SpecificGameConfig", which only really works as a type if you know *what*
* GameConfig you're working with.
*/
export type GamePTConfig = {
defaultProfileRatingAlg: ProfileRatingAlgorithms[GPTString];
defaultScoreRatingAlg: ScoreRatingAlgorithms[GPTString];
defaultSessionRatingAlg: SessionRatingAlgorithms[GPTString];
} & INTERNAL_GAME_PT_CONFIG;
export type GameConfig = {
defaultProfileRatingAlg: ProfileRatingAlgorithms[V3Game];
defaultScoreRatingAlg: ScoreRatingAlgorithms[V3Game];
defaultSessionRatingAlg: SessionRatingAlgorithms[V3Game];
} & INTERNAL_GAME_CONFIG;
/**
* Configuration for a GPT. This declares *almost everything* about how this game is
* implemented in Tachi, such as what metrics it supports, how it handles chart
* difficulties, etc.
*
* To get a GamePTConfig for a given Game + Playtype, @see {GetGamePTConfig}
* To get a GameConfig for a given Game + Playtype, @see {GetGameConfig}
*/
export interface SpecificGamePTConfig<GPT extends GPTString> {
export interface SpecificGameConfig<TGame extends V3Game> {
/**
* What metrics **must** be provided in order for this score to be usable by
* Tachi?
@@ -332,7 +394,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* This is intended for things like Score, Lamp, etc. Things that quite fundamentally
* *are* the metrics of the score.
*/
providedMetrics: ConfProvidedMetrics[GPT];
providedMetrics: ConfProvidedMetrics[TGame];
/**
* What metrics do we want to exist on score documents, but don't need to be
@@ -354,7 +416,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* Another good example would be "Grade" for most games, as a grade is often just
* cutoffs applied on score values.
*/
derivedMetrics: ConfDerivedMetrics[GPT];
derivedMetrics: ConfDerivedMetrics[TGame];
/**
* What's the default metric for this GPT?
@@ -363,7 +425,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
*
* @note This **MUST** be one of the mandatory or derived keys.
*/
defaultMetric: keyof ConfDerivedMetrics[GPT] | keyof ConfProvidedMetrics[GPT];
defaultMetric: keyof ConfDerivedMetrics[TGame] | keyof ConfProvidedMetrics[TGame];
/**
* What's the preferred default enum for this GPT?
@@ -374,7 +436,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* @note This **MUST** be one of the mandatory or derived keys.
*/
preferredDefaultEnum: ExtractEnumMetricNames<
ConfDerivedMetrics[GPT] & ConfProvidedMetrics[GPT]
ConfDerivedMetrics[TGame] & ConfProvidedMetrics[TGame]
>;
/**
@@ -387,7 +449,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* The idea of additionalMetrics allow us to store useful metrics about scores
* without necessitating that they exist on arrival. Incredibly convenient.
*/
additionalMetrics: ConfOptionalMetrics[GPT];
additionalMetrics: ConfOptionalMetrics[TGame];
/**
* What rating algorithms may a score have attached onto it for this GPT?
@@ -396,7 +458,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* server config. By defining them here, the typesystem will enforce that you
* implement them elsewhere.
*/
scoreRatingAlgs: Record<ScoreRatingAlgorithms[GPT], RatingAlgorithmConfig>;
scoreRatingAlgs: Record<ScoreRatingAlgorithms[TGame], RatingAlgorithmConfig>;
/**
* What rating algorithms may a session have attached onto it for this GPT?
@@ -405,7 +467,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* server config. By defining them here, the typesystem will enforce that you
* implement them elsewhere.
*/
sessionRatingAlgs: Record<SessionRatingAlgorithms[GPT], RatingAlgorithmConfig>;
sessionRatingAlgs: Record<SessionRatingAlgorithms[TGame], RatingAlgorithmConfig>;
/**
* What rating algorithms may a profile have attached onto it for this GPT?
@@ -425,7 +487,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* server config. By defining them here, the typesystem will enforce that you
* implement them elsewhere.
*/
profileRatingAlgs: Record<ProfileRatingAlgorithms[GPT], ProfileRatingAlgorithmConfig>;
profileRatingAlgs: Record<ProfileRatingAlgorithms[TGame], ProfileRatingAlgorithmConfig>;
/**
* What classes may a profile have attached onto it for this GPT?
@@ -436,28 +498,28 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* or they may be provided by score imports, such as "dans", which cannot be
* derived from a player's scores or profile ratings.
*/
classes: ClassConfigs[GPT];
classes: ClassConfigs[TGame];
/**
* What's the default score rating algorithm for this GPT?
*
* @note This should be one of the keys in scoreRatingAlgs.
*/
defaultScoreRatingAlg: ScoreRatingAlgorithms[GPT];
defaultScoreRatingAlg: ScoreRatingAlgorithms[TGame];
/**
* What's the default session rating algorithm for this GPT?
*
* @note This should be one of the keys in sessionRatingAlgs.
*/
defaultSessionRatingAlg: SessionRatingAlgorithms[GPT];
defaultSessionRatingAlg: SessionRatingAlgorithms[TGame];
/**
* What's the default profile rating algorithm for this GPT1?
*
* @note This should be one of the keys in sessionRatingAlgs.
*/
defaultProfileRatingAlg: ProfileRatingAlgorithms[GPT];
defaultProfileRatingAlg: ProfileRatingAlgorithms[TGame];
/**
* How does this GPT handle difficulties?
@@ -470,14 +532,14 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* allowing any string (as long as its unique.)
*
*/
difficulties: DifficultyConfigs[GPT];
difficulties: DifficultyConfigs[TGame];
/**
* What judgements does this GPT have? These are typically timing-window names.
*
* These should be ordered from **best to worst**.
*/
orderedJudgements: ReadonlyArray<Judgements[GPT]>;
orderedJudgements: ReadonlyArray<Judgements[TGame]>;
/**
* What versions do we support for this GPT?
@@ -500,7 +562,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* version this score was on. That way, we can make sure they resolve to the right
* chart.
*/
versions: Record<Versions[GPT], string>;
versions: Record<Versions[TGame], string>;
/**
* Chart documents get their own GPT-specific record that they use for whatever
@@ -532,3 +594,6 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
*/
supportedMatchTypes: ReadonlyArray<MatchTypes>;
}
// Games that are BMS-like.
export type BMSGames = GamesForGroup["bms" | "pms"];
+11 -9
View File
@@ -9,17 +9,18 @@ import type {
} from "./game-config-utils";
import type { ConfScoreMetric } from "./metrics";
import { type V3Game } from "../types";
/**
* What's the "mold" for a GPT config? All GPT configs *must* satisfy this interface,
* but aren't necessarily this type.
* What's the "mold" for a game config? All game configs *must* satisfy this interface.
*
* @see {GamePTConfig} for the intended-to-use type. This is an *outline* for a GPT
* @see {GameConfig} for the intended-to-use type. This is an *outline* for a GPT
* config, and has significantly less typesafety.
*
* Documentation for this type can be found in `game-support.ts`, which has the
* GamePTConfig type, which actually has documentation.
* GameConfig type, which actually has documentation.
*/
export type INTERNAL_GAME_PT_CONFIG = Readonly<{
export type INTERNAL_GAME_CONFIG = Readonly<{
chartData: ZodObject;
classes: Record<string, ClassConfig>;
@@ -49,6 +50,7 @@ export type INTERNAL_GAME_PT_CONFIG = Readonly<{
} & ConfScoreMetric
>;
orderedJudgements: ReadonlyArray<string>;
preferences: ZodObject;
preferredDefaultEnum: string;
@@ -60,20 +62,20 @@ export type INTERNAL_GAME_PT_CONFIG = Readonly<{
scoreMeta: ZodObject;
scoreRatingAlgs: Record<string, RatingAlgorithmConfig>;
sessionRatingAlgs: Record<string, RatingAlgorithmConfig>;
supportedMatchTypes: ReadonlyArray<MatchTypes>;
versions: Record<string, string>;
}>;
/**
* A game config *must* satisfy this, but we don't export this kind of game config.
* A game group config *must* satisfy this, but we don't export this kind of game config.
*
* Think of this like a "mold" for a game config, it's gotta be shaped like this,
* but interacting with the mold is a little too malleable for the rest of the
* codebase. @see {GameConfig} for the exported version.
* codebase. @see {GameGroupConfig} for the exported version.
*/
export type INTERNAL_GAME_CONFIG<PT extends string = string> = Readonly<{
export type INTERNAL_GAME_GROUP_CONFIG<PT extends string = string> = Readonly<{
games: ReadonlyArray<V3Game>;
name: string;
playtypes: ReadonlyArray<PT>;
songData: ZodObject;
+12 -36
View File
@@ -1,11 +1,6 @@
import type { integer } from "../types";
import type { MONGO_ChartDocument } from "./documents";
import type {
ConfDerivedMetrics,
ConfOptionalMetrics,
ConfProvidedMetrics,
GPTString,
} from "./game-config";
import type { integer, V3Game } from "../types";
import type { ChartDocument } from "./documents";
import type { ConfDerivedMetrics, ConfOptionalMetrics, ConfProvidedMetrics } from "./game-config";
// WARNING! THIS FILE IS VERY COMPLEX. THERE IS SOME HIGH-TIER TYPESCRIPT MAGIC GOING
// ON HERE.
@@ -175,9 +170,9 @@ export type ExtractEnumMetricNames<R extends Record<string, ConfScoreMetric>> =
* What are all the metrics available for this GPT?
*/
export type AllConfMetrics = {
[GPT in GPTString]: ConfDerivedMetrics[GPT] &
ConfOptionalMetrics[GPT] &
ConfProvidedMetrics[GPT];
[TGame in V3Game]: ConfDerivedMetrics[TGame] &
ConfOptionalMetrics[TGame] &
ConfProvidedMetrics[TGame];
};
/**
@@ -186,10 +181,10 @@ export type AllConfMetrics = {
* @usage GetEnumValue<"iidx:SP", "lamp"> = "FAILED" | "ASSIST CLEAR" | "EASY CLEAR" ...
*/
export type GetEnumValue<
GPT extends GPTString,
MetricName extends ExtractEnumMetricNames<AllConfMetrics[GPT]>,
TGame extends V3Game,
MetricName extends ExtractEnumMetricNames<AllConfMetrics[TGame]>,
> =
AllConfMetrics[GPT][MetricName] extends ConfEnumScoreMetric<infer EnumValues>
AllConfMetrics[TGame][MetricName] extends ConfEnumScoreMetric<infer EnumValues>
? EnumValues
: never;
@@ -220,30 +215,11 @@ export type DerivedMetricValue = number | string | Array<number> | Array<number
export type MetricValue = MongoExtractMetricValue<ConfScoreMetric>;
export type MetricDeriver<
GPT extends GPTString,
TGame extends V3Game,
// possible return values
// from a derived fn
V extends DerivedMetricValue = DerivedMetricValue,
> = (
mandatoryMetrics: MongoExtractMetrics<ConfProvidedMetrics[GPT]>,
chart: MONGO_ChartDocument<GPT>,
mandatoryMetrics: MongoExtractMetrics<ConfProvidedMetrics[TGame]>,
chart: ChartDocument<TGame>,
) => V;
/**
* A function that will derive this metric, given a function of other metrics and
* a chart for this GPT.
*/
export type __OLD_KILL_ScoreMetricDeriver<M extends ConfScoreMetric, GPT extends GPTString> =
// graph score metrics correspond to Array<number>
M extends ConfGraphScoreMetric
? MetricDeriver<GPT, Array<number>>
: // nullable graphs correspond to Array<number | null>
M extends ConfNullableGraphScoreMetric
? MetricDeriver<GPT, Array<number | null>>
: // enums correspond to their string unions ("FAILED"|"CLEAR")
M extends ConfEnumScoreMetric<infer V>
? MetricDeriver<GPT, V>
: // the other two are obvious
M extends ConfIntegerScoreMetric
? MetricDeriver<GPT, integer>
: MetricDeriver<GPT, number>;
+3 -5
View File
@@ -1,4 +1,4 @@
import type { GameGroup, integer, Playtype } from "../types";
import type { integer, V3Game } from "../types";
export interface BaseNotification {
title: string;
@@ -13,16 +13,14 @@ export interface BaseNotification {
export type NotificationBody =
| {
content: {
game: GameGroup;
playtype: Playtype;
game: V3Game;
questID: string;
};
type: "QUEST_CHANGED"; // Emitted when a quest the user is subscribed to changed.
}
| {
content: {
game: GameGroup;
playtype: Playtype;
game: V3Game;
userID: integer;
};
type: "RIVALED_BY"; // Emitted when the user is rivalled by someone.
+87 -184
View File
@@ -3,16 +3,15 @@ import type { ZodObject } from "zod";
import type { GradeBoundary, IIDXLikes } from "../constants/grade-boundaries";
import type {
BMSCourseDocument,
BMSGames,
ChartDocument,
GameGroup,
GPTString,
GPTStrings,
GamesForGroup,
integer,
MONGO_BMSCourseDocument,
MONGO_ChartDocument,
MONGO_SongDocument,
Playtypes,
LEGACY_Playtypes,
SongDocument,
V3Game,
V3GameToGPTString,
} from "../types";
import type {
AllConfMetrics,
@@ -22,22 +21,42 @@ import type {
} from "../types/metrics";
import {
ALL_GAMES,
GameToGameGroup,
GetGameConfig,
GetGameGroupConfig,
GetGamePTConfig,
GetGPTString,
GetSpecificGPTConfig,
SplitGPT,
V3GetGameConfig,
V3ToGPTString,
LEGACY_GameToGameGroupPT,
} from "../config/config";
/**
* Stick this in the "default" branch of switch exprs to statically typecheck that your
* switch is exhaustive.
*
* This works because the argument to this function should be "never" because all of its
* variants should be exhausted.
*/
export function staticAssertUnreachable(_: never): never {
throw new Error(`unreachable (Got ${JSON.stringify(_)})`);
}
export function FormatInt(v: number): string {
return Math.floor(v).toFixed(0);
}
export function FormatDifficulty(chart: MONGO_ChartDocument, game: GameGroup): string {
if (game === "bms" || game === "pms") {
const bmsChart = chart as MONGO_ChartDocument<"bms:7K" | "bms:14K">;
export function FormatDifficulty(chart: ChartDocument): string {
return FormatDifficultyShort(chart);
}
/**
* Formats a chart's difficulty into a shorter variant. This handles a lot of
* game-specific strange edge cases.
*/
export function FormatDifficultyShort(chart: ChartDocument): string {
const gameGroup = GameToGameGroup(chart.game);
const gameConfig = GetGameConfig(chart.game);
if (gameGroup === "bms" || gameGroup === "pms") {
const bmsChart = chart as ChartDocument<BMSGames>;
return (
Object.entries(bmsChart.data.tableFolders)
@@ -45,9 +64,8 @@ export function FormatDifficulty(chart: MONGO_ChartDocument, game: GameGroup): s
.join(", ") || "Unrated"
);
}
if (game === "itg") {
const itgChart = chart as MONGO_ChartDocument<"itg:Stamina">;
if (gameGroup === "itg") {
const itgChart = chart as ChartDocument<"itg-stamina">;
const level = itgChart.data.rankedLevel ?? itgChart.data.chartLevel;
const unranked = itgChart.data.rankedLevel === null ? "UNRANKED " : "";
@@ -55,111 +73,33 @@ export function FormatDifficulty(chart: MONGO_ChartDocument, game: GameGroup): s
return `${unranked}${itgChart.data.difficultyTag} ${level} (${itgChart.data.charter})`;
}
if (game === "gitadora") {
const ch = chart as MONGO_ChartDocument<GPTStrings["gitadora"]>;
// use the difficulty format there
if (gameConfig.difficulties.type === "FIXED") {
const diff = gameConfig.difficulties.format[chart.difficulty] ?? chart.difficulty;
const gptConfig = GetSpecificGPTConfig<GPTStrings["gitadora"]>(
GetGPTString(game, chart.playtype) as GPTStrings["gitadora"],
);
// it's complaining that since the dora config doesn't have shorthand for
// "BASS BASIC", this assignment may fail.
// it's technically correct, but in the worst way, since this isn't
// actually possible.
// todo: come up with something better.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore this fails in my IDE but is fine in the compiler, can't use ts-expect-err here.
const shortDiff = gptConfig.difficulties.shorthand[ch.difficulty];
// gitadora should always use short diffs. they just look better.
return `${shortDiff} ${chart.level}`;
}
const gameConfig = GetGameGroupConfig(game);
if (gameConfig.playtypes.length > 1) {
return `${chart.playtype} ${chart.difficulty} ${chart.level}`;
return `${diff} ${chart.level}`;
}
// TODO cap string length
return `${chart.difficulty} ${chart.level}`;
}
/**
* Formats a chart's difficulty into a shorter variant. This handles a lot of
* game-specific strange edge cases.
*/
export function FormatDifficultyShort(chart: MONGO_ChartDocument, game: GameGroup): string {
const gameConfig = GetGameGroupConfig(game);
const gptConfig = GetGamePTConfig(game, chart.playtype);
if (game === "itg") {
const itgChart = chart as MONGO_ChartDocument<"itg:Stamina">;
return `S${itgChart.data.difficultyTag} ${chart.level}`;
}
if (gptConfig.difficulties.type === "DYNAMIC") {
// TODO cap string length
return `${chart.difficulty} ${chart.level}`;
}
const shortDiff = gptConfig.difficulties.shorthand[chart.difficulty] ?? chart.difficulty;
if (gameConfig.playtypes.length === 1 || game === "gitadora") {
return `${shortDiff} ${chart.level}`;
}
if (game === "usc") {
return `${chart.playtype === "Controller" ? "CON" : "KB"} ${shortDiff} ${chart.level}`;
}
return `${chart.playtype}${shortDiff} ${chart.level}`;
}
/**
* Formats a chart's difficulty for searching, such as forwarding this query to youtube.
*/
export function FormatDifficultySearch(chart: MONGO_ChartDocument, game: GameGroup): string | null {
const gptConfig = GetGamePTConfig(game, chart.playtype);
if (game === "itg") {
const itgChart = chart as MONGO_ChartDocument<"itg:Stamina">;
return `S${itgChart.data.difficultyTag} ${chart.level}`;
}
if (gptConfig.difficulties.type === "DYNAMIC") {
// TODO cap string length
return chart.difficulty;
}
const shortDiff = gptConfig.difficulties.shorthand[chart.difficulty] ?? chart.difficulty;
switch (game) {
case "jubeat":
case "maimai":
case "museca":
case "maimaidx":
case "popn":
case "sdvx":
case "wacca":
case "arcaea":
case "ongeki":
case "chunithm":
case "ddr":
case "usc":
return chart.difficulty;
case "iidx":
return `${chart.playtype}${shortDiff}`;
case "gitadora":
return shortDiff;
case "bms":
case "pms":
return null;
}
export function FormatDifficultySearch(chart: ChartDocument): string | null {
return FormatDifficulty(chart);
}
export function FormatGameGroup(game: GameGroup, playtype: Playtypes[GameGroup]): string {
export function FormatGame(game: V3Game): string {
const { gameGroup, playtype } = LEGACY_GameToGameGroupPT(game);
return LEGACY_FormatGameGroupPT(gameGroup, playtype);
}
export function LEGACY_FormatGameGroupPT(
game: GameGroup,
playtype: LEGACY_Playtypes[GameGroup],
): string {
const gameConfig = GetGameGroupConfig(game);
if (gameConfig.playtypes.length === 1) {
@@ -173,22 +113,12 @@ export function FormatGameGroup(game: GameGroup, playtype: Playtypes[GameGroup])
return `${gameConfig.name} (${playtype})`;
}
export function V3FormatGame(game: V3Game): string {
const [g, p] = SplitGPT(V3ToGPTString(game));
export function FormatChart(chart: ChartDocument): string {
const gameGroup = GameToGameGroup(chart.game);
if (gameGroup === "bms") {
const tables = (chart as ChartDocument<GamesForGroup["bms"]>).data.tableFolders;
return FormatGameGroup(g, p);
}
export function FormatChart(
game: GameGroup,
song: MONGO_SongDocument,
chart: MONGO_ChartDocument,
short = false,
): string {
if (game === "bms") {
const tables = (chart as MONGO_ChartDocument<GPTStrings["bms"]>).data.tableFolders;
const bmsSong = song as MONGO_SongDocument<"bms">;
const bmsSong = chart.song as SongDocument<"bms">;
let realTitle = bmsSong.title;
@@ -207,13 +137,14 @@ export function FormatChart(
return `${realTitle} (${Object.entries(tables)
.map(([table, level]) => `${table}${level}`)
.join(", ")})`;
} else if (game === "usc") {
const uscChart = chart as MONGO_ChartDocument<GPTStrings["usc"]>;
} else if (gameGroup === "usc") {
const uscChart = chart as ChartDocument<GamesForGroup["usc"]>;
const inputType = chart.game === "usc-controller" ? "Controller" : "Keyboard";
// If this chart isn't an official, render it differently
if (!uscChart.data.isOfficial) {
// Same as BMS. turn this into SongTitle (Keyboard MXM normal1, insane2)
return `${song.title} (${chart.playtype} ${chart.difficulty} ${Object.entries(
return `${chart.song.title} (${inputType} ${chart.difficulty} ${Object.entries(
uscChart.data.tableFolders,
)
.map(([table, level]) => `${table}${level}`)
@@ -223,7 +154,7 @@ export function FormatChart(
// it as so:
// SongTitle (Keyboard MXM 17, normal1, insane2)
return `${song.title} (${chart.playtype} ${chart.difficulty} ${
return `${chart.song.title} (${inputType} ${chart.difficulty} ${
chart.level
}, ${Object.entries(uscChart.data.tableFolders)
.map(([table, level]) => `${table}${level}`)
@@ -231,9 +162,9 @@ export function FormatChart(
}
// otherwise, it's just an official and should be rendered like any other game.
} else if (game === "itg") {
const itgChart = chart as MONGO_ChartDocument<"itg:Stamina">;
const itgSong = song as MONGO_SongDocument<"itg">;
} else if (gameGroup === "itg") {
const itgChart = chart as ChartDocument<GamesForGroup["itg"]>;
const itgSong = chart.song as SongDocument<"itg">;
const level = itgChart.data.rankedLevel ?? `${itgChart.data.chartLevel}?`;
@@ -242,43 +173,23 @@ export function FormatChart(
} ${level}`;
}
const gameConfig = GetGameGroupConfig(game);
let playtypeStr = `${chart.playtype}`;
if (gameConfig.playtypes.length === 1) {
playtypeStr = "";
}
const gptConfig = GetGamePTConfig(game, chart.playtype);
const gameConfig = GetGameConfig(chart.game);
let diff: string;
if (gptConfig.difficulties.type === "DYNAMIC") {
if (gameConfig.difficulties.type === "DYNAMIC") {
diff = chart.difficulty;
} else if (short) {
diff = gptConfig.difficulties.shorthand[chart.difficulty] ?? chart.difficulty;
} else {
diff = chart.difficulty;
}
// iidx formats things like SPA instead of SP A.
// this is a hack, this should be part of the gptConfig, tbh.
let space = "";
if ((game === "iidx" && short) || !playtypeStr) {
space = "";
} else {
space = " ";
diff = gameConfig.difficulties.format[chart.difficulty] ?? chart.difficulty;
}
// return the most recent version this chart appeared in if it
// is not primary.
if (!chart.isPrimary) {
return `${song.title} (${playtypeStr}${space}${diff} ${chart.level} ${chart.versions[0]})`;
return `${chart.song.title} (${diff} ${chart.level} ${chart.versions[0]})`;
}
return `${song.title} (${playtypeStr}${space}${diff} ${chart.level})`;
return `${chart.song.title} (${diff} ${chart.level})`;
}
/**
@@ -446,10 +357,8 @@ export function GetCloserGradeDelta<G extends string>(
return lower;
}
export function CreateSongMap<G extends GameGroup = GameGroup>(
songs: Array<MONGO_SongDocument<G>>,
) {
const songMap = new Map<integer, MONGO_SongDocument<G>>();
export function CreateSongMap<G extends GameGroup = GameGroup>(songs: Array<SongDocument<G>>) {
const songMap = new Map<string, SongDocument<G>>();
for (const song of songs) {
songMap.set(song.id, song);
@@ -458,10 +367,8 @@ export function CreateSongMap<G extends GameGroup = GameGroup>(
return songMap;
}
export function CreateChartMap<GPT extends GPTString = GPTString>(
charts: Array<MONGO_ChartDocument<GPT>>,
) {
const chartMap = new Map<string, MONGO_ChartDocument<GPT>>();
export function CreateChartMap<TGame extends V3Game = V3Game>(charts: Array<ChartDocument<TGame>>) {
const chartMap = new Map<string, ChartDocument<TGame>>();
for (const chart of charts) {
chartMap.set(chart.chartID, chart);
@@ -470,11 +377,6 @@ export function CreateChartMap<GPT extends GPTString = GPTString>(
return chartMap;
}
/** TODO(zk) remove this function */
export function MongoChartLegacyId(chart: MONGO_ChartDocument): string {
return chart.chartID;
}
/**
* Formats a PrudenceError into something a little more readable.
* @param err - The prudence error to format.
@@ -489,10 +391,10 @@ export function FormatPrError(err: PrudenceError, foreword = "Error"): string {
return `${foreword}: ${err.keychain} | ${err.message}${receivedText}.`;
}
export function GetBMSCourseIndex(course: MONGO_BMSCourseDocument) {
const gptConf = GetGamePTConfig("bms", course.playtype);
export function GetBMSCourseIndex(course: BMSCourseDocument) {
const gameConfig = GetGameConfig(course.game);
const cls = gptConf.classes[course.set];
const cls = gameConfig.classes[course.set as keyof typeof gameConfig.classes];
if (!cls) {
throw new Error(
@@ -549,10 +451,10 @@ export function IIDXLikeGetGrade(
}
export function EnumIndexToValue<
G extends V3Game,
EV extends ExtractEnumMetricNames<AllConfMetrics[V3GameToGPTString[G]]>,
>(game: G, enumMetric: EV, index: integer): GetEnumValue<V3GameToGPTString[G], EV> {
const config = V3GetGameConfig(game);
TGame extends V3Game,
EV extends ExtractEnumMetricNames<AllConfMetrics[TGame]>,
>(game: TGame, enumMetric: EV, index: integer): GetEnumValue<TGame, EV> {
const config = GetGameConfig(game);
let metric = config.providedMetrics[enumMetric];
if (!metric) {
@@ -566,8 +468,9 @@ export function EnumIndexToValue<
throw new Error(`Invalid enum metric ${enumMetric} for game ${game}.`);
}
return (metric as ConfEnumScoreMetric<string>).values[index] as GetEnumValue<
V3GameToGPTString[G],
EV
>;
return (metric as ConfEnumScoreMetric<string>).values[index] as GetEnumValue<TGame, EV>;
}
export function IsValidGame(game: string): game is V3Game {
return ALL_GAMES.includes(game as unknown as V3Game);
}
+7 -9
View File
@@ -1,5 +1,5 @@
import type { GoalImportStat, integer, QuestImportStat } from "./types";
import type { Classes, GameGroup, GPTString, Playtypes } from "./types/game-config";
import type { GoalImportStat, integer, QuestImportStat, V3Game } from "./types";
import type { Classes } from "./types/game-config";
/**
* An event fired when a users class improves.
@@ -7,11 +7,10 @@ import type { Classes, GameGroup, GPTString, Playtypes } from "./types/game-conf
export interface WebhookEventClassUpdateV1 {
type: "class-update/v1";
content: {
game: GameGroup;
game: V3Game;
new: string;
old: string | null;
playtype: Playtypes[GameGroup];
set: Classes[GPTString];
set: Classes[V3Game];
userID: integer;
};
}
@@ -22,12 +21,12 @@ export interface WebhookEventClassUpdateV1 {
export interface WebhookEventGoalAchievedV1 {
type: "goals-achieved/v1";
content: {
game: GameGroup;
game: V3Game;
goals: Array<{
game: V3Game;
goalID: string;
new: GoalImportStat;
old: GoalImportStat;
playtype: Playtypes[GameGroup];
}>;
userID: integer;
};
@@ -39,10 +38,9 @@ export interface WebhookEventGoalAchievedV1 {
export interface WebhookEventQuestAchievedV1 {
type: "quest-achieved/v1";
content: {
game: GameGroup;
game: V3Game;
new: QuestImportStat;
old: QuestImportStat;
playtype: Playtypes[GameGroup];
questID: string;
userID: integer;
};
+4
View File
@@ -2,6 +2,8 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
passWithNoTests: true,
env: {
NODE_ENV: "test",
},
@@ -10,7 +12,9 @@ export default defineConfig({
"build/**",
],
coverage: {
enabled: true,
provider: "v8",
reporter: ["text", "html", "clover", "json", "lcov"],
include: ["src/**/*.ts"],
exclude: ["src/**/*.test.ts", "build/**"],
},