checkpoint: 8

This commit is contained in:
zkldi
2023-01-19 16:39:21 +00:00
parent 6cc12387e7
commit 5f979f2790
46 changed files with 239 additions and 2329 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ export function ParseGPT(str: string) {
// this is an interesting way of game checking noah
const gameConfig = GetGameConfig(game) as GameConfig | undefined;
if (!gameConfig || !gameConfig.validPlaytypes.includes(playtype)) {
if (!gameConfig || !gameConfig.playtypes.includes(playtype)) {
throw new Error(`Invalid GPT Combination '${str}'.`);
}
+1 -1
View File
@@ -11,7 +11,7 @@ const GPTChoices: Array<[string, string]> = [];
for (const game of ServerConfig.games) {
const gameConfig = GetGameConfig(game);
for (const playtype of gameConfig.validPlaytypes) {
for (const playtype of gameConfig.playtypes) {
GPTChoices.push([FormatGame(game, playtype), `${game}:${playtype}`]);
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ export async function HandleGoalAchievedV1(
const gameConfig = GetGameConfig(game);
const shouldShowPlaytype = gameConfig.validPlaytypes.length !== 1;
const shouldShowPlaytype = gameConfig.playtypes.length !== 1;
const embed = CreateEmbed(userDoc.id)
.setTitle(
+1 -1
View File
@@ -27,7 +27,7 @@ export async function HandleQuestAchievedV1(
const quest = await GetQuestWithID(event.questID, game, playtype);
const gameConfig = GetGameConfig(game);
const shouldShowPlaytype = gameConfig.validPlaytypes.length > 1 ? ` (${playtype})` : "";
const shouldShowPlaytype = gameConfig.playtypes.length > 1 ? ` (${playtype})` : "";
const embed = CreateEmbed(userDoc.id).setTitle(
`${userDoc.username} just completed the ${quest.name}${shouldShowPlaytype} quest!`
@@ -29,7 +29,7 @@ export default function PlaytypeSelect({
</div>
<div className="card-footer">
<div className="d-flex justify-content-center btn-group">
{gameConfig.validPlaytypes.map((pt) => (
{gameConfig.playtypes.map((pt) => (
<Link
key={pt}
className="btn btn-outline-primary float-right"
@@ -35,7 +35,7 @@ export default function BatchManualPage() {
if (!gptConfig) {
throw new Error(
`Invalid Playtype ${data.meta.playtype}. Expected any of ${gameConfig.validPlaytypes}.`
`Invalid Playtype ${data.meta.playtype}. Expected any of ${gameConfig.playtypes}.`
);
}
@@ -22,7 +22,7 @@ const PR_LOCAL_QUESTS_SCHEMA: PrudenceSchema = {
playtype: (self, parent) => {
const gameConfig = GetGameConfig(parent.game as any);
if (!gameConfig.validPlaytypes.includes(self)) {
if (!gameConfig.playtypes.includes(self)) {
return `Invalid playtype '${self}' for ${parent.game}`;
}
+2 -2
View File
@@ -59,8 +59,8 @@ export default function GameRoutes() {
return (
<Switch>
<Route exact path="/games/:game">
{gameConfig.validPlaytypes.length === 1 ? (
<Redirect to={`/games/${game}/${gameConfig.validPlaytypes[0]}`} />
{gameConfig.playtypes.length === 1 ? (
<Redirect to={`/games/${game}/${gameConfig.playtypes[0]}`} />
) : (
<PlaytypeSelect
subheaderCrumbs={["Games", gameConfig.name]}
+2 -2
View File
@@ -154,9 +154,9 @@ function UserGameRoutes({ reqUser }: { reqUser: UserDocument }) {
return (
<Switch>
<Route exact path="/u/:userID/games/:game">
{gameConfig.validPlaytypes.length === 1 ? (
{gameConfig.playtypes.length === 1 ? (
<Redirect
to={`/u/${reqUser.username}/games/${game}/${gameConfig.validPlaytypes[0]}`}
to={`/u/${reqUser.username}/games/${game}/${gameConfig.playtypes[0]}`}
/>
) : (
<PlaytypeSelect
@@ -10,7 +10,7 @@ export default function AllGames() {
for (const game of TachiConfig.games) {
const gameConfig = GetGameConfig(game);
for (const playtype of gameConfig.validPlaytypes) {
for (const playtype of gameConfig.playtypes) {
links.push(
<MenuLink
key={`${game}:${playtype}`}
@@ -74,7 +74,7 @@ export function HeaderMenu({
}
for (const game of TachiConfig.games) {
for (const playtype of GetGameConfig(game).validPlaytypes) {
for (const playtype of GetGameConfig(game).playtypes) {
const e = ugsMap.get(`${game}:${playtype}`);
if (!e) {
@@ -137,9 +137,7 @@ function SearchResults({ results }: { results: SearchReturns }) {
<SearchResult
key={s.id + s.game}
link={`/games/${s.game}/${
GetGameConfig(s.game).validPlaytypes.includes(
playtype as Playtype
)
GetGameConfig(s.game).playtypes.includes(playtype as Playtype)
? playtype
: GetGameConfig(s.game).defaultPlaytype
}/songs/${s.id}`}
@@ -36,7 +36,7 @@ export default function AddNewQuestModal({
{TachiConfig.games.flatMap((game) => {
const gameConfig = GetGameConfig(game);
return gameConfig.validPlaytypes.map((playtype) => (
return gameConfig.playtypes.map((playtype) => (
<option
key={`${game}:${playtype}`}
value={`${game}:${playtype}`}
+1 -1
View File
@@ -18,7 +18,7 @@ export function IsSupportedPlaytype<G extends Game = Game>(
): str is Playtypes[G] {
const gameConfig = GetGameConfig(game);
return gameConfig.validPlaytypes.includes(str as unknown as Playtypes[G]);
return gameConfig.playtypes.includes(str as unknown as Playtypes[G]);
}
export function IsScore<I extends IDStrings>(
+1 -1
View File
@@ -5,7 +5,7 @@ export function GetSortedGPTs() {
const arr = [];
for (const game of TachiConfig.games) {
const gameConfig = GetGameConfig(game);
for (const playtype of gameConfig.validPlaytypes) {
for (const playtype of gameConfig.playtypes) {
arr.push({ game, playtype });
}
}
+35 -12
View File
@@ -1,12 +1,21 @@
import { GetGameConfig, GetGamePTConfig } from "./old-config";
import { allSupportedGames } from "../constants/import-types";
import { GetGameConfig, GetGamePTConfig, allSupportedGames } from "./config";
import t from "tap";
t.test("#GetGameConfig", (t) => {
t.plan(allSupportedGames.length);
for (const game of allSupportedGames) {
t.equal(GetGameConfig(game).internalName, game);
// i don't feel *that* strongly about this restriction, but game IDs *definitely*
// can't have things like `:` in them.
t.match(game, /^[a-z]+$/u, "Game IDs must be a-z only.");
const conf = GetGameConfig(game);
t.not(conf, undefined, `'${game}' should have a config defined.`);
t.ok(
conf.playtypes.includes(conf.defaultPlaytype),
"Default playtype should be in the array of playtypes."
);
}
});
@@ -14,20 +23,34 @@ t.test("#GetGamePTConfig", (t) => {
for (const game of allSupportedGames) {
const gameConfig = GetGameConfig(game);
for (const playtype of gameConfig.validPlaytypes) {
for (const playtype of gameConfig.playtypes) {
const conf = GetGamePTConfig(game, playtype);
t.equal(
conf.idString,
`${game}:${playtype}`,
`Should return the right GamePTConfig (${game} ${playtype})`
t.not(conf, undefined, `'${game}:${playtype}' should have a config defined.`);
t.ok(
conf.scoreRatingAlgs[conf.defaultScoreRatingAlg],
"The default score rating alg should have an implementation."
);
t.equal(
conf.gradeBoundaries.length,
conf.grades.length,
`Should have the same amount of grades as grade boundaries. (${game} ${playtype})`
t.ok(
conf.sessionRatingAlgs[conf.defaultSessionRatingAlg],
"The default session rating alg should have an implementation."
);
t.ok(
conf.profileRatingAlgs[conf.defaultProfileRatingAlg],
"The default profile rating alg should have an implementation."
);
if (conf.difficultyConfig.type === "FIXED") {
t.ok(
conf.difficultyConfig.difficultyOrder.includes(
conf.difficultyConfig.defaultDifficulty
),
"The default difficulty should be part of difficultyOrder."
);
}
}
}
+2 -2
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const BMS_CONF = {
defaultPlaytype: "7K",
name: "BMS",
validPlaytypes: ["7K", "14K"],
playtypes: ["7K", "14K"],
songData: z.strictObject({
genre: z.nullable(z.string()),
subtitle: z.nullable(z.string()),
@@ -178,7 +178,7 @@ export const BMS_7K_CONF = {
egr: { type: "INTEGER" },
egd: { type: "INTEGER" },
ebd: { type: "INTEGER" },
epr: { type: "INTEGER" }, //
epr: { type: "INTEGER" },
lpg: { type: "INTEGER" },
lgr: { type: "INTEGER" },
lgd: { type: "INTEGER" },
+1 -1
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const CHUNITHM_CONF = {
defaultPlaytype: "Single",
name: "CHUNITHM",
validPlaytypes: ["Single"],
playtypes: ["Single"],
songData: z.strictObject({
genre: z.string(),
displayVersion: z.string(),
+1 -1
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const GITADORA_CONF = {
defaultPlaytype: "Dora",
name: "GITADORA",
validPlaytypes: ["Gita", "Dora"],
playtypes: ["Gita", "Dora"],
songData: z.strictObject({
isHot: z.boolean(),
displayVersion: z.string(),
+1 -1
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const IIDX_CONF = {
defaultPlaytype: "SP",
name: "beatmania IIDX",
validPlaytypes: ["SP", "DP"],
playtypes: ["SP", "DP"],
songData: z.strictObject({
genre: z.string(),
displayVersion: z.nullable(z.string()),
+1 -1
View File
@@ -5,7 +5,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const ITG_CONF = {
defaultPlaytype: "Stamina",
name: "ITG",
validPlaytypes: ["Stamina"],
playtypes: ["Stamina"],
songData: z.strictObject({
subtitle: z.string(),
originalPack: z.string(),
+1 -1
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const JUBEAT_CONF = {
defaultPlaytype: "Single",
name: "jubeat",
validPlaytypes: ["Single"],
playtypes: ["Single"],
songData: z.strictObject({
displayVersion: z.string(),
}),
+1 -1
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const MAIMAI_DX_CONF = {
defaultPlaytype: "Single",
name: "maimai DX",
validPlaytypes: ["Single"],
playtypes: ["Single"],
songData: z.strictObject({
displayVersion: z.string(),
}),
+1 -1
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const MUSECA_CONF = {
defaultPlaytype: "Single",
name: "MÚSECA",
validPlaytypes: ["Single"],
playtypes: ["Single"],
songData: z.strictObject({
titleJP: z.string(),
artistJP: z.string(),
+14 -2
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const PMS_CONF = {
defaultPlaytype: "Controller",
name: "PMS",
validPlaytypes: ["Controller", "Keyboard"],
playtypes: ["Controller", "Keyboard"],
songData: z.strictObject({
genre: z.nullable(z.string()),
subtitle: z.nullable(z.string()),
@@ -78,7 +78,19 @@ export const PMS_CONTROLLER_CONF = {
additionalMetrics: {
...FAST_SLOW_MAXCOMBO,
// TODO
bp: { type: "INTEGER" },
gauge: { type: "DECIMAL" },
gaugeHistory: { type: "GRAPH" },
epg: { type: "INTEGER" },
egr: { type: "INTEGER" },
egd: { type: "INTEGER" },
ebd: { type: "INTEGER" },
epr: { type: "INTEGER" },
lpg: { type: "INTEGER" },
lgr: { type: "INTEGER" },
lgd: { type: "INTEGER" },
lbd: { type: "INTEGER" },
lpr: { type: "INTEGER" },
},
scoreRatingAlgs: {
+1 -1
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const POPN_CONF = {
defaultPlaytype: "9B",
name: "pop'n music",
validPlaytypes: ["9B"],
playtypes: ["9B"],
songData: z.strictObject({
displayVersion: z.nullable(z.string()),
genre: z.string(),
+1 -1
View File
@@ -5,7 +5,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const SDVX_CONF = {
defaultPlaytype: "Single",
name: "SOUND VOLTEX",
validPlaytypes: ["Single"],
playtypes: ["Single"],
songData: z.strictObject({
displayVersion: z.string(),
}),
+1 -1
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const USC_CONF = {
defaultPlaytype: "Controller",
name: "USC",
validPlaytypes: ["Controller", "Keyboard"],
playtypes: ["Controller", "Keyboard"],
songData: z.strictObject({}),
} as const satisfies INTERNAL_GAME_CONFIG;
+1 -1
View File
@@ -6,7 +6,7 @@ import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals";
export const WACCA_CONF = {
defaultPlaytype: "Single",
name: "WACCA",
validPlaytypes: ["Single"],
playtypes: ["Single"],
songData: z.strictObject({
titleJP: z.string(),
artistJP: z.string(),
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ import t from "tap";
t.test("BMS Tables should be unique.", (t) => {
const allKeys = BMS_TABLES.map((e) => `${e.playtype}-${e.prefix}`);
t.strictSame(allKeys, [...new Set(allKeys)], "There should be no duplicate tables prefixes.");
t.strictSame(allKeys, [...new Set(allKeys)], "There should be no duplicate table prefixes.");
t.end();
});
-1
View File
@@ -5,7 +5,6 @@ import t from "tap";
t.test("Property Checks", (t) => {
t.type(tachiCommon.GetGameConfig, "function");
t.type(tachiCommon.GetGamePTConfig, "function");
t.type(tachiCommon.gameClasses, "object");
t.type(tachiCommon.COLOUR_SET, "object");
t.end();
+1 -3
View File
@@ -1,8 +1,6 @@
export * from "./types";
export * from "./config/old-config";
export * as gameClasses from "./config/game-classes";
export * as ESDCore from "./lib/esd";
export * from "./config/config";
export * as StaticConfig from "./constants/import-types";
export * from "./utils/util";
export * from "./webhooks";
+10 -10
View File
@@ -2,15 +2,15 @@
// their name in the database to the object they schemaify.
// The schemas themselves are wrapped in functions that throw on error.
import { GetGameConfig, GetGamePTConfig } from "../config/old-config";
import { allIDStrings, allImportTypes, allSupportedGames } from "../constants/import-types";
import { GetGameConfig, allSupportedGames, GetGamePTConfig, allGPTStrings } from "../config/config";
import { allImportTypes } from "../constants/import-types";
import { ALL_PERMISSIONS } from "../constants/permissions";
import { UserAuthLevels } from "../types";
import deepmerge from "deepmerge";
import { p } from "prudence";
import type { GamePTConfig } from "../config/old-config";
import type { AllClassSets } from "../config/game-classes";
import type { Game, IDStrings, NotificationBody, Playtype, Playtypes } from "../types";
import type { Game, Playtypes, Playtype } from "../types/game-config";
import type { GamePTConfig } from "../types/internals";
import type { NotificationBody } from "../types/notifications";
import type {
PrudenceSchema,
ValidationFunctionParentOptionsKeychain,
@@ -219,7 +219,7 @@ const GetScoreMeta = (game: Game, playtype: Playtypes[Game]): PrudenceSchema =>
};
function IsValidPlaytype(game: Game, str: string): str is Playtypes[Game] {
return GetGameConfig(game).validPlaytypes.includes(str as Playtypes[Game]);
return GetGameConfig(game).playtypes.includes(str as Playtypes[Game]);
}
function IsValidGame(str: string): str is Game {
@@ -238,7 +238,7 @@ const getPlaytype = (game: Game, self: unknown): Playtypes[Game] => {
const playtype = s.playtype as string;
if (!IsValidPlaytype(game, playtype)) {
throw new Error(`Expected any of ${gameConfig.validPlaytypes.join(", ")}`);
throw new Error(`Expected any of ${gameConfig.playtypes.join(", ")}`);
}
return playtype;
@@ -811,13 +811,13 @@ const PRE_SCHEMAS = {
userID: p.isPositiveNonZeroInteger,
timeStarted: p.isPositive,
timeFinished: p.isPositive,
idStrings: [p.isIn(allIDStrings)],
idStrings: [p.isIn(allGPTStrings)],
importID: "string",
scoreIDs: ["string"],
game: p.isIn(games),
// @ts-expect-error We've asserted this is definitely a game.
playtypes: [(self) => p.isIn(GetGameConfig(self.game).validPlaytypes)(self)],
playtypes: [(self) => p.isIn(GetGameConfig(self.game).playtypes)(self)],
errors: [
{
type: "string",
@@ -1024,7 +1024,7 @@ const PRE_SCHEMAS = {
const { game } = extractGPTIDString(self);
return prSchemaFnWrap({
idString: p.isIn(allIDStrings),
idString: p.isIn(allGPTStrings),
chartDoc: PR_CHART_DOCUMENT(game),
songDoc: PR_SONG_DOCUMENT(game),
userIDs: [p.isPositiveNonZeroInteger],
+12 -29
View File
@@ -1,7 +1,8 @@
import { allSupportedGames } from "../config/config";
import type {
Game,
ChartDocument,
GameToIDStrings,
GameToGPTStrings,
SongDocument,
BMSCourseDocument,
FolderDocument,
@@ -15,7 +16,7 @@ import type {
// 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<GameToIDStrings[G]>>;
[G in Game as `charts-${G}.json`]: Array<ChartDocument<GameToGPTStrings[G]>>;
};
type SongDBSeeds = {
@@ -34,41 +35,23 @@ interface OtherDBSeeds {
export type AllDatabaseSeeds = ChartDBSeeds & OtherDBSeeds & SongDBSeeds;
// Nifty trick to enforce that we always specify all database seeds :)
const CURRENT_DATABASE_SEEDS: Record<keyof AllDatabaseSeeds, true> = {
const CURRENT_DATABASE_SEEDS: Record<keyof OtherDBSeeds, true> = {
"bms-course-lookup.json": true,
"charts-bms.json": true,
"charts-chunithm.json": true,
"charts-gitadora.json": true,
"charts-iidx.json": true,
"charts-itg.json": true,
"charts-jubeat.json": true,
"charts-maimaidx.json": true,
"charts-museca.json": true,
"charts-pms.json": true,
"charts-popn.json": true,
"charts-sdvx.json": true,
"charts-usc.json": true,
"charts-wacca.json": true,
"folders.json": true,
"goals.json": true,
"questlines.json": true,
"quests.json": true,
"songs-bms.json": true,
"songs-chunithm.json": true,
"songs-gitadora.json": true,
"songs-iidx.json": true,
"songs-itg.json": true,
"songs-jubeat.json": true,
"songs-maimaidx.json": true,
"songs-museca.json": true,
"songs-pms.json": true,
"songs-popn.json": true,
"songs-sdvx.json": true,
"songs-usc.json": true,
"songs-wacca.json": true,
"tables.json": true,
};
for (const game of allSupportedGames) {
// @ts-expect-error define all games' seeds on the object.
CURRENT_DATABASE_SEEDS[`songs-${game}`] = true;
// @ts-expect-error define all games' seeds on the object.
CURRENT_DATABASE_SEEDS[`charts-${game}`] = true;
}
export const DatabaseSeedNames = Object.keys(CURRENT_DATABASE_SEEDS) as Array<
keyof AllDatabaseSeeds
>;
+6 -6
View File
@@ -12,9 +12,9 @@ export enum UserAuthLevels {
ADMIN = 3,
}
// export * from "./types/api";
// export * from "./types/batch-manual";
// export * from "./types/documents";
// export * from "./types/game-implementations";
// export * from "./types/import-types";
// export * from "./types/notifications";
export * from "./types/api";
export * from "./types/batch-manual";
export * from "./types/documents";
export * from "./types/game-config";
export * from "./types/import-types";
export * from "./types/notifications";
+52 -44
View File
@@ -1,4 +1,26 @@
import type { UserAuthLevels, integer } from "../types";
import type {
ChartDocumentData,
ChartSets,
Classes,
Difficulties,
ExtractedAdditionalMetrics,
ExtractedScoreMetrics,
GPTString,
GPTStringToGame,
GPTStringToPlaytype,
Game,
Judgements,
Playtype,
Playtypes,
Preferences,
ProfileRatingAlgorithms,
ScoreMeta,
ScoreRatingAlgorithms,
SessionRatingAlgorithms,
SongDocumentData,
UserAuthLevels,
integer,
} from "../types";
import type { APIPermissions } from "./api";
import type { ImportTypes } from "./import-types";
import type { BaseNotification, NotificationBody } from "./notifications";
@@ -112,7 +134,7 @@ interface SessionScoreNewInfo {
export type SessionScoreInfo = SessionScoreNewInfo | SessionScorePBInfo;
export interface SessionDocument<GPT extends GPTStrings = GPTStrings> {
export interface SessionDocument<GPT extends GPTString = GPTString> {
userID: integer;
sessionID: string;
scoreIDs: Array<string>;
@@ -140,8 +162,8 @@ export interface ImportDocument {
timeStarted: number;
timeFinished: number;
// Contains an array of GPTStrings, which dictates what (game:playtype)s were involved in this import.
gptStrings: Array<GPTStrings>;
// Contains an array of GPTString, which dictates what (game:playtype)s were involved in this import.
GPTString: Array<GPTString>;
importID: string;
scoreIDs: Array<string>;
game: Game;
@@ -290,12 +312,12 @@ export interface UserDocument {
authLevel: UserAuthLevels;
}
export interface UserGameStats<GPT extends GPTStrings = GPTStrings> {
export interface UserGameStats<GPT extends GPTString = GPTString> {
userID: integer;
game: GPTStringToGame[GPT];
playtype: GPTStringToPlaytype[GPT];
ratings: Partial<Record<ProfileRatingAlgorithms[GPT], number | null>>;
classes: Partial<GameClasses<GPT>>;
classes: Partial<Classes[GPT]>;
}
export interface ChartTierlistInfo {
@@ -304,7 +326,7 @@ export interface ChartTierlistInfo {
individualDifference?: boolean;
}
export interface ChartDocument<GPT extends GPTStrings = GPTStrings> {
export interface ChartDocument<GPT extends GPTString = GPTString> {
chartID: string;
songID: integer;
level: string;
@@ -312,8 +334,8 @@ export interface ChartDocument<GPT extends GPTStrings = GPTStrings> {
isPrimary: boolean;
difficulty: Difficulties[GPT];
playtype: GPTStringToPlaytype[GPT];
data: ChartData[GPT];
chartSets: Array<SupportedVersions[GPT]>;
data: ChartDocumentData[GPT];
chartSets: Array<ChartSets[GPT]>;
}
export interface SongDocument<G extends Game = Game> {
@@ -322,7 +344,7 @@ export interface SongDocument<G extends Game = Game> {
artist: string;
searchTerms: Array<string>;
altTitles: Array<string>;
data: SongData[G];
data: SongDocumentData[G];
}
export interface TableDocument {
@@ -391,22 +413,17 @@ export type QuestSubscriptionDocument = {
}
);
export interface ScoreDocument<GPT extends GPTStrings = GPTStrings> {
export type ScoreData<GPT extends GPTString = GPTString> = ExtractedScoreMetrics[GPT] & {
judgements: Partial<Record<Judgements[GPT], integer | null>>;
additionalMetrics: Partial<ExtractedAdditionalMetrics[GPT]>;
};
export interface ScoreDocument<GPT extends GPTString = GPTString> {
service: string;
game: GPTStringToGame[GPT];
playtype: GPTStringToPlaytype[GPT];
userID: integer;
scoreData: {
score: number;
lamp: Lamps[GPT];
percent: number;
grade: Grades[GPT];
lampIndex: integer;
gradeIndex: integer;
esd: number | null;
judgements: Partial<Record<Judgements[GPT], integer | null>>;
additionalMetrics: Partial<AdditionalMetrics[GPT]>;
};
scoreData: ScoreData<GPT>;
scoreMeta: Partial<ScoreMeta[GPT]>;
calculatedData: Partial<Record<ScoreRatingAlgorithms[GPT], number | null>>;
timeAchieved: integer | null;
@@ -425,7 +442,7 @@ export interface PBScoreComposedReference {
scoreID: string;
}
export interface PBScoreDocument<GPT extends GPTStrings = GPTStrings> {
export interface PBScoreDocument<GPT extends GPTString = GPTString> {
composedFrom: {
scorePB: string;
lampPB: string;
@@ -449,17 +466,7 @@ export interface PBScoreDocument<GPT extends GPTStrings = GPTStrings> {
highlight: boolean;
isPrimary: boolean;
timeAchieved: number | null;
scoreData: {
score: number;
lamp: Lamps[GPT];
percent: number;
grade: Grades[GPT];
lampIndex: integer;
gradeIndex: integer;
esd: number | null;
judgements: Partial<Record<Judgements[GPT], integer | null>>;
additionalMetrics: Partial<AdditionalMetrics[GPT]>;
};
scoreData: ScoreData<GPT>;
calculatedData: Partial<Record<ScoreRatingAlgorithms[GPT], number | null>>;
}
@@ -479,7 +486,7 @@ export interface ImportProcessInfoInvalidDatapoint {
content: Record<string, never>;
}
export interface ImportProcessInfoScoreImported<GPT extends GPTStrings = GPTStrings> {
export interface ImportProcessInfoScoreImported<GPT extends GPTString = GPTString> {
success: true;
type: "ScoreImported";
message: string;
@@ -500,13 +507,14 @@ export interface ImportProcessInfoSongOrChartNotFound {
type: "SongOrChartNotFound";
message: string;
content: {
// these are too complex to type. Not bothering.
data: unknown;
context: unknown; // @TODO Type these properly.
context: unknown;
orphanID: string;
};
}
export type ImportProcessingInfo<GPT extends GPTStrings = GPTStrings> =
export type ImportProcessingInfo<GPT extends GPTString = GPTString> =
| ImportProcessInfoInternalError
| ImportProcessInfoInvalidDatapoint
| ImportProcessInfoOrphanExists
@@ -582,7 +590,7 @@ export interface ShowcaseStatChart {
property: "grade" | "lamp" | "percent" | "playcount" | "score";
}
export interface UGPTSettingsDocument<GPT extends GPTStrings = GPTStrings> {
export interface UGPTSettingsDocument<GPT extends GPTString = GPTString> {
userID: integer;
game: GPTStringToGame[GPT];
playtype: GPTStringToPlaytype[GPT];
@@ -594,12 +602,12 @@ export interface UGPTSettingsDocument<GPT extends GPTStrings = GPTStrings> {
scoreBucket: "grade" | "lamp" | null;
defaultTable: string | null;
preferredRanking: "global" | "rival" | null;
gameSpecific: UGPTSpecificPreferences[GPT];
gameSpecific: Preferences[GPT];
};
rivals: Array<integer>;
}
export interface UserGameStatsSnapshotDocument<GPT extends GPTStrings = GPTStrings>
export interface UserGameStatsSnapshotDocument<GPT extends GPTString = GPTString>
extends UserGameStats<GPT> {
rankings: Record<ProfileRatingAlgorithms[GPT], { ranking: integer | null; outOf: integer }>;
playcount: integer;
@@ -641,7 +649,7 @@ export interface KsHookSettingsDocument {
forceStaticImport: boolean;
}
export interface OrphanChartDocument<GPT extends GPTStrings = GPTStrings> {
export interface OrphanChartDocument<GPT extends GPTString = GPTString> {
gptString: GPT;
chartDoc: ChartDocument<GPT>;
songDoc: SongDocument<GPTStringToGame[GPT]>;
@@ -650,16 +658,16 @@ export interface OrphanChartDocument<GPT extends GPTStrings = GPTStrings> {
export interface ClassDelta {
game: Game;
set: AllClassSets;
set: Classes[GPTString];
playtype: Playtype;
old: integer | null;
new: integer;
}
export interface ClassAchievementDocument<GPT extends GPTStrings = GPTStrings> {
export interface ClassAchievementDocument<GPT extends GPTString = GPTString> {
game: GPTStringToGame[GPT];
playtype: GPTStringToPlaytype[GPT];
classSet: AllClassSets;
classSet: Classes[GPT];
classOldValue: integer | null;
classValue: integer;
timeAchieved: number;
+13 -10
View File
@@ -7,7 +7,7 @@ import type {
} from "./game-config-utils";
import type { ExtractEnumMetricNames, ExtractMetrics } from "./metrics";
import type { ExtractArrayElementType } from "./utils";
import type { z } from "zod";
import type { AnyZodObject, z } from "zod";
/**
* All the games Tachi supports.
@@ -30,7 +30,7 @@ export type Game = keyof typeof GAME_CONFIGS;
* is the go-to.
*/
export type Playtypes = {
[G in Game]: typeof GAME_CONFIGS[G]["validPlaytypes"][number];
[G in Game]: typeof GAME_CONFIGS[G]["playtypes"][number];
};
/**
@@ -49,13 +49,13 @@ export type SongDocumentData = {
export interface GameConfig<G extends Game = Game> {
readonly name: string;
readonly defaultPlaytype: Playtypes[G];
readonly validPlaytypes: ReadonlyArray<Playtypes[G]>;
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.
*/
readonly songData: SongDocumentData[G];
readonly songData: AnyZodObject;
}
/**
@@ -71,7 +71,7 @@ export type GPTString = keyof {
[G in Game as `${G}:${Playtypes[G]}`]: never;
};
export type GameToGPTStrings = {
export type GPTStrings = {
[G in Game]: `${G}:${Playtypes[G]}`;
};
@@ -145,7 +145,7 @@ export type ExtractedClasses = {
};
export type Classes = {
[G in GPTString]: typeof GAME_PT_CONFIGS[G]["classes"];
[G in GPTString]: keyof typeof GAME_PT_CONFIGS[G]["classes"];
};
export type ChartDocumentData = {
@@ -357,13 +357,16 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* Chart documents get their own GPT-specific record that they use for whatever
* they want. IIDX documents store BPI information like kaiden averages, BMS
* charts store sha256/md5 hashes, etc.
*
* This is a zod schema that can be used to validate that input.
*/
chartData: ChartDocumentData[GPT];
chartData: AnyZodObject;
/**
* What game-specific user-settings exist for this GPT?
* This is a zod schema that can be used to validate provided GPT-specific
* preferences.
*/
preferences: Preferences[GPT];
preferences: AnyZodObject;
/**
* What game-specific metadata should be stored on scores for this GPT?
@@ -371,7 +374,7 @@ export interface SpecificGamePTConfig<GPT extends GPTString> {
* These are for things like what options were used (RANDOM, MIRROR etc.)
* and don't exist on PBs.
*/
scoreMeta: ScoreMeta[GPT];
scoreMeta: AnyZodObject;
/**
* What "matchTypes" should this game support for batch-manual imports? This
+1 -1
View File
@@ -56,7 +56,7 @@ export type GamePTConfig = Readonly<{
*/
export type INTERNAL_GAME_CONFIG<PT extends string = string> = Readonly<{
name: string;
validPlaytypes: ReadonlyArray<PT>;
playtypes: ReadonlyArray<PT>;
defaultPlaytype: PT;
songData: AnyZodObject;
}>;
+57 -25
View File
@@ -1,6 +1,16 @@
import { GetGameConfig, GetGamePTConfig } from "../config/old-config";
import type { ChartDocument, Game, IDStrings, Playtypes, SongDocument } from "..";
import type { Grades, integer, PBScoreDocument } from "../types";
import { GetGameConfig, GetGamePTConfig, GetSpecificGPTConfig } from "../config/config";
import type {
integer,
PBScoreDocument,
ChartDocument,
Game,
Playtypes,
SongDocument,
GameToGPTStrings,
SpecificGamePTConfig,
GPTString,
GPTStrings,
} from "../types";
import type { PrudenceError } from "prudence";
export function FormatInt(v: number): string {
@@ -23,9 +33,21 @@ export function FormatDifficulty(chart: ChartDocument, game: Game): string {
}
if (game === "gitadora") {
const gptConfig = GetGamePTConfig(game, chart.playtype);
const ch = chart as ChartDocument<GPTStrings["gitadora"]>;
const shortDiff = gptConfig.shortDifficulties[chart.difficulty] ?? chart.difficulty;
const gptConfig = GetSpecificGPTConfig<GPTStrings["gitadora"]>(
game,
chart.playtype as Playtypes["gitadora"]
);
// @ts-expect-error maybe this new config format was a mistake.
// 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/no-unsafe-assignment
const shortDiff = gptConfig.difficultyConfig.difficultyShorthand[ch.difficulty];
// gitadora should always use short diffs. they just look better.
return `${shortDiff} ${chart.level}`;
@@ -33,7 +55,7 @@ export function FormatDifficulty(chart: ChartDocument, game: Game): string {
const gameConfig = GetGameConfig(game);
if (gameConfig.validPlaytypes.length > 1) {
if (gameConfig.playtypes.length > 1) {
return `${chart.playtype} ${chart.difficulty} ${chart.level}`;
}
@@ -54,9 +76,15 @@ export function FormatDifficultyShort(chart: ChartDocument, game: Game): string
return `S${itgChart.data.difficultyTag} ${chart.level}`;
}
const shortDiff = gptConfig.shortDifficulties[chart.difficulty] ?? chart.difficulty;
if (gptConfig.difficultyConfig.type === "DYNAMIC") {
// TODO cap string length
return `${chart.difficulty} ${chart.level}`;
}
if (gameConfig.validPlaytypes.length === 1 || game === "gitadora") {
const shortDiff =
gptConfig.difficultyConfig.difficultyShorthand[chart.difficulty] ?? chart.difficulty;
if (gameConfig.playtypes.length === 1 || game === "gitadora") {
return `${shortDiff} ${chart.level}`;
}
@@ -70,7 +98,7 @@ export function FormatDifficultyShort(chart: ChartDocument, game: Game): string
export function FormatGame(game: Game, playtype: Playtypes[Game]): string {
const gameConfig = GetGameConfig(game);
if (gameConfig.validPlaytypes.length === 1) {
if (gameConfig.playtypes.length === 1) {
return gameConfig.name;
}
@@ -84,7 +112,7 @@ export function FormatChart(
short = false
): string {
if (game === "bms") {
const tables = (chart as ChartDocument<"bms:7K" | "bms:14K">).data.tableFolders;
const tables = (chart as ChartDocument<GPTStrings["bms"]>).data.tableFolders;
const bmsSong = song as SongDocument<"bms">;
@@ -104,7 +132,7 @@ export function FormatChart(
return `${realTitle} (${tables.map((e) => `${e.table}${e.level}`).join(", ")})`;
} else if (game === "usc") {
const uscChart = chart as ChartDocument<"usc:Controller" | "usc:Keyboard">;
const uscChart = chart as ChartDocument<GPTStrings["usc"]>;
// If this chart isn't an official, render it differently
if (!uscChart.data.isOfficial) {
@@ -136,15 +164,19 @@ export function FormatChart(
let playtypeStr = `${chart.playtype}`;
if (gameConfig.validPlaytypes.length === 1) {
if (gameConfig.playtypes.length === 1) {
playtypeStr = "";
}
const gptConfig = GetGamePTConfig(game, chart.playtype);
const diff = short
? gptConfig.shortDifficulties[chart.difficulty] ?? chart.difficulty
: chart.difficulty;
let diff: string;
if (gptConfig.difficultyConfig.type === "DYNAMIC") {
diff = chart.difficulty;
} else {
diff = gptConfig.difficultyConfig.difficultyShorthand[chart.difficulty] ?? chart.difficulty;
}
// iidx formats things like SPA instead of SP A.
// this is a hack, this should be part of the gptConfig, tbh.
@@ -156,16 +188,16 @@ export function FormatChart(
space = " ";
}
// return the most recent version this chart appeared in if it
// return the most recent chartSet this chart appeared in if it
// is not primary.
if (!chart.isPrimary) {
return `${song.title} (${playtypeStr}${space}${diff} ${chart.level} ${chart.versions[0]})`;
return `${song.title} (${playtypeStr}${space}${diff} ${chart.level} ${chart.chartSets[0]})`;
}
return `${song.title} (${playtypeStr}${space}${diff} ${chart.level})`;
}
export function AbsoluteGradeDelta<I extends IDStrings = IDStrings>(
export function AbsoluteGradeDelta<I extends GPTString = GPTString>(
game: Game,
playtype: Playtypes[Game],
score: number,
@@ -190,12 +222,12 @@ export function AbsoluteGradeDelta<I extends IDStrings = IDStrings>(
return score - gradeScore;
}
export function RelativeGradeDelta<I extends IDStrings = IDStrings>(
export function RelativeGradeDelta<GPT extends GPTString = GPTString>(
game: Game,
playtype: Playtypes[Game],
score: number,
percent: number,
grade: Grades[I],
grade: Grades[GPT],
relativeIndex: integer
): { grade: string; delta: number } | null {
const gptConfig = GetGamePTConfig(game, playtype);
@@ -240,7 +272,7 @@ type FormatGradeDeltaReturns =
closer: "lower";
};
export function GenericFormatGradeDelta<I extends IDStrings = IDStrings>(
export function GenericFormatGradeDelta<I extends GPTString = GPTString>(
game: Game,
playtype: Playtypes[Game],
score: number,
@@ -278,7 +310,7 @@ export function GenericFormatGradeDelta<I extends IDStrings = IDStrings>(
};
}
export function GetCloserGradeDelta<I extends IDStrings = IDStrings>(
export function GetCloserGradeDelta<I extends GPTString = GPTString>(
game: Game,
playtype: Playtypes[Game],
score: number,
@@ -315,7 +347,7 @@ export function CreateSongMap<G extends Game = Game>(songs: Array<SongDocument<G
return songMap;
}
export function CreateChartMap<I extends IDStrings = IDStrings>(charts: Array<ChartDocument<I>>) {
export function CreateChartMap<I extends GPTString = GPTString>(charts: Array<ChartDocument<I>>) {
const chartMap = new Map<string, ChartDocument<I>>();
for (const chart of charts) {
@@ -395,8 +427,8 @@ export function GradeGoalFormatter(pb: PBScoreDocument) {
// For games with 'BP', show that next to the clear.
export function IIDXBMSLampGoalFormatter(pb: PBScoreDocument<IIDXBMSGPTs>) {
if (typeof pb.scoreData.hitMeta.bp === "number") {
return `${pb.scoreData.lamp} (BP: ${pb.scoreData.hitMeta.bp})`;
if (typeof pb.scoreData.additionalMetrics.bp === "number") {
return `${pb.scoreData.lamp.string} (BP: ${pb.scoreData.additionalMetrics.bp})`;
}
return pb.scoreData.lamp;
@@ -392,7 +392,7 @@ async function UpdateUsersGameStats(
// Instead of using the provided playtypes, run the classHandler on all
// playtypes. This should only happen if a classHandler is provided, and is
// a hack fix for things like #480.
const allPlaytypes = GetGameConfig(game).validPlaytypes;
const allPlaytypes = GetGameConfig(game).playtypes;
const playtypes = classHandler ? allPlaytypes : modifiedPlaytypes;
@@ -71,7 +71,7 @@ export function ParseBatchManualFromObject(
if (!(typeof maybePlaytype === "string" && IsValidPlaytype(game, maybePlaytype))) {
throw new ScoreImportFatalError(
400,
`Invalid playtype '${maybePlaytype}' - expected any of ${gameConfig.validPlaytypes.join(
`Invalid playtype '${maybePlaytype}' - expected any of ${gameConfig.playtypes.join(
", "
)}.`
);
@@ -29,7 +29,7 @@ router.get("/", async (req, res) => {
const gpts = [];
for (const game of TachiConfig.GAMES) {
const playtypes = GetGameConfig(game).validPlaytypes;
const playtypes = GetGameConfig(game).playtypes;
for (const playtype of playtypes) {
gpts.push({ game, playtype, query: {} });
@@ -8,7 +8,7 @@ export const ValidatePlaytypeFromParam: RequestHandler = (req, res, next) => {
const gameConfig = GetGameConfig(game);
if (!gameConfig.validPlaytypes.includes(req.params.playtype as Playtype)) {
if (!gameConfig.playtypes.includes(req.params.playtype as Playtype)) {
return res.status(400).json({
success: false,
description: `The playtype ${req.params.playtype} is not supported.`,
@@ -25,7 +25,7 @@ export const ValidatePlaytypeFromParamFor =
(req, res, next) => {
const gameConfig = GetGameConfig(game);
if (!gameConfig.validPlaytypes.includes(req.params.playtype as Playtype)) {
if (!gameConfig.playtypes.includes(req.params.playtype as Playtype)) {
return res.status(400).json({
success: false,
description: `The playtype ${req.params.playtype} is not supported.`,
@@ -100,7 +100,7 @@ export async function UpdateAllPBs(userIDs?: Array<integer>, filter = {}) {
for (const game of TachiConfig.GAMES) {
const gameConfig = GetGameConfig(game);
for (const playtype of gameConfig.validPlaytypes) {
for (const playtype of gameConfig.playtypes) {
const scores = await db.scores.find(
deepmerge({ userID: user.id, game, playtype }, filter),
{
+1 -1
View File
@@ -79,7 +79,7 @@ export function IsValidGame(str: string): str is Game {
}
export function IsValidPlaytype(game: Game, str: string): str is Playtype {
return GetGameConfig(game).validPlaytypes.includes(str as Playtype);
return GetGameConfig(game).playtypes.includes(str as Playtype);
}
export function IsValidScoreAlg(