mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-13 09:48:27 +03:00
checkpoint: 22
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user