checkpoint: 4

This commit is contained in:
zkldi
2023-01-19 16:39:16 +00:00
parent 727387dfd1
commit d39b541767
22 changed files with 70 additions and 557 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
* Utilities *specifically* for game/gpt configuration.
*/
import type { integer } from "../types";
import type { ClassInfo } from "../types/_TEMPNAME-game-config-inner";
import type { ClassInfo } from "../types/game-config";
export function NoDecimalPlace(value: number): string {
return value.toFixed(0);
+14 -2
View File
@@ -162,7 +162,19 @@ export const BMS_7K_CONF = {
additionalMetrics: {
...FAST_SLOW_MAXCOMBO,
// TODO
bp: { type: "INTEGER" },
gauge: { type: "DECIMAL" },
gaugeHistory: { type: "GRAPH" },
epg: { type: "INTEGER" },
egr: { type: "INTEGER" },
egd: { type: "INTEGER" },
ebd: { type: "INTEGER" },
epr: { type: "INTEGER" }, //
lpg: { type: "INTEGER" },
lgr: { type: "INTEGER" },
lgd: { type: "INTEGER" },
lbd: { type: "INTEGER" },
lpr: { type: "INTEGER" },
},
scoreRatingAlgs: {
@@ -218,7 +230,7 @@ export const BMS_7K_CONF = {
orderedJudgements: ["pgreat", "great", "good", "bad", "poor"],
supportedVersions: [],
chartSets: [],
supportedTierlists: {
"sgl-EC": { description: "Sieglinde Easy Clear ratings." },
+1 -1
View File
@@ -90,7 +90,7 @@ export const CHUNITHM_SINGLE_CONF = {
orderedJudgements: ["jcrit", "justice", "attack", "miss"],
supportedVersions: ["paradiselost"],
chartSets: ["Paradise Lost"],
supportedTierlists: {},
+1 -1
View File
@@ -102,7 +102,7 @@ export const GITADORA_GITA_CONF = {
orderedJudgements: ["perfect", "great", "good", "ok", "miss"],
supportedVersions: ["Konaste"],
chartSets: ["Konaste"],
supportedTierlists: {},
+4 -5
View File
@@ -1,10 +1,9 @@
import { FAST_SLOW_MAXCOMBO } from "./_common";
import { ExtractMetrics } from "../../types/metrics";
import { ClassValue } from "../config-utils";
import type {
import {
INTERNAL_GAME_CONFIG,
INTERNAL_GPT_CONFIG as INTERNAL_GPT_CONFIG,
INTERNAL_GPT_CONFIG,
} from "../../types/internals";
import { ClassValue } from "../config-utils";
export const IIDX_CONF = {
defaultPlaytype: "SP",
@@ -159,7 +158,7 @@ export const IIDX_SP_CONF = {
orderedJudgements: ["pgreat", "great", "good", "bad", "poor"],
supportedVersions: [
chartSets: [
"3rd Style CS",
"4th Style CS",
"5th Style CS",
+1 -1
View File
@@ -92,7 +92,7 @@ export const ITG_STAMINA_CONF = {
orderedJudgements: ["15ms", "fantastic", "excellent", "great", "decent", "wayoff", "miss"],
supportedVersions: [],
chartSets: [],
supportedTierlists: {},
+2 -2
View File
@@ -86,14 +86,14 @@ export const JUBEAT_SINGLE_CONF = {
orderedJudgements: ["perfect", "great", "good", "poor", "miss"],
supportedVersions: [
chartSets: [
"jubeat",
"ripples",
"knit",
"copious",
"saucer",
"prop",
"qubell",
"Qubell",
"clan",
"festo",
],
+1 -1
View File
@@ -155,7 +155,7 @@ export const MAIMAI_DX_SINGLE_CONF = {
orderedJudgements: ["pcrit", "perfect", "great", "good", "miss"],
supportedVersions: ["universeplus"],
chartSets: ["universeplus"],
supportedTierlists: {},
+1 -6
View File
@@ -67,18 +67,13 @@ export const MUSECA_SINGLE_CONF = {
difficultyOrder: ["Green", "Yellow", "Red"],
difficultyShorthand: { Green: "G", Yellow: "Y", Red: "R" },
defaultDifficulty: "Red",
difficultyColours: {
Green: COLOUR_SET.green,
Yellow: COLOUR_SET.vibrantYellow,
Red: COLOUR_SET.red,
},
},
supportedClasses: {},
orderedJudgements: ["critical", "near", "miss"],
supportedVersions: ["1 + 1/2", "1 + 1/2 Rev. B"],
chartSets: ["1 + 1/2", "1 + 1/2 Rev. B"],
supportedTierlists: {},
+1 -1
View File
@@ -112,7 +112,7 @@ export const PMS_CONTROLLER_CONF = {
orderedJudgements: ["cool", "great", "good", "bad", "poor"],
supportedVersions: [],
chartSets: [],
supportedTierlists: {
"sgl-EC": { description: "Sieglinde Easy Clear ratings." },
+1 -1
View File
@@ -110,7 +110,7 @@ export const POPN_9B_CONF = {
orderedJudgements: ["cool", "great", "good", "bad"],
supportedVersions: ["peace", "kaimei"],
chartSets: ["peace", "Kaimei Riddles"],
supportedTierlists: {},
+1 -1
View File
@@ -145,7 +145,7 @@ export const SDVX_SINGLE_CONF = {
orderedJudgements: ["critical", "near", "miss"],
supportedVersions: [
chartSets: [
"BOOTH",
"infinite infection",
"GRAVITY WARS",
+1 -1
View File
@@ -78,7 +78,7 @@ export const USC_CONTROLLER_CONF = {
orderedJudgements: ["critical", "near", "miss"],
supportedVersions: [],
chartSets: [],
supportedTierlists: {},
+1 -1
View File
@@ -123,7 +123,7 @@ export const WACCA_SINGLE_CONF = {
orderedJudgements: ["marvelous", "great", "good", "miss"],
supportedVersions: ["REVERSE"],
chartSets: ["REVERSE"],
supportedTierlists: {},
+5 -1
View File
@@ -1,6 +1,10 @@
/* eslint-disable lines-around-comment */
import { FormatSieglindeBMS, FormatSieglindePMS, FormatMaimaiDXRating } from "./config-utils";
import { COLOUR_SET } from "../constants/colour-set";
import {
FormatSieglindeBMS,
FormatSieglindePMS,
FormatMaimaiDXRating,
} from "./config-utils";
import type { GPTTierlists } from "..";
import type { ClassInfo } from "./game-classes";
-73
View File
@@ -1,73 +0,0 @@
import { GetGameConfig } from "./old-config";
import type { IDStrings, GPTSupportedVersions, Game, Playtype } from "../types";
export type Versions = {
[I in IDStrings]: {
[K in GPTSupportedVersions[I]]: string;
};
};
const prettyGitadoraVersions: Versions["gitadora:Dora"] = {
konaste: "Konaste",
};
// WHEN YOU UPDATE THIS, **MAKE SURE** YOU UPDATE CONFIG.TS ACCORDINGLY.
export const PrettyVersions: Versions = {
"popn:9B": { peace: "peace", kaimei: "Kaimei Riddles" },
"bms:7K": {},
"bms:14K": {},
"chunithm:Single": {
paradiselost: "Paradise Lost",
},
"gitadora:Gita": prettyGitadoraVersions,
"gitadora:Dora": prettyGitadoraVersions,
"museca:Single": {
1.5: "1 + 1/2",
"1.5-b": "1 + 1/2 Rev. B",
},
"sdvx:Single": {
booth: "BOOTH",
inf: "Infinite Infection",
gw: "GRAVITY WARS",
heaven: "HEAVENLY HAVEN",
vivid: "VIVID WAVE",
exceed: "EXCEED GEAR",
konaste: "Konaste",
},
"usc:Keyboard": {},
"usc:Controller": {},
"wacca:Single": {
reverse: "REVERSE",
},
"jubeat:Single": {
festo: "festo",
clan: "clan",
qubell: "Qubell",
saucer: "saucer",
prop: "prop",
copious: "copious",
knit: "knit",
ripples: "ripples",
jubeat: "jubeat",
},
"pms:Controller": {},
"pms:Keyboard": {},
"itg:Stamina": {},
"maimaidx:Single": {
universeplus: "UNiVERSE PLUS",
},
};
export function PrettyFormatGameVersion<I extends IDStrings>(
idString: I,
version: GPTSupportedVersions[I]
) {
// lol
const [game] = idString.split(":") as [Game, Playtype];
const ver = PrettyVersions[idString][version];
const gameConfig = GetGameConfig(game);
return `${gameConfig.name} (${ver})`;
}
-46
View File
@@ -1,46 +0,0 @@
import { CalculateESD, ESDCompare, PercentCompare } from "./esd";
import { GetGamePTConfig } from "../config/old-config";
import t from "tap";
function approx(t: Tap.Test, given: number, recieved: number, acc = 0.01) {
if (Math.abs(given - recieved) < acc) {
return t.pass();
}
return t.fail(`Expected ${given} to be within ${acc} of ${recieved}.`);
}
const gptConfig = GetGamePTConfig("iidx", "SP");
if (!gptConfig.supportsESD) {
throw new Error(`IIDX:SP no longer supports esd, these tests won't work.`);
}
const judgementWindows = gptConfig.judgementWindows;
t.test("#CalculateESD", (t) => {
const jdg = judgementWindows;
approx(t, CalculateESD(jdg, 0.8), 17.58);
approx(t, CalculateESD(jdg, 0), 200);
t.throws(() => CalculateESD(jdg, 0, true));
t.end();
});
t.test("#ESDCompare", (t) => {
approx(t, ESDCompare(16, 14), 6.42);
approx(t, ESDCompare(16, 14, 1), ESDCompare(16, 14));
approx(t, ESDCompare(0, 200, 1), -68.27);
t.end();
});
t.test("#PercentCompare", (t) => {
const jdg = judgementWindows;
approx(t, PercentCompare(jdg, 0.99, 0.98), -5.53);
approx(t, PercentCompare(jdg, 0.99, 0.98), PercentCompare(jdg, 0.99, 0.98, 1));
t.end();
});
-192
View File
@@ -1,192 +0,0 @@
/**
* Cumulative Distribution Function
* https://en.wikipedia.org/wiki/Cumulative_distribution_function
*/
function cdf(x: number, mean: number, variance: number) {
return 0.5 * (1 + erf((x - mean) / Math.sqrt(2 * variance)));
}
/**
* Error Function
* https://en.wikipedia.org/wiki/Error_function
*/
function erf(x: number) {
// save the sign of x
const sign = x >= 0 ? 1 : -1;
const absX = Math.abs(x);
// constants
const a1 = 0.254829592;
const a2 = -0.284496736;
const a3 = 1.421413741;
const a4 = -1.453152027;
const a5 = 1.061405429;
const p = 0.3275911;
// A&S formula 7.1.26
const t = 1.0 / (1.0 + p * absX);
const y = 1.0 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-absX * absX);
return sign * y; // erf(-x) = -erf(x);
}
function CDFBetween(lowBound: number, highBound: number, mean: number, variance: number) {
// Since the normal distribution is symmetrical, we want to double this, as the cut will only get one tail.
return 2 * (cdf(highBound, mean, variance) - cdf(lowBound, mean, variance));
}
// This is a direct port of ESD-JS to typescript. Maybe this should be released as its own module some day.
const MEAN = 0;
/**
* Gets the percent this score would roughly be, given a standard deviation.
* @param judgements - The judgements this game uses.
* @param stddev - The standard deviation to estimate the percent of.
* @param largestValue - The largest "value" of a judgement in this game. This lets us normalise results.
* @returns The percent this std. deviation would produce.
*/
function StdDeviationToPercent(
judgements: Array<ESDJudgementFormat>,
stddev: number,
largestValue: number
) {
let lastJudgeMSBorder = 0;
let prbSum = 0;
for (const judge of judgements) {
const nVal = CDFBetween(lastJudgeMSBorder, judge.msBorder, MEAN, stddev ** 2) * judge.value;
lastJudgeMSBorder = judge.msBorder;
prbSum = prbSum + nVal;
}
prbSum = prbSum / largestValue;
return prbSum;
}
const ACCEPTABLE_ERROR = 0.001;
const MAX_ITERATIONS = 50;
export interface ESDJudgementFormat {
name: string;
msBorder: number;
value: number;
}
/**
* Given judgements and a percent, estimate the standard deviation needed to get a score
* with that percent.
* @param judgements - The judgements for this game.
* @param percent - The percent to estimate SD needed to get.
* @param errOnInaccuracy - Whether or whether not to throw if the estimate is not accurate enough.
* @returns
*/
export function CalculateESD(
judgements: Array<ESDJudgementFormat>,
percent: number,
errOnInaccuracy = false
): number {
if (percent > 1 || percent < 0 || Number.isNaN(percent)) {
throw new Error(
"(ESD) Invalid percent. Percent must be between 0 and 1, and also a number."
);
}
const largestValue = judgements.slice(0).sort((a, b) => b.value - a.value)[0]?.value;
if (largestValue === undefined) {
throw new Error(`Empty array of judgements passed?`);
}
// massive optimisation possible here by using better initial estimates with precalc'd table of values.
// until then, it's just kinda slow.
let minSD = 0;
let maxSD = 200;
let estSD = (minSD + maxSD) / 2;
// So, fundamentally the function that takes SD and returns estimated percent is NOT invertible.
// as in, it is very literally not invertible.
// if you figure it out, let me know.
// until then; we make MAX_ITERATIONS attempts at finding a value within the acceptable range of error.
// the defaults for these are 100 for iterations, and 0.001 for error.
// for most of what i've tested, this has been fine.
for (let i = 0; i < MAX_ITERATIONS; i++) {
const estimatedPercent = StdDeviationToPercent(judgements, estSD, largestValue);
if (Math.abs(estimatedPercent - percent) < ACCEPTABLE_ERROR) {
return estSD;
}
if (estimatedPercent < percent) {
maxSD = estSD;
} else {
minSD = estSD;
}
if (estSD === (minSD + maxSD) / 2) {
// if it isn't moving, just terminate
break;
}
estSD = (minSD + maxSD) / 2;
}
if (errOnInaccuracy) {
throw new Error(`(ESD-JS) Did not reach value within MAX_ITERATIONS (${MAX_ITERATIONS})`);
}
return estSD;
}
/**
* Compares two ESD values such that 1->2 produces a larger value than 101->102.
* @param baseESD - The first ESD to compare.
* @param compareESD - The second ESD to compare.
* @param cdeg - The degrees of confidence to use. This should be 1.
* @returns A number between -100 and 100.
*/
export function ESDCompare(baseESD: number, compareESD: number, cdeg = 1): number {
const CONFIDENCE_DEGREE = cdeg;
const BASE_CASE = CDFBetween(-1 * CONFIDENCE_DEGREE, CONFIDENCE_DEGREE, 0, 1) / 2;
let inv = false;
let variance;
let bound;
if (compareESD > baseESD) {
inv = true;
variance = compareESD ** 2;
bound = CONFIDENCE_DEGREE * baseESD;
} else {
variance = baseESD ** 2;
bound = CONFIDENCE_DEGREE * compareESD;
}
const esdc = CDFBetween(-1 * bound, bound, 0, variance) / 2;
let besdc = BASE_CASE - esdc;
if (inv) {
besdc = besdc * -1;
}
return besdc * 100;
}
/**
* Converts two percents to ESD, then runs ESDCompare.
*/
export function PercentCompare(
judgements: Array<ESDJudgementFormat>,
baseP: number,
compareP: number,
cdeg = 1
): number {
const e1 = CalculateESD(judgements, baseP);
const e2 = CalculateESD(judgements, compareP);
return ESDCompare(e1, e2, cdeg);
}
@@ -1,3 +1,6 @@
// Types related to game configuration. These are used by our `internal` game config
// types
export interface RatingAlgorithmConfig {
/**
* Write a short descrption for this rating algorithm.
@@ -12,14 +15,6 @@ export interface RatingAlgorithmConfig {
formatter?: (value: number) => string;
}
export interface TierlistConfig {
/**
* Write a short description explaining what this tierlist is and what the values
* generally mean.
*/
description: string;
}
export interface ClassInfo {
display: string;
id: string;
+23 -35
View File
@@ -1,5 +1,6 @@
import type { GAME_CONFIGS, GAME_PT_CONFIGS } from "../config/config";
import type { FixedDifficulties } from "./_TEMPNAME-game-config-inner";
import type { MatchTypes } from "./batch-manual";
import type { ClassConfig, FixedDifficulties, RatingAlgorithmConfig } from "./game-config";
import type { ExtractEnumMetricNames, ExtractMetrics } from "./metrics";
import type { ExtractArrayElementType } from "./utils";
@@ -81,6 +82,10 @@ export type Judgements = {
[G in GPTString]: ExtractArrayElementType<typeof GAME_PT_CONFIGS[G]["orderedJudgements"]>;
};
export type ChartSets = {
[G in GPTString]: ExtractArrayElementType<typeof GAME_PT_CONFIGS[G]["chartSets"]>;
};
export type ScoreRatingAlgorithms = {
[G in GPTString]: keyof typeof GAME_PT_CONFIGS[G]["scoreRatingAlgs"];
};
@@ -186,7 +191,7 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
* The idea of additionalMetrics allow us to store useful metrics about scores
* without necessitating that they exist on arrival. Incredibly convenient.
*/
additionalMetrics: Record<string, ScoreMetric>;
additionalMetrics: AdditionalMetrics[GPT];
/**
* What rating algorithms may a score have attached onto it for this GPT?
@@ -195,7 +200,7 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
* server config. By defining them here, the typesystem will enforce that you
* implement them elsewhere.
*/
scoreRatingAlgs: Record<string, RatingAlgorithmConfig>;
scoreRatingAlgs: Record<ScoreRatingAlgorithms[GPT], RatingAlgorithmConfig>;
/**
* What rating algorithms may a session have attached onto it for this GPT?
@@ -204,7 +209,7 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
* server config. By defining them here, the typesystem will enforce that you
* implement them elsewhere.
*/
sessionRatingAlgs: Record<string, RatingAlgorithmConfig>;
sessionRatingAlgs: Record<SessionRatingAlgorithms[GPT], RatingAlgorithmConfig>;
/**
* What rating algorithms may a profile have attached onto it for this GPT?
@@ -224,7 +229,7 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
* server config. By defining them here, the typesystem will enforce that you
* implement them elsewhere.
*/
profileRatingAlgs: Record<string, RatingAlgorithmConfig>;
profileRatingAlgs: Record<ProfileRatingAlgorithms[GPT], RatingAlgorithmConfig>;
/**
* What classes may a profile have attached onto it for this GPT?
@@ -242,21 +247,21 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
*
* @note This should be one of the keys in scoreRatingAlgs.
*/
defaultScoreRatingAlg: string;
defaultScoreRatingAlg: ScoreRatingAlgorithms[GPT];
/**
* What's the default session rating algorithm for this GPT?
*
* @note This should be one of the keys in sessionRatingAlgs.
*/
defaultSessionRatingAlg: string;
defaultSessionRatingAlg: SessionRatingAlgorithms[GPT];
/**
* What's the default profile rating algorithm for this GPT1?
*
* @note This should be one of the keys in sessionRatingAlgs.
*/
defaultProfileRatingAlg: string;
defaultProfileRatingAlg: ProfileRatingAlgorithms[GPT];
/**
* How does this GPT handle difficulties?
@@ -276,39 +281,27 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
*
* These should be ordered from **best to worst**.
*/
orderedJudgements: ReadonlyArray<string>;
orderedJudgements: ReadonlyArray<Judgements[GPT]>;
/**
* What versions do we support for this GPT?
* What sets of charts do we have for this GPT?
*
* Versions are the way tachi disambiguates cases (typically in arcade games) where
* Chart Sets are the way tachi disambiguates cases (typically in arcade games) where
* a chart is modified.
* For example, Rising in the Sun
* (https://remywiki.com/Rising_in_the_Sun(original_mix))
* was removed in IIDX 21 ("spada"), and revived in IIDX 27 with entirely different
* was removed in IIDX 21, and revived in IIDX 27 with entirely different
* charts. Although these charts are completely different,
* they use the same song and difficulty
* so Rising in the Sun SP ANOTHER could mean two things!.
*
* We need to handle these cases, so we disambiguate by attaching "versions" onto
* every chart. These "versions" indicate what sets of chart states they appeared in
* for this GPT. Then, when a score is coming in, it can indicate what version this
* score was from. That way, we can make sure they resolve to the right chart.
* We need to handle these cases, so we disambiguate by attaching "chart sets" onto
* every chart. These "chart sets" indicate what sets of chart states they
* appeared in for this GPT. Then, when a score is coming in, it can indicate what
* chartSet this score was on. That way, we can make sure they resolve to the right
* chart.
*/
supportedVersions: ReadonlyArray<string>;
/**
* What tierlists does this GPT have?
*
* Tierlists are a way of attaching custom rating values onto a chart.
* Unlike "chart.data", which may be overloaded with anything, defined tierlists
* *must* fit a known schema, allowing the UI to visually render any tierlists
* defined here.
*
* This feature is a bit redundant given "chart.data", and should be further
* investigated. My bad.
*/
supportedTierlists: Record<string, TierlistConfig>;
chartSets: ReadonlyArray<ChartSets[GPT]>;
/**
* What "matchTypes" should this game support for batch-manual imports? This
@@ -317,8 +310,3 @@ export interface GamePTConfig<GPT extends GPTString = GPTString> {
*/
supportedMatchTypes: ReadonlyArray<MatchTypes>;
}
const gptConf: ExtractEnumMetricNames<DerivedMetrics["iidx:SP"]> = {
lamp: "a",
asdf: 213,
};
+5 -170
View File
@@ -1,10 +1,5 @@
import type {
RatingAlgorithmConfig,
ClassConfig,
DifficultyConfig,
TierlistConfig,
} from "./_TEMPNAME-game-config-inner";
import type { MatchTypes } from "./batch-manual";
import type { RatingAlgorithmConfig, ClassConfig, DifficultyConfig } from "./game-config";
import type { ScoreMetric } from "./metrics";
/**
@@ -15,197 +10,37 @@ import type { ScoreMetric } from "./metrics";
*
* @see {GamePTConfig} for the intended-to-use type. This is an *outline* for a GPT
* config, and has significantly less typesafety.
*
* Documentation for this type can be found in `game-support.ts`, which has the
* GamePTConfig type, which actually has documentation.
*/
export type INTERNAL_GPT_CONFIG = Readonly<{
/**
* What metrics **must** be provided in order for this score to be usable by
* Tachi?
*
* This is intended for things like Score, Lamp, etc. Things that quite fundamentally
* *are* the metrics of the score.
*/
providedMetrics: Record<string, ScoreMetric>;
/**
* What metrics do we want to exist on score documents, but don't need to be
* provided?
*
* In simple terms, all of these metrics **MUST** be derivable by a DETERMINISTIC
* function of f(mandatoryMetrics, chartThisScoreWasOn).
*
* This is for convenience/efficiency mainly. A good example would be "percent" for
* IIDX. Technically, we could recalculate it every single time we want to display
* it by dividing score by chart.data.notecount * 2, but that's horrendously
* inefficient.
*
* Furthermore, since these things are derived deterministically, they only ever
* need to be recalculated in extreme circumstances (an IIDX chart has changed its
* notecounts!!!). If mandatory metrics were to change, it's just now a different
* score.
*
* Another good example would be "Grade" for most games, as a grade is often just
* cutoffs applied on score values.
*/
derivedMetrics: Record<string, ScoreMetric>;
/**
* What's the default metric for this GPT?
*
* This will be used to order leaderboard rankings.
*
* @note This **MUST** be one of the mandatory or derived keys.
*/
defaultMetric: string;
/**
* What's the preferred default enum for this GPT?
*
* Enum types are used across the UI (think folder breakdown charts), and the game
* should generally declare a default.
*
* @note This **MUST** be one of the mandatory or derived keys.
*/
preferredDefaultEnum: string;
/**
* What metrics *can* we store about scores, but don't necessarily *need*?
*
* Of course, in a perfect world we'd store all the metrics always all the time!
* But a lot of import methods (eamusement CSV, etc) would be filtered out by
* mandating the existence of a lot of these metrics.
*
* The idea of additionalMetrics allow us to store useful metrics about scores
* without necessitating that they exist on arrival. Incredibly convenient.
*/
additionalMetrics: Record<string, ScoreMetric>;
/**
* What rating algorithms may a score have attached onto it for this GPT?
*
* @note The implementations for these rating algorithms are handled in the
* server config. By defining them here, the typesystem will enforce that you
* implement them elsewhere.
*/
scoreRatingAlgs: Record<string, RatingAlgorithmConfig>;
/**
* What rating algorithms may a session have attached onto it for this GPT?
*
* @note The implementations for these rating algorithms are handled in the
* server config. By defining them here, the typesystem will enforce that you
* implement them elsewhere.
*/
sessionRatingAlgs: Record<string, RatingAlgorithmConfig>;
/**
* What rating algorithms may a profile have attached onto it for this GPT?
*
* @note This is **SPECIFICALLY** for numeric, calculatable metrics. This means
* that the metric *must* be calculatable *at all times* from the set of all
* scores this user has on this GPT.
*
* This is intended for numeric, continous data.
* If you want to store something with a fixed set of values, such as a user's
* "rating colour", use `supportedClasses`.
*
* If you want to store something that cannot be derived from the user's scores,
* such as their "Dan", use `supportedClasses`.
*
* @note The implementations for these rating algorithms are handled in the
* server config. By defining them here, the typesystem will enforce that you
* implement them elsewhere.
*/
profileRatingAlgs: Record<string, RatingAlgorithmConfig>;
/**
* What classes may a profile have attached onto it for this GPT?
*
* Classes are a *fixed*, *ordered* set of values.
* They may be a function of existing state (like "rating colours", where a user
* gets a new discrete colour when they go up certain ratings),
* or they may be provided by score imports, such as "dans", which cannot be
* derived from a player's scores or profile ratings.
*/
supportedClasses: Record<string, ClassConfig>;
/**
* What's the default score rating algorithm for this GPT?
*
* @note This should be one of the keys in scoreRatingAlgs.
*/
defaultScoreRatingAlg: string;
/**
* What's the default session rating algorithm for this GPT?
*
* @note This should be one of the keys in sessionRatingAlgs.
*/
defaultSessionRatingAlg: string;
/**
* What's the default profile rating algorithm for this GPT1?
*
* @note This should be one of the keys in sessionRatingAlgs.
*/
defaultProfileRatingAlg: string;
/**
* How does this GPT handle difficulties?
*
* "Difficulties" are used to allow one song to have multiple charts. Some games
* may have a known set of possible difficulties, such as "Easy", "Normal" and
* "Hard".
*
* Other games may have an unknown set of possible difficulties, such as osu!
* allowing any string (as long as its unique.)
*
*/
difficultyConfig: DifficultyConfig;
/**
* What judgements does this GPT have? These are typically timing-window names.
*
* These should be ordered from **best to worst**.
*/
orderedJudgements: ReadonlyArray<string>;
/**
* What versions do we support for this GPT?
*
* Versions are the way tachi disambiguates cases (typically in arcade games) where
* a chart is modified.
* For example, Rising in the Sun
* (https://remywiki.com/Rising_in_the_Sun(original_mix))
* was removed in IIDX 21 ("spada"), and revived in IIDX 27 with entirely different
* charts. Although these charts are completely different,
* they use the same song and difficulty
* so Rising in the Sun SP ANOTHER could mean two things!.
*
* We need to handle these cases, so we disambiguate by attaching "versions" onto
* every chart. These "versions" indicate what sets of chart states they appeared in
* for this GPT. Then, when a score is coming in, it can indicate what version this
* score was from. That way, we can make sure they resolve to the right chart.
*/
supportedVersions: ReadonlyArray<string>;
chartSets: ReadonlyArray<string>;
/**
* What tierlists does this GPT have?
*
* Tierlists are a way of attaching custom rating values onto a chart.
* Unlike "chart.data", which may be overloaded with anything, defined tierlists
* *must* fit a known schema, allowing the UI to visually render any tierlists
* defined here.
*
* This feature is a bit redundant given "chart.data", and should be further
* investigated. My bad.
*/
supportedTierlists: Record<string, TierlistConfig>;
/**
* What "matchTypes" should this game support for batch-manual imports? This
* allows us to disable things like "songTitle" resolutions for games like BMS,
* where song titles are absolutely not guaranteed to be unique.
*/
supportedMatchTypes: ReadonlyArray<MatchTypes>;
}>;
+3 -7
View File
@@ -69,13 +69,9 @@ export type ExtractMetricType<M extends ScoreMetric> = M extends DecimalScoreMet
* ExtractEnumMetricNames<{ score: { type: "INTEGER" } }, grade: { type: "ENUM" ... }>
* would return a type of "grade"
*/
export type ExtractEnumMetricNames<R extends Record<string, ScoreMetric>> = Omit<
R,
keyof {
// vvvv this is stupid
[K in keyof R]: R[K] extends EnumScoreMetric ? never : K;
}
>;
export type ExtractEnumMetricNames<R extends Record<string, ScoreMetric>> = {
[K in keyof R]: R[K] extends EnumScoreMetric ? K : never;
}[keyof R];
type PossibleMetrics = ExtractMetricType<ScoreMetric>;