checkpoint: 14

This commit is contained in:
zkldi
2023-01-19 16:39:31 +00:00
parent 8db7bc3391
commit a3208f3ce5
25 changed files with 133 additions and 330 deletions
+1 -1
View File
@@ -167,7 +167,7 @@ export function FormatScoreData<I extends GPTString = GPTString>(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"})`;
+1
View File
@@ -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({
+7 -5
View File
@@ -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",
+2
View File
@@ -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" },
},
+27 -21
View File
@@ -20,16 +20,10 @@ export type GraphMetricValidator<GPT extends GPTString> = (
chart: ChartDocument<GPT>
) => 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<V extends string> {
}
/**
* 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<number>
*/
export interface ConfGraphScoreMetric {
type: "GRAPH";
}
/**
* Corresponds to Array<number | null>.
*/
export interface ConfNullableGraphScoreMetric {
type: "NULLABLE_GRAPH";
}
export type ConfScoreMetric =
| ConfDecimalScoreMetric
| ConfEnumScoreMetric<string>
| 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 ConfScoreMetric> = M extends ConfDecimal
? EnumValue<V>
: M extends ConfGraphScoreMetric
? Array<number>
: M extends ConfNullableGraphScoreMetric
? Array<number | null>
: never;
/**
@@ -136,7 +138,7 @@ export type ExtractMetrics<R extends Record<string, ConfScoreMetric>> = {
[K in keyof R]: ExtractMetricType<R[K]>;
};
export type DerivedMetricValue = Array<number> | integer | number | string;
export type DerivedMetricValue = Array<number | null> | Array<number> | integer | number | string;
export type MetricDeriver<
M extends Record<string, PossibleMetrics>,
@@ -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<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, Array<number>>
: M extends ConfEnumScoreMetric<infer V>
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, V>
: M extends ConfIntegerScoreMetric
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, integer>
: MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, number>;
export type ScoreMetricDeriver<M extends ConfScoreMetric, GPT extends GPTString> =
// graph score metrics correspond to Array<number>
M extends ConfGraphScoreMetric
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, Array<number>>
: // nullable graphs correspond to Array<number | null>
M extends ConfNullableGraphScoreMetric
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, Array<number | null>>
: // enums correspond to their string unions ("FAILED"|"CLEAR")
M extends ConfEnumScoreMetric<infer V>
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, V>
: // the other two are obvious
M extends ConfIntegerScoreMetric
? MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, integer>
: MetricDeriver<ExtractMetrics<ConfProvidedMetrics[GPT]>, GPT, number>;
@@ -13,7 +13,7 @@ const migration: Migration = {
},
{
$set: {
"scoreData.hitMeta.bp": null,
"scoreData.optional.bp": null,
},
},
{
@@ -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
);
@@ -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<I extends GPTString = GPTString>(
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<G extends Game>(
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) {
@@ -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],
@@ -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,
};
@@ -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<ScoreDocument> = {
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<ScoreDocument> = {
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 }];
}
@@ -127,7 +127,7 @@ export const ConverterBatchManual: ConverterFunction<BatchManualScore, BatchManu
grade,
percent,
judgements: data.judgements ?? {},
optional: data.hitMeta ?? {},
optional: data.optional ?? {},
},
scoreMeta: data.scoreMeta ?? {},
};
@@ -515,11 +515,11 @@ t.test("#ParserFn", (t) => {
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();
});
@@ -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}.`
@@ -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<string, Difficulties["sdvx:Single"]> = new Map([
@@ -105,8 +105,6 @@ const ConvertEamSDVXCSV: ConverterFunction<SDVXEamusementCSVData, EmptyObject> =
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<SDVXEamusementCSVData, EmptyObject> =
scoreData: {
score,
lamp,
percent,
grade,
judgements: {},
optional: {
exScore,
@@ -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<MerScore, EmptyObject> = 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<MerScore, EmptyObject> = asyn
service: "MER",
scoreData: {
score: data.score,
percent,
grade,
lamp,
judgements: {},
optional: {
@@ -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,
@@ -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<BeatorajaScore, BeatorajaCo
throw new InternalFailure(`Song-Chart Desync with ${game} ${chart.chartID}.`);
}
const { grade, percent } = GenericGetGradeAndPercent(game, data.exscore, chart);
const optional: DryScore<
"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard"
>["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<BeatorajaScore, BeatorajaCo
"epr",
"lpr",
] as const) {
hitMeta[k] = data[k];
optional[k] = data[k];
}
hitMeta.epr = hitMeta.epr! + data.ems;
hitMeta.lpr = hitMeta.lpr! + data.lms;
optional.epr = (optional.epr ?? 0) + (data.ems ?? 0);
optional.lpr = (optional.lpr ?? 0) + (data.lms ?? 0);
const judgements = {
[game === "pms" ? "cool" : "pgreat"]: data.epg + data.lpg,
@@ -212,8 +210,8 @@ export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaCo
poor: data.epr + data.lpr + data.ems + data.lms,
};
hitMeta.fast = (["ebd", "egr", "epg", "epr", "ems"] as const).reduce((a, e) => 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<BeatorajaScore, BeatorajaCo
game,
importType,
scoreData: {
grade,
percent,
score: data.exscore,
lamp,
hitMeta,
optional,
judgements,
},
scoreMeta: {
client: context.client,
inputDevice: data.deviceType,
random,
// silly hack
// it's complaining that this might be assigned for 14k
// but it's not because of the conditions under which this is assigned.
// sorry!
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
random: random as any,
},
timeAchieved: context.timeReceived,
service: "Beatoraja IR",
@@ -257,15 +259,16 @@ function ConvertBeatorajaChartToTachi(chart: BeatorajaChart, playtype: Playtypes
level: "?",
levelNum: 0,
playtype,
rgcID: null,
songID: 0,
versions: [],
tierlistInfo: {},
data: {
hashMD5: chart.md5,
hashSHA256: chart.sha256,
notecount: chart.notes,
tableFolders: [],
aiLevel: null,
sglEC: null,
sglHC: null,
},
};
@@ -2,14 +2,12 @@ import {
InternalFailure,
SongOrChartNotFoundFailure,
} from "../../../framework/common/converter-failures";
import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils";
import { FERVIDEX_LAMP_LOOKUP, SplitFervidexChartRef } from "../fervidex/converter";
import { FindChartOnInGameIDVersion } 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 { FervidexStaticContext, FervidexStaticScore } from "./types";
import type { Lamps } from "tachi-common";
export const ConverterIRFervidexStatic: ConverterFunction<
FervidexStaticScore,
@@ -41,12 +39,10 @@ export const ConverterIRFervidexStatic: ConverterFunction<
throw new InternalFailure(`Song ${chart.songID} (iidx) has no parent song?`);
}
const { percent, grade } = GenericGetGradeAndPercent("iidx", data.ex_score, chart);
const optional: { bp?: number | null } = {};
if (data.miss_count !== undefined) {
hitMeta.bp = data.miss_count === -1 ? null : data.miss_count;
optional.bp = data.miss_count === -1 ? null : data.miss_count;
}
const dryScore: DryScore<"iidx:DP" | "iidx:SP"> = {
@@ -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: {},
};
@@ -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<FervidexScore>["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<FervidexScore, FervidexConte
throw new InvalidScoreFailure(`Invalid value of gauge ${gauge}.`);
}
const { percent, grade } = GenericGetGradeAndPercent("iidx", data.ex_score, chart);
let bp: number | null = data.bad + data.poor;
if (data.dead) {
@@ -207,9 +203,7 @@ export const ConverterIRFervidex: ConverterFunction<FervidexScore, FervidexConte
timeAchieved: context.timeReceived,
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: {
pgreat: data.pgreat,
great: data.great,
@@ -226,15 +220,16 @@ export const ConverterIRFervidex: ConverterFunction<FervidexScore, FervidexConte
gauge,
bp,
comboBreak: data.combo_break,
gsm: data["2dx-gsm"],
gsmEasy: data["2dx-gsm"]?.EASY,
gsmNormal: data["2dx-gsm"]?.NORMAL,
gsmHard: data["2dx-gsm"]?.HARD,
gsmEXHard: data["2dx-gsm"]?.EX_HARD,
},
},
scoreMeta: {
assist: TachifyAssist(data.option?.assist),
gauge: TachifyGauge(data.option?.gauge),
// @ts-expect-error Awkward expansion of iidx:SP|DP strings here causes this
// to complain that [x,x] is not assignable to SP randoms. This is a lazy ignore!
random:
chart.playtype === "SP"
? TachifyRandom(data.option?.style)
@@ -2,13 +2,12 @@ import {
InternalFailure,
SongOrChartNotFoundFailure,
} from "lib/score-import/framework/common/converter-failures";
import { GenericGetGradeAndPercent } from "lib/score-import/framework/common/score-utils";
import { FindSDVXChartOnInGameIDVersion } from "utils/queries/charts";
import { FindSongOnID } from "utils/queries/songs";
import type { ConverterFunction } from "../../common/types";
import type { KsHookSV6CContext, KsHookSV6CScore } from "./types";
import type { DryScore } from "lib/score-import/framework/common/types";
import type { Lamps } from "tachi-common";
import type { GetEnumValue } from "tachi-common/types/metrics";
export const ConverterIRKsHookSV6C: ConverterFunction<KsHookSV6CScore, KsHookSV6CContext> = async (
data,
@@ -36,8 +35,6 @@ export const ConverterIRKsHookSV6C: ConverterFunction<KsHookSV6CScore, KsHookSV6
throw new InternalFailure(`Song ${chart.songID} (sdvx) has no parent song?`);
}
const { percent, grade } = GenericGetGradeAndPercent("sdvx", data.score, chart);
const dryScore: DryScore<"sdvx:Single"> = {
game: "sdvx",
service: "kshook SV6C",
@@ -46,8 +43,6 @@ export const ConverterIRKsHookSV6C: ConverterFunction<KsHookSV6CScore, KsHookSV6
timeAchieved: context.timeReceived,
scoreData: {
score: data.score,
percent,
grade,
lamp: SV6CConvertLamp(data.clear),
judgements: {
critical: data.critical,
@@ -66,7 +61,9 @@ export const ConverterIRKsHookSV6C: ConverterFunction<KsHookSV6CScore, KsHookSV6
return { song, chart, dryScore };
};
export function SV6CConvertLamp(clear: KsHookSV6CScore["clear"]): GetEnumValue<"sdvx:Single", "lamp"> {
export function SV6CConvertLamp(
clear: KsHookSV6CScore["clear"]
): GetEnumValue<"sdvx:Single", "lamp"> {
if (clear === "CLEAR_PLAYED") {
return "FAILED";
} else if (clear === "CLEAR_EFFECTIVE") {
+2 -2
View File
@@ -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)) {
@@ -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
);
@@ -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<BeatorajaJudgements>) {
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.
+3
View File
@@ -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<T> = { -readonly [K in keyof T]: T[K] };