checkpoint: 22

This commit is contained in:
zkldi
2023-01-19 16:39:38 +00:00
parent e23e290efc
commit bae2775cf0
44 changed files with 289 additions and 253 deletions
+22 -9
View File
@@ -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<GPTString, GamePTConfig>;
} as const satisfies Record<GPTString, INTERNAL_GAME_PT_CONFIG>;
/**
* 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 extends GPTString>(gpt: GPTString) {
return GAME_PT_CONFIGS[gpt] as unknown as SpecificGamePTConfig<GPT>;
export function GetSpecificGPTConfig<GPT extends GPTString>(gpt: GPT) {
return GAME_PT_CONFIGS[gpt];
}
export const allSupportedGames = Object.keys(GAME_CONFIGS) as Array<Game>;
export const allGPTStrings = Object.keys(GAME_PT_CONFIGS) as Array<GPTString>;
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]);
}
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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;
+3 -3
View File
@@ -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;
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
-2
View File
@@ -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";
+2 -2
View File
@@ -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),
+4 -1
View File
@@ -438,7 +438,10 @@ export type QuestSubscriptionDocument = {
export type ScoreData<GPT extends GPTString = GPTString> = DerivedMetrics[GPT] &
ProvidedMetrics[GPT] & {
judgements: Partial<Record<Judgements[GPT], integer | null>>;
optional: Partial<OptionalMetrics[GPT]>;
optional: Partial<OptionalMetrics[GPT]> & {
enumIndexes: OptionalEnumIndexes<GPT>;
};
enumIndexes: ScoreEnumIndexes<GPT>;
};
export interface ScoreDocument<GPT extends GPTString = GPTString> {
+37
View File
@@ -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<typeof GAME_PT_CONFIGS[G]["derivedMetrics"]>;
};
/**
* Top level metrics on a score.
*/
export type ScoreMetrics = {
[G in GPTString]: DerivedMetrics[G] & ProvidedMetrics[G];
};
export type OptionalMetrics = {
[G in GPTString]: AllFieldsNullableOptional<
ExtractMetrics<typeof GAME_PT_CONFIGS[G]["optionalMetrics"]>
>;
};
/**
* Alongside strings, we want to store integers to represent the integer value
* of enums.
*/
export type ScoreEnumIndexes<GPT extends GPTString> = Record<
ExtractEnumMetricNames<ConfDerivedMetrics[GPT] & ConfProvidedMetrics[GPT]>,
integer
>;
/**
* Same as ScoreEnumIndexes but for the optional properties on a score.
*/
export type OptionalEnumIndexes<GPT extends GPTString> = Record<
ExtractEnumMetricNames<ConfOptionalMetrics[GPT]>,
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
+1 -1
View File
@@ -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<string, ConfScoreMetric>;
derivedMetrics: Record<string, ConfScoreMetric>;
+2 -46
View File
@@ -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<S extends string> {
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 ConfScoreMetric> = M extends ConfDecimalScoreMetric
? number
: M extends ConfIntegerScoreMetric
? integer
: M extends ConfEnumScoreMetric<infer V>
? EnumValue<V>
: M extends ConfGraphScoreMetric
? Array<number>
: M extends ConfNullableGraphScoreMetric
? Array<number | null>
: never;
/**
* Given a Metric Type, turn it into its evaluated form with the caveat that
* enums become strings instead of EnumValue<string>.
*
* 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 ConfScoreMetric> = M extends ConfDecimalScoreMetric
? number
: M extends ConfIntegerScoreMetric
? integer
@@ -176,19 +145,6 @@ export type ExtractMetrics<R extends Record<string, ConfScoreMetric>> = {
[K in keyof R]: ExtractMetricValue<R[K]>;
};
/**
* 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<R extends Record<string, ConfScoreMetric>> = {
[K in keyof R]: DryExtractMetricType<R[K]>;
};
// 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<ConfProvidedMetrics[GPT]>, chart: ChartDocument<GPT>) => V;
> = (mandatoryMetrics: ExtractMetrics<ConfProvidedMetrics[GPT]>, chart: ChartDocument<GPT>) => V;
/**
* A function that will derive this metric, given a function of other metrics and
+1 -1
View File
@@ -200,7 +200,7 @@ export function FormatChart(
// For games with 'BP', show that next to the clear.
export function IIDXBMSLampGoalFormatter(pb: PBScoreDocument<GPTStrings["bms" | "iidx" | "pms"]>) {
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;
+16 -3
View File
@@ -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,
});
}
-4
View File
@@ -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
);
}
@@ -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;
@@ -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<GPT extends GPTString, V extends string> = (
profileRatings: UserGameStats<GPT>["ratings"]
profileRatings: SpecificUserGameStats<GPT>["ratings"]
) => V | null;
// absolutely stupid magic.
@@ -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<TMetrics extends Record<string, unknown>> = {
[K in keyof TMetrics]: TMetrics[K] extends EnumValue<infer V> ? V : TMetrics[K];
};
export type DryScoreData<GPT extends GPTString> = DryExtractMetrics<ConfProvidedMetrics[GPT]> & {
optional: Mutable<ExtractEnumValues<OptionalMetrics[GPT]>>;
export type DryScoreData<GPT extends GPTString> = ExtractMetrics<ConfProvidedMetrics[GPT]> & {
optional: Mutable<OptionalMetrics[GPT]>;
judgements: Partial<Record<Judgements[GPT], integer | null>>;
};
@@ -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<GPT extends GPTString>(
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 extends GPTString>(gpt: GPT, metrics: any, logger: KtLogger) {
const gptConfig = GetGPTConfig(gpt);
const indexes: Record<string, integer> = {};
const optionalIndexes: Record<string, integer> = {};
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 extends GPTString>(
gpt: GPT,
dryScoreData: DryScore<GPT>["scoreData"],
chart: ChartDocument<GPT>
chart: ChartDocument<GPT>,
logger: KtLogger
) {
const derivedMetrics = DeriveMetrics(gpt, dryScoreData, chart);
@@ -170,5 +220,12 @@ export function CreateFullScoreData<GPT extends GPTString>(
} as unknown as ScoreData<GPT>;
// ^ 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;
}
@@ -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"),
@@ -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,
@@ -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> {
): 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,
@@ -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,
@@ -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) {
@@ -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<ScoreDocument | null> {
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;
@@ -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,
@@ -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 {};
}
@@ -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<Record<Classes[GPTString], string | null>>
): ClassProvider {
return (game, playtype, userID, ratings, logger) => {
const gptConfig = GetGamePTConfig(game, playtype);
return (gptString, userID, ratings, logger) => {
const gptConfig = GetGPTConfig(gptString);
const newObj: Partial<ClassRecords<GPTString>> = {};
@@ -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;
}
+11 -10
View File
@@ -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;
}
+1 -1
View File
@@ -248,7 +248,7 @@ export function HumaniseGoalProgress(
playtype,
userPB.scoreData.score,
userPB.scoreData.percent,
userPB.scoreData.grade.string,
userPB.scoreData.grade,
fmtFn
);
@@ -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);
}
@@ -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) {
@@ -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",
+13 -5
View File
@@ -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;
+3 -5
View File
@@ -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<PBScoreDocument>) {
const gradeDist: Partial<Record<Grades[GPTString], integer>> = {};
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;
}
}
+8 -6
View File
@@ -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);