diff --git a/bot/src/utils/misc.ts b/bot/src/utils/misc.ts index 9c052ae31..f3b116356 100644 --- a/bot/src/utils/misc.ts +++ b/bot/src/utils/misc.ts @@ -167,7 +167,7 @@ export function FormatScoreData(score: ScOrPBDo score as ScOrPBDoc< "bms:7K" | "bms:14K" | "iidx:DP" | "iidx:SP" | "pms:Controller" | "pms:Keyboard" > - ).scoreData.hitMeta.bp; + ).scoreData.optional.bp; lampStr = `${score.scoreData.lamp} (BP: ${bp ?? "No Data"})`; diff --git a/common/src/config/game-support/bms.ts b/common/src/config/game-support/bms.ts index e1501fbdb..f557261f2 100644 --- a/common/src/config/game-support/bms.ts +++ b/common/src/config/game-support/bms.ts @@ -247,6 +247,7 @@ export const BMS_7K_CONF = { tableFolders: z.array(z.strictObject({ table: z.string(), level: z.string() })), sglEC: z.number().nullable(), sglHC: z.number().nullable(), + aiLevel: z.string().nullable(), }), preferences: z.strictObject({ diff --git a/common/src/config/game-support/iidx.ts b/common/src/config/game-support/iidx.ts index 8f52abb38..72a0e65de 100644 --- a/common/src/config/game-support/iidx.ts +++ b/common/src/config/game-support/iidx.ts @@ -88,14 +88,16 @@ export const IIDX_SP_CONF = { comboBreak: { type: "INTEGER" }, // The players history for the gauge type they were playing on. - gaugeHistory: { type: "GRAPH" }, + // this may fall into "NULL" if the user fails. + gaugeHistory: { type: "NULLABLE_GRAPH" }, + scoreHistory: { type: "NULLABLE_GRAPH" }, // if "GSM" is enabled (via fervidex.dll) then all graphs // are sent. we should store all of them. - gsmEasy: { type: "GRAPH" }, - gsmNormal: { type: "GRAPH" }, - gsmHard: { type: "GRAPH" }, - gsmEXHard: { type: "GRAPH" }, + gsmEasy: { type: "NULLABLE_GRAPH" }, + gsmNormal: { type: "NULLABLE_GRAPH" }, + gsmHard: { type: "NULLABLE_GRAPH" }, + gsmEXHard: { type: "NULLABLE_GRAPH" }, }, defaultScoreRatingAlg: "ktLampRating", diff --git a/common/src/config/game-support/sdvx.ts b/common/src/config/game-support/sdvx.ts index b9f14dc80..788661dcb 100644 --- a/common/src/config/game-support/sdvx.ts +++ b/common/src/config/game-support/sdvx.ts @@ -1,3 +1,4 @@ +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"; @@ -97,6 +98,7 @@ export const SDVX_SINGLE_CONF = { preferredDefaultEnum: "grade", optionalMetrics: { + ...FAST_SLOW_MAXCOMBO, exScore: { type: "INTEGER" }, gauge: { type: "DECIMAL" }, }, diff --git a/common/src/types/metrics.ts b/common/src/types/metrics.ts index 845588cec..6e984cc5d 100644 --- a/common/src/types/metrics.ts +++ b/common/src/types/metrics.ts @@ -20,16 +20,10 @@ export type GraphMetricValidator = ( chart: ChartDocument ) => string | true; -/** - * A metric for a score that's a floating point number. - */ export interface ConfDecimalScoreMetric { type: "DECIMAL"; } -/** - * A metric for a score that's an integer. - */ export interface ConfIntegerScoreMetric { type: "INTEGER"; } @@ -49,19 +43,25 @@ export interface ConfEnumScoreMetric { } /** - * A metric for a score that represents an array of numbers. - * - * This is intended for use by graphs and other equivalent things. + * Corresponds to Array */ export interface ConfGraphScoreMetric { type: "GRAPH"; } +/** + * Corresponds to Array. + */ +export interface ConfNullableGraphScoreMetric { + type: "NULLABLE_GRAPH"; +} + export type ConfScoreMetric = | ConfDecimalScoreMetric | ConfEnumScoreMetric | ConfGraphScoreMetric - | ConfIntegerScoreMetric; + | ConfIntegerScoreMetric + | ConfNullableGraphScoreMetric; /** * When we store and interact with enum values, we want them to be both strings @@ -84,6 +84,8 @@ export type ExtractMetricType = M extends ConfDecimal ? EnumValue : M extends ConfGraphScoreMetric ? Array + : M extends ConfNullableGraphScoreMetric + ? Array : never; /** @@ -136,7 +138,7 @@ export type ExtractMetrics> = { [K in keyof R]: ExtractMetricType; }; -export type DerivedMetricValue = Array | integer | number | string; +export type DerivedMetricValue = Array | Array | integer | number | string; export type MetricDeriver< M extends Record, @@ -150,13 +152,17 @@ export type MetricDeriver< * A function that will derive this metric, given a function of other metrics and * a chart for this GPT. */ -export type ScoreMetricDeriver< - M extends ConfScoreMetric, - GPT extends GPTString -> = M extends ConfGraphScoreMetric - ? MetricDeriver, GPT, Array> - : M extends ConfEnumScoreMetric - ? MetricDeriver, GPT, V> - : M extends ConfIntegerScoreMetric - ? MetricDeriver, GPT, integer> - : MetricDeriver, GPT, number>; +export type ScoreMetricDeriver = + // graph score metrics correspond to Array + M extends ConfGraphScoreMetric + ? MetricDeriver, GPT, Array> + : // nullable graphs correspond to Array + M extends ConfNullableGraphScoreMetric + ? MetricDeriver, GPT, Array> + : // enums correspond to their string unions ("FAILED"|"CLEAR") + M extends ConfEnumScoreMetric + ? MetricDeriver, GPT, V> + : // the other two are obvious + M extends ConfIntegerScoreMetric + ? MetricDeriver, GPT, integer> + : MetricDeriver, GPT, number>; diff --git a/server/src/lib/migration/migrations/null-lr2hook-failed-bps.ts b/server/src/lib/migration/migrations/null-lr2hook-failed-bps.ts index 93b1e3ce5..fd222dce7 100644 --- a/server/src/lib/migration/migrations/null-lr2hook-failed-bps.ts +++ b/server/src/lib/migration/migrations/null-lr2hook-failed-bps.ts @@ -13,7 +13,7 @@ const migration: Migration = { }, { $set: { - "scoreData.hitMeta.bp": null, + "scoreData.optional.bp": null, }, }, { 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 654ea313b..f020e5484 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 @@ -234,7 +234,7 @@ function CalculateDataITGStamina( chart: ChartDocument<"itg:Stamina"> ): CalculatedData<"itg:Stamina"> { // If the user failed -- and we don't know when, return null for everything. - if (dryScore.scoreData.lamp === "FAILED" && IsNullish(dryScore.scoreData.hitMeta.diedAt)) { + if (dryScore.scoreData.lamp === "FAILED" && IsNullish(dryScore.scoreData.optional.diedAt)) { return { blockRating: null, highest32: null, @@ -245,14 +245,14 @@ function CalculateDataITGStamina( let h32 = ITGHighestUnbroken.calculateFromNPSPerMeasure( chart.data.breakdown.npsPerMeasure, chart.data.breakdown.notesPerMeasure, - dryScore.scoreData.hitMeta.diedAt, + dryScore.scoreData.optional.diedAt, 32 ); let h256 = ITGHighestUnbroken.calculateFromNPSPerMeasure( chart.data.breakdown.npsPerMeasure, chart.data.breakdown.notesPerMeasure, - dryScore.scoreData.hitMeta.diedAt, + dryScore.scoreData.optional.diedAt, 256 ); diff --git a/server/src/lib/score-import/framework/common/score-utils.ts b/server/src/lib/score-import/framework/common/score-utils.ts index f713a3ed9..994eb11e2 100644 --- a/server/src/lib/score-import/framework/common/score-utils.ts +++ b/server/src/lib/score-import/framework/common/score-utils.ts @@ -1,190 +1,6 @@ -import { InternalFailure, InvalidScoreFailure } from "./converter-failures"; -import CreateLogCtx from "lib/logger/logger"; -import { ESDCore, GetGamePTConfig } from "tachi-common"; -import { IsNullish, NotNullish } from "utils/misc"; -import type { - ChartDocument, - Game, - GameToGPTString, - Grades, - GPTString, - Playtype, - Lamps, - integer, -} from "tachi-common"; - -const logger = CreateLogCtx(__filename); - -/** - * Util for getting a games' grade for a given percent. - */ -export function GetGradeFromPercent( - game: Game, - playtype: Playtype, - percent: number -): Grades[I] { - const gptConfig = GetGamePTConfig(game, playtype); - const boundaries = gptConfig.gradeBoundaries; - const grades = gptConfig.grades; - - // (hey, this for loop is backwards!) - for (let i = boundaries.length - 1; i >= 0; i--) { - if (percent + Number.EPSILON >= NotNullish(boundaries[i])) { - if (IsNullish(grades[i])) { - logger.error( - `Attempted to get the ${i}th grade for ${game} (${playtype}) for a percent of ${percent}%. The grade fetched was null or undefined. Refusing to process this score.` - ); - throw new InternalFailure( - `Failed to process a score with a percent of ${percent}. This has been reported.` - ); - } - - return grades[i] as Grades[I]; - } - } - - logger.error(`Could not resolve grade for percent ${percent} on game ${game}`); - throw new InternalFailure(`Could not resolve grade for percent ${percent} on game ${game}.`); -} - -/** - * A generic function for calculating a percent from a given score on - * a given game. - */ -export function GenericCalculatePercent(game: Game, score: number, chart?: ChartDocument): number { - switch (game) { - case "museca": - case "chunithm": - case "wacca": - return (score / 1_000_000) * 100; - case "sdvx": - case "usc": - return (score / 10_000_000) * 100; - case "popn": - return (score / 100_000) * 100; - - case "bms": - case "pms": - case "iidx": { - if (!chart) { - logger.severe("No Chart passed to GenericCalcPercent but game was iidx/bms/pms."); - throw new InternalFailure( - "No Chart passed to GenericCalcPercent but game was iidx/bms/pms." - ); - } - - // Yeah, we declare it like this so the below return is actually clear. - // eslint-disable-next-line no-case-declarations - const MAX = - ( - chart as ChartDocument< - | "bms:7K" - | "bms:14K" - | "iidx:DP" - | "iidx:SP" - | "pms:Controller" - | "pms:Keyboard" - > - ).data.notecount * 2; - - return (100 * score) / MAX; - } - - case "jubeat": - throw new Error(`Cannot calculate percent for jubeat. Music Rate must be provided.`); - - // gitadora's score is just a percent - // this is a hack: we need a refactor for this. - case "gitadora": - case "itg": - case "maimaidx": - return score; - } -} - -/** - * Helper utility for validating percents on a game. This throws an InvalidScoreFailure if the percent is - * invalid, and returns void on success. - */ -export function ValidatePercent( - game: Game, - playtype: Playtype, - percent: number, - chart: ChartDocument -) { - const gptConfig = GetGamePTConfig(game, playtype); - - if (percent > gptConfig.percentMax) { - throw new InvalidScoreFailure( - `Invalid percent of ${percent} - expected a value less than ${gptConfig.percentMax}% (${chart.songID} ${chart.playtype} ${chart.difficulty}).` - ); - } - - if (percent < 0) { - throw new InvalidScoreFailure( - `Invalid percent of ${percent} - Expected a positive number? (${chart.songID} ${chart.playtype} ${chart.difficulty})` - ); - } -} - -/** - * Generically gets the grade and percent for a given score on a given game. This only works for games where - * grades are just percent boundaries. This will throw an InvalidScoreFailure if the percent is invalid, - * or if the grade is invalid. - */ -export function GenericGetGradeAndPercent( - game: G, - score: number, - chart: ChartDocument -): { percent: number; grade: Grades[GameToGPTString[G]] } { - const percent = GenericCalculatePercent(game, score, chart); - - ValidatePercent(game, chart.playtype, percent, chart); - - const grade: Grades[GameToGPTString[G]] = GetGradeFromPercent(game, chart.playtype, percent); - - return { percent, grade }; -} - -export function JubeatGetGrade(score: number): GetEnumValue<"jubeat:Single", "grade"> { - if (score === 1_000_000) { - return "EXC"; - } else if (score >= 980_000) { - return "SSS"; - } else if (score >= 950_000) { - return "SS"; - } else if (score >= 900_000) { - return "S"; - } else if (score >= 850_000) { - return "A"; - } else if (score >= 800_000) { - return "B"; - } else if (score >= 700_000) { - return "C"; - } else if (score >= 500_000) { - return "D"; - } - - return "E"; -} - -/** - * Calculates the ESD for a given game + percent combo. This function returns - * null if the game does not support support ESD. - */ -export function CalculateESDForGame( - game: Game, - playtype: Playtype, - percent: number -): number | null { - const gptConfig = GetGamePTConfig(game, playtype); - - if (!gptConfig.supportsESD) { - return null; - } - - return ESDCore.CalculateESD(gptConfig.judgementWindows, percent); -} +import { InvalidScoreFailure } from "./converter-failures"; +import type { integer } from "tachi-common"; +import type { GetEnumValue } from "tachi-common/types/metrics"; /** * Parses and validates a date from a string. @@ -211,7 +27,10 @@ export function ParseDateFromString(str: string | null | undefined): number | nu * Museca actually handles clears differently with a bunch of grafica nonsense, * but nobody actually cares about it, so... */ -export function MusecaGetLamp(score: integer, missCount: integer): GetEnumValue<"museca:Single", "lamp"> { +export function MusecaGetLamp( + score: integer, + missCount: integer +): GetEnumValue<"museca:Single", "lamp"> { if (score === 1_000_000) { return "PERFECT CONNECT ALL"; } else if (missCount === 0) { @@ -223,7 +42,10 @@ export function MusecaGetLamp(score: integer, missCount: integer): GetEnumValue< return "FAILED"; } -export function JubeatGetLamp(score: integer, missCount: integer): GetEnumValue<"jubeat:Single", "lamp"> { +export function JubeatGetLamp( + score: integer, + missCount: integer +): GetEnumValue<"jubeat:Single", "lamp"> { if (score === 1_000_000) { return "EXCELLENT"; } else if (missCount === 0) { 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 407c065bb..58377c34f 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 @@ -232,7 +232,7 @@ t.test("#CreatePBDoc", (t) => { t.not(res, undefined, "Should actually return something."); t.equal( - res?.scoreData.hitMeta.bp, + res?.scoreData.optional.bp, 1, "Should select the best BP's BP and not the score PBs." ); @@ -292,7 +292,7 @@ t.test("#CreatePBDoc", (t) => { t.not(res, undefined, "Should actually return something."); t.equal( - res?.scoreData.hitMeta.bp, + res?.scoreData.optional.bp, 1, "Should select the best BP's BP and not the score PBs." ); @@ -341,7 +341,7 @@ t.test("#CreatePBDoc", (t) => { t.not(res, undefined, "Should actually return something."); t.hasStrict( - res?.scoreData.hitMeta, + res?.scoreData.optional, { gauge: 12, gaugeHistory: [20, 20, 21, 12], 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 2f3f01d40..2c125ba48 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 @@ -190,7 +190,7 @@ async function MergeScoreLampIntoPB( judgements: scorePB.scoreData.judgements, // this will probably be overrode by game-specific fns - optional: scorePB.scoreData.hitMeta, + optional: scorePB.scoreData.optional, }, calculatedData: scorePB.calculatedData, }; diff --git a/server/src/lib/score-import/framework/pb/game-specific-merge.ts b/server/src/lib/score-import/framework/pb/game-specific-merge.ts index 7bc06e6f4..d8643b9cb 100644 --- a/server/src/lib/score-import/framework/pb/game-specific-merge.ts +++ b/server/src/lib/score-import/framework/pb/game-specific-merge.ts @@ -19,13 +19,13 @@ export async function IIDXMergeFn( pbDoc.calculatedData.ktLampRating = lampPB.calculatedData.ktLampRating; // Update lamp related iidx-specific info from the lampPB. - pbDoc.scoreData.hitMeta.gsm = lampPB.scoreData.hitMeta.gsm; - pbDoc.scoreData.hitMeta.gauge = lampPB.scoreData.hitMeta.gauge; - pbDoc.scoreData.hitMeta.gaugeHistory = lampPB.scoreData.hitMeta.gaugeHistory; + pbDoc.scoreData.optional.gsm = lampPB.scoreData.optional.gsm; + pbDoc.scoreData.optional.gauge = lampPB.scoreData.optional.gauge; + pbDoc.scoreData.optional.gaugeHistory = lampPB.scoreData.optional.gaugeHistory; - pbDoc.scoreData.hitMeta.comboBreak = lampPB.scoreData.hitMeta.comboBreak; + pbDoc.scoreData.optional.comboBreak = lampPB.scoreData.optional.comboBreak; - DeleteUndefinedProps(pbDoc.scoreData.hitMeta); + DeleteUndefinedProps(pbDoc.scoreData.optional); await MergeBPPB(pbDoc, scorePB, lampPB, logger, asOfTimestamp); @@ -38,7 +38,7 @@ export function PopnMergeFn( lampPB: ScoreDocument<"popn:9B">, _logger: KtLogger ) { - pbDoc.scoreData.hitMeta.specificClearType = lampPB.scoreData.hitMeta.specificClearType; + pbDoc.scoreData.optional.specificClearType = lampPB.scoreData.optional.specificClearType; return true; } @@ -52,8 +52,8 @@ export async function BMSMergeFn( ) { pbDoc.calculatedData.sieglinde = lampPB.calculatedData.sieglinde; - pbDoc.scoreData.hitMeta.gaugeHistory = lampPB.scoreData.hitMeta.gaugeHistory; - pbDoc.scoreData.hitMeta.gauge = lampPB.scoreData.hitMeta.gauge; + pbDoc.scoreData.optional.gaugeHistory = lampPB.scoreData.optional.gaugeHistory; + pbDoc.scoreData.optional.gauge = lampPB.scoreData.optional.gauge; await MergeBPPB(pbDoc, scorePB, lampPB, logger, asOfTimestamp); @@ -133,7 +133,7 @@ export async function SDVXMergeFn( const query: FilterQuery = { chartID: pbDoc.chartID, - "scoreData.hitMeta.exScore": { $type: "number" }, + "scoreData.optional.exScore": { $type: "number" }, }; if (asOfTimestamp !== undefined) { @@ -143,14 +143,14 @@ export async function SDVXMergeFn( // find the users score with the highest exScore const bestExScore = (await db.scores.findOne(query, { sort: { - "scoreData.hitMeta.exScore": -1, + "scoreData.optional.exScore": -1, }, })) as ScoreDocument<"sdvx:Single"> | null; if (!bestExScore) { - pbDoc.scoreData.hitMeta.exScore = undefined; + pbDoc.scoreData.optional.exScore = undefined; } else { - pbDoc.scoreData.hitMeta.exScore = bestExScore.scoreData.hitMeta.exScore; + pbDoc.scoreData.optional.exScore = bestExScore.scoreData.optional.exScore; pbDoc.composedFrom.other = [{ name: "exScorePB", scoreID: bestExScore.scoreID }]; } @@ -183,7 +183,7 @@ async function MergeBPPB( const query: FilterQuery = { userID: scorePB.userID, chartID: scorePB.chartID, - "scoreData.hitMeta.bp": { $exists: true }, + "scoreData.optional.bp": { $exists: true }, }; if (asOfTimestamp !== undefined) { @@ -193,7 +193,7 @@ async function MergeBPPB( const bpPB = (await db.scores.findOne(query, { sort: { // bp 0 is the best BP, bp 1 is worse, so on - "scoreData.hitMeta.bp": 1, + "scoreData.optional.bp": 1, }, })) as ScoreDocument<"iidx:DP" | "iidx:SP"> | null; @@ -207,17 +207,17 @@ async function MergeBPPB( return; } - // by default scorePB is chosen for hitMeta fields, so, we can skip any assignments here by returning here. + // by default scorePB is chosen for optional fields, so, we can skip any assignments here by returning here. if (bpPB.scoreID === scorePB.scoreID) { logger.debug(`Skipped merging BP PB as scorePB was also BP PB.`); return true; } else if (bpPB.scoreID === lampPB.scoreID) { - pbDoc.scoreData.hitMeta.bp = lampPB.scoreData.hitMeta.bp; + pbDoc.scoreData.optional.bp = lampPB.scoreData.optional.bp; logger.debug(`Skipped adding BP PB as composedFrom because lampPB was also BP PB.`); return; } - pbDoc.scoreData.hitMeta.bp = bpPB.scoreData.hitMeta.bp; + pbDoc.scoreData.optional.bp = bpPB.scoreData.optional.bp; pbDoc.composedFrom.other = [{ name: "Best BP", scoreID: bpPB.scoreID }]; } diff --git a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts index 84a08c16d..5837a2ced 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts @@ -127,7 +127,7 @@ export const ConverterBatchManual: ConverterFunction { const fn = () => ParserFn(dm({ optional: { not_key: 123 } }), "file/batch-manual", logger); - t.throws(fn, mockErr("scores[0].hitMeta | Unexpected")); + t.throws(fn, mockErr("scores[0].optional | Unexpected")); const fn2 = () => ParserFn(dm({ optional: { bp: -1 } }), "file/batch-manual", logger); - t.throws(fn2, mockErr("scores[0].hitMeta.bp | Expected a positive integer")); + t.throws(fn2, mockErr("scores[0].optional.bp | Expected a positive integer")); t.end(); }); diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts index a433a3c8b..fd93ff0a4 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts @@ -166,7 +166,7 @@ const ConvertEamIIDXCSV: ConverterFunction< ); } - dryScore.scoreData.hitMeta.bp = numBP; + dryScore.scoreData.optional.bp = numBP; } else if (eamScore.bp === "---") { logger.debug( `Skipped assigning BP for score as it had expected null value of ${eamScore.bp}.` diff --git a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.ts b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.ts index 5cb358ca5..b73b18372 100644 --- a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.ts +++ b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.ts @@ -2,14 +2,14 @@ import { InvalidScoreFailure, SongOrChartNotFoundFailure, } from "lib/score-import/framework/common/converter-failures"; -import { GenericGetGradeAndPercent } from "lib/score-import/framework/common/score-utils"; import { AssertStrAsPositiveInt } from "lib/score-import/framework/common/string-asserts"; import { FindChartWithPTDF } from "utils/queries/charts"; import { FindSongOnTitle } from "utils/queries/songs"; import type { ConverterFunction } from "../../common/types"; import type { SDVXEamusementCSVData } from "./types"; import type { DryScore } from "lib/score-import/framework/common/types"; -import type { Difficulties, Lamps } from "tachi-common"; +import type { Difficulties } from "tachi-common"; +import type { GetEnumValue } from "tachi-common/types/metrics"; import type { EmptyObject } from "utils/types"; const DIFFICULTY_MAP: Map = new Map([ @@ -105,8 +105,6 @@ const ConvertEamSDVXCSV: ConverterFunction = throw new InvalidScoreFailure(`${humanisedChartTitle} - Invalid lamp of ${data.lamp}.`); } - const { percent, grade } = GenericGetGradeAndPercent("sdvx", score, chart); - const dryScore: DryScore<"sdvx:Single"> = { service: "e-amusement", game: "sdvx", @@ -119,8 +117,6 @@ const ConvertEamSDVXCSV: ConverterFunction = scoreData: { score, lamp, - percent, - grade, judgements: {}, optional: { exScore, diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts b/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts index d8c22ef11..6967372f8 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts @@ -2,16 +2,13 @@ import { InternalFailure, SongOrChartNotFoundFailure, } from "../../../framework/common/converter-failures"; -import { - GenericGetGradeAndPercent, - ParseDateFromString, -} from "../../../framework/common/score-utils"; +import { ParseDateFromString } from "../../../framework/common/score-utils"; import { FindIIDXChartOnInGameID } from "utils/queries/charts"; import { FindSongOnID } from "utils/queries/songs"; import type { DryScore } from "../../../framework/common/types"; import type { ConverterFunction } from "../../common/types"; import type { MerScore } from "./types"; -import type { Lamps } from "tachi-common"; +import type { GetEnumValue } from "tachi-common/types/metrics"; import type { EmptyObject } from "utils/types"; function ConvertMERLamp(lamp: MerScore["clear_type"]): GetEnumValue<"iidx:DP" | "iidx:SP", "lamp"> { @@ -48,8 +45,6 @@ export const ConvertFileMerIIDX: ConverterFunction = asyn throw new InternalFailure(`Song-Chart Desync on songID ${chart.songID}`); } - const { percent, grade } = GenericGetGradeAndPercent("iidx", data.score, chart); - const lamp = ConvertMERLamp(data.clear_type); const timeAchieved = ParseDateFromString(ConvertDateToJST(data.update_time)); @@ -61,8 +56,6 @@ export const ConvertFileMerIIDX: ConverterFunction = asyn service: "MER", scoreData: { score: data.score, - percent, - grade, lamp, judgements: {}, optional: { diff --git a/server/src/lib/score-import/import-types/ir/barbatos/converter.ts b/server/src/lib/score-import/import-types/ir/barbatos/converter.ts index fd8b706d0..966d065e3 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/converter.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/converter.ts @@ -2,13 +2,11 @@ import { InternalFailure, SongOrChartNotFoundFailure, } from "../../../framework/common/converter-failures"; -import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import { FindSDVXChartOnInGameIDVersion } from "utils/queries/charts"; import { FindSongOnID } from "utils/queries/songs"; import type { DryScore } from "../../../framework/common/types"; import type { ConverterFunction } from "../../common/types"; import type { BarbatosContext, BarbatosScore, BarbatosSDVX6Score } from "./types"; -import type { Lamps } from "tachi-common"; const LAMP_LOOKUP = { 1: "FAILED", @@ -16,7 +14,7 @@ const LAMP_LOOKUP = { 3: "EXCESSIVE CLEAR", 4: "ULTIMATE CHAIN", 5: "PERFECT ULTIMATE CHAIN", -}; +} as const; const DIFFICULTY_LOOKUP = { 0: "NOV", @@ -26,18 +24,13 @@ const DIFFICULTY_LOOKUP = { // special case for inf/grv/hvn/vvd - which are all the same diff internally. (kinda). 3: "ANY_INF", 4: "MXM", -}; +} as const; export const ConverterIRBarbatos: ConverterFunction< BarbatosScore | BarbatosSDVX6Score, BarbatosContext > = async (data, context, importType, logger) => { - const difficulty = DIFFICULTY_LOOKUP[data.difficulty] as - | "ADV" - | "ANY_INF" - | "EXH" - | "MXM" - | "NOV"; + const difficulty = DIFFICULTY_LOOKUP[data.difficulty]; const chart = await FindSDVXChartOnInGameIDVersion(data.song_id, difficulty, context.version); @@ -57,8 +50,6 @@ export const ConverterIRBarbatos: ConverterFunction< throw new InternalFailure(`Song ${chart.songID} (sdvx) has no parent song?`); } - const { percent, grade } = GenericGetGradeAndPercent("sdvx", data.score, chart); - const { critical, near, miss } = GetJudgements(data); const { fast, slow } = GetFastSlow(data); @@ -70,9 +61,7 @@ export const ConverterIRBarbatos: ConverterFunction< timeAchieved: context.timeReceived, scoreData: { score: data.score, - percent, - grade, - lamp: LAMP_LOOKUP[data.clear_type] as GetEnumValue<"sdvx:Single", "lamp">, + lamp: LAMP_LOOKUP[data.clear_type], judgements: { critical, near, diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts index 6f3e159b1..cce510dce 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts @@ -3,7 +3,6 @@ import { InvalidScoreFailure, SongOrChartNotFoundFailure, } from "../../../framework/common/converter-failures"; -import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import db from "external/mongo/db"; import { HandleOrphanQueue } from "lib/orphan-queue/orphan-queue"; import { ReprocessOrphan } from "lib/score-import/framework/orphans/orphans"; @@ -17,6 +16,7 @@ import type { ConverterFunction } from "../../common/types"; import type { BeatorajaChart, BeatorajaContext, BeatorajaScore } from "./types"; import type { KtLogger } from "lib/logger/logger"; import type { ChartDocument, SongDocument, Playtypes } from "tachi-common"; +import type { Mutable } from "utils/types"; const LAMP_LOOKUP = { NoPlay: "NO PLAY", @@ -177,11 +177,9 @@ export const ConverterIRBeatoraja: ConverterFunction["scoreData"]["hitMeta"] = { + const optional: Mutable< + DryScore<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard">["scoreData"]["optional"] + > = { bp: data.minbp === -1 ? null : data.minbp, gauge: data.gauge === -1 ? null : data.gauge, }; @@ -198,11 +196,11 @@ export const ConverterIRBeatoraja: ConverterFunction a + data[e], 0); - hitMeta.slow = (["lbd", "lgr", "lpg", "lpr", "lms"] as const).reduce((a, e) => a + data[e], 0); + optional.fast = (["ebd", "egr", "epg", "epr", "ems"] as const).reduce((a, e) => a + data[e], 0); + optional.slow = (["lbd", "lgr", "lpg", "lpr", "lms"] as const).reduce((a, e) => a + data[e], 0); let random = null; @@ -230,17 +228,21 @@ export const ConverterIRBeatoraja: ConverterFunction = { @@ -57,11 +53,9 @@ export const ConverterIRFervidexStatic: ConverterFunction< timeAchieved: null, scoreData: { score: data.ex_score, - percent, - grade, - lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type] as GetEnumValue<"iidx:DP" | "iidx:SP", "lamp">, + lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type], judgements: {}, - hitMeta, + optional, }, scoreMeta: {}, }; diff --git a/server/src/lib/score-import/import-types/ir/fervidex/converter.ts b/server/src/lib/score-import/import-types/ir/fervidex/converter.ts index 6717118e2..f735210c7 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/converter.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/converter.ts @@ -1,9 +1,9 @@ import { InternalFailure, InvalidScoreFailure, + SkipScoreFailure, SongOrChartNotFoundFailure, } from "../../../framework/common/converter-failures"; -import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import db from "external/mongo/db"; import { CreateScoreID } from "lib/score-import/framework/score-importing/score-id"; import { IsNullishOrEmptyStr } from "utils/misc"; @@ -12,7 +12,7 @@ import { FindSongOnID } from "utils/queries/songs"; import type { DryScore } from "../../../framework/common/types"; import type { ConverterFunction } from "../../common/types"; import type { FervidexContext, FervidexScore } from "./types"; -import type { Difficulties, Lamps, Playtypes } from "tachi-common"; +import type { Difficulties, Playtypes } from "tachi-common"; export const FERVIDEX_LAMP_LOOKUP = { 0: "NO PLAY", @@ -23,7 +23,7 @@ export const FERVIDEX_LAMP_LOOKUP = { 5: "HARD CLEAR", 6: "EX HARD CLEAR", 7: "FULL COMBO", -}; +} as const; export function TachifyAssist( assist: Required["option"]["assist"] @@ -108,10 +108,8 @@ export function SplitFervidexChartRef(ferDif: FervidexScore["chart"]) { let difficulty: Difficulties["iidx:DP" | "iidx:SP"]; switch (ferDif[ferDif.length - 1]) { - case "b": { - difficulty = "BEGINNER"; - break; - } + case "b": + throw new SkipScoreFailure(`BEGINNER charts are not supported.`); case "n": { difficulty = "NORMAL"; @@ -191,8 +189,6 @@ export const ConverterIRFervidex: ConverterFunction, + lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type], judgements: { pgreat: data.pgreat, great: data.great, @@ -226,15 +220,16 @@ export const ConverterIRFervidex: ConverterFunction = async ( data, @@ -36,8 +35,6 @@ export const ConverterIRKsHookSV6C: ConverterFunction = { game: "sdvx", service: "kshook SV6C", @@ -46,8 +43,6 @@ export const ConverterIRKsHookSV6C: ConverterFunction { +export function SV6CConvertLamp( + clear: KsHookSV6CScore["clear"] +): GetEnumValue<"sdvx:Single", "lamp"> { if (clear === "CLEAR_PLAYED") { return "FAILED"; } else if (clear === "CLEAR_EFFECTIVE") { diff --git a/server/src/lib/targets/goals.ts b/server/src/lib/targets/goals.ts index 0734bc33b..d35b6db69 100644 --- a/server/src/lib/targets/goals.ts +++ b/server/src/lib/targets/goals.ts @@ -275,7 +275,7 @@ export function HumaniseGoalProgress( case "pms": { // @ts-expect-error This is guaranteed to exist, we're going to ignore it. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const maybeBP: number | null | undefined = userPB.scoreData.hitMeta.bp; + const maybeBP: number | null | undefined = userPB.scoreData.optional.bp; // render BP if it exists if (!IsNullish(maybeBP)) { @@ -288,7 +288,7 @@ export function HumaniseGoalProgress( case "itg": { // @ts-expect-error This is guaranteed to exist, we're going to ignore it. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const maybeDiedAt: number | null | undefined = userPB.scoreData.hitMeta.diedAt; + const maybeDiedAt: number | null | undefined = userPB.scoreData.optional.diedAt; // render diedAt if it exists as "FAILED (Died 25% in)" if (!IsNullish(maybeDiedAt)) { 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 350e65a4e..b7e589939 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,7 +143,7 @@ router.get( pb.scoreData.score.toString(), // ex pb.scoreData.judgements.pgreat?.toString() ?? "0", // pgreat pb.scoreData.judgements.great?.toString() ?? "0", // great - pb.scoreData.hitMeta.bp?.toString() ?? "0", // BP + pb.scoreData.optional.bp?.toString() ?? "0", // BP ConvertEamLamp(pb.scoreData.lamp), // lamp ConvertEamGrade(pb.scoreData.grade) // grade ); diff --git a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts index be9764f77..b0b006c83 100644 --- a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts @@ -59,14 +59,14 @@ export function TachiScoreDataToBeatorajaFormat( playcount, clear: LAMP_TO_BEATORAJA[scoreData.lampIndex] ?? 0, date: pbScore.timeAchieved ?? 0, - maxcombo: scoreData.hitMeta.maxCombo ?? 0, - gauge: scoreData.hitMeta.gauge ?? 0, + maxcombo: scoreData.optional.maxCombo ?? 0, + gauge: scoreData.optional.gauge ?? 0, // These two are now unsupported due to performance concerns. deviceType: null, random: null, - minbp: scoreData.hitMeta.bp ?? 0, + minbp: scoreData.optional.bp ?? 0, passnotes: 0, notes: notecount, }; @@ -85,7 +85,7 @@ export function TachiScoreDataToBeatorajaFormat( "ems", "lms", ] as Array) { - judgements[key] = scoreData.hitMeta[key] ?? 0; + judgements[key] = scoreData.optional[key] ?? 0; } // // If we have no epg/egr data, we can't calculate EX score on the beatoraja client. diff --git a/server/src/utils/types.ts b/server/src/utils/types.ts index b5ce00773..5ce83fc89 100644 --- a/server/src/utils/types.ts +++ b/server/src/utils/types.ts @@ -120,3 +120,6 @@ export type MigrationDocument = { status: "pending"; } ); + +// https://www.designcise.com/web/tutorial/how-to-change-readonly-properties-to-be-writable-in-typescript +export type Mutable = { -readonly [K in keyof T]: T[K] };