diff --git a/common/src/config/config.ts b/common/src/config/config.ts index ea9fd22ff..cad348cef 100644 --- a/common/src/config/config.ts +++ b/common/src/config/config.ts @@ -97,6 +97,10 @@ export function GetGamePTConfig(game: Game, playtype: Playtypes[Game]): GamePTCo return GAME_PT_CONFIGS[gptString]; } +export function GetGPTConfig(gptString: GPTString): GamePTConfig { + return GAME_PT_CONFIGS[gptString]; +} + /** * 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. diff --git a/common/src/types/batch-manual.ts b/common/src/types/batch-manual.ts index 818bebdca..1e82ab9d1 100644 --- a/common/src/types/batch-manual.ts +++ b/common/src/types/batch-manual.ts @@ -1,6 +1,6 @@ import type { integer } from "../types"; import type { - AdditionalMetrics, + OptionalMetrics, Versions, Difficulties, ExtractedClasses, @@ -28,7 +28,7 @@ export type BatchManualScore = ExtractMetrics comment?: string | null; judgements?: Record; timeAchieved?: number | null; - additionalMetrics?: Partial>; + additionalMetrics?: Partial>; scoreMeta?: Partial; } & ( | { diff --git a/common/src/types/documents.ts b/common/src/types/documents.ts index 8dab34819..8c0b9299b 100644 --- a/common/src/types/documents.ts +++ b/common/src/types/documents.ts @@ -3,7 +3,7 @@ import type { Versions, Classes, Difficulties, - ExtractedAdditionalMetrics, + ExtractedOptionalMetrics as ExtractedOptionalMetrics, ExtractedScoreMetrics, GPTString, GPTStringToGame, @@ -415,7 +415,7 @@ export type QuestSubscriptionDocument = { export type ScoreData = ExtractedScoreMetrics[GPT] & { judgements: Partial>; - additionalMetrics: Partial; + optional: Partial; }; export interface ScoreDocument { diff --git a/common/src/types/game-config.ts b/common/src/types/game-config.ts index 29df58bfd..e76e94caf 100644 --- a/common/src/types/game-config.ts +++ b/common/src/types/game-config.ts @@ -134,7 +134,7 @@ export type DerivedMetrics = { [G in GPTString]: typeof GAME_PT_CONFIGS[G]["derivedMetrics"]; }; -export type AdditionalMetrics = { +export type OptionalMetrics = { [G in GPTString]: typeof GAME_PT_CONFIGS[G]["optionalMetrics"]; }; @@ -167,7 +167,7 @@ export type ExtractedScoreMetrics = { ExtractMetrics; }; -export type ExtractedAdditionalMetrics = { +export type ExtractedOptionalMetrics = { [G in GPTString]: ExtractMetrics; }; @@ -239,7 +239,7 @@ export interface SpecificGamePTConfig { * The idea of additionalMetrics allow us to store useful metrics about scores * without necessitating that they exist on arrival. Incredibly convenient. */ - additionalMetrics: AdditionalMetrics[GPT]; + additionalMetrics: OptionalMetrics[GPT]; /** * What rating algorithms may a score have attached onto it for this GPT? diff --git a/common/src/types/metrics.ts b/common/src/types/metrics.ts index 49871bc23..c5900582d 100644 --- a/common/src/types/metrics.ts +++ b/common/src/types/metrics.ts @@ -1,6 +1,6 @@ import type { integer } from "../types"; import type { ChartDocument } from "./documents"; -import type { AdditionalMetrics, DerivedMetrics, GPTString, ProvidedMetrics } from "./game-config"; +import type { OptionalMetrics, DerivedMetrics, GPTString, ProvidedMetrics } from "./game-config"; export type DecimalMetricValidator = ( metric: number, @@ -101,10 +101,10 @@ export type ExtractEnumMetricNames> = { export type ExtractEnumValues< GPT extends GPTString, MetricName extends ExtractEnumMetricNames< - AdditionalMetrics[GPT] & DerivedMetrics[GPT] & ProvidedMetrics[GPT] + DerivedMetrics[GPT] & OptionalMetrics[GPT] & ProvidedMetrics[GPT] > -> = (AdditionalMetrics[GPT] & - DerivedMetrics[GPT] & +> = (DerivedMetrics[GPT] & + OptionalMetrics[GPT] & ProvidedMetrics[GPT])[MetricName] extends EnumScoreMetric ? EnumValues : never; @@ -122,12 +122,14 @@ export type ExtractMetrics> = { [K in keyof R]: ExtractMetricType; }; +export type DerivedMetricValue = Array | integer | number | string; + export type MetricDeriver< M extends Record, GPT extends GPTString, // possible return values // from a derived fn - V extends Array | integer | number | string + V extends DerivedMetricValue = DerivedMetricValue > = (mandatoryMetrics: M, chart: ChartDocument) => V; /** diff --git a/common/src/utils/util.ts b/common/src/utils/util.ts index d0e4d9afa..8fa0998cd 100644 --- a/common/src/utils/util.ts +++ b/common/src/utils/util.ts @@ -195,8 +195,8 @@ export function FormatChart( // For games with 'BP', show that next to the clear. export function IIDXBMSLampGoalFormatter(pb: PBScoreDocument) { - if (typeof pb.scoreData.additionalMetrics.bp === "number") { - return `${pb.scoreData.lamp.string} (BP: ${pb.scoreData.additionalMetrics.bp})`; + if (typeof pb.scoreData.optional.bp === "number") { + return `${pb.scoreData.lamp.string} (BP: ${pb.scoreData.optional.bp})`; } return pb.scoreData.lamp; diff --git a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts index dc9e75739..4ebb29d51 100644 --- a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts +++ b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts @@ -24,7 +24,6 @@ import type { ChartDocument, Game, GPTString, Lamps, Playtypes, ScoreDocument } export async function CreateCalculatedData( dryScore: DryScore, chart: ChartDocument, - esd: number | null, logger: KtLogger ): Promise { const game = dryScore.game; diff --git a/server/src/lib/score-import/framework/common/types.ts b/server/src/lib/score-import/framework/common/types.ts index 423027bc9..a7a6a9413 100644 --- a/server/src/lib/score-import/framework/common/types.ts +++ b/server/src/lib/score-import/framework/common/types.ts @@ -1,14 +1,25 @@ -import type { GPTString, Playtype, ScoreDocument } from "tachi-common"; +import type { + GPTString, + Playtype, + ProvidedMetrics, + ScoreDocument, + ExtractedOptionalMetrics, + Judgements, + integer, +} from "tachi-common"; /** - * An intermediate score format that will be filled out by + * An intermediate score format that will be fully filled out by * HydrateScore. */ -export type DryScore = Pick< - ScoreDocument, +export type DryScore = Pick< + ScoreDocument, "comment" | "game" | "importType" | "scoreMeta" | "service" | "timeAchieved" > & { - scoreData: Omit["scoreData"], "esd" | "gradeIndex" | "lampIndex">; + scoreData: ProvidedMetrics[GPT] & { + optional: ExtractedOptionalMetrics[GPT]; + judgements: Partial>; + }; }; export type ScorePlaytypeMap = Partial>>; diff --git a/server/src/lib/score-import/framework/derivers/derivers.ts b/server/src/lib/score-import/framework/derivers/derivers.ts index 7ab25fbe9..3717e026a 100644 --- a/server/src/lib/score-import/framework/derivers/derivers.ts +++ b/server/src/lib/score-import/framework/derivers/derivers.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ // ^ ts eslint currently gets very confused about the complexity on show here @@ -6,9 +9,11 @@ import { GetGrade } from "./common-utils"; import { IIDXLIKE_DERIVERS } from "./games/iidx-like"; import { SDVXLIKE_DERIVERS } from "./games/sdvx-like"; +import { InternalFailure } from "../common/converter-failures"; import { CHUNITHM_GBOUNDARIES, GITADORA_GBOUNDARIES, + GetGPTConfig, ITG_GBOUNDARIES, JUBEAT_GBOUNDARIES, MAIMAIDX_GBOUNDARIES, @@ -16,8 +21,16 @@ import { POPN_GBOUNDARIES, WACCA_GBOUNDARIES, } from "tachi-common"; +import type { DryScore } from "../common/types"; import type { GPTDerivers } from "./types"; -import type { GPTString } from "tachi-common"; +import type { + GPTString, + ChartDocument, + ProvidedMetrics, + DerivedMetrics, + ScoreData, +} from "tachi-common"; +import type { MetricDeriver } from "tachi-common/types/metrics"; type AllGPTDerivers = { [GPT in GPTString]: GPTDerivers; @@ -26,7 +39,7 @@ type AllGPTDerivers = { /** * How do we derive the "derivedMetrics" for each game? */ -export const GPT_DERIVERS: AllGPTDerivers = { +const GPT_DERIVERS: AllGPTDerivers = { // these games quite literally *all* work the same way. "bms:14K": IIDXLIKE_DERIVERS, "bms:7K": IIDXLIKE_DERIVERS, @@ -119,3 +132,74 @@ export const GPT_DERIVERS: AllGPTDerivers = { }, }, }; + +/** + * Given the providedMetrics and chart this score is on, derive the rest of the metrics + * we want to store. + */ +function DeriveMetrics( + gpt: GPTString, + metrics: ProvidedMetrics[GPT], + chart: ChartDocument +) { + const deriverImplementation: Record< + string, + MetricDeriver + > = GPT_DERIVERS[gpt]; + + const derivedMetrics: Record = {}; + + const gptConfig = GetGPTConfig(gpt); + + for (const [key, fn] of Object.entries(deriverImplementation)) { + const metricConfig = gptConfig.derivedMetrics[key]; + + if (!metricConfig) { + throw new InternalFailure( + `${gpt} has a deriver defined for '${key}', but no such field exists in the config?` + ); + } + + const value = fn(metrics, chart); + + // enum values on scores are stored as { string, index }. One is convenient + // for sorting and stuff in the DB, the other is convenient to know what + // actually is what. Integers only sucks. + if (metricConfig.type === "ENUM") { + const index = metricConfig.values.indexOf(value); + + if (index === -1) { + throw new InternalFailure( + `Failed to get the index for ENUM ${gpt} '${key}' for value ${value}. This should never happen!` + ); + } + + derivedMetrics[key] = { + string: value, + index, + }; + } else { + derivedMetrics[key] = value; + } + } + + return derivedMetrics as DerivedMetrics[GPT]; +} + +/** + * Return a full piece of scoreData. + */ +export function CreateFullScoreData( + gpt: GPTString, + dryScoreData: DryScore["scoreData"], + chart: ChartDocument +) { + const derivedMetrics = DeriveMetrics(gpt, dryScoreData, chart); + + const scoreData: ScoreData = { + ...dryScoreData, + derivedMetrics, + }; + + return scoreData; +} 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 8d934a920..178c3938c 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,6 +1,6 @@ import { CreateCalculatedData } from "../calculated-data/calculated-data"; -import { CalculateESDForGame } from "../common/score-utils"; -import { GetGamePTConfig } from "tachi-common"; +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"; @@ -18,32 +18,17 @@ export async function HydrateScore( scoreID: string, logger: KtLogger ): Promise { - const esd = CalculateESDForGame( - dryScore.game, - chart.playtype, - dryScore.scoreData.percent / 100 - ); + const gpt = GetGPTString(dryScore.game, chart.playtype); - const calculatedData = await CreateCalculatedData(dryScore, chart, esd, logger); + const scoreData = CreateFullScoreData(gpt, dryScore.scoreData, chart); - const gptConfig = GetGamePTConfig(dryScore.game, chart.playtype); - - // Fill out the rest of the fields we want for scoreData - const scoreData = { - lampIndex: gptConfig.lamps.indexOf(dryScore.scoreData.lamp), - gradeIndex: gptConfig.grades.indexOf(dryScore.scoreData.grade), - esd, - ...dryScoreData, - }; + const calculatedData = await CreateCalculatedData(dryScore, chart, logger); const score: ScoreDocument = { ...dryScore, - // then push our score data. - scoreData: { - ...dryScore.scoreData, - ...derivedMetrics, - }, + // then push our new score data. + scoreData, // everything below this point is sane highlight: false,