diff --git a/common/src/config/config.ts b/common/src/config/config.ts index 1b79afdd8..d299f2cde 100644 --- a/common/src/config/config.ts +++ b/common/src/config/config.ts @@ -15,15 +15,15 @@ import { USC_CONF, USC_CONTROLLER_CONF, USC_KEYBOARD_CONF } from "./game-support import { WACCA_CONF, WACCA_SINGLE_CONF } from "./game-support/wacca"; import type { GPTString, - GPTStringToGame, - GPTStringToPlaytype, Game, GameConfig, + GamePTConfig, Playtype, Playtypes, - SpecificGamePTConfig, + ProvidedMetrics, } from "../types/game-config"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../types/internals"; +import type { ConfScoreMetric } from "../types/metrics"; /** * All game configurations that Tachi supports. @@ -85,7 +85,7 @@ export const GAME_PT_CONFIGS = { "usc:Controller": USC_CONTROLLER_CONF, "usc:Keyboard": USC_KEYBOARD_CONF, "itg:Stamina": ITG_STAMINA_CONF, -} as const satisfies Record; +} as const satisfies Record; /** * Returns the configuration for this Game + Playtype. The type here is expanded to @@ -94,20 +94,33 @@ export const GAME_PT_CONFIGS = { export function GetGamePTConfig(game: Game, playtype: Playtypes[Game]): GamePTConfig { const gptString = GetGPTString(game, playtype); - return GAME_PT_CONFIGS[gptString]; + return GAME_PT_CONFIGS[gptString] as unknown as GamePTConfig; } export function GetGPTConfig(gptString: GPTString): GamePTConfig { - return GAME_PT_CONFIGS[gptString]; + return GAME_PT_CONFIGS[gptString] as unknown as GamePTConfig; } /** * Returns the configuration for this specific Game + Playtype. This type is narrowed * down to its least generic form, and is instead for gpt-specific use cases. */ -export function GetSpecificGPTConfig(gpt: GPTString) { - return GAME_PT_CONFIGS[gpt] as unknown as SpecificGamePTConfig; +export function GetSpecificGPTConfig(gpt: GPT) { + return GAME_PT_CONFIGS[gpt]; } export const allSupportedGames = Object.keys(GAME_CONFIGS) as Array; export const allGPTStrings = Object.keys(GAME_PT_CONFIGS) as Array; + +export function GetScoreMetrics(gptConfig: GamePTConfig, type?: ConfScoreMetric["type"]) { + let metrics = [ + ...Object.entries(gptConfig.providedMetrics), + ...Object.entries(gptConfig.derivedMetrics), + ]; + + if (type) { + metrics = metrics.filter(([_key, conf]) => conf.type === type); + } + + return metrics.map((e) => e[0]); +} diff --git a/common/src/config/game-support/bms.ts b/common/src/config/game-support/bms.ts index 020b2ff4e..51129db71 100644 --- a/common/src/config/game-support/bms.ts +++ b/common/src/config/game-support/bms.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const BMS_CONF = { defaultPlaytype: "7K", @@ -262,7 +262,7 @@ export const BMS_7K_CONF = { }), supportedMatchTypes: ["bmsChartHash", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; export const BMS_14K_CONF = { ...BMS_7K_CONF, @@ -278,4 +278,4 @@ export const BMS_14K_CONF = { client: z.enum(["lr2oraja", "LR2"]).optional().nullable(), gauge: z.enum(["EASY", "NORMAL", "HARD", "EX-HARD"]).optional().nullable(), }), -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/chunithm.ts b/common/src/config/game-support/chunithm.ts index 48bd8d5fc..0a2721b3f 100644 --- a/common/src/config/game-support/chunithm.ts +++ b/common/src/config/game-support/chunithm.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const CHUNITHM_CONF = { defaultPlaytype: "Single", @@ -106,4 +106,4 @@ export const CHUNITHM_SINGLE_CONF = { scoreMeta: z.strictObject({}), supportedMatchTypes: ["inGameID", "songTitle", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/gitadora.ts b/common/src/config/game-support/gitadora.ts index 1bf8dbdc1..0be9deac0 100644 --- a/common/src/config/game-support/gitadora.ts +++ b/common/src/config/game-support/gitadora.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const GITADORA_CONF = { defaultPlaytype: "Dora", @@ -118,7 +118,7 @@ export const GITADORA_GITA_CONF = { scoreMeta: z.strictObject({}), supportedMatchTypes: ["inGameID", "songTitle", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; export const GITADORA_DORA_CONF = { ...GITADORA_GITA_CONF, @@ -134,4 +134,4 @@ export const GITADORA_DORA_CONF = { }, default: "EXTREME", }, -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/iidx.ts b/common/src/config/game-support/iidx.ts index 72a0e65de..9d7ee4827 100644 --- a/common/src/config/game-support/iidx.ts +++ b/common/src/config/game-support/iidx.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue, zodNonNegativeInt, zodTierlistData } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const IIDX_CONF = { defaultPlaytype: "SP", @@ -230,7 +230,7 @@ export const IIDX_SP_CONF = { }), supportedMatchTypes: ["inGameID", "tachiSongID", "songTitle"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; export const IIDX_DP_CONF = { ...IIDX_SP_CONF, @@ -247,4 +247,4 @@ export const IIDX_DP_CONF = { range: z.enum(["NONE", "HIDDEN+", "SUDDEN+", "LIFT", "LIFT SUD+", "SUD+ HID+"]).optional(), gauge: z.enum(["ASSISTED EASY", "EASY", "NORMAL", "HARD", "EX-HARD"]).optional(), }), -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/itg.ts b/common/src/config/game-support/itg.ts index addbc4129..04e8760e3 100644 --- a/common/src/config/game-support/itg.ts +++ b/common/src/config/game-support/itg.ts @@ -1,6 +1,6 @@ import { NoDecimalPlace, zodNonNegativeInt } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const ITG_CONF = { defaultPlaytype: "Stamina", @@ -128,4 +128,4 @@ export const ITG_STAMINA_CONF = { scoreMeta: z.strictObject({}), supportedMatchTypes: ["itgChartHash", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/jubeat.ts b/common/src/config/game-support/jubeat.ts index 18f30ee95..6dc010a08 100644 --- a/common/src/config/game-support/jubeat.ts +++ b/common/src/config/game-support/jubeat.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const JUBEAT_CONF = { defaultPlaytype: "Single", @@ -111,4 +111,4 @@ export const JUBEAT_SINGLE_CONF = { scoreMeta: z.strictObject({}), supportedMatchTypes: ["inGameID", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/maimai-dx.ts b/common/src/config/game-support/maimai-dx.ts index c03f812f1..3abdc9db1 100644 --- a/common/src/config/game-support/maimai-dx.ts +++ b/common/src/config/game-support/maimai-dx.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue, NoDecimalPlace } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const MAIMAI_DX_CONF = { defaultPlaytype: "Single", @@ -169,4 +169,4 @@ export const MAIMAI_DX_SINGLE_CONF = { scoreMeta: z.strictObject({}), supportedMatchTypes: ["songTitle", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/museca.ts b/common/src/config/game-support/museca.ts index 51a547009..13720e7a1 100644 --- a/common/src/config/game-support/museca.ts +++ b/common/src/config/game-support/museca.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { NoDecimalPlace, zodNonNegativeInt } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const MUSECA_CONF = { defaultPlaytype: "Single", @@ -87,4 +87,4 @@ export const MUSECA_SINGLE_CONF = { scoreMeta: z.strictObject({}), supportedMatchTypes: ["songTitle", "tachiSongID", "inGameID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/pms.ts b/common/src/config/game-support/pms.ts index c702ecbac..64066f752 100644 --- a/common/src/config/game-support/pms.ts +++ b/common/src/config/game-support/pms.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const PMS_CONF = { defaultPlaytype: "Controller", @@ -149,6 +149,6 @@ export const PMS_CONTROLLER_CONF = { }), supportedMatchTypes: ["bmsChartHash", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; export const PMS_KEYBOARD_CONF = PMS_CONTROLLER_CONF; diff --git a/common/src/config/game-support/popn.ts b/common/src/config/game-support/popn.ts index 4f1383c45..ea34e1d02 100644 --- a/common/src/config/game-support/popn.ts +++ b/common/src/config/game-support/popn.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue, zodNonNegativeInt } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const POPN_CONF = { defaultPlaytype: "9B", @@ -135,4 +135,4 @@ export const POPN_9B_CONF = { preferences: z.strictObject({}), supportedMatchTypes: ["inGameID", "tachiSongID", "popnChartHash"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/sdvx.ts b/common/src/config/game-support/sdvx.ts index a49e3786c..3d96e19a7 100644 --- a/common/src/config/game-support/sdvx.ts +++ b/common/src/config/game-support/sdvx.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue, ToDecimalPlaces, zodNonNegativeInt, zodTierlistData } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const SDVX_CONF = { defaultPlaytype: "Single", @@ -166,4 +166,4 @@ export const SDVX_SINGLE_CONF = { scoreMeta: z.strictObject({}), supportedMatchTypes: ["sdvxInGameID", "songTitle", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/config/game-support/usc.ts b/common/src/config/game-support/usc.ts index 7a1707333..a6964d364 100644 --- a/common/src/config/game-support/usc.ts +++ b/common/src/config/game-support/usc.ts @@ -2,7 +2,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { SDVXVFClasses } from "./sdvx"; import { ToDecimalPlaces } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const USC_CONF = { defaultPlaytype: "Controller", @@ -104,6 +104,6 @@ export const USC_CONTROLLER_CONF = { }), supportedMatchTypes: ["uscChartHash", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; export const USC_KEYBOARD_CONF = USC_CONTROLLER_CONF; diff --git a/common/src/config/game-support/wacca.ts b/common/src/config/game-support/wacca.ts index 5f9e19d16..096c9ca6e 100644 --- a/common/src/config/game-support/wacca.ts +++ b/common/src/config/game-support/wacca.ts @@ -1,7 +1,7 @@ import { FAST_SLOW_MAXCOMBO } from "./_common"; import { ClassValue } from "../config-utils"; import { z } from "zod"; -import type { INTERNAL_GAME_CONFIG, GamePTConfig } from "../../types/internals"; +import type { INTERNAL_GAME_CONFIG, INTERNAL_GAME_PT_CONFIG } from "../../types/internals"; export const WACCA_CONF = { defaultPlaytype: "Single", @@ -140,4 +140,4 @@ export const WACCA_SINGLE_CONF = { scoreMeta: z.strictObject({ mirror: z.boolean().optional() }), supportedMatchTypes: ["songTitle", "tachiSongID"], -} as const satisfies GamePTConfig; +} as const satisfies INTERNAL_GAME_PT_CONFIG; diff --git a/common/src/index.ts b/common/src/index.ts index a78661801..8fe803838 100644 --- a/common/src/index.ts +++ b/common/src/index.ts @@ -3,8 +3,6 @@ export * from "./types"; export * from "./config/config"; export * as StaticConfig from "./constants/import-types"; -export type { GamePTConfig } from "./types/internals"; - export * from "./utils/util"; export * from "./webhooks"; export * from "./constants/game"; diff --git a/common/src/lib/schemas.ts b/common/src/lib/schemas.ts index ab78accab..773a952f2 100644 --- a/common/src/lib/schemas.ts +++ b/common/src/lib/schemas.ts @@ -9,7 +9,7 @@ import { UserAuthLevels } from "../types"; import { PrudenceZodShim } from "../utils/util"; import { p } from "prudence"; import type { Game, Playtype, Playtypes } from "../types/game-config"; -import type { GamePTConfig } from "../types/internals"; +import type { INTERNAL_GAME_PT_CONFIG } from "../types/internals"; import type { ConfScoreMetric } from "../types/metrics"; import type { NotificationBody } from "../types/notifications"; import type { @@ -39,7 +39,7 @@ function prSchemaFnWrap(schema: PrudenceSchema) { }; } -const PR_GAME_STATS = (game: Game, playtype: Playtypes[Game], gptConfig: GamePTConfig) => ({ +const PR_GAME_STATS = (game: Game, playtype: Playtypes[Game], gptConfig: INTERNAL_GAME_PT_CONFIG) => ({ userID: p.isPositiveNonZeroInteger, game: p.is(game), playtype: p.is(playtype), diff --git a/common/src/types/documents.ts b/common/src/types/documents.ts index 0ff4817ed..df153054e 100644 --- a/common/src/types/documents.ts +++ b/common/src/types/documents.ts @@ -438,7 +438,10 @@ export type QuestSubscriptionDocument = { export type ScoreData = DerivedMetrics[GPT] & ProvidedMetrics[GPT] & { judgements: Partial>; - optional: Partial; + optional: Partial & { + enumIndexes: OptionalEnumIndexes; + }; + enumIndexes: ScoreEnumIndexes; }; export interface ScoreDocument { diff --git a/common/src/types/game-config.ts b/common/src/types/game-config.ts index c5dc006a8..ebdcbeb4d 100644 --- a/common/src/types/game-config.ts +++ b/common/src/types/game-config.ts @@ -1,10 +1,12 @@ import type { GAME_CONFIGS, GAME_PT_CONFIGS } from "../config/config"; +import type { integer } from "../types"; import type { MatchTypes } from "./batch-manual"; import type { ExtractClassValues, FixedDifficulties, RatingAlgorithmConfig, } from "./game-config-utils"; +import type { INTERNAL_GAME_PT_CONFIG } from "./internals"; import type { ExtractEnumMetricNames, ExtractMetrics } from "./metrics"; import type { AllFieldsNullableOptional, ExtractArrayElementType } from "./utils"; import type { AnyZodObject, z } from "zod"; @@ -178,12 +180,47 @@ export type DerivedMetrics = { [G in GPTString]: ExtractMetrics; }; +/** + * Top level metrics on a score. + */ +export type ScoreMetrics = { + [G in GPTString]: DerivedMetrics[G] & ProvidedMetrics[G]; +}; + export type OptionalMetrics = { [G in GPTString]: AllFieldsNullableOptional< ExtractMetrics >; }; +/** + * Alongside strings, we want to store integers to represent the integer value + * of enums. + */ +export type ScoreEnumIndexes = Record< + ExtractEnumMetricNames, + integer +>; + +/** + * Same as ScoreEnumIndexes but for the optional properties on a score. + */ +export type OptionalEnumIndexes = Record< + ExtractEnumMetricNames, + integer +>; + +/** + * A generic GamePTConfig. This type is significantly less specific than the + * "SpecificGamePTConfig", which only really works as a type if you know *what* + * GamePTConfig you're working with. + */ +export type GamePTConfig = INTERNAL_GAME_PT_CONFIG & { + defaultScoreRatingAlg: ScoreRatingAlgorithms[GPTString]; + defaultSessionRatingAlg: SessionRatingAlgorithms[GPTString]; + defaultProfileRatingAlg: ProfileRatingAlgorithms[GPTString]; +}; + /** * Configuration for a GPT. This declares *almost everything* about how this game is * implemented in Tachi, such as what metrics it supports, how it handles chart diff --git a/common/src/types/internals.ts b/common/src/types/internals.ts index c2b2edbbe..2a5302231 100644 --- a/common/src/types/internals.ts +++ b/common/src/types/internals.ts @@ -13,7 +13,7 @@ import type { AnyZodObject } from "zod"; * Documentation for this type can be found in `game-support.ts`, which has the * GamePTConfig type, which actually has documentation. */ -export type GamePTConfig = Readonly<{ +export type INTERNAL_GAME_PT_CONFIG = Readonly<{ providedMetrics: Record; derivedMetrics: Record; diff --git a/common/src/types/metrics.ts b/common/src/types/metrics.ts index 068e8b3f8..84f282994 100644 --- a/common/src/types/metrics.ts +++ b/common/src/types/metrics.ts @@ -76,42 +76,11 @@ export type ConfScoreMetric = | ConfIntegerScoreMetric | ConfNullableGraphScoreMetric; -/** - * When we store and interact with enum values, we want them to be both strings - * and integers. - * - * This is used by score documents that are stored in the DB. - */ -export interface EnumValue { - string: S; - index: integer; -} - /** * Given a Metric Type, turn it into its evaluated form. An IntegerScoreMetric - * becomes an integer and an enum becomes an EnumValue. + * becomes an integer and an enum becomes a string union, etc. */ export type ExtractMetricValue = M extends ConfDecimalScoreMetric - ? number - : M extends ConfIntegerScoreMetric - ? integer - : M extends ConfEnumScoreMetric - ? EnumValue - : M extends ConfGraphScoreMetric - ? Array - : M extends ConfNullableGraphScoreMetric - ? Array - : never; - -/** - * Given a Metric Type, turn it into its evaluated form with the caveat that - * enums become strings instead of EnumValue. - * - * This is used on the intermediate score importing step, where ENUM metrics aren't - * fully realised. We'd be working with { lamp: "FAILED" } instead of - * { lamp: { string: "FAILED", index: 1 }}. - */ -export type DryExtractMetricType = M extends ConfDecimalScoreMetric ? number : M extends ConfIntegerScoreMetric ? integer @@ -176,19 +145,6 @@ export type ExtractMetrics> = { [K in keyof R]: ExtractMetricValue; }; -/** - * Turn a record of score metrics in to their "dry" values. These are used by dry - * scores and consist of one change: - * - * Enums would normally be { string: "FAILED" | ..., index: number }. In dry extraction - * they are extracted to just "FAILED" | ... - * - * This is because dry scores don't fill out these values. - */ -export type DryExtractMetrics> = { - [K in keyof R]: DryExtractMetricType; -}; - // We want some signatures for implementing metric "derivers". // This complex type nonsense effectively gives us a typesafe form for: // MetricDeriver<"iidx:SP", number> @@ -203,7 +159,7 @@ export type MetricDeriver< // possible return values // from a derived fn V extends DerivedMetricValue = DerivedMetricValue -> = (mandatoryMetrics: DryExtractMetrics, chart: ChartDocument) => V; +> = (mandatoryMetrics: ExtractMetrics, chart: ChartDocument) => V; /** * A function that will derive this metric, given a function of other metrics and diff --git a/common/src/utils/util.ts b/common/src/utils/util.ts index 4093ddacc..0c876e9dc 100644 --- a/common/src/utils/util.ts +++ b/common/src/utils/util.ts @@ -200,7 +200,7 @@ export function FormatChart( // For games with 'BP', show that next to the clear. export function IIDXBMSLampGoalFormatter(pb: PBScoreDocument) { if (typeof pb.scoreData.optional.bp === "number") { - return `${pb.scoreData.lamp.string} (BP: ${pb.scoreData.optional.bp})`; + return `${pb.scoreData.lamp} (BP: ${pb.scoreData.optional.bp})`; } return pb.scoreData.lamp; diff --git a/server/src/lib/game-specific/iidx-playlists.ts b/server/src/lib/game-specific/iidx-playlists.ts index f5c3a60c2..934ab933d 100644 --- a/server/src/lib/game-specific/iidx-playlists.ts +++ b/server/src/lib/game-specific/iidx-playlists.ts @@ -68,15 +68,28 @@ function ChartsToPlaylistFormat( continue; } + let barStyle: "event" | undefined; + + // highlight individual difference charts as "event". + if ( + chart.playtype === "SP" && + (chart as ChartDocument<"iidx:SP">).data.hcTier?.individualDifference + ) { + barStyle = "event"; + } else if ( + chart.playtype === "DP" && + (chart as ChartDocument<"iidx:DP">).data.dpTier?.individualDifference + ) { + barStyle = "event"; + } + arr.push({ entry_id: inGameID, // @ts-expect-error yeah, i know. it'll be fine. difficulty: chart.difficulty.toLowerCase(), - // highlight individual difference charts as "event". - bar_style: - chart.tierlistInfo["kt-HC"]?.individualDifference === true ? "event" : undefined, + bar_style: barStyle, }); } diff --git a/server/src/lib/migration/migrations.ts b/server/src/lib/migration/migrations.ts index 0c9053742..5d55fc22f 100644 --- a/server/src/lib/migration/migrations.ts +++ b/server/src/lib/migration/migrations.ts @@ -4,12 +4,10 @@ import UGPTRivalsMigration from "./migrations/add-rivals-to-ugpt"; import FixUndefinedBMSData from "./migrations/fix-undefined-bms-data"; import JoinINFCastHourCharts from "./migrations/join-inf-casthour-charts"; import JoinINFCastHourSongs from "./migrations/join-inf-casthour-songs"; -import JubeatMusicRate1DP from "./migrations/jubeat-musicrate-1dp"; import KtRatingToCuratorSkill from "./migrations/ktRating-to-curator-skill"; import MarkFervidexAsWithoutIntent from "./migrations/mark-fervidex-as-without-intent"; import MarkKsHookAsWithoutIntent from "./migrations/mark-kshook-as-without-intent"; import NullLR2HookFailedBPs from "./migrations/null-lr2hook-failed-bps"; -import RecalcBrokenIIDXNotecounts from "./migrations/recalc-broken-iidx-notecounts"; import RemoveIIDXBeginners from "./migrations/remove-iidx-beginners"; import RemoveMultifolderStats from "./migrations/remove-multifolder-stats"; import RemoveRandom from "./migrations/remove-random"; @@ -54,14 +52,12 @@ if (Environment.nodeEnv !== "test") { // kamaitachi specific migrations if (TachiConfig.TYPE !== "btchi") { REGISTERED_MIGRATIONS.push( - RecalcBrokenIIDXNotecounts, RemoveIIDXBeginners, UpdateJubeatPreferredTables, JoinINFCastHourCharts, JoinINFCastHourSongs, MarkFervidexAsWithoutIntent, MarkKsHookAsWithoutIntent, - JubeatMusicRate1DP, KtRatingToCuratorSkill ); } diff --git a/server/src/lib/migration/migrations/jubeat-musicrate-1dp.ts b/server/src/lib/migration/migrations/jubeat-musicrate-1dp.ts deleted file mode 100644 index 891e7f8a0..000000000 --- a/server/src/lib/migration/migrations/jubeat-musicrate-1dp.ts +++ /dev/null @@ -1,38 +0,0 @@ -import db from "external/mongo/db"; -import UpdateScore from "lib/score-mutation/update-score"; -import { EfficientDBIterate } from "utils/efficient-db-iterate"; -import { FloorToNDP } from "utils/misc"; -import type { Migration } from "utils/types"; - -const migration: Migration = { - id: "jubeat-musicrate-1dp", - up: async () => { - await EfficientDBIterate( - db.scores, - async (oldScore) => { - const newScore = { - ...oldScore, - scoreData: { - ...oldScore.scoreData, - percent: FloorToNDP(oldScore.scoreData.percent, 1), - }, - }; - - // migrate the score in the callback function. - await UpdateScore(oldScore, newScore); - }, - - // we don't need to save this back in the database, UpdateScore - // already handles that. - // eslint-disable-next-line @typescript-eslint/require-await - async () => void 0, - { game: "jubeat" }, - 1000 - ); - }, - down: () => { - throw new Error(`Cannot undo migration.`); - }, -}; - -export default migration; diff --git a/server/src/lib/score-import/framework/calculated-data/types.ts b/server/src/lib/score-import/framework/calculated-data/types.ts index 0833f8518..d2f0d0906 100644 --- a/server/src/lib/score-import/framework/calculated-data/types.ts +++ b/server/src/lib/score-import/framework/calculated-data/types.ts @@ -10,7 +10,7 @@ import type { ScoreDocument, ScoreRatingAlgorithms, SessionRatingAlgorithms, - UserGameStats, + SpecificUserGameStats, integer, } from "tachi-common"; import type { DerivedClassConfig, ProvidedClassConfig } from "tachi-common/types/game-config-utils"; @@ -46,7 +46,7 @@ export type GPTProfileCalculators = { }; export type ClassDeriver = ( - profileRatings: UserGameStats["ratings"] + profileRatings: SpecificUserGameStats["ratings"] ) => V | null; // absolutely stupid magic. diff --git a/server/src/lib/score-import/framework/common/types.ts b/server/src/lib/score-import/framework/common/types.ts index 098163f28..e23df875f 100644 --- a/server/src/lib/score-import/framework/common/types.ts +++ b/server/src/lib/score-import/framework/common/types.ts @@ -7,22 +7,14 @@ import type { OptionalMetrics, ConfProvidedMetrics, } from "tachi-common"; -import type { DryExtractMetrics, EnumValue } from "tachi-common/types/metrics"; +import type { ExtractMetrics } from "tachi-common/types/metrics"; import type { Mutable } from "utils/types"; /** - * For dry scores, we don't want to make converters fill out enum values as - * - * grade: { string: "A", index: 5 } - * - * The index part can *always* be derived. This function turns + * ScoreData, but it's just the provided metrics (and enumIndexes don't exist). */ -export type ExtractEnumValues> = { - [K in keyof TMetrics]: TMetrics[K] extends EnumValue ? V : TMetrics[K]; -}; - -export type DryScoreData = DryExtractMetrics & { - optional: Mutable>; +export type DryScoreData = ExtractMetrics & { + optional: Mutable; judgements: Partial>; }; diff --git a/server/src/lib/score-import/framework/derivers/derivers.ts b/server/src/lib/score-import/framework/derivers/derivers.ts index be835fccc..daff081ac 100644 --- a/server/src/lib/score-import/framework/derivers/derivers.ts +++ b/server/src/lib/score-import/framework/derivers/derivers.ts @@ -15,6 +15,7 @@ import { CHUNITHM_GBOUNDARIES, GITADORA_GBOUNDARIES, GetGPTConfig, + GetScoreMetrics, ITG_GBOUNDARIES, JUBEAT_GBOUNDARIES, MAIMAIDX_GBOUNDARIES, @@ -24,7 +25,8 @@ import { } from "tachi-common"; import type { DryScore, DryScoreData } from "../common/types"; import type { GPTDerivers } from "./types"; -import type { ChartDocument, DerivedMetrics, GPTString, ScoreData } from "tachi-common"; +import type { KtLogger } from "lib/logger/logger"; +import type { ChartDocument, DerivedMetrics, GPTString, ScoreData, integer } from "tachi-common"; import type { MetricValue } from "tachi-common/types/metrics"; type AllGPTDerivers = { @@ -133,34 +135,82 @@ function DeriveMetrics( const value = fn(metrics, chart); - if (metricConfig.type === "ENUM") { - const index = metricConfig.values.indexOf(value); - - if (index === -1) { - throw new InternalFailure( - `Tried to turn a ${key} enum value of ${value} into an index, got -1?` - ); - } - - derivedMetrics[key] = { - string: value, - index, - }; - } else { - derivedMetrics[key] = value; - } + derivedMetrics[key] = value; } return derivedMetrics as DerivedMetrics[GPT]; } +export function CreateEnumIndexes(gpt: GPT, metrics: any, logger: KtLogger) { + const gptConfig = GetGPTConfig(gpt); + + const indexes: Record = {}; + const optionalIndexes: Record = {}; + + for (const [key, conf] of [ + ...Object.entries(gptConfig.providedMetrics), + ...Object.entries(gptConfig.derivedMetrics), + ]) { + if (conf.type !== "ENUM") { + continue; + } + + const index = conf.values.indexOf(metrics[key]); + + if (index === -1) { + logger.error( + `Got an invalid enum value of ${metrics[key]} for ${gpt} ${key} on DryScore. Can't add indexes?`, + { metrics, key, conf } + ); + + throw new InternalFailure( + `Got an invalid enum value of ${metrics[key]} for ${gpt} ${key} on DryScore. Can't add indexes?` + ); + } + + indexes[key] = index; + } + + for (const [key, conf] of [ + ...Object.entries(gptConfig.providedMetrics), + ...Object.entries(gptConfig.derivedMetrics), + ]) { + if (conf.type !== "ENUM") { + continue; + } + + // skip undefined metrics + if (!metrics.optional[key]) { + continue; + } + + const index = conf.values.indexOf(metrics.optional[key]); + + if (index === -1) { + logger.error( + `Got an invalid enum value of ${metrics.optional[key]} for ${gpt} optional.${key} on DryScore. Can't add indexes?`, + { metrics, key, conf } + ); + + throw new InternalFailure( + `Got an invalid enum value of ${metrics.optional[key]} for ${gpt} optional.${key} on DryScore. Can't add indexes?` + ); + } + + indexes[key] = index; + } + + return { indexes, optionalIndexes }; +} + /** * Return a full piece of scoreData. */ export function CreateFullScoreData( gpt: GPT, dryScoreData: DryScore["scoreData"], - chart: ChartDocument + chart: ChartDocument, + logger: KtLogger ) { const derivedMetrics = DeriveMetrics(gpt, dryScoreData, chart); @@ -170,5 +220,12 @@ export function CreateFullScoreData( } as unknown as ScoreData; // ^ hacky force-cast because these types are *really* unstable. + const { indexes, optionalIndexes } = CreateEnumIndexes(gpt, scoreData, logger); + + // again, silly hacks aorund typesafety here because to be honest + // this stuff is more generic than TS really should ever have to implement. + scoreData.enumIndexes = indexes; + scoreData.optional.enumIndexes = optionalIndexes; + return scoreData; } diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts index 0b97c7535..65fcebb4f 100644 --- a/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts +++ b/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts @@ -40,8 +40,8 @@ t.test("#CreatePBDoc", (t) => { score: IIDXScore.scoreData.score, percent: IIDXScore.scoreData.percent, esd: IIDXScore.scoreData.esd, - grade: IIDXScore.scoreData.grade.string, - gradeIndex: IIDXScore.scoreData.grade.string, + grade: IIDXScore.scoreData.grade, + gradeIndex: IIDXScore.scoreData.grade, judgements: IIDXScore.scoreData.judgements, lamp: "FULL COMBO", lampIndex: lamps.indexOf("FULL COMBO"), diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.ts index 289f2f915..3e8d09fb0 100644 --- a/server/src/lib/score-import/framework/pb/create-pb-doc.ts +++ b/server/src/lib/score-import/framework/pb/create-pb-doc.ts @@ -183,8 +183,8 @@ async function MergeScoreLampIntoPB( score: scorePB.scoreData.score, percent: scorePB.scoreData.percent, esd: scorePB.scoreData.esd, - grade: scorePB.scoreData.grade.string, - gradeIndex: scorePB.scoreData.grade.string, + grade: scorePB.scoreData.grade, + gradeIndex: scorePB.scoreData.grade, lamp: lampPB.scoreData.lamp, lampIndex: lampPB.scoreData.lamp.index, judgements: scorePB.scoreData.judgements, diff --git a/server/src/lib/score-import/framework/score-importing/hydrate-score.ts b/server/src/lib/score-import/framework/score-importing/hydrate-score.ts index f1a6abe66..ce45eb1b0 100644 --- a/server/src/lib/score-import/framework/score-importing/hydrate-score.ts +++ b/server/src/lib/score-import/framework/score-importing/hydrate-score.ts @@ -1,28 +1,28 @@ -import { CreateCalculatedData } from "../score-calculated-data/calculated-data"; +import { CreateScoreCalcData } from "../calculated-data/score"; import { CreateFullScoreData } from "../derivers/derivers"; import { GetGPTString } from "tachi-common"; import type { DryScore } from "../common/types"; import type { KtLogger } from "lib/logger/logger"; -import type { ChartDocument, integer, ScoreDocument, SongDocument } from "tachi-common"; +import type { ChartDocument, ScoreDocument, SongDocument, integer } from "tachi-common"; /** * Takes an "intermediate" score and appends the rest of the data it needs. * @param dryScore The intermediate score to make into a real score. * @param userID The userID this score is for. */ -export async function HydrateScore( +export function HydrateScore( userID: integer, dryScore: DryScore, chart: ChartDocument, song: SongDocument, scoreID: string, logger: KtLogger -): Promise { +): ScoreDocument { const gpt = GetGPTString(dryScore.game, chart.playtype); - const scoreData = CreateFullScoreData(gpt, dryScore.scoreData, chart); + const scoreData = CreateFullScoreData(gpt, dryScore.scoreData, chart, logger); - const calculatedData = await CreateCalculatedData(dryScore, chart, logger); + const calculatedData = CreateScoreCalcData(dryScore, chart); const score: ScoreDocument = { ...dryScore, diff --git a/server/src/lib/score-import/framework/score-importing/score-id.test.ts b/server/src/lib/score-import/framework/score-importing/score-id.test.ts index 57cf9845d..306c9cef8 100644 --- a/server/src/lib/score-import/framework/score-importing/score-id.test.ts +++ b/server/src/lib/score-import/framework/score-importing/score-id.test.ts @@ -44,7 +44,7 @@ t.test("#CreateScoreID", (t) => { scoreData: { percent: TestingIIDXSPDryScore.scoreData.percent, score: TestingIIDXSPDryScore.scoreData.score, - grade: TestingIIDXSPDryScore.scoreData.grade.string, + grade: TestingIIDXSPDryScore.scoreData.grade, lamp: TestingIIDXSPDryScore.scoreData.lamp, }, }; @@ -82,7 +82,7 @@ t.test("#CreateScoreID", (t) => { ); minimalScore.scoreData.lamp = TestingIIDXSPDryScore.scoreData.lamp; - minimalScore.scoreData.grade.string = "F"; + minimalScore.scoreData.grade = "F"; t.not( scoreID, diff --git a/server/src/lib/score-import/framework/score-importing/score-id.ts b/server/src/lib/score-import/framework/score-importing/score-id.ts index 70266888f..2840b3bcc 100644 --- a/server/src/lib/score-import/framework/score-importing/score-id.ts +++ b/server/src/lib/score-import/framework/score-importing/score-id.ts @@ -1,7 +1,8 @@ import db from "external/mongo/db"; +import { GetGPTConfig } from "tachi-common"; import crypto from "crypto"; import type { DryScore } from "../common/types"; -import type { integer } from "tachi-common"; +import type { integer, GPTString, ProvidedMetrics } from "tachi-common"; /** * Performs sha256 hashing on the input data. @@ -17,12 +18,25 @@ function HashScoreIDString(scoreIDString: string) { * This is used to deduplicate repeated scores. * @returns @see HashScoreIDString - prefixed with R. */ -export function CreateScoreID(userID: integer, dryScore: DryScore, chartID: string) { - const hash = HashScoreIDString( - `${userID}|${chartID}|${dryScore.scoreData.lamp}|${dryScore.scoreData.grade.string}|${dryScore.scoreData.score}|${dryScore.scoreData.percent}` - ); +export function CreateScoreID( + gptString: GPTString, + userID: integer, + dryScore: DryScore, + chartID: string +) { + const elements = [userID, chartID]; - return `R${hash}`; + const gptConfig = GetGPTConfig(gptString); + + for (const m of Object.keys(gptConfig.providedMetrics)) { + const metric = m as keyof ProvidedMetrics[GPTString]; + + elements.push(dryScore.scoreData[metric]); + } + + const hash = HashScoreIDString(elements.join("\0")); + + return `T${hash}`; } export function GetWithScoreID(scoreID: string) { diff --git a/server/src/lib/score-import/framework/score-importing/score-importing.ts b/server/src/lib/score-import/framework/score-importing/score-importing.ts index 755c25734..54a43aa7b 100644 --- a/server/src/lib/score-import/framework/score-importing/score-importing.ts +++ b/server/src/lib/score-import/framework/score-importing/score-importing.ts @@ -5,6 +5,7 @@ import { IsConverterFailure } from "../common/converter-failures"; import { OrphanScore } from "../orphans/orphans"; import db from "external/mongo/db"; import { AppendLogCtx } from "lib/logger/logger"; +import { GetGPTString } from "tachi-common"; import { ClassToObject } from "utils/misc"; import type { ConverterFnSuccessReturn, ConverterFunction } from "../../import-types/common/types"; import type { ConverterFailure, SongOrChartNotFoundFailure } from "../common/converter-failures"; @@ -16,9 +17,9 @@ import type { Game, ImportProcessingInfo, ImportTypes, - integer, ScoreDocument, SongDocument, + integer, } from "tachi-common"; /** @@ -296,7 +297,9 @@ async function HydrateAndInsertScore( importLogger: KtLogger, force = false ): Promise { - const scoreID = CreateScoreID(userID, dryScore, chart.chartID); + const gptString = GetGPTString(dryScore.game, chart.playtype); + + const scoreID = CreateScoreID(gptString, userID, dryScore, chart.chartID); // sub-context the logger so the below logs are more accurate const logger = AppendLogCtx(scoreID, importLogger); @@ -331,7 +334,7 @@ async function HydrateAndInsertScore( return null; } - const score = await HydrateScore(userID, dryScore, chart, song, scoreID, logger); + const score = HydrateScore(userID, dryScore, chart, song, scoreID, logger); let res; diff --git a/server/src/lib/score-import/framework/sessions/sessions.ts b/server/src/lib/score-import/framework/sessions/sessions.ts index a41cde86d..f151eddbb 100644 --- a/server/src/lib/score-import/framework/sessions/sessions.ts +++ b/server/src/lib/score-import/framework/sessions/sessions.ts @@ -66,7 +66,7 @@ function ScoreToSessionScoreInfo( return { scoreID: score.scoreID, isNewScore: false, - gradeDelta: score.scoreData.grade.string - previousPB.scoreData.grade.string, + gradeDelta: score.scoreData.grade - previousPB.scoreData.grade, lampDelta: score.scoreData.lamp.index - previousPB.scoreData.lamp.index, percentDelta: score.scoreData.percent - previousPB.scoreData.percent, scoreDelta: score.scoreData.score - previousPB.scoreData.score, diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts index f7f55b8c5..b3775d5de 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts @@ -3,7 +3,7 @@ import { IIDX_DANS } from "tachi-common"; import nodeFetch from "utils/fetch"; import { IsRecord } from "utils/misc"; import type { KaiAPIReauthFunction } from "../traverse-api"; -import type { ClassProvider } from "lib/score-import/framework/profile-calculated-data/types"; +import type { ClassProvider } from "lib/score-import/framework/calculated-data/types"; export async function CreateKaiIIDXClassProvider( kaiType: "EAG" | "FLO", @@ -48,7 +48,7 @@ export async function CreateKaiIIDXClassProvider( err = e; } - return (game, playtype, userID, ratings, logger) => { + return (gptString, userID, ratings, logger) => { if (err !== undefined) { logger.error(`An error occured while updating classes for ${baseUrl}.`, { err }); return {}; @@ -63,12 +63,12 @@ export async function CreateKaiIIDXClassProvider( let maybeIIDXDan: unknown; - if (playtype === "SP") { + if (gptString === "iidx:SP") { maybeIIDXDan = json.sp; - } else if (playtype === "DP") { + } else if (gptString === "iidx:DP") { maybeIIDXDan = json.dp; } else { - logger.warn(`KAIIIDXClassUpdater called with invalid playtype of ${playtype}.`); + logger.warn(`KAIIIDXClassUpdater called with invalid gptString of ${gptString}.`); return {}; } @@ -77,7 +77,7 @@ export async function CreateKaiIIDXClassProvider( maybeIIDXDan === undefined || typeof maybeIIDXDan !== "number" ) { - logger.info(`User has no ${playtype} dan. Not updating anything.`); + logger.info(`User has no ${gptString} dan. Not updating anything.`); return {}; } diff --git a/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts b/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts index cc4ecdc8b..069fd1e19 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts @@ -1,14 +1,15 @@ import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; -import { GetGamePTConfig } from "tachi-common"; -import type { ClassProvider } from "lib/score-import/framework/profile-calculated-data/types"; +import { GetGPTConfig, GetGamePTConfig } from "tachi-common"; +import { ClassToIndex } from "utils/class"; +import type { ClassProvider } from "lib/score-import/framework/calculated-data/types"; import type { GamePTConfig, GPTString, ClassRecords, Classes } from "tachi-common"; // Note: This is tested by batch-manuals parser.test.ts. export function CreateBatchManualClassProvider( classes: Partial> ): ClassProvider { - return (game, playtype, userID, ratings, logger) => { - const gptConfig = GetGamePTConfig(game, playtype); + return (gptString, userID, ratings, logger) => { + const gptConfig = GetGPTConfig(gptString); const newObj: Partial> = {}; @@ -19,7 +20,7 @@ export function CreateBatchManualClassProvider( const set = s as Classes[GPTString]; - const index = ClassIDToIndex(gptConfig, set, classID); + const index = ClassToIndex(gptString, set, classID); if (index === null) { logger.warn( @@ -42,21 +43,3 @@ export function CreateBatchManualClassProvider( return newObj; }; } - -/** - * Given a gpt classes ID, return its index value. - * - * Returns null if the classID doesn't exist. - * @returns - */ -function ClassIDToIndex(gptConfig: GamePTConfig, classSet: Classes[GPTString], classID: string) { - const classes = gptConfig.classes[classSet]!; - - const index = classes.values.map((e) => e.id).indexOf(classID); - - if (index === -1) { - return null; - } - - return index; -} diff --git a/server/src/lib/score-mutation/update-score.ts b/server/src/lib/score-mutation/update-score.ts index f79dea59c..eefc49c02 100644 --- a/server/src/lib/score-mutation/update-score.ts +++ b/server/src/lib/score-mutation/update-score.ts @@ -1,10 +1,10 @@ /* eslint-disable no-await-in-loop */ import db from "external/mongo/db"; import { rootLogger } from "lib/logger/logger"; -import { CreateCalculatedData } from "lib/score-import/framework/score-calculated-data/calculated-data"; +import { CreateScoreCalcData } from "lib/score-import/framework/calculated-data/score"; import { UpdateChartRanking } from "lib/score-import/framework/pb/create-pb-doc"; import { CreateScoreID } from "lib/score-import/framework/score-importing/score-id"; -import { CreateSessionCalcData } from "lib/score-import/framework/sessions/calculated-data"; +import { GetGPTString } from "tachi-common"; import { UpdateAllPBs } from "utils/calculations/recalc-scores"; import { FormatUserDoc, GetUserWithID } from "utils/user"; import type { KtLogger } from "lib/logger/logger"; @@ -54,7 +54,12 @@ export default async function UpdateScore( const oldScoreID = oldScore.scoreID; - const newScoreID = CreateScoreID(newScore.userID, newScore, newScore.chartID); + const newScoreID = CreateScoreID( + GetGPTString(newScore.game, newScore.playtype), + newScore.userID, + newScore, + newScore.chartID + ); // We need to change *so* many references to score IDs, and recalculate *so* // much stored state. Obviously, changing a scoreID is an exceptional circumstance @@ -70,22 +75,18 @@ export default async function UpdateScore( logger.verbose("Received Update Score request."); // eslint-disable-next-line require-atomic-updates - newScore.calculatedData = await CreateCalculatedData( - newScore, - chart, - newScore.scoreData.esd, - logger - ); + newScore.calculatedData = CreateScoreCalcData(newScore, chart); try { // Having _id defined will cause this to throw, causing it to not apply // the update. + // @ts-expect-error this shouldn't happen according to types. if (newScore._id) { logger.warn( `Passed a score with _id to UpdateScore. This property should not be set. Deleting this property and continuing anyway.` ); - // This property shouldn't be defined. + // @ts-expect-error this shouldn't happen according to types. delete newScore._id; } diff --git a/server/src/lib/targets/goals.ts b/server/src/lib/targets/goals.ts index c7f20426a..d35b6db69 100644 --- a/server/src/lib/targets/goals.ts +++ b/server/src/lib/targets/goals.ts @@ -248,7 +248,7 @@ export function HumaniseGoalProgress( playtype, userPB.scoreData.score, userPB.scoreData.percent, - userPB.scoreData.grade.string, + userPB.scoreData.grade, fmtFn ); diff --git a/server/src/server/router/api/v1/sessions/_sessionID/router.ts b/server/src/server/router/api/v1/sessions/_sessionID/router.ts index 3163fac02..e05bb5e1f 100644 --- a/server/src/server/router/api/v1/sessions/_sessionID/router.ts +++ b/server/src/server/router/api/v1/sessions/_sessionID/router.ts @@ -159,14 +159,14 @@ router.get("/folder-raises", async (req, res) => { // new scores may also be grade raises for (const score of [...gradeRaises, ...newRaises]) { // skip things that aren't good enough - if (score.scoreData.grade.string < clearGradeIndex) { + if (score.scoreData.grade < clearGradeIndex) { continue; } const exists = gradeRaiseMap.get(score.chartID); if (exists) { - if (exists.scoreData.grade.string < score.scoreData.grade.string) { + if (exists.scoreData.grade < score.scoreData.grade) { // this one is more important gradeRaiseMap.set(score.chartID, score); } @@ -257,7 +257,7 @@ router.get("/folder-raises", async (req, res) => { // and EX HARD CLEAR, so // we want ["HARD CLEAR", "EX HARD CLEAR"]. const originalGradeIndex = - gradeRaise.scoreData.grade.string - + gradeRaise.scoreData.grade - (gradeDeltas[gradeRaise.scoreID] ?? Infinity) + 1; @@ -267,7 +267,7 @@ router.get("/folder-raises", async (req, res) => { for (const grade of gptConfig.grades.slice( minimumGrade, - gradeRaise.scoreData.grade.string + 1 + gradeRaise.scoreData.grade + 1 )) { AddToSetInRecord(grade, raiseGradeDist, gradeRaise.chartID); } diff --git a/server/src/server/router/api/v1/users/_userID/games/@gameSpecificRoutes/iidx/router.ts b/server/src/server/router/api/v1/users/_userID/games/@gameSpecificRoutes/iidx/router.ts index b9a007b28..31e69167b 100644 --- a/server/src/server/router/api/v1/users/_userID/games/@gameSpecificRoutes/iidx/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/@gameSpecificRoutes/iidx/router.ts @@ -143,8 +143,8 @@ router.get( pb.scoreData.judgements.pgreat?.toString() ?? "0", // pgreat pb.scoreData.judgements.great?.toString() ?? "0", // great pb.scoreData.optional.bp?.toString() ?? "0", // BP - ConvertEamLamp(pb.scoreData.lamp.string), // lamp - ConvertEamGrade(pb.scoreData.grade.string) // grade + ConvertEamLamp(pb.scoreData.lamp), // lamp + ConvertEamGrade(pb.scoreData.grade) // grade ); if (pb.timeAchieved !== null && lastPlayed < pb.timeAchieved) { diff --git a/server/src/server/router/ir/usc/_playtype/usc.ts b/server/src/server/router/ir/usc/_playtype/usc.ts index 82bb3a8f4..69956728a 100644 --- a/server/src/server/router/ir/usc/_playtype/usc.ts +++ b/server/src/server/router/ir/usc/_playtype/usc.ts @@ -80,7 +80,7 @@ export async function TachiScoreToServerScore( near: tachiScore.scoreData.judgements.near ?? 0, error: tachiScore.scoreData.judgements.miss ?? 0, ranking: tachiScore.rankingData.rank, - lamp: TACHI_LAMP_TO_USC[tachiScore.scoreData.lamp.string], + lamp: TACHI_LAMP_TO_USC[tachiScore.scoreData.lamp], username: userDoc.username, noteMod: scorePB.scoreMeta.noteMod ?? "NORMAL", gaugeMod: scorePB.scoreMeta.gaugeMod ?? "NORMAL", diff --git a/server/src/utils/class.ts b/server/src/utils/class.ts index d9026b43d..8db9d4ebd 100644 --- a/server/src/utils/class.ts +++ b/server/src/utils/class.ts @@ -17,7 +17,7 @@ export function ReturnClassIfGreater( classSet: Classes[GPTString], classVal: string, userGameStats?: UserGameStats | null -) { +): boolean | null { const gptConfig = GetGPTConfig(gptString); const classInfo = gptConfig.classes[classSet]; @@ -43,6 +43,14 @@ export function ReturnClassIfGreater( const previousClassIndex = ClassToIndex(gptString, classSet, prevClass); const newClassIndex = ClassToIndex(gptString, classSet, classVal); + if (previousClassIndex === null && newClassIndex === null) { + return null; + } else if (newClassIndex === null) { + return null; + } else if (previousClassIndex === null) { + return true; + } + return newClassIndex > previousClassIndex; } @@ -53,18 +61,18 @@ export function ClassToIndex(gptString: GPTString, classSet: Classes[GPTString], if (!classInfo) { logger.warn( - `Invalid ClassToIndex call. Attempted to index set '${classSet}' on ${gptString}. No such class is defined for this game. Returning -1.` + `Invalid ClassToIndex call. Attempted to index set '${classSet}' on ${gptString}. No such class is defined for this game. Returning null.` ); - - return -1; + return null; } const v = classInfo.values.map((e) => e.id).indexOf(classVal); if (v === -1) { logger.warn( - `Attempted to index a class that doesn't exist: ${classVal} on ${classSet} (${gptString}). Returning -1.` + `Attempted to index a class that doesn't exist: ${classVal} on ${classSet} (${gptString}). Returning null.` ); + return null; } return v; diff --git a/server/src/utils/folder.ts b/server/src/utils/folder.ts index 4e6eed3b7..96102f045 100644 --- a/server/src/utils/folder.ts +++ b/server/src/utils/folder.ts @@ -11,10 +11,8 @@ import type { FolderChartLookup, FolderDocument, Game, - Grades, GPTString, integer, - Lamps, PBScoreDocument, Playtype, SongDocument, @@ -324,10 +322,10 @@ export function CalculateGradeDistribution(pbs: Array) { const gradeDist: Partial> = {}; for (const pb of pbs) { - if (gradeDist[pb.scoreData.grade.string] !== undefined) { - gradeDist[pb.scoreData.grade.string]!++; + if (gradeDist[pb.scoreData.grade] !== undefined) { + gradeDist[pb.scoreData.grade]!++; } else { - gradeDist[pb.scoreData.grade.string] = 1; + gradeDist[pb.scoreData.grade] = 1; } } diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts index 4625f492c..06cad2e99 100644 --- a/server/src/utils/user.ts +++ b/server/src/utils/user.ts @@ -1,7 +1,6 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { GetGamePTConfig, UserAuthLevels } from "tachi-common"; -import type { ProfileRatingAlgs } from "./string-checks"; import type { FindOneResult } from "monk"; import type { APITokenDocument, @@ -10,8 +9,8 @@ import type { integer, Playtype, UserDocument, - ProfileRatingLookup, UserGameStats, + ProfileRatingAlgorithms, } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -179,20 +178,23 @@ export async function GetAllRankings(stats: UserGameStats) { const gptConfig = GetGamePTConfig(stats.game, stats.playtype); const entries = await Promise.all( - gptConfig.profileRatingAlgs.map((k) => - GetUsersRankingAndOutOf(stats, k).then((r) => [k, r]) + Object.keys(gptConfig.profileRatingAlgs).map((k) => + GetUsersRankingAndOutOf(stats, k as ProfileRatingAlgorithms[GPTString]).then((r) => [ + k, + r, + ]) ) ); return Object.fromEntries(entries) as Record< - ProfileRatingAlgs, + ProfileRatingAlgorithms[GPTString], { ranking: integer; outOf: integer } >; } export async function GetUsersRankingAndOutOf( stats: UserGameStats, - alg?: ProfileRatingLookup[GPTString] + alg?: ProfileRatingAlgorithms[GPTString] ) { const gptConfig = GetGamePTConfig(stats.game, stats.playtype);