mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-08 06:48:04 +03:00
more (#53)
* 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:
@@ -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 .",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)]);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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/**"],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user