From b7086edb89072bc80a8fa17a118bb573e08e2b39 Mon Sep 17 00:00:00 2001 From: zkldi Date: Wed, 7 Apr 2021 12:07:55 +0100 Subject: [PATCH] Properly Type scoreMeta using new IDstrings --- common/js/config.d.ts | 6 +- common/js/types.d.ts | 203 ++++++++++++++++++--------------- common/src/types.ts | 260 +++++++++++++++++++++++++++--------------- 3 files changed, 280 insertions(+), 189 deletions(-) diff --git a/common/js/config.d.ts b/common/js/config.d.ts index 92ed4591d..8a82486ae 100644 --- a/common/js/config.d.ts +++ b/common/js/config.d.ts @@ -46,7 +46,7 @@ declare const _default: { lampColours: ColourStuff; gradeColours: ColourStuff; serviceSupportedGames: Record; - defaultPlaytype: Record; + defaultPlaytype: Record; gameChartIndicators: { iidx: string[]; popn: string[]; @@ -120,7 +120,7 @@ declare const _default: { clearExpMultiplier: number; }; }; - validPlaytypes: Record; + validPlaytypes: Record; ScoreGradeDelta: typeof ScoreGradeDelta; judgeColours: { iidx: { @@ -339,7 +339,7 @@ declare const _default: { validHitData: Record; validHitMeta: Record; boundaryHCF: Record; - gameSpecificCalc: Partial>>>; + gameSpecificCalc: Partial>>>; expChartScale: Record; FormatDifficulty: typeof FormatDifficulty; DirectScoreGradeDelta: typeof DirectScoreGradeDelta; diff --git a/common/js/types.d.ts b/common/js/types.d.ts index db5f23d1a..2bae0854e 100644 --- a/common/js/types.d.ts +++ b/common/js/types.d.ts @@ -3,6 +3,20 @@ export interface CounterDocument { counterName: string; value: integer; } +/** + * IDStrings are an internal (ish) identifier used to identify + * Game + Playtype combos. These are used because TypeScript cannot accurately + * apply two levels of indexing to types, which makes writing generic interfaces + * like the ScoreDocument (Especially the ScoreDocument) *very* hard! + */ +export declare type IDStrings = "iidx:SP" | "iidx:DP" | "popn:9B" | "sdvx:Single" | "usc:Single" | "ddr:SP" | "maimai:Single" | "jubeat:Single" | "bms:7K" | "bms:14K" | "bms:5K" | "chunithm:Single" | "gitadora:Gita" | "gitadora:Dora"; +/** + * A utility type for creating an ID string given a game and playtype. + * It should be noted that typescript refuses to assert that + * IDString is a member of IDStrings. You're free to attempt to + * rewrite IDStrings to try and get this to work, I promise you it doesn't. + */ +export declare type IDString = `${G}:${P}`; /** * All MongoDB Documents require this field, or atleast they all have them in ktchi's DB. */ @@ -457,6 +471,44 @@ export interface UserMilestoneDocument extends MongoDBDocument { timeAchieved: integer | null; progress: integer; } +declare type RanOptions = "NONRAN" | "RANDOM" | "R-RANDOM" | "S-RANDOM" | "MIRROR"; +interface IIDXSPScoreMeta { + optionsRandom: RanOptions; + optionsAssist: "AUTO SCRATCH" | "5KEYS" | "LEGACY NOTE" | "ASCR + 5KEY" | "ASCR + LEGACY" | "5KEYS + LEGACY" | "FULL ASSIST"; + optionsRange: "SUDDEN+" | "HIDDEN+" | "SUD+ HID+" | "LIFT" | "LIFT SUD+"; + optionsGauge: "ASSISTED EASY" | "EASY" | "HARD" | "EX-HARD"; + pacemaker: "NO GRAPH" | "MY BEST" | "RIVAL 1" | "RIVAL 2" | "RIVAL 3" | "RIVAL 4" | "RIVAL 5" | "RIVAL NEXT" | "RIVAL BEST" | "RIVAL AVERAGE" | "NATIONAL BEST" | "NATIONAL AVERAGE" | "PREFECTURE BEST" | "PREFECTURE AVERAGE" | "CLASS BEST" | "CLASS AVERAGE" | "VENUE BEST" | "VENUE NEXT" | "PREVIOUS GHOST" | "PACEMAKER AAA" | "PACEMAKER AA" | "PACEMAKER A" | "PACEMAKER" | /* ??? */ "PACEMAKER NEXT" | "PACEMAKER NEXT+" | "PLAYER 1" | "PLAYER 2"; + pacemakerName: string; + pacemakerTarget: integer; +} +interface BMS7KScoreMeta { + optionsRandom: RanOptions; + inputDevice: string; +} +interface ScoreMetaLookup { + "iidx:SP": IIDXSPScoreMeta; + "iidx:DP": IIDXSPScoreMeta & { + optionsRandom: [RanOptions, RanOptions]; + }; + "popn:9B": Record; + "sdvx:Single": Record; + "usc:Single": BASE_VALID_HIT_META & { + gauge: number; + }; + "ddr:SP": Record; + "ddr:DP": Record; + "maimai:Single": Record; + "jubeat:Single": Record; + "museca:Single": Record; + "bms:7K": BMS7KScoreMeta; + "bms:14K": BMS7KScoreMeta & { + optionsRandom: [RanOptions, RanOptions]; + }; + "bms:5K": Record; + "chunithm:Single": Record; + "gitadora:Gita": Record; + "gitadora:Dora": Record; +} interface BASE_VALID_HIT_META { fast: integer; slow: integer; @@ -474,108 +526,77 @@ declare type BMSHitMeta = BASE_VALID_HIT_META & { } & { bp: integer; gauge: number; - random: "NONRAN" | "MIRROR" | "RANDOM" | "R-RANDOM" | "S-RANDOM"; diedAt: integer | null; - inputDevice: string; }; export interface HitMetaLookup { - iidx: { - SP: IIDXHitMeta; - DP: IIDXHitMeta; + "iidx:SP": IIDXHitMeta; + "iidx:DP": IIDXHitMeta; + "popn:9B": BASE_VALID_HIT_META & { + gauge: number; }; - museca: { - Single: BASE_VALID_HIT_META; + "sdvx:Single": BASE_VALID_HIT_META & { + gauge: number; }; - ddr: { - SP: BASE_VALID_HIT_META; - DP: BASE_VALID_HIT_META; - }; - maimai: { - SP: BASE_VALID_HIT_META; - DP: BASE_VALID_HIT_META; - }; - jubeat: { - Single: BASE_VALID_HIT_META; - }; - popn: { - "9B": BASE_VALID_HIT_META & { - gauge: number; - }; - }; - sdvx: { - Single: BASE_VALID_HIT_META & { - gauge: number; - }; - }; - chunithm: { - Single: BASE_VALID_HIT_META; - }; - gitadora: { - Gita: BASE_VALID_HIT_META; - Dora: BASE_VALID_HIT_META; - }; - usc: { - Single: BASE_VALID_HIT_META & { - gauge: number; - }; - }; - bms: { - "7K": BMSHitMeta; - "14K": BMSHitMeta; - "5K": BMSHitMeta; + "usc:Single": BASE_VALID_HIT_META & { + gauge: number; }; + "ddr:SP": BASE_VALID_HIT_META; + "ddr:DP": BASE_VALID_HIT_META; + "maimai:Single": BASE_VALID_HIT_META; + "jubeat:Single": BASE_VALID_HIT_META; + "museca:Single": BASE_VALID_HIT_META; + "bms:7K": BMSHitMeta; + "bms:14K": BMSHitMeta; + "bms:5K": BMSHitMeta; + "chunithm:Single": BASE_VALID_HIT_META; + "gitadora:Gita": BASE_VALID_HIT_META; + "gitadora:Dora": BASE_VALID_HIT_META; } declare type IIDXJudges = "pgreat" | "great" | "good" | "bad" | "poor"; declare type DDRJudges = "marvelous" | "perfect" | "great" | "good" | "boo" | "miss" | "ok" | "ng"; declare type GitadoraJudges = "perfect" | "great" | "good" | "ok" | "miss"; declare type SDVXJudges = "critical" | "near" | "miss"; export interface JudgementLookup { - iidx: { - SP: IIDXJudges; - DP: IIDXJudges & { - foo: string; - }; - }; - bms: { - "7K": IIDXJudges; - "14K": IIDXJudges; - "5K": IIDXJudges; - }; - museca: { - Single: "critical" | "near" | "miss"; - }; - ddr: { - SP: DDRJudges; - DP: DDRJudges; - }; - sdvx: { - Single: SDVXJudges; - }; - maimai: { - Single: "perfect" | "great" | "good" | "miss"; - }; - jubeat: { - Single: "perfect" | "great" | "good" | "bad" | "miss"; - }; - popn: { - "9B": "cool" | "great" | "good" | "miss"; - }; - chunithm: { - Single: "jcrit" | "justice" | "attack" | "miss"; - }; - gitadora: { - Gita: GitadoraJudges; - Dora: GitadoraJudges; - }; - usc: { - Single: SDVXJudges; - }; + "iidx:SP": IIDXJudges; + "iidx:DP": IIDXJudges; + "popn:9B": "cool" | "great" | "good" | "miss"; + "sdvx:Single": SDVXJudges; + "usc:Single": SDVXJudges; + "ddr:SP": DDRJudges; + "ddr:DP": DDRJudges; + "maimai:Single": "perfect" | "great" | "good" | "miss"; + "jubeat:Single": "perfect" | "great" | "good" | "bad" | "miss"; + "museca:Single": "critical" | "near" | "miss"; + "bms:7K": IIDXJudges; + "bms:14K": IIDXJudges; + "bms:5K": IIDXJudges; + "chunithm:Single": "jcrit" | "justice" | "attack" | "miss"; + "gitadora:Gita": GitadoraJudges; + "gitadora:Dora": GitadoraJudges; } -export interface ScoreDocument extends MongoDBDocument { +export interface GameSpecificCalcLookup { + "iidx:SP": "BPI" | "K%"; + "iidx:DP": "BPI"; + "popn:9B": never; + "sdvx:Single": "VF4" | "VF5"; + "usc:Single": "VF4" | "VF5"; + "ddr:SP": "MFCP"; + "ddr:DP": "MFCP"; + "maimai:Single": never; + "jubeat:Single": never; + "museca:Single": never; + "bms:7K": never; + "bms:14K": never; + "bms:5K": never; + "chunithm:Single": never; + "gitadora:Gita": never; + "gitadora:Dora": never; +} +export interface ScoreDocument> extends MongoDBDocument { service: string; - game: T; + game: G; playtype: P; - difficulty: Difficulties[T]; + difficulty: Difficulties[G]; userID: integer; scoreData: { score: number; @@ -585,14 +606,14 @@ export interface ScoreDocument extends M lampIndex: integer; gradeIndex: integer; esd: number | null; - hitData: Record; - hitMeta: Partial; + hitData: Record; + hitMeta: Partial; }; - scoreMeta: Record; + scoreMeta: ScoreMetaLookup[I]; calculatedData: { rating: number; lampRating: number; - gameSpecific: Record; + gameSpecific: Partial>; ranking: integer | null; outOf: integer | null; }; diff --git a/common/src/types.ts b/common/src/types.ts index 57685fe66..7f1e0937f 100644 --- a/common/src/types.ts +++ b/common/src/types.ts @@ -5,6 +5,36 @@ export interface CounterDocument { value: integer; } +/** + * IDStrings are an internal (ish) identifier used to identify + * Game + Playtype combos. These are used because TypeScript cannot accurately + * apply two levels of indexing to types, which makes writing generic interfaces + * like the ScoreDocument (Especially the ScoreDocument) *very* hard! + */ +export type IDStrings = + | "iidx:SP" + | "iidx:DP" + | "popn:9B" + | "sdvx:Single" + | "usc:Single" + | "ddr:SP" + | "maimai:Single" + | "jubeat:Single" + | "bms:7K" + | "bms:14K" + | "bms:5K" + | "chunithm:Single" + | "gitadora:Gita" + | "gitadora:Dora"; + +/** + * A utility type for creating an ID string given a game and playtype. + * It should be noted that typescript refuses to assert that + * IDString is a member of IDStrings. You're free to attempt to + * rewrite IDStrings to try and get this to work, I promise you it doesn't. + */ +export type IDString = `${G}:${P}`; + /** * All MongoDB Documents require this field, or atleast they all have them in ktchi's DB. */ @@ -575,6 +605,77 @@ export interface UserMilestoneDocument extends MongoDBDocument { progress: integer; } +type RanOptions = "NONRAN" | "RANDOM" | "R-RANDOM" | "S-RANDOM" | "MIRROR"; + +interface IIDXSPScoreMeta { + optionsRandom: RanOptions; + optionsAssist: + | "AUTO SCRATCH" + | "5KEYS" + | "LEGACY NOTE" + | "ASCR + 5KEY" + | "ASCR + LEGACY" + | "5KEYS + LEGACY" + | "FULL ASSIST"; + optionsRange: "SUDDEN+" | "HIDDEN+" | "SUD+ HID+" | "LIFT" | "LIFT SUD+"; + optionsGauge: "ASSISTED EASY" | "EASY" | "HARD" | "EX-HARD"; + // oh my + pacemaker: + | "NO GRAPH" + | "MY BEST" + | "RIVAL 1" + | "RIVAL 2" + | "RIVAL 3" + | "RIVAL 4" + | "RIVAL 5" + | "RIVAL NEXT" + | "RIVAL BEST" + | "RIVAL AVERAGE" + | "NATIONAL BEST" + | "NATIONAL AVERAGE" + | "PREFECTURE BEST" + | "PREFECTURE AVERAGE" + | "CLASS BEST" + | "CLASS AVERAGE" + | "VENUE BEST" + | "VENUE NEXT" + | "PREVIOUS GHOST" + | "PACEMAKER AAA" + | "PACEMAKER AA" + | "PACEMAKER A" + | "PACEMAKER" + | /* ??? */ "PACEMAKER NEXT" + | "PACEMAKER NEXT+" + | "PLAYER 1" + | "PLAYER 2"; + pacemakerName: string; // contains DJ Name + pacemakerTarget: integer; // the EXScore the pacemaker was aimed at +} + +interface BMS7KScoreMeta { + optionsRandom: RanOptions; + inputDevice: string; +} + +interface ScoreMetaLookup { + "iidx:SP": IIDXSPScoreMeta; + "iidx:DP": IIDXSPScoreMeta & { optionsRandom: [RanOptions, RanOptions] }; + "popn:9B": Record; + "sdvx:Single": Record; + "usc:Single": BASE_VALID_HIT_META & { gauge: number }; + "ddr:SP": Record; + "ddr:DP": Record; + "maimai:Single": Record; + "jubeat:Single": Record; + "museca:Single": Record; + "bms:7K": BMS7KScoreMeta; + "bms:14K": BMS7KScoreMeta & { optionsRandom: [RanOptions, RanOptions] }; + "bms:5K": Record; + "chunithm:Single": Record; + "gitadora:Gita": Record; + "gitadora:Dora": Record; +} + interface BASE_VALID_HIT_META { fast: integer; slow: integer; @@ -596,51 +697,26 @@ type BMSHitMeta = BASE_VALID_HIT_META & } & { bp: integer; gauge: number; - random: "NONRAN" | "MIRROR" | "RANDOM" | "R-RANDOM" | "S-RANDOM"; diedAt: integer | null; - inputDevice: string; }; export interface HitMetaLookup { - iidx: { - SP: IIDXHitMeta; - DP: IIDXHitMeta; - }; - museca: { - Single: BASE_VALID_HIT_META; - }; - ddr: { - SP: BASE_VALID_HIT_META; - DP: BASE_VALID_HIT_META; - }; - maimai: { - SP: BASE_VALID_HIT_META; - DP: BASE_VALID_HIT_META; - }; - jubeat: { - Single: BASE_VALID_HIT_META; - }; - popn: { - "9B": BASE_VALID_HIT_META & { gauge: number }; - }; - sdvx: { - Single: BASE_VALID_HIT_META & { gauge: number }; - }; - chunithm: { - Single: BASE_VALID_HIT_META; - }; - gitadora: { - Gita: BASE_VALID_HIT_META; - Dora: BASE_VALID_HIT_META; - }; - usc: { - Single: BASE_VALID_HIT_META & { gauge: number }; - }; - bms: { - "7K": BMSHitMeta; - "14K": BMSHitMeta; - "5K": BMSHitMeta; - }; + "iidx:SP": IIDXHitMeta; + "iidx:DP": IIDXHitMeta; + "popn:9B": BASE_VALID_HIT_META & { gauge: number }; + "sdvx:Single": BASE_VALID_HIT_META & { gauge: number }; + "usc:Single": BASE_VALID_HIT_META & { gauge: number }; + "ddr:SP": BASE_VALID_HIT_META; + "ddr:DP": BASE_VALID_HIT_META; + "maimai:Single": BASE_VALID_HIT_META; + "jubeat:Single": BASE_VALID_HIT_META; + "museca:Single": BASE_VALID_HIT_META; + "bms:7K": BMSHitMeta; + "bms:14K": BMSHitMeta; + "bms:5K": BMSHitMeta; + "chunithm:Single": BASE_VALID_HIT_META; + "gitadora:Gita": BASE_VALID_HIT_META; + "gitadora:Dora": BASE_VALID_HIT_META; } type IIDXJudges = "pgreat" | "great" | "good" | "bad" | "poor"; @@ -651,55 +727,58 @@ type GitadoraJudges = "perfect" | "great" | "good" | "ok" | "miss"; type SDVXJudges = "critical" | "near" | "miss"; export interface JudgementLookup { - iidx: { - SP: IIDXJudges; - DP: IIDXJudges & { foo: string }; - }; - bms: { - "7K": IIDXJudges; - "14K": IIDXJudges; - "5K": IIDXJudges; - }; - museca: { - Single: "critical" | "near" | "miss"; - }; - ddr: { - SP: DDRJudges; - DP: DDRJudges; - }; - sdvx: { - Single: SDVXJudges; - }; - maimai: { - Single: "perfect" | "great" | "good" | "miss"; - }; - jubeat: { - Single: "perfect" | "great" | "good" | "bad" | "miss"; - }; - popn: { - "9B": "cool" | "great" | "good" | "miss"; - }; - chunithm: { - Single: "jcrit" | "justice" | "attack" | "miss"; - }; - gitadora: { - Gita: GitadoraJudges; - Dora: GitadoraJudges; - }; - usc: { - Single: SDVXJudges; - }; + "iidx:SP": IIDXJudges; + "iidx:DP": IIDXJudges; + "popn:9B": "cool" | "great" | "good" | "miss"; + "sdvx:Single": SDVXJudges; + "usc:Single": SDVXJudges; + "ddr:SP": DDRJudges; + "ddr:DP": DDRJudges; + "maimai:Single": "perfect" | "great" | "good" | "miss"; + "jubeat:Single": "perfect" | "great" | "good" | "bad" | "miss"; + "museca:Single": "critical" | "near" | "miss"; + "bms:7K": IIDXJudges; + "bms:14K": IIDXJudges; + "bms:5K": IIDXJudges; + "chunithm:Single": "jcrit" | "justice" | "attack" | "miss"; + "gitadora:Gita": GitadoraJudges; + "gitadora:Dora": GitadoraJudges; } -export interface ScoreDocument extends MongoDBDocument { +export interface GameSpecificCalcLookup { + "iidx:SP": "BPI" | "K%"; + "iidx:DP": "BPI"; + "popn:9B": never; + "sdvx:Single": "VF4" | "VF5"; + "usc:Single": "VF4" | "VF5"; + "ddr:SP": "MFCP"; + "ddr:DP": "MFCP"; + "maimai:Single": never; + "jubeat:Single": never; + "museca:Single": never; + "bms:7K": never; + "bms:14K": never; + "bms:5K": never; + "chunithm:Single": never; + "gitadora:Gita": never; + "gitadora:Dora": never; +} + +export interface ScoreDocument< + G extends Game, + P extends Playtypes[G], + // @ts-expect-error Typescript thinks this is illegal. + // It *ABSOLUTELY* isn't. + I extends IDStrings = IDString +> extends MongoDBDocument { service: string; - game: T; + game: G; playtype: P; // @todo - allow different game+pt combinations to specify different difficulties? // this happens in kamaitachi with gitadora, so I don't see why it shouldn't // be constrained here. // other than the fact that TS can't do it :( - difficulty: Difficulties[T]; + difficulty: Difficulties[G]; userID: integer; scoreData: { score: number; @@ -709,23 +788,14 @@ export interface ScoreDocument extends M lampIndex: integer; gradeIndex: integer; esd: number | null; - // @ts-expect-error Typescript does not currently support - // two levels of indexing (which is what we want). Instead - // of indexing all P where matches T, it indexes all permutations - // of P + T. - // - // If we expectErr this, it kinda works (but loses the ability to - // perform the second lookup). At the time, we don't have any - // scenario where one playtype has different hitMeta/hitData to another. - hitData: Record; - // @ts-expect-error see above - hitMeta: Partial; + hitData: Record; + hitMeta: Partial; }; - scoreMeta: Record; + scoreMeta: ScoreMetaLookup[I]; calculatedData: { rating: number; lampRating: number; - gameSpecific: Record; + gameSpecific: Partial>; ranking: integer | null; outOf: integer | null; };