mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-03 04:18:16 +03:00
checkpoint: 9 (COMMON COMPILES)
This commit is contained in:
@@ -2,8 +2,6 @@ import { GetGameConfig, GetGamePTConfig, allSupportedGames } from "./config";
|
||||
import t from "tap";
|
||||
|
||||
t.test("#GetGameConfig", (t) => {
|
||||
t.plan(allSupportedGames.length);
|
||||
|
||||
for (const game of allSupportedGames) {
|
||||
// i don't feel *that* strongly about this restriction, but game IDs *definitely*
|
||||
// can't have things like `:` in them.
|
||||
@@ -17,6 +15,8 @@ t.test("#GetGameConfig", (t) => {
|
||||
"Default playtype should be in the array of playtypes."
|
||||
);
|
||||
}
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#GetGamePTConfig", (t) => {
|
||||
@@ -43,11 +43,9 @@ t.test("#GetGamePTConfig", (t) => {
|
||||
"The default profile rating alg should have an implementation."
|
||||
);
|
||||
|
||||
if (conf.difficultyConfig.type === "FIXED") {
|
||||
if (conf.difficulties.type === "FIXED") {
|
||||
t.ok(
|
||||
conf.difficultyConfig.difficultyOrder.includes(
|
||||
conf.difficultyConfig.defaultDifficulty
|
||||
),
|
||||
conf.difficulties.order.includes(conf.difficulties.default),
|
||||
"The default difficulty should be part of difficultyOrder."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,13 +221,13 @@ export const BMS_7K_CONF = {
|
||||
// with no possible method for determining what "song" a chart belongs to.
|
||||
// As such, the only correct method left is to assert that every bit of song
|
||||
// metadata is just its own thing.
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
// "CHART" isn't a very creative name, but this is in a similar vein to how
|
||||
// games with no playtypes use the value "Single".
|
||||
difficultyOrder: ["CHART"],
|
||||
difficultyShorthand: {},
|
||||
defaultDifficulty: "CHART",
|
||||
order: ["CHART"],
|
||||
shorthand: {},
|
||||
default: "CHART",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -239,7 +239,7 @@ export const BMS_7K_CONF = {
|
||||
|
||||
orderedJudgements: ["pgreat", "great", "good", "bad", "poor"],
|
||||
|
||||
chartSets: [],
|
||||
versions: [],
|
||||
|
||||
chartData: z.strictObject({
|
||||
notecount: zodNonNegativeInt,
|
||||
|
||||
@@ -74,16 +74,16 @@ export const CHUNITHM_SINGLE_CONF = {
|
||||
// This game technically has a dynamic set of difficulties, with a chart being
|
||||
// able to have as many WORLD'S END charts as it likes. However, this is a little
|
||||
// awkward to implement, and I can't be bothered. Sorry!
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: ["BASIC", "ADVANCED", "EXPERT", "MASTER"],
|
||||
difficultyShorthand: {
|
||||
order: ["BASIC", "ADVANCED", "EXPERT", "MASTER"],
|
||||
shorthand: {
|
||||
BASIC: "B",
|
||||
ADVANCED: "A",
|
||||
EXPERT: "E",
|
||||
MASTER: "M",
|
||||
},
|
||||
defaultDifficulty: "MASTER",
|
||||
default: "MASTER",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -95,7 +95,7 @@ export const CHUNITHM_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["jcrit", "justice", "attack", "miss"],
|
||||
|
||||
chartSets: ["Paradise Lost"],
|
||||
versions: ["Paradise Lost"],
|
||||
|
||||
chartData: z.strictObject({
|
||||
inGameID: zodNonNegativeInt,
|
||||
|
||||
@@ -76,9 +76,9 @@ export const GITADORA_GITA_CONF = {
|
||||
defaultSessionRatingAlg: "skill",
|
||||
defaultProfileRatingAlg: "skill",
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: [
|
||||
order: [
|
||||
"BASIC",
|
||||
"ADVANCED",
|
||||
"EXTREME",
|
||||
@@ -88,7 +88,7 @@ export const GITADORA_GITA_CONF = {
|
||||
"BASS EXTREME",
|
||||
"BASS MASTER",
|
||||
],
|
||||
difficultyShorthand: {
|
||||
shorthand: {
|
||||
BASIC: "G-BSC",
|
||||
ADVANCED: "G-ADV",
|
||||
EXTREME: "G-EXT",
|
||||
@@ -98,7 +98,7 @@ export const GITADORA_GITA_CONF = {
|
||||
"BASS EXTREME": "B-EXT",
|
||||
"BASS MASTER": "B-MAS",
|
||||
},
|
||||
defaultDifficulty: "EXTREME",
|
||||
default: "EXTREME",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -107,7 +107,7 @@ export const GITADORA_GITA_CONF = {
|
||||
|
||||
orderedJudgements: ["perfect", "great", "good", "ok", "miss"],
|
||||
|
||||
chartSets: ["Konaste"],
|
||||
versions: ["Konaste"],
|
||||
|
||||
chartData: z.strictObject({
|
||||
inGameID: zodNonNegativeInt,
|
||||
@@ -123,15 +123,15 @@ export const GITADORA_GITA_CONF = {
|
||||
export const GITADORA_DORA_CONF = {
|
||||
...GITADORA_GITA_CONF,
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: ["BASIC", "ADVANCED", "EXTREME", "MASTER"],
|
||||
difficultyShorthand: {
|
||||
order: ["BASIC", "ADVANCED", "EXTREME", "MASTER"],
|
||||
shorthand: {
|
||||
BASIC: "D-BSC",
|
||||
ADVANCED: "D-ADV",
|
||||
EXTREME: "D-EXT",
|
||||
MASTER: "D-MAS",
|
||||
},
|
||||
defaultDifficulty: "EXTREME",
|
||||
default: "EXTREME",
|
||||
},
|
||||
} as const satisfies GamePTConfig;
|
||||
|
||||
@@ -122,9 +122,9 @@ export const IIDX_SP_CONF = {
|
||||
BPI: { description: `An average of the best 10 BPIs this session.` },
|
||||
},
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: [
|
||||
order: [
|
||||
"NORMAL",
|
||||
"HYPER",
|
||||
"ANOTHER",
|
||||
@@ -142,7 +142,7 @@ export const IIDX_SP_CONF = {
|
||||
"Kiraku ANOTHER",
|
||||
"Kiraku LEGGENDARIA",
|
||||
],
|
||||
difficultyShorthand: {
|
||||
shorthand: {
|
||||
NORMAL: "N",
|
||||
HYPER: "H",
|
||||
ANOTHER: "A",
|
||||
@@ -160,7 +160,7 @@ export const IIDX_SP_CONF = {
|
||||
"Kiraku ANOTHER": "A (Kr.)",
|
||||
"Kiraku LEGGENDARIA": "L (Kr.)",
|
||||
},
|
||||
defaultDifficulty: "ANOTHER",
|
||||
default: "ANOTHER",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -172,7 +172,7 @@ export const IIDX_SP_CONF = {
|
||||
|
||||
orderedJudgements: ["pgreat", "great", "good", "bad", "poor"],
|
||||
|
||||
chartSets: [
|
||||
versions: [
|
||||
"3rd Style CS",
|
||||
"4th Style CS",
|
||||
"5th Style CS",
|
||||
|
||||
@@ -89,7 +89,7 @@ export const ITG_STAMINA_CONF = {
|
||||
defaultSessionRatingAlg: "blockRating",
|
||||
defaultProfileRatingAlg: "highestBlock",
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "DYNAMIC",
|
||||
},
|
||||
|
||||
@@ -97,7 +97,7 @@ export const ITG_STAMINA_CONF = {
|
||||
|
||||
orderedJudgements: ["15ms", "fantastic", "excellent", "great", "decent", "wayoff", "miss"],
|
||||
|
||||
chartSets: [],
|
||||
versions: [],
|
||||
|
||||
chartData: z.strictObject({
|
||||
hashGSv3: z.string(),
|
||||
|
||||
@@ -70,10 +70,10 @@ export const JUBEAT_SINGLE_CONF = {
|
||||
defaultSessionRatingAlg: "jubility",
|
||||
defaultProfileRatingAlg: "jubility",
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: ["BSC", "ADV", "EXT", "HARD BSC", "HARD ADV", "HARD EXT"],
|
||||
difficultyShorthand: {
|
||||
order: ["BSC", "ADV", "EXT", "HARD BSC", "HARD ADV", "HARD EXT"],
|
||||
shorthand: {
|
||||
BSC: "BSC",
|
||||
ADV: "ADV",
|
||||
EXT: "EXT",
|
||||
@@ -81,7 +81,7 @@ export const JUBEAT_SINGLE_CONF = {
|
||||
"HARD ADV": "H. ADV",
|
||||
"HARD EXT": "H. EXT",
|
||||
},
|
||||
defaultDifficulty: "EXT",
|
||||
default: "EXT",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -90,7 +90,7 @@ export const JUBEAT_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["perfect", "great", "good", "poor", "miss"],
|
||||
|
||||
chartSets: [
|
||||
versions: [
|
||||
"jubeat",
|
||||
"ripples",
|
||||
"knit",
|
||||
|
||||
@@ -117,9 +117,9 @@ export const MAIMAI_DX_SINGLE_CONF = {
|
||||
defaultSessionRatingAlg: "rate",
|
||||
defaultProfileRatingAlg: "naiveRate",
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: [
|
||||
order: [
|
||||
"Basic",
|
||||
"Advanced",
|
||||
"Expert",
|
||||
@@ -131,7 +131,7 @@ export const MAIMAI_DX_SINGLE_CONF = {
|
||||
"DX Master",
|
||||
"DX Re:Master",
|
||||
],
|
||||
difficultyShorthand: {
|
||||
shorthand: {
|
||||
Basic: "BAS",
|
||||
Advanced: "ADV",
|
||||
Expert: "EXP",
|
||||
@@ -143,7 +143,7 @@ export const MAIMAI_DX_SINGLE_CONF = {
|
||||
"DX Master": "DX MAS",
|
||||
"DX Re:Master": "DX Re:MAS",
|
||||
},
|
||||
defaultDifficulty: "Master",
|
||||
default: "Master",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -159,7 +159,7 @@ export const MAIMAI_DX_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["pcrit", "perfect", "great", "good", "miss"],
|
||||
|
||||
chartSets: ["UNiVERSE PLUS"],
|
||||
versions: ["UNiVERSE PLUS"],
|
||||
|
||||
chartData: z.strictObject({
|
||||
isLatest: z.boolean(),
|
||||
|
||||
@@ -67,18 +67,18 @@ export const MUSECA_SINGLE_CONF = {
|
||||
defaultSessionRatingAlg: "curatorSkill",
|
||||
defaultProfileRatingAlg: "curatorSkill",
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: ["Green", "Yellow", "Red"],
|
||||
difficultyShorthand: { Green: "G", Yellow: "Y", Red: "R" },
|
||||
defaultDifficulty: "Red",
|
||||
order: ["Green", "Yellow", "Red"],
|
||||
shorthand: { Green: "G", Yellow: "Y", Red: "R" },
|
||||
default: "Red",
|
||||
},
|
||||
|
||||
classes: {},
|
||||
|
||||
orderedJudgements: ["critical", "near", "miss"],
|
||||
|
||||
chartSets: ["1 + 1/2", "1 + 1/2 Rev. B"],
|
||||
versions: ["1 + 1/2", "1 + 1/2 Rev. B"],
|
||||
|
||||
chartData: z.strictObject({ inGameID: zodNonNegativeInt }),
|
||||
|
||||
|
||||
@@ -118,11 +118,11 @@ export const PMS_CONTROLLER_CONF = {
|
||||
defaultProfileRatingAlg: "sieglinde",
|
||||
|
||||
// See `bms.ts` for an explanation of why this is like this.
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: ["CHART"],
|
||||
difficultyShorthand: {},
|
||||
defaultDifficulty: "CHART",
|
||||
order: ["CHART"],
|
||||
shorthand: {},
|
||||
default: "CHART",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -131,7 +131,7 @@ export const PMS_CONTROLLER_CONF = {
|
||||
|
||||
orderedJudgements: ["cool", "great", "good", "bad", "poor"],
|
||||
|
||||
chartSets: [],
|
||||
versions: [],
|
||||
|
||||
chartData: z.strictObject({
|
||||
notecount: zodNonNegativeInt,
|
||||
|
||||
@@ -95,16 +95,16 @@ export const POPN_9B_CONF = {
|
||||
defaultSessionRatingAlg: "classPoints",
|
||||
defaultProfileRatingAlg: "naiveClassPoints",
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: ["Easy", "Normal", "Hyper", "EX"],
|
||||
difficultyShorthand: {
|
||||
order: ["Easy", "Normal", "Hyper", "EX"],
|
||||
shorthand: {
|
||||
Easy: "E",
|
||||
Normal: "N",
|
||||
Hyper: "H",
|
||||
EX: "EX",
|
||||
},
|
||||
defaultDifficulty: "EX",
|
||||
default: "EX",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -116,7 +116,7 @@ export const POPN_9B_CONF = {
|
||||
|
||||
orderedJudgements: ["cool", "great", "good", "bad"],
|
||||
|
||||
chartSets: ["peace", "Kaimei Riddles"],
|
||||
versions: ["peace", "Kaimei Riddles"],
|
||||
|
||||
chartData: z.strictObject({
|
||||
// Array<string> | string | null
|
||||
|
||||
@@ -129,11 +129,11 @@ export const SDVX_SINGLE_CONF = {
|
||||
defaultSessionRatingAlg: "ProfileVF6",
|
||||
defaultProfileRatingAlg: "VF6",
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: ["NOV", "ADV", "EXH", "INF", "GRV", "HVN", "VVD", "XCD", "MXM"],
|
||||
difficultyShorthand: {}, // they're all already short enough
|
||||
defaultDifficulty: "EXH",
|
||||
order: ["NOV", "ADV", "EXH", "INF", "GRV", "HVN", "VVD", "XCD", "MXM"],
|
||||
shorthand: {}, // they're all already short enough
|
||||
default: "EXH",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -149,7 +149,7 @@ export const SDVX_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["critical", "near", "miss"],
|
||||
|
||||
chartSets: [
|
||||
versions: [
|
||||
"BOOTH",
|
||||
"infinite infection",
|
||||
"GRAVITY WARS",
|
||||
|
||||
@@ -69,18 +69,18 @@ export const USC_CONTROLLER_CONF = {
|
||||
defaultSessionRatingAlg: "ProfileVF6",
|
||||
defaultProfileRatingAlg: "VF6",
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: ["NOV", "ADV", "EXH", "INF"],
|
||||
difficultyShorthand: {}, // they're all already short enough.
|
||||
defaultDifficulty: "EXH",
|
||||
order: ["NOV", "ADV", "EXH", "INF"],
|
||||
shorthand: {}, // they're all already short enough.
|
||||
default: "EXH",
|
||||
},
|
||||
|
||||
classes: {},
|
||||
|
||||
orderedJudgements: ["critical", "near", "miss"],
|
||||
|
||||
chartSets: [],
|
||||
versions: [],
|
||||
|
||||
chartData: z.strictObject({
|
||||
hashSHA1: z.union([z.array(z.string()), z.string()]),
|
||||
|
||||
@@ -105,16 +105,16 @@ export const WACCA_SINGLE_CONF = {
|
||||
defaultProfileRatingAlg: "naiveRate",
|
||||
defaultSessionRatingAlg: "rate",
|
||||
|
||||
difficultyConfig: {
|
||||
difficulties: {
|
||||
type: "FIXED",
|
||||
difficultyOrder: ["NORMAL", "HARD", "EXPERT", "INFERNO"],
|
||||
difficultyShorthand: {
|
||||
order: ["NORMAL", "HARD", "EXPERT", "INFERNO"],
|
||||
shorthand: {
|
||||
NORMAL: "NRM",
|
||||
HARD: "HRD",
|
||||
EXPERT: "EXP",
|
||||
INFERNO: "INF",
|
||||
},
|
||||
defaultDifficulty: "EXPERT",
|
||||
default: "EXPERT",
|
||||
},
|
||||
|
||||
classes: {
|
||||
@@ -130,7 +130,7 @@ export const WACCA_SINGLE_CONF = {
|
||||
|
||||
orderedJudgements: ["marvelous", "great", "good", "miss"],
|
||||
|
||||
chartSets: ["REVERSE"],
|
||||
versions: ["REVERSE"],
|
||||
|
||||
chartData: z.strictObject({
|
||||
isHot: z.boolean(),
|
||||
|
||||
+83
-542
@@ -2,19 +2,20 @@
|
||||
// their name in the database to the object they schemaify.
|
||||
// The schemas themselves are wrapped in functions that throw on error.
|
||||
|
||||
import { GetGameConfig, allSupportedGames, GetGamePTConfig, allGPTStrings } from "../config/config";
|
||||
import { GetGameConfig, GetGamePTConfig, allGPTStrings, allSupportedGames } from "../config/config";
|
||||
import { allImportTypes } from "../constants/import-types";
|
||||
import { ALL_PERMISSIONS } from "../constants/permissions";
|
||||
import { UserAuthLevels } from "../types";
|
||||
import deepmerge from "deepmerge";
|
||||
import { PrudenceZodShim } from "../utils/util";
|
||||
import { p } from "prudence";
|
||||
import type { Game, Playtypes, Playtype } from "../types/game-config";
|
||||
import type { Game, Playtype, Playtypes } from "../types/game-config";
|
||||
import type { GamePTConfig } from "../types/internals";
|
||||
import type { ScoreMetric } from "../types/metrics";
|
||||
import type { NotificationBody } from "../types/notifications";
|
||||
import type {
|
||||
PrudenceSchema,
|
||||
ValidationFunctionParentOptionsKeychain,
|
||||
ValidSchemaValue,
|
||||
ValidationFunctionParentOptionsKeychain,
|
||||
} from "prudence";
|
||||
|
||||
export const optNull = (v: ValidSchemaValue): ValidationFunctionParentOptionsKeychain =>
|
||||
@@ -38,40 +39,13 @@ function prSchemaFnWrap(schema: PrudenceSchema) {
|
||||
};
|
||||
}
|
||||
|
||||
export const PR_GAMESPECIFIC_SETTINGS = (game: Game): PrudenceSchema => {
|
||||
switch (game) {
|
||||
case "iidx":
|
||||
return {
|
||||
display2DXTra: p.optional("boolean"),
|
||||
bpiTarget: p.optional(p.isBoundedInteger(0, 100)),
|
||||
};
|
||||
case "sdvx":
|
||||
case "usc":
|
||||
return {
|
||||
vf6Target: p.optional(p.isBetween(0, 0.5)),
|
||||
};
|
||||
case "bms":
|
||||
return {
|
||||
displayTables: ["string"],
|
||||
};
|
||||
case "jubeat":
|
||||
return {
|
||||
// 165.1 is the max jubility.
|
||||
jubilityTarget: p.optional(p.isBetween(0, 165.1)),
|
||||
};
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const PR_GAME_STATS = (game: Game, playtype: Playtypes[Game], gptConfig: GamePTConfig) => ({
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
game: p.is(game),
|
||||
playtype: p.is(playtype),
|
||||
ratings: Object.fromEntries(gptConfig.scoreRatingAlgs.map((s) => [s, "*number"])),
|
||||
ratings: Object.fromEntries(Object.keys(gptConfig.scoreRatingAlgs).map((s) => [s, "*number"])),
|
||||
classes: Object.fromEntries(
|
||||
Object.keys(gptConfig.classHumanisedFormat).map((e) => [e, p.optional(p.isInteger)])
|
||||
Object.keys(gptConfig.classes).map((e) => [e, p.optional(p.isInteger)])
|
||||
),
|
||||
});
|
||||
|
||||
@@ -83,42 +57,6 @@ const PR_GOAL_INFO = {
|
||||
achieved: "boolean",
|
||||
};
|
||||
|
||||
const GetHitMeta = (game: Game): PrudenceSchema => {
|
||||
if (game === "iidx") {
|
||||
return {
|
||||
bp: optNull(p.isPositiveInteger),
|
||||
gauge: optNull(p.isBetween(0, 100)),
|
||||
gaugeHistory: optNull([p.nullable(p.isBetween(0, 100))]),
|
||||
comboBreak: optNull(p.isPositiveInteger),
|
||||
gsm: optNull({
|
||||
EASY: [p.nullable(p.isBetween(0, 100))],
|
||||
NORMAL: [p.nullable(p.isBetween(0, 100))],
|
||||
HARD: [p.nullable(p.isBetween(0, 100))],
|
||||
EX_HARD: [p.nullable(p.isBetween(0, 100))],
|
||||
}),
|
||||
};
|
||||
} else if (game === "bms") {
|
||||
return {
|
||||
epg: optNull(p.isPositiveInteger),
|
||||
egr: optNull(p.isPositiveInteger),
|
||||
egd: optNull(p.isPositiveInteger),
|
||||
epr: optNull(p.isPositiveInteger),
|
||||
ebd: optNull(p.isPositiveInteger),
|
||||
lpg: optNull(p.isPositiveInteger),
|
||||
lgr: optNull(p.isPositiveInteger),
|
||||
lgd: optNull(p.isPositiveInteger),
|
||||
lpr: optNull(p.isPositiveInteger),
|
||||
lbd: optNull(p.isPositiveInteger),
|
||||
bp: optNull(p.isPositiveInteger),
|
||||
gauge: optNull(p.isBetween(0, 100)),
|
||||
};
|
||||
} else if (game === "sdvx" || game === "usc") {
|
||||
return { gauge: optNull(p.isBetween(0, 100)) };
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const extractGPTIDString = (self: unknown) => {
|
||||
if (typeof self !== "object" || !self) {
|
||||
throw new Error("Expected an object.");
|
||||
@@ -171,53 +109,6 @@ const extractGPT = (self: unknown) => {
|
||||
return { game: s.game, playtype: s.playtype };
|
||||
};
|
||||
|
||||
const PR_RANDOM = p.isIn("NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR");
|
||||
|
||||
const GetScoreMeta = (game: Game, playtype: Playtypes[Game]): PrudenceSchema => {
|
||||
if (game === "iidx" && playtype === "SP") {
|
||||
return {
|
||||
random: optNull(PR_RANDOM),
|
||||
assist: optNull(p.isIn("NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "FULL ASSIST")),
|
||||
range: optNull(p.isIn("NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+")),
|
||||
gauge: optNull(p.isIn("ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD")),
|
||||
};
|
||||
} else if (game === "iidx" && playtype === "DP") {
|
||||
return {
|
||||
random: optNull({
|
||||
left: PR_RANDOM,
|
||||
right: PR_RANDOM,
|
||||
}),
|
||||
assist: optNull(p.isIn("NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "FULL ASSIST")),
|
||||
range: optNull(p.isIn("NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+")),
|
||||
gauge: optNull(p.isIn("ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD")),
|
||||
};
|
||||
} else if (game === "sdvx") {
|
||||
return { inSkillAnalyser: "*?boolean" };
|
||||
} else if (game === "usc") {
|
||||
return {
|
||||
noteMod: optNull(p.isIn("NORMAL", "MIRROR", "RANDOM", "MIR-RAN")),
|
||||
gaugeMod: optNull(p.isIn("NORMAL", "HARD", "PERMISSIVE")),
|
||||
};
|
||||
} else if (game === "bms" && playtype === "7K") {
|
||||
return {
|
||||
random: optNull(PR_RANDOM),
|
||||
inputDevice: optNull(p.isIn("KEYBOARD", "BM_CONTROLLER")),
|
||||
client: optNull(p.isIn("LR2", "lr2oraja")),
|
||||
};
|
||||
} else if (game === "bms" && playtype === "14K") {
|
||||
return {
|
||||
random: optNull({
|
||||
left: PR_RANDOM,
|
||||
right: PR_RANDOM,
|
||||
}),
|
||||
inputDevice: optNull(p.isIn("KEYBOARD", "BM_CONTROLLER")),
|
||||
client: optNull(p.isIn("LR2", "lr2oraja")),
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
function IsValidPlaytype(game: Game, str: string): str is Playtypes[Game] {
|
||||
return GetGameConfig(game).playtypes.includes(str as Playtypes[Game]);
|
||||
}
|
||||
@@ -262,193 +153,6 @@ const isValidPlaytype = (self: unknown, parent: Record<string, unknown>) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
function GetSongDataForGame(game: Game): PrudenceSchema {
|
||||
switch (game) {
|
||||
case "bms":
|
||||
return {
|
||||
genre: "?string",
|
||||
subtitle: "?string",
|
||||
subartist: "?string",
|
||||
tableString: "?string",
|
||||
};
|
||||
case "chunithm":
|
||||
return {
|
||||
genre: "string",
|
||||
displayVersion: "string",
|
||||
};
|
||||
case "sdvx":
|
||||
return {
|
||||
displayVersion: "string",
|
||||
};
|
||||
case "maimaidx":
|
||||
return {
|
||||
displayVersion: "string",
|
||||
};
|
||||
case "museca":
|
||||
return {
|
||||
titleJP: "string",
|
||||
artistJP: "string",
|
||||
displayVersion: "string",
|
||||
};
|
||||
case "iidx":
|
||||
return {
|
||||
genre: "string",
|
||||
displayVersion: "string",
|
||||
};
|
||||
case "wacca":
|
||||
return {
|
||||
genre: "string",
|
||||
displayVersion: "?string",
|
||||
titleJP: "string",
|
||||
artistJP: "string",
|
||||
};
|
||||
case "pms":
|
||||
return {
|
||||
genre: "?string",
|
||||
subtitle: "?string",
|
||||
subartist: "?string",
|
||||
tableString: "?string",
|
||||
};
|
||||
case "popn":
|
||||
return {
|
||||
genre: "string",
|
||||
genreEN: "?string",
|
||||
displayVersion: "?string",
|
||||
};
|
||||
case "jubeat":
|
||||
return {
|
||||
displayVersion: "string",
|
||||
};
|
||||
case "itg":
|
||||
return {
|
||||
subtitle: "string",
|
||||
banner: "?string",
|
||||
};
|
||||
case "gitadora":
|
||||
return {
|
||||
isHot: "boolean",
|
||||
};
|
||||
case "usc":
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function GetChartDataForGPT(idString: IDStrings): PrudenceSchema {
|
||||
switch (idString) {
|
||||
case "bms:7K":
|
||||
case "bms:14K":
|
||||
return {
|
||||
notecount: p.isPositiveNonZeroInteger,
|
||||
hashSHA256: "?string",
|
||||
hashMD5: "?string",
|
||||
aiLevel: "*?string",
|
||||
tableFolders: [
|
||||
{
|
||||
table: "string",
|
||||
level: "string",
|
||||
},
|
||||
],
|
||||
};
|
||||
case "jubeat:Single":
|
||||
return {
|
||||
inGameID: p.or(p.isPositiveInteger, [p.isPositiveInteger]),
|
||||
isHardMode: "boolean",
|
||||
};
|
||||
case "chunithm:Single":
|
||||
return {
|
||||
inGameID: p.isPositiveInteger,
|
||||
};
|
||||
case "iidx:SP":
|
||||
case "iidx:DP":
|
||||
return {
|
||||
notecount: p.isPositiveNonZeroInteger,
|
||||
inGameID: p.or(p.isPositiveNonZeroInteger, [p.isPositiveNonZeroInteger]),
|
||||
hashSHA256: "?string",
|
||||
"2dxtraSet": "?string",
|
||||
kaidenAverage: "?number",
|
||||
worldRecord: "?number",
|
||||
bpiCoefficient: "?number",
|
||||
};
|
||||
case "maimaidx:Single":
|
||||
return {
|
||||
isLatest: "boolean",
|
||||
};
|
||||
case "museca:Single":
|
||||
return {
|
||||
inGameID: p.isPositiveInteger,
|
||||
};
|
||||
case "sdvx:Single":
|
||||
return {
|
||||
inGameID: p.isPositiveInteger,
|
||||
};
|
||||
case "usc:Controller":
|
||||
case "usc:Keyboard":
|
||||
return {
|
||||
hashSHA1: p.or("string", ["string"]),
|
||||
isOfficial: "boolean",
|
||||
effector: "string",
|
||||
tableFolders: [
|
||||
{
|
||||
table: "string",
|
||||
level: "string",
|
||||
},
|
||||
],
|
||||
};
|
||||
case "wacca:Single":
|
||||
return {
|
||||
isHot: "boolean",
|
||||
};
|
||||
case "pms:Controller":
|
||||
case "pms:Keyboard":
|
||||
return {
|
||||
notecount: p.isPositiveNonZeroInteger,
|
||||
hashSHA256: "?string",
|
||||
hashMD5: "?string",
|
||||
tableFolders: [
|
||||
{
|
||||
table: "string",
|
||||
level: "string",
|
||||
},
|
||||
],
|
||||
};
|
||||
case "popn:9B":
|
||||
return {
|
||||
hashSHA256: p.or("?string", ["string"]),
|
||||
inGameID: p.isPositiveInteger,
|
||||
};
|
||||
case "itg:Stamina":
|
||||
return {
|
||||
chartHash: "string",
|
||||
breakdown: {
|
||||
detailed: "string",
|
||||
partiallySimplified: "string",
|
||||
simplified: "string",
|
||||
total: "string",
|
||||
npsPerMeasure: [p.isPositive],
|
||||
notesPerMeasure: [p.isPositive],
|
||||
},
|
||||
tech: {
|
||||
crossovers: p.isPositiveInteger,
|
||||
jacks: p.isPositiveInteger,
|
||||
brackets: p.isPositiveInteger,
|
||||
footswitches: p.isPositiveInteger,
|
||||
sideswitches: p.isPositiveInteger,
|
||||
},
|
||||
length: p.isPositive,
|
||||
charter: "string",
|
||||
displayBPM: p.isPositive,
|
||||
|
||||
// todo, maybe constrain this a bit?
|
||||
difficultyTag: "string",
|
||||
};
|
||||
case "gitadora:Dora":
|
||||
case "gitadora:Gita":
|
||||
return {
|
||||
inGameID: p.isPositiveInteger,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const PR_GOAL_SCHEMA = {
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
@@ -494,7 +198,7 @@ export const PR_GOAL_SCHEMA = {
|
||||
};
|
||||
|
||||
const PR_SONG_DOCUMENT = (game: Game): PrudenceSchema => {
|
||||
const data = GetSongDataForGame(game);
|
||||
const conf = GetGameConfig(game);
|
||||
|
||||
return {
|
||||
id: p.isPositiveInteger,
|
||||
@@ -502,7 +206,7 @@ const PR_SONG_DOCUMENT = (game: Game): PrudenceSchema => {
|
||||
artist: "string",
|
||||
searchTerms: ["string"],
|
||||
altTitles: ["string"],
|
||||
data,
|
||||
data: PrudenceZodShim(conf.songData),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -511,39 +215,28 @@ const PR_CHART_DOCUMENT = (game: Game) => (self: unknown) => {
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const data = GetChartDataForGPT(`${game}:${playtype}` as IDStrings);
|
||||
|
||||
return prSchemaFnWrap({
|
||||
songID: p.isPositiveInteger,
|
||||
chartID: "string",
|
||||
rgcID: "?string",
|
||||
|
||||
// TODO REMOVE
|
||||
level: "string",
|
||||
levelNum: "number",
|
||||
|
||||
isPrimary: "boolean",
|
||||
|
||||
// temp hack
|
||||
difficulty: game === "itg" ? "string" : p.isIn(gptConfig.difficulties),
|
||||
playtype: p.is(playtype),
|
||||
data,
|
||||
tierlistInfo: Object.fromEntries(
|
||||
gptConfig.tierlists.map((t) => [
|
||||
t,
|
||||
p.optional({ text: "string", value: "number", individualDifference: "*boolean" }),
|
||||
])
|
||||
),
|
||||
versions: [p.isIn(gptConfig.orderedSupportedVersions)],
|
||||
|
||||
difficulty:
|
||||
gptConfig.difficulties.type === "DYNAMIC"
|
||||
? "string"
|
||||
: p.isIn(gptConfig.difficulties.order),
|
||||
|
||||
data: PrudenceZodShim(gptConfig.chartData),
|
||||
|
||||
versions: [p.isIn(gptConfig.versions)],
|
||||
})(self);
|
||||
};
|
||||
|
||||
const PR_CHALLENGE = {
|
||||
chartID: "string",
|
||||
authorID: p.isPositiveNonZeroInteger,
|
||||
type: p.isIn("lamp", "score"),
|
||||
|
||||
game: p.isIn(games),
|
||||
playtype: isValidPlaytype,
|
||||
};
|
||||
|
||||
// Returns true on success, throws on failure.
|
||||
export type SchemaValidatorFunction = (self: unknown) => true;
|
||||
|
||||
@@ -563,14 +256,6 @@ function CreateGameSchemas() {
|
||||
|
||||
const GAME_SCHEMAS = CreateGameSchemas();
|
||||
|
||||
// OK, here's an insane hack.
|
||||
// We want schemas to be a a record of key -> SchemaValidatorFunction, but it can't
|
||||
// be naively done with Record<string, SchemaValidatorFunction>, because that results
|
||||
// in you losing the actual hardcoded keys in the map.
|
||||
|
||||
// The alternative? Declare it const and redeclare it with those keys on export!
|
||||
// This is typesafe to including garbage in PRE_SCHEMAS, although with a slightly more
|
||||
// obtuse error message.
|
||||
const PRE_SCHEMAS = {
|
||||
"bms-course-lookup": prSchemaFnWrap({
|
||||
title: "string",
|
||||
@@ -684,15 +369,6 @@ const PRE_SCHEMAS = {
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const hitMeta = {
|
||||
fast: optNull(p.isPositiveInteger),
|
||||
slow: optNull(p.isPositiveInteger),
|
||||
maxCombo: optNull(p.isPositiveInteger),
|
||||
...GetHitMeta(game),
|
||||
};
|
||||
|
||||
const scoreMeta = GetScoreMeta(game, playtype);
|
||||
|
||||
return prSchemaFnWrap({
|
||||
service: "string",
|
||||
game: p.is(game),
|
||||
@@ -708,21 +384,20 @@ const PRE_SCHEMAS = {
|
||||
importType: p.nullable(p.isIn(allImportTypes)),
|
||||
timeAchieved: p.nullable(p.isPositive),
|
||||
scoreData: {
|
||||
score: "number",
|
||||
lamp: p.isIn(gptConfig.lamps),
|
||||
lampIndex: p.isIn(Object.keys(gptConfig.lamps).map(Number)),
|
||||
grade: p.isIn(gptConfig.grades),
|
||||
gradeIndex: p.isIn(Object.keys(gptConfig.grades).map(Number)),
|
||||
percent: p.isBetween(0, gptConfig.percentMax),
|
||||
esd: "?number",
|
||||
...PR_METRICS(gptConfig.providedMetrics),
|
||||
...PR_METRICS(gptConfig.derivedMetrics),
|
||||
|
||||
judgements: Object.fromEntries(
|
||||
gptConfig.judgements.map((j) => [j, optNull(p.isInteger)])
|
||||
gptConfig.orderedJudgements.map((j) => [j, optNull(p.isInteger)])
|
||||
),
|
||||
hitMeta,
|
||||
|
||||
optional: PR_METRICS(gptConfig.additionalMetrics, true),
|
||||
},
|
||||
scoreMeta,
|
||||
|
||||
scoreMeta: PrudenceZodShim(gptConfig.scoreMeta),
|
||||
|
||||
calculatedData: Object.fromEntries(
|
||||
gptConfig.scoreRatingAlgs.map((a) => [a, "*?number"])
|
||||
Object.keys(gptConfig.scoreRatingAlgs).map((a) => [a, "*?number"])
|
||||
),
|
||||
})(self);
|
||||
},
|
||||
@@ -731,13 +406,6 @@ const PRE_SCHEMAS = {
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const hitMeta = {
|
||||
fast: optNull(p.isPositiveInteger),
|
||||
slow: optNull(p.isPositiveInteger),
|
||||
maxCombo: optNull(p.isPositiveInteger),
|
||||
...GetHitMeta(game),
|
||||
};
|
||||
|
||||
return prSchemaFnWrap({
|
||||
composedFrom: {
|
||||
scorePB: "string",
|
||||
@@ -757,20 +425,16 @@ const PRE_SCHEMAS = {
|
||||
isPrimary: "boolean",
|
||||
timeAchieved: p.nullable(p.isPositive),
|
||||
scoreData: {
|
||||
score: "number",
|
||||
lamp: p.isIn(gptConfig.lamps),
|
||||
lampIndex: p.isIn(Object.keys(gptConfig.lamps).map(Number)),
|
||||
grade: p.isIn(gptConfig.grades),
|
||||
gradeIndex: p.isIn(Object.keys(gptConfig.grades).map(Number)),
|
||||
percent: p.isBetween(0, gptConfig.percentMax),
|
||||
esd: "?number",
|
||||
...PR_METRICS(gptConfig.providedMetrics),
|
||||
...PR_METRICS(gptConfig.derivedMetrics),
|
||||
|
||||
judgements: Object.fromEntries(
|
||||
gptConfig.judgements.map((j) => [j, optNull(p.isInteger)])
|
||||
gptConfig.orderedJudgements.map((j) => [j, optNull(p.isInteger)])
|
||||
),
|
||||
hitMeta,
|
||||
optional: PR_METRICS(gptConfig.additionalMetrics, true),
|
||||
},
|
||||
calculatedData: Object.fromEntries(
|
||||
gptConfig.scoreRatingAlgs.map((a) => [a, "*?number"])
|
||||
Object.keys(gptConfig.scoreRatingAlgs).map((a) => [a, "*?number"])
|
||||
),
|
||||
})(self);
|
||||
},
|
||||
@@ -802,7 +466,7 @@ const PRE_SCHEMAS = {
|
||||
timeStarted: p.isPositiveInteger,
|
||||
highlight: "boolean",
|
||||
calculatedData: Object.fromEntries(
|
||||
gptConfig.sessionRatingAlgs.map((k) => [k, "*?number"])
|
||||
Object.keys(gptConfig.sessionRatingAlgs).map((k) => [k, "*?number"])
|
||||
),
|
||||
scoreIDs: ["string"],
|
||||
})(self);
|
||||
@@ -880,7 +544,7 @@ const PRE_SCHEMAS = {
|
||||
return prSchemaFnWrap(
|
||||
Object.assign(PR_GAME_STATS(game, playtype, gptConfig), {
|
||||
rankings: Object.fromEntries(
|
||||
gptConfig.profileRatingAlgs.map((e) => [
|
||||
Object.keys(gptConfig.profileRatingAlgs).map((e) => [
|
||||
e,
|
||||
{ outOf: p.isPositiveNonZeroInteger, ranking: p.isPositiveNonZeroInteger },
|
||||
])
|
||||
@@ -925,7 +589,7 @@ const PRE_SCHEMAS = {
|
||||
),
|
||||
scoreBucket: p.isIn(null, "grade", "lamp"),
|
||||
|
||||
gameSpecific: PR_GAMESPECIFIC_SETTINGS(game),
|
||||
gameSpecific: PrudenceZodShim(gptConfig.preferences),
|
||||
},
|
||||
})(s);
|
||||
},
|
||||
@@ -942,7 +606,7 @@ const PRE_SCHEMAS = {
|
||||
parent.playtype as Playtypes[Game]
|
||||
);
|
||||
|
||||
const keys = Object.keys(gptConfig.classHumanisedFormat);
|
||||
const keys = Object.keys(gptConfig.classes);
|
||||
|
||||
if (typeof self !== "string") {
|
||||
return "Expected a string.";
|
||||
@@ -1086,14 +750,6 @@ const PRE_SCHEMAS = {
|
||||
},
|
||||
}),
|
||||
|
||||
"challenge-wall": prSchemaFnWrap(PR_CHALLENGE),
|
||||
"challenge-subs": prSchemaFnWrap({
|
||||
...PR_CHALLENGE,
|
||||
userID: p.isPositiveNonZeroInteger,
|
||||
achieved: "boolean",
|
||||
achievedAt: "?number",
|
||||
}),
|
||||
|
||||
notifications: prSchemaFnWrap({
|
||||
title: "string",
|
||||
notifID: "string",
|
||||
@@ -1140,154 +796,16 @@ const PRE_SCHEMAS = {
|
||||
},
|
||||
},
|
||||
}),
|
||||
} as const;
|
||||
} as const satisfies Record<string, SchemaValidatorFunction>;
|
||||
|
||||
export const SCHEMAS: Record<keyof typeof PRE_SCHEMAS, SchemaValidatorFunction> = PRE_SCHEMAS;
|
||||
|
||||
const BaseValidHitMeta = {
|
||||
fast: optNull(p.isPositiveInteger),
|
||||
slow: optNull(p.isPositiveInteger),
|
||||
maxCombo: optNull(p.isPositiveInteger),
|
||||
};
|
||||
|
||||
const PR_DP_RANDOM = (self: unknown) => {
|
||||
if (!Array.isArray(self) || self.length !== 2) {
|
||||
return "Expected an array with length 2";
|
||||
}
|
||||
|
||||
for (const a of self) {
|
||||
if (p.isIn("NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR")(a) !== true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const PR_SCORE_META = (game: Game, playtype: Playtype): PrudenceSchema => {
|
||||
if (game === "iidx") {
|
||||
const random =
|
||||
playtype === "SP"
|
||||
? optNull(p.isIn("NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"))
|
||||
: PR_DP_RANDOM;
|
||||
|
||||
return {
|
||||
random,
|
||||
assist: optNull(p.isIn("NO ASSIST", "AUTO SCRATCH", "LEGACY NOTE", "FULL ASSIST")),
|
||||
range: optNull(p.isIn("NONE", "SUDDEN+", "HIDDEN+", "SUD+ HID+", "LIFT", "LIFT SUD+")),
|
||||
gauge: optNull(p.isIn("ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX HARD")),
|
||||
};
|
||||
} else if (game === "bms") {
|
||||
const random =
|
||||
playtype === "7K"
|
||||
? optNull(p.isIn("NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR"))
|
||||
: optNull(PR_DP_RANDOM);
|
||||
|
||||
return {
|
||||
random,
|
||||
inputDevice: optNull(p.isIn("KEYBOARD", "BM_CONTROLLER")),
|
||||
client: optNull(p.isIn("LR2", "lr2oraja")),
|
||||
};
|
||||
} else if (game === "usc") {
|
||||
return {
|
||||
noteMod: optNull(p.isIn("NORMAL", "MIRROR", "RANDOM", "MIR-RAN")),
|
||||
gaugeMod: optNull(p.isIn("NORMAL", "HARD", "PERMISSIVE")),
|
||||
};
|
||||
} else if (game === "sdvx") {
|
||||
return {
|
||||
inSkillAnalyser: "*?boolean",
|
||||
};
|
||||
} else if (game === "wacca") {
|
||||
return { mirror: "*?boolean" };
|
||||
} else if (game === "popn") {
|
||||
return {
|
||||
hiSpeed: optNull(p.isPositive),
|
||||
hidden: optNull(p.isInteger),
|
||||
sudden: optNull(p.isInteger),
|
||||
random: optNull(p.isIn("NONRAN", "MIRROR", "RANDOM", "S-RANDOM")),
|
||||
gauge: optNull(p.isIn("NORMAL", "EASY", "HARD", "DANGER")),
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const PR_HIT_META = (game: Game): PrudenceSchema => {
|
||||
if (game === "iidx") {
|
||||
return {
|
||||
bp: optNull(p.isPositiveInteger),
|
||||
gauge: optNull(p.isBetween(0, 100)),
|
||||
gaugeHistory: optNull([p.isBetween(0, 100)]),
|
||||
scoreHistory: optNull([p.isPositiveInteger]),
|
||||
comboBreak: optNull(p.isPositiveInteger),
|
||||
gsm: optNull({
|
||||
EASY: [p.nullable(p.isBetween(0, 100))],
|
||||
NORMAL: [p.nullable(p.isBetween(0, 100))],
|
||||
HARD: [p.nullable(p.isBetween(0, 100))],
|
||||
EX_HARD: [p.nullable(p.isBetween(0, 100))],
|
||||
}),
|
||||
};
|
||||
} else if (game === "sdvx") {
|
||||
return {
|
||||
gauge: optNull(p.isBetween(0, 100)),
|
||||
};
|
||||
} else if (game === "usc") {
|
||||
return { gauge: optNull(p.isBetween(0, 1)) };
|
||||
} else if (game === "bms") {
|
||||
return {
|
||||
bp: optNull(p.isPositiveInteger),
|
||||
gauge: optNull(p.isBetween(0, 100)),
|
||||
lbd: optNull(p.isPositiveInteger),
|
||||
ebd: optNull(p.isPositiveInteger),
|
||||
lpr: optNull(p.isPositiveInteger),
|
||||
epr: optNull(p.isPositiveInteger),
|
||||
lgd: optNull(p.isPositiveInteger),
|
||||
egd: optNull(p.isPositiveInteger),
|
||||
lgr: optNull(p.isPositiveInteger),
|
||||
egr: optNull(p.isPositiveInteger),
|
||||
lpg: optNull(p.isPositiveInteger),
|
||||
epg: optNull(p.isPositiveInteger),
|
||||
};
|
||||
} else if (game === "popn") {
|
||||
return {
|
||||
gauge: optNull(p.isBetween(0, 100)),
|
||||
specificClearType: optNull(
|
||||
p.isIn(
|
||||
"failedUnknown",
|
||||
"failedCircle",
|
||||
"failedDiamond",
|
||||
"failedStar",
|
||||
"easyClear",
|
||||
"clearCircle",
|
||||
"clearDiamond",
|
||||
"clearStar",
|
||||
"fullComboCircle",
|
||||
"fullComboDiamond",
|
||||
"fullComboStar",
|
||||
"perfect"
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const PR_BATCH_MANUAL_SCORE = (game: Game, playtype: Playtype): PrudenceSchema => {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
let scoreChecker: ValidSchemaValue = p.isPositiveInteger;
|
||||
|
||||
// DISGUSTING TEMPORARY HACK. NEEDS REFACTOR
|
||||
if (game === "itg" || game === "gitadora") {
|
||||
scoreChecker = p.isBetween(0, 100);
|
||||
} else if (game === "maimaidx") {
|
||||
scoreChecker = p.isBetween(0, 101);
|
||||
}
|
||||
|
||||
return {
|
||||
score: scoreChecker,
|
||||
lamp: p.isIn(gptConfig.lamps),
|
||||
...PR_METRICS(gptConfig.providedMetrics),
|
||||
|
||||
matchType: p.isIn(
|
||||
"songTitle",
|
||||
"tachiSongID",
|
||||
@@ -1299,7 +817,6 @@ const PR_BATCH_MANUAL_SCORE = (game: Game, playtype: Playtype): PrudenceSchema =
|
||||
"popnChartHash"
|
||||
),
|
||||
identifier: "string",
|
||||
percent: game === "jubeat" ? p.isBetween(0, 120) : p.is(undefined),
|
||||
comment: optNull(p.isBoundedString(3, 240)),
|
||||
difficulty: "*?string",
|
||||
|
||||
@@ -1318,10 +835,10 @@ const PR_BATCH_MANUAL_SCORE = (game: Game, playtype: Playtype): PrudenceSchema =
|
||||
}
|
||||
|
||||
for (const [key, v] of Object.entries(self)) {
|
||||
// @ts-expect-error Frustrating check as it's a string but not a judgement so it's not really
|
||||
// happy with it.
|
||||
if (!gptConfig.judgements.includes(key)) {
|
||||
return `Invalid Key ${key}. Expected any of ${gptConfig.judgements.join(", ")}`;
|
||||
if (!gptConfig.orderedJudgements.includes(key)) {
|
||||
return `Invalid Key ${key}. Expected any of ${gptConfig.orderedJudgements.join(
|
||||
", "
|
||||
)}`;
|
||||
}
|
||||
|
||||
if ((!Number.isSafeInteger(v) || v < 0) && v !== null) {
|
||||
@@ -1331,13 +848,41 @@ const PR_BATCH_MANUAL_SCORE = (game: Game, playtype: Playtype): PrudenceSchema =
|
||||
|
||||
return true;
|
||||
}),
|
||||
hitMeta: optNull(
|
||||
deepmerge(BaseValidHitMeta, PR_HIT_META(game)) as unknown as ValidSchemaValue
|
||||
),
|
||||
scoreMeta: optNull(PR_SCORE_META(game, playtype)),
|
||||
additionalMetrics: optNull(PR_METRICS(gptConfig.additionalMetrics, true)),
|
||||
scoreMeta: optNull(PrudenceZodShim(gptConfig.scoreMeta)),
|
||||
};
|
||||
};
|
||||
|
||||
function PR_METRIC(metric: ScoreMetric): ValidSchemaValue {
|
||||
switch (metric.type) {
|
||||
case "DECIMAL":
|
||||
return "number";
|
||||
|
||||
case "INTEGER":
|
||||
return p.isInteger;
|
||||
|
||||
case "GRAPH":
|
||||
return ["number"];
|
||||
|
||||
case "ENUM":
|
||||
return p.isIn(metric.values);
|
||||
}
|
||||
}
|
||||
|
||||
function PR_METRICS(metrics: Record<string, ScoreMetric>, shouldAllBeOptNull?: boolean) {
|
||||
const schema: PrudenceSchema = {};
|
||||
|
||||
for (const [key, value] of Object.entries(metrics)) {
|
||||
if (shouldAllBeOptNull === true) {
|
||||
schema[key] = optNull(PR_METRIC(value));
|
||||
} else {
|
||||
schema[key] = PR_METRIC(value);
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
const PR_BATCH_MANUAL_CLASSES = (game: Game, playtype: Playtype): PrudenceSchema => {
|
||||
const config = GetGamePTConfig(game, playtype);
|
||||
|
||||
@@ -1345,13 +890,9 @@ const PR_BATCH_MANUAL_CLASSES = (game: Game, playtype: Playtype): PrudenceSchema
|
||||
|
||||
// for all classes this GPT supports
|
||||
// if `canBeBatchManualSubmitted` is true, allow it to be batchManualSubmitted.
|
||||
for (const [s, v] of Object.entries(config.classHumanisedFormat)) {
|
||||
const set = s as AllClassSets;
|
||||
|
||||
const classProperties = config.classProperties[set];
|
||||
|
||||
if (classProperties.canBeBatchManualSubmitted) {
|
||||
schema[set] = optNull(p.isIn(v.map((e) => e.id)));
|
||||
for (const [s, v] of Object.entries(config.classes)) {
|
||||
if (v.type === "PROVIDED") {
|
||||
schema[s] = optNull(p.isIn(v.values));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { allSupportedGames } from "../config/config";
|
||||
import type {
|
||||
Game,
|
||||
ChartDocument,
|
||||
GameToGPTStrings,
|
||||
SongDocument,
|
||||
BMSCourseDocument,
|
||||
FolderDocument,
|
||||
@@ -10,13 +9,14 @@ import type {
|
||||
QuestlineDocument,
|
||||
QuestDocument,
|
||||
TableDocument,
|
||||
GPTStrings,
|
||||
} from "../types";
|
||||
|
||||
// lazy, but kinda cool macros.
|
||||
// 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 Game as `charts-${G}.json`]: Array<ChartDocument<GameToGPTStrings[G]>>;
|
||||
[G in Game as `charts-${G}.json`]: Array<ChartDocument<GPTStrings[G]>>;
|
||||
};
|
||||
|
||||
type SongDBSeeds = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { integer } from "../types";
|
||||
import type {
|
||||
AdditionalMetrics,
|
||||
ChartSets,
|
||||
Versions,
|
||||
Difficulties,
|
||||
ExtractedClasses,
|
||||
GPTString,
|
||||
@@ -47,12 +47,7 @@ export interface BatchManual<GPT extends GPTString = GPTString> {
|
||||
playtype: GPTStringToPlaytype[GPT];
|
||||
service: string;
|
||||
|
||||
chartSet?: ChartSets[GPT];
|
||||
|
||||
/**
|
||||
* @deprecated Use `meta.chartSet` instead. This is an alias for `meta.chartSet`.
|
||||
*/
|
||||
version?: ChartSets[GPT];
|
||||
version?: Versions[GPT];
|
||||
};
|
||||
scores: Array<BatchManualScore<GPT>>;
|
||||
classes?: ExtractedClasses[GPT] | null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {
|
||||
ChartDocumentData,
|
||||
ChartSets,
|
||||
Versions,
|
||||
Classes,
|
||||
Difficulties,
|
||||
ExtractedAdditionalMetrics,
|
||||
@@ -335,7 +335,7 @@ export interface ChartDocument<GPT extends GPTString = GPTString> {
|
||||
difficulty: Difficulties[GPT];
|
||||
playtype: GPTStringToPlaytype[GPT];
|
||||
data: ChartDocumentData[GPT];
|
||||
chartSets: Array<ChartSets[GPT]>;
|
||||
versions: Array<Versions[GPT]>;
|
||||
}
|
||||
|
||||
export interface SongDocument<G extends Game = Game> {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Types related to game configuration. These are used by our `internal` game config
|
||||
// types
|
||||
|
||||
import type { integer } from "../types";
|
||||
|
||||
export interface RatingAlgorithmConfig {
|
||||
/**
|
||||
* Write a short descrption for this rating algorithm.
|
||||
@@ -97,7 +99,7 @@ export interface DynamicDifficulties {
|
||||
export interface FixedDifficulties<Difficulty extends string> {
|
||||
type: "FIXED";
|
||||
|
||||
difficultyOrder: ReadonlyArray<Difficulty>;
|
||||
order: ReadonlyArray<Difficulty>;
|
||||
|
||||
/**
|
||||
* For mobile view (and certain pages where things are generally smaller)
|
||||
@@ -105,9 +107,9 @@ export interface FixedDifficulties<Difficulty extends string> {
|
||||
*
|
||||
* For example, in IIDX "ANOTHER" is generally shortened to "A".
|
||||
*/
|
||||
difficultyShorthand: Partial<Record<Difficulty, string>>;
|
||||
shorthand: Partial<Record<Difficulty, string>>;
|
||||
|
||||
defaultDifficulty: Difficulty;
|
||||
default: Difficulty;
|
||||
}
|
||||
|
||||
export type DifficultyConfig<D extends string = string> =
|
||||
|
||||
@@ -97,23 +97,21 @@ export type GPTStringToPlaytype = {
|
||||
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]["difficultyConfig"] extends FixedDifficulties<
|
||||
infer D
|
||||
>
|
||||
[G in GPTString]: typeof GAME_PT_CONFIGS[G]["difficulties"] extends FixedDifficulties<infer D>
|
||||
? D
|
||||
: string;
|
||||
};
|
||||
|
||||
export type DifficultyConfigs = {
|
||||
[G in GPTString]: typeof GAME_PT_CONFIGS[G]["difficultyConfig"];
|
||||
[G in GPTString]: typeof GAME_PT_CONFIGS[G]["difficulties"];
|
||||
};
|
||||
|
||||
export type Judgements = {
|
||||
[G in GPTString]: ExtractArrayElementType<typeof GAME_PT_CONFIGS[G]["orderedJudgements"]>;
|
||||
};
|
||||
|
||||
export type ChartSets = {
|
||||
[G in GPTString]: ExtractArrayElementType<typeof GAME_PT_CONFIGS[G]["chartSets"]>;
|
||||
export type Versions = {
|
||||
[G in GPTString]: ExtractArrayElementType<typeof GAME_PT_CONFIGS[G]["versions"]>;
|
||||
};
|
||||
|
||||
export type ScoreRatingAlgorithms = {
|
||||
@@ -324,7 +322,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
|
||||
* allowing any string (as long as its unique.)
|
||||
*
|
||||
*/
|
||||
difficultyConfig: DifficultyConfigs[GPT];
|
||||
difficulties: DifficultyConfigs[GPT];
|
||||
|
||||
/**
|
||||
* What judgements does this GPT have? These are typically timing-window names.
|
||||
@@ -334,9 +332,9 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
|
||||
orderedJudgements: ReadonlyArray<Judgements[GPT]>;
|
||||
|
||||
/**
|
||||
* What sets of charts do we have for this GPT?
|
||||
* What versions do we support for this GPT?
|
||||
*
|
||||
* Chart Sets are the way tachi disambiguates cases (typically in arcade games) where
|
||||
* Version are the way tachi disambiguates cases (typically in arcade games) where
|
||||
* a chart is modified.
|
||||
* For example, Rising in the Sun
|
||||
* (https://remywiki.com/Rising_in_the_Sun(original_mix))
|
||||
@@ -348,10 +346,10 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
|
||||
* We need to handle these cases, so we disambiguate by attaching "chart sets" onto
|
||||
* every chart. These "chart sets" indicate what sets of chart states they
|
||||
* appeared in for this GPT. Then, when a score is coming in, it can indicate what
|
||||
* chartSet this score was on. That way, we can make sure they resolve to the right
|
||||
* version this score was on. That way, we can make sure they resolve to the right
|
||||
* chart.
|
||||
*/
|
||||
chartSets: ReadonlyArray<ChartSets[GPT]>;
|
||||
versions: ReadonlyArray<Versions[GPT]>;
|
||||
|
||||
/**
|
||||
* Chart documents get their own GPT-specific record that they use for whatever
|
||||
|
||||
@@ -34,11 +34,11 @@ export type GamePTConfig = Readonly<{
|
||||
|
||||
classes: Record<string, ClassConfig>;
|
||||
|
||||
difficultyConfig: DifficultyConfig;
|
||||
difficulties: DifficultyConfig;
|
||||
|
||||
orderedJudgements: ReadonlyArray<string>;
|
||||
|
||||
chartSets: ReadonlyArray<string>;
|
||||
versions: ReadonlyArray<string>;
|
||||
|
||||
supportedMatchTypes: ReadonlyArray<MatchTypes>;
|
||||
|
||||
|
||||
@@ -2,6 +2,19 @@ import type { integer } from "../types";
|
||||
import type { ChartDocument } from "./documents";
|
||||
import type { GPTString } from "./game-config";
|
||||
|
||||
export type DecimalMetricValidator<GPT extends GPTString> = (
|
||||
metric: number,
|
||||
chart: ChartDocument<GPT>
|
||||
) => string | true;
|
||||
export type IntegerMetricValidator<GPT extends GPTString> = (
|
||||
metric: integer,
|
||||
chart: ChartDocument<GPT>
|
||||
) => string | true;
|
||||
export type GraphMetricValidator<GPT extends GPTString> = (
|
||||
metric: Array<number>,
|
||||
chart: ChartDocument<GPT>
|
||||
) => string | true;
|
||||
|
||||
/**
|
||||
* A metric for a score that's a floating point number.
|
||||
*/
|
||||
|
||||
+27
-244
@@ -1,17 +1,14 @@
|
||||
import { GetGameConfig, GetGamePTConfig, GetSpecificGPTConfig } from "../config/config";
|
||||
import type {
|
||||
integer,
|
||||
PBScoreDocument,
|
||||
ChartDocument,
|
||||
GPTStrings,
|
||||
Game,
|
||||
PBScoreDocument,
|
||||
Playtypes,
|
||||
SongDocument,
|
||||
GameToGPTStrings,
|
||||
SpecificGamePTConfig,
|
||||
GPTString,
|
||||
GPTStrings,
|
||||
} from "../types";
|
||||
import type { PrudenceError } from "prudence";
|
||||
import type { PrudenceSchema, ValidSchemaValue } from "prudence";
|
||||
import type { AnyZodObject } from "zod";
|
||||
|
||||
export function FormatInt(v: number): string {
|
||||
return Math.floor(v).toFixed(0);
|
||||
@@ -47,7 +44,7 @@ export function FormatDifficulty(chart: ChartDocument, game: Game): string {
|
||||
// actually possible.
|
||||
// todo: come up with something better.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const shortDiff = gptConfig.difficultyConfig.difficultyShorthand[ch.difficulty];
|
||||
const shortDiff = gptConfig.difficulties.difficultyShorthand[ch.difficulty];
|
||||
|
||||
// gitadora should always use short diffs. they just look better.
|
||||
return `${shortDiff} ${chart.level}`;
|
||||
@@ -76,13 +73,12 @@ export function FormatDifficultyShort(chart: ChartDocument, game: Game): string
|
||||
return `S${itgChart.data.difficultyTag} ${chart.level}`;
|
||||
}
|
||||
|
||||
if (gptConfig.difficultyConfig.type === "DYNAMIC") {
|
||||
if (gptConfig.difficulties.type === "DYNAMIC") {
|
||||
// TODO cap string length
|
||||
return `${chart.difficulty} ${chart.level}`;
|
||||
}
|
||||
|
||||
const shortDiff =
|
||||
gptConfig.difficultyConfig.difficultyShorthand[chart.difficulty] ?? chart.difficulty;
|
||||
const shortDiff = gptConfig.difficulties.shorthand[chart.difficulty] ?? chart.difficulty;
|
||||
|
||||
if (gameConfig.playtypes.length === 1 || game === "gitadora") {
|
||||
return `${shortDiff} ${chart.level}`;
|
||||
@@ -172,10 +168,10 @@ export function FormatChart(
|
||||
|
||||
let diff: string;
|
||||
|
||||
if (gptConfig.difficultyConfig.type === "DYNAMIC") {
|
||||
if (gptConfig.difficulties.type === "DYNAMIC") {
|
||||
diff = chart.difficulty;
|
||||
} else {
|
||||
diff = gptConfig.difficultyConfig.difficultyShorthand[chart.difficulty] ?? chart.difficulty;
|
||||
diff = gptConfig.difficulties.shorthand[chart.difficulty] ?? chart.difficulty;
|
||||
}
|
||||
|
||||
// iidx formats things like SPA instead of SP A.
|
||||
@@ -188,248 +184,35 @@ export function FormatChart(
|
||||
space = " ";
|
||||
}
|
||||
|
||||
// return the most recent chartSet this chart appeared in if it
|
||||
// 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.chartSets[0]})`;
|
||||
return `${song.title} (${playtypeStr}${space}${diff} ${chart.level} ${chart.versions[0]})`;
|
||||
}
|
||||
|
||||
return `${song.title} (${playtypeStr}${space}${diff} ${chart.level})`;
|
||||
}
|
||||
|
||||
export function AbsoluteGradeDelta<I extends GPTString = GPTString>(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
score: number,
|
||||
percent: number,
|
||||
gradeOrIndex: Grades[I] | integer
|
||||
): number {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const maxScore = Math.round(score * (100 / percent));
|
||||
|
||||
const gradeIndex =
|
||||
typeof gradeOrIndex === "number" ? gradeOrIndex : gptConfig.grades.indexOf(gradeOrIndex);
|
||||
|
||||
const gradeValue = gptConfig.gradeBoundaries[gradeIndex];
|
||||
|
||||
if (gradeValue === undefined) {
|
||||
throw new Error(`Grade Index ${gradeIndex} has no corresponding grade value?`);
|
||||
}
|
||||
|
||||
const gradeScore = Math.ceil((gradeValue / 100) * maxScore);
|
||||
|
||||
return score - gradeScore;
|
||||
}
|
||||
|
||||
export function RelativeGradeDelta<GPT extends GPTString = GPTString>(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
score: number,
|
||||
percent: number,
|
||||
grade: Grades[GPT],
|
||||
relativeIndex: integer
|
||||
): { grade: string; delta: number } | null {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const nextGradeIndex = gptConfig.grades.indexOf(grade) + relativeIndex;
|
||||
|
||||
if (nextGradeIndex < 0 || nextGradeIndex >= gptConfig.grades.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextGrade = gptConfig.grades[nextGradeIndex];
|
||||
|
||||
if (nextGrade === undefined) {
|
||||
throw new Error(
|
||||
`Unexpectedly found no grade at index ${nextGradeIndex} for ${game} ${playtype}.`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
grade: nextGrade,
|
||||
delta: AbsoluteGradeDelta(game, playtype, score, percent, nextGradeIndex),
|
||||
};
|
||||
}
|
||||
|
||||
function WrapGrade(grade: string) {
|
||||
if (grade.endsWith("-") || grade.endsWith("+")) {
|
||||
return `(${grade})`;
|
||||
}
|
||||
|
||||
return grade;
|
||||
}
|
||||
|
||||
type FormatGradeDeltaReturns =
|
||||
| {
|
||||
lower: string;
|
||||
upper: string;
|
||||
closer: "upper";
|
||||
}
|
||||
| {
|
||||
lower: string;
|
||||
upper?: string;
|
||||
closer: "lower";
|
||||
};
|
||||
|
||||
export function GenericFormatGradeDelta<I extends GPTString = GPTString>(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
score: number,
|
||||
percent: number,
|
||||
grade: Grades[I],
|
||||
formatNumFn: (n: number) => string = (s) => s.toString()
|
||||
): FormatGradeDeltaReturns {
|
||||
const upper = RelativeGradeDelta(game, playtype, score, percent, grade, 1);
|
||||
const lower = AbsoluteGradeDelta(game, playtype, score, percent, grade);
|
||||
|
||||
const formatLower = `${WrapGrade(grade)}+${formatNumFn(lower)}`;
|
||||
|
||||
if (!upper) {
|
||||
return {
|
||||
lower: formatLower,
|
||||
closer: "lower",
|
||||
};
|
||||
}
|
||||
|
||||
const formatUpper = `${WrapGrade(upper.grade)}${formatNumFn(upper.delta)}`;
|
||||
|
||||
// are we closer to the lower bound, or the upper one?
|
||||
let closer: "lower" | "upper" = upper.delta + lower < 0 ? "lower" : "upper";
|
||||
|
||||
// lovely hardcoded exception for IIDX - (MAX-)+ is always a stupid metric
|
||||
// so always mute it.
|
||||
if (game === "iidx" && formatLower.startsWith("(MAX-)+")) {
|
||||
closer = "upper";
|
||||
}
|
||||
|
||||
return {
|
||||
lower: formatLower,
|
||||
upper: formatUpper,
|
||||
closer,
|
||||
};
|
||||
}
|
||||
|
||||
export function GetCloserGradeDelta<I extends GPTString = GPTString>(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
score: number,
|
||||
percent: number,
|
||||
grade: Grades[I],
|
||||
formatNumFn: (n: number) => string = (s) => s.toString()
|
||||
): string {
|
||||
const { lower, upper, closer } = GenericFormatGradeDelta(
|
||||
game,
|
||||
playtype,
|
||||
score,
|
||||
percent,
|
||||
grade,
|
||||
formatNumFn
|
||||
);
|
||||
|
||||
if (closer === "upper") {
|
||||
// this type assertion is unecessary in theory, but in practice older versions
|
||||
// of TS aren't happy with it.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
return upper as string;
|
||||
}
|
||||
|
||||
return lower;
|
||||
}
|
||||
|
||||
export function CreateSongMap<G extends Game = Game>(songs: Array<SongDocument<G>>) {
|
||||
const songMap = new Map<integer, SongDocument<G>>();
|
||||
|
||||
for (const song of songs) {
|
||||
songMap.set(song.id, song);
|
||||
}
|
||||
|
||||
return songMap;
|
||||
}
|
||||
|
||||
export function CreateChartMap<I extends GPTString = GPTString>(charts: Array<ChartDocument<I>>) {
|
||||
const chartMap = new Map<string, ChartDocument<I>>();
|
||||
|
||||
for (const chart of charts) {
|
||||
chartMap.set(chart.chartID, chart);
|
||||
}
|
||||
|
||||
return chartMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a PrudenceError into something a little more readable.
|
||||
* @param err - The prudence error to format.
|
||||
* @param foreword - A description of what kind of error this was. Defaults to "Error".
|
||||
*/
|
||||
export function FormatPrError(err: PrudenceError, foreword = "Error"): string {
|
||||
const receivedText =
|
||||
typeof err.userVal === "object" && err.userVal !== null
|
||||
? ""
|
||||
: ` | Received ${err.userVal} [${err.userVal === null ? "null" : typeof err.userVal}]`;
|
||||
|
||||
return `${foreword}: ${err.keychain} | ${err.message}${receivedText}.`;
|
||||
}
|
||||
|
||||
// games that have funny relative grades, basically.
|
||||
type IIDXBMSGPTs = "bms:7K" | "bms:14K" | "iidx:DP" | "iidx:SP" | "pms:Controller" | "pms:Keyboard";
|
||||
export function IIDXBMSGradeGoalFormatter(pb: PBScoreDocument<IIDXBMSGPTs>) {
|
||||
const { closer, lower, upper } = GenericFormatGradeDelta(
|
||||
pb.game,
|
||||
pb.playtype,
|
||||
pb.scoreData.score,
|
||||
pb.scoreData.percent,
|
||||
pb.scoreData.grade
|
||||
);
|
||||
|
||||
// if upper doesn't exist, we have to return lower (this is a MAX)
|
||||
// or something.
|
||||
if (!upper) {
|
||||
return lower;
|
||||
}
|
||||
|
||||
// if the upper bound is relevant to the grade we're looking for
|
||||
// i.e. the goal is to AAA a chart and the user has AA+20/AAA-100
|
||||
// prefer AAA-100 instead of AA+20.
|
||||
if (upper.startsWith(`${pb.scoreData.grade}-`)) {
|
||||
return upper;
|
||||
}
|
||||
|
||||
// otherwise, return whichever is closer.
|
||||
return closer === "lower" ? lower : upper;
|
||||
}
|
||||
|
||||
export function GradeGoalFormatter(pb: PBScoreDocument) {
|
||||
const { closer, lower, upper } = GenericFormatGradeDelta(
|
||||
pb.game,
|
||||
pb.playtype,
|
||||
pb.scoreData.score,
|
||||
pb.scoreData.percent,
|
||||
pb.scoreData.grade
|
||||
);
|
||||
|
||||
// if upper doesn't exist, we have to return lower (this is a MAX)
|
||||
// or something.
|
||||
if (!upper) {
|
||||
return lower;
|
||||
}
|
||||
|
||||
// if the upper bound is relevant to the grade we're looking for
|
||||
// i.e. the goal is to AAA a chart and the user has AA+20/AAA-100
|
||||
// prefer AAA-100 instead of AA+20.
|
||||
if (upper.startsWith(`${pb.scoreData.grade}-`)) {
|
||||
return upper;
|
||||
}
|
||||
|
||||
// otherwise, return whichever is closer.
|
||||
return closer === "lower" ? lower : upper;
|
||||
}
|
||||
|
||||
// For games with 'BP', show that next to the clear.
|
||||
export function IIDXBMSLampGoalFormatter(pb: PBScoreDocument<IIDXBMSGPTs>) {
|
||||
export function IIDXBMSLampGoalFormatter(pb: PBScoreDocument<GPTStrings["bms" | "iidx" | "pms"]>) {
|
||||
if (typeof pb.scoreData.additionalMetrics.bp === "number") {
|
||||
return `${pb.scoreData.lamp.string} (BP: ${pb.scoreData.additionalMetrics.bp})`;
|
||||
}
|
||||
|
||||
return pb.scoreData.lamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a zod schema inside prudence.
|
||||
*/
|
||||
export function PrudenceZodShim(zodSchema: AnyZodObject): ValidSchemaValue {
|
||||
return (self) => {
|
||||
const res = zodSchema.safeParse(self);
|
||||
|
||||
if (res.success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return res.error.message;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Game, Playtypes } from ".";
|
||||
import type { GameClassSets } from "./config/game-classes";
|
||||
import type { GoalImportStat, IDStrings, integer, QuestImportStat } from "./types";
|
||||
import type { GoalImportStat, integer, QuestImportStat } from "./types";
|
||||
import type { Classes, Game, GPTString, Playtypes } from "./types/game-config";
|
||||
|
||||
/**
|
||||
* An event fired when a users class improves.
|
||||
@@ -9,7 +8,7 @@ export interface WebhookEventClassUpdateV1 {
|
||||
type: "class-update/v1";
|
||||
content: {
|
||||
userID: integer;
|
||||
set: GameClassSets[IDStrings];
|
||||
set: Classes[GPTString];
|
||||
old: integer | null;
|
||||
new: integer;
|
||||
game: Game;
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { p } from "prudence";
|
||||
import { allImportTypes } from "tachi-common/config/static-config";
|
||||
import { allImportTypes } from "tachi-common/constants/import-types";
|
||||
import { SCHEMAS } from "tachi-common/lib/schemas";
|
||||
import type { Databases } from "./db";
|
||||
import type { PrudenceSchema } from "prudence";
|
||||
|
||||
Reference in New Issue
Block a user