diff --git a/common/src/config/game-support/sdvx.ts b/common/src/config/game-support/sdvx.ts index addae3810..a49e3786c 100644 --- a/common/src/config/game-support/sdvx.ts +++ b/common/src/config/game-support/sdvx.ts @@ -27,7 +27,7 @@ const SDVXDans = [ ClassValue("INF", "LV.INF", "Inf. Dan"), ]; -const SDVXVFClasses = [ +export const SDVXVFClasses = [ ClassValue("SIENNA_I", "Sienna I", "0 - 2.499VF"), ClassValue("SIENNA_II", "Sienna II", "2.5 - 4.999VF"), ClassValue("SIENNA_III", "Sienna III", "5.0 - 7.499VF"), diff --git a/common/src/config/game-support/usc.ts b/common/src/config/game-support/usc.ts index 06107a8ca..7a1707333 100644 --- a/common/src/config/game-support/usc.ts +++ b/common/src/config/game-support/usc.ts @@ -1,4 +1,5 @@ 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"; @@ -73,7 +74,12 @@ export const USC_CONTROLLER_CONF = { default: "EXH", }, - classes: {}, + classes: { + vfClass: { + type: "DERIVED", + values: SDVXVFClasses, + }, + }, orderedJudgements: ["critical", "near", "miss"], diff --git a/common/src/types/documents.ts b/common/src/types/documents.ts index 3d872cf6c..0ff4817ed 100644 --- a/common/src/types/documents.ts +++ b/common/src/types/documents.ts @@ -1,11 +1,14 @@ import type { + AnyClasses, ChartDocumentData, Classes, DerivedMetrics, Difficulties, + ExtractedClasses, GPTString, GPTStringToGame, GPTStringToPlaytype, + GPTStrings, Game, Judgements, OptionalMetrics, @@ -42,20 +45,20 @@ export interface ChartFolderLookupDocument { folderID: string; } -export interface BaseGoalDocument { +export interface BaseGoalDocument { game: Game; playtype: Playtype; name: string; goalID: string; - criteria: GoalCountCriteria | GoalSingleCriteria; + criteria: GoalCountCriteria | GoalSingleCriteria; } -interface GoalCriteria { - key: "scoreData.gradeIndex" | "scoreData.lampIndex" | "scoreData.percent" | "scoreData.score"; +interface GoalCriteria { + key: keyof DerivedMetrics[GPT] | keyof ProvidedMetrics[GPT]; value: number; } -export interface GoalSingleCriteria extends GoalCriteria { +export interface GoalSingleCriteria extends GoalCriteria { mode: "single"; } @@ -63,7 +66,7 @@ export interface GoalSingleCriteria extends GoalCriteria { * Criteria for a score to match this criteria - this is a "count" mode, which means that * atleast N scores have to match this criteria. This is for things like folders. */ -export interface GoalCountCriteria extends GoalCriteria { +export interface GoalCountCriteria extends GoalCriteria { mode: "absolute" | "proportion"; countNum: number; } @@ -71,7 +74,8 @@ export interface GoalCountCriteria extends GoalCriteria { /** * Goal Document - Single. A goal document that is only for one specific chart. */ -export interface GoalDocumentSingle extends BaseGoalDocument { +export interface GoalDocumentSingle + extends BaseGoalDocument { charts: { type: "single"; data: string; @@ -82,7 +86,8 @@ export interface GoalDocumentSingle extends BaseGoalDocument { * Goal Document - Multi. A goal document whos set of charts is the array of * chartIDs inside "charts". */ -export interface GoalDocumentMulti extends BaseGoalDocument { +export interface GoalDocumentMulti + extends BaseGoalDocument { charts: { type: "multi"; data: Array; @@ -93,14 +98,18 @@ export interface GoalDocumentMulti extends BaseGoalDocument { * Goal Document - Folder. A goal document whos set of charts is derived from * the folderID inside "charts". */ -export interface GoalDocumentFolder extends BaseGoalDocument { +export interface GoalDocumentFolder + extends BaseGoalDocument { charts: { type: "folder"; data: string; }; } -export type GoalDocument = GoalDocumentFolder | GoalDocumentMulti | GoalDocumentSingle; +export type GoalDocument = + | GoalDocumentFolder + | GoalDocumentMulti + | GoalDocumentSingle; interface BaseInviteCodeDocument { createdBy: integer; @@ -164,7 +173,7 @@ export interface ImportDocument { timeFinished: number; // Contains an array of GPTString, which dictates what (game:playtype)s were involved in this import. - GPTString: Array; + gptStrings: Array; importID: string; scoreIDs: Array; game: Game; @@ -313,12 +322,24 @@ export interface UserDocument { authLevel: UserAuthLevels; } -export interface UserGameStats { +export interface SpecificUserGameStats { userID: integer; game: GPTStringToGame[GPT]; playtype: GPTStringToPlaytype[GPT]; ratings: Partial>; - classes: Partial; + classes: Partial; +} + +/** + * GPT agnostic stats for a game. This type is easier to work with than the + * specificUserGameStats one for general cases. + */ +export interface UserGameStats { + userID: integer; + game: Game; + playtype: Playtype; + ratings: Partial>; + classes: AnyClasses; } export interface ChartTierlistInfo { @@ -548,15 +569,19 @@ export interface CGCardInfo { pin: string; } +interface BMSCourseInner { + set: Set; + playtype: GPTStringToPlaytype[GPT]; + value: ExtractedClasses[GPT][Set]; +} + /** * Used to resolve beatoraja IR courses. */ -export interface BMSCourseDocument { +export interface BMSCourseDocument + extends BMSCourseInner { title: string; md5sums: string; - set: "genocideDan" | "lnDan" | "scratchDan" | "stslDan"; - playtype: "7K" | "14K"; - value: integer; } /** @@ -610,7 +635,7 @@ export interface UGPTSettingsDocument { } export interface UserGameStatsSnapshotDocument - extends UserGameStats { + extends SpecificUserGameStats { rankings: Record; playcount: integer; timestamp: integer; @@ -662,16 +687,16 @@ export interface ClassDelta { game: Game; set: Classes[GPTString]; playtype: Playtype; - old: integer | null; - new: integer; + old: string | null; + new: string; } export interface ClassAchievementDocument { game: GPTStringToGame[GPT]; playtype: GPTStringToPlaytype[GPT]; classSet: Classes[GPT]; - classOldValue: integer | null; - classValue: integer; + classOldValue: string | null; + classValue: string; timeAchieved: number; userID: integer; } diff --git a/common/src/types/game-config-utils.ts b/common/src/types/game-config-utils.ts index eccc900f5..4441af593 100644 --- a/common/src/types/game-config-utils.ts +++ b/common/src/types/game-config-utils.ts @@ -21,13 +21,13 @@ export interface ClassInfo { hoverText?: string; } -interface BaseClassConfig { +interface BaseClassConfig { /** * What are the possible values for this class field? * * @note This should be in ascending order. */ - values: Array>; + values: Array>; } /** @@ -42,7 +42,7 @@ interface BaseClassConfig { * the user is 3rd dan when they've cleared 5th dan in the past, this value will not go * back down at any point. */ -export interface ProvidedClassConfig extends BaseClassConfig { +export interface ProvidedClassConfig extends BaseClassConfig { type: "PROVIDED"; } @@ -56,7 +56,7 @@ export interface ProvidedClassConfig extends BaseClassConfig { * @note "DERIVED" classes are always downgradable, as they are a function of state * and might go down at any time for any reason. */ -export interface DerivedClassConfig extends BaseClassConfig { +export interface DerivedClassConfig extends BaseClassConfig { type: "DERIVED"; } diff --git a/common/src/types/game-config.ts b/common/src/types/game-config.ts index d23947f63..c5dc006a8 100644 --- a/common/src/types/game-config.ts +++ b/common/src/types/game-config.ts @@ -1,5 +1,4 @@ import type { GAME_CONFIGS, GAME_PT_CONFIGS } from "../config/config"; -import type { integer } from "../types"; import type { MatchTypes } from "./batch-manual"; import type { ExtractClassValues, @@ -147,12 +146,16 @@ export type ExtractedClasses = { [G in GPTString]: ExtractClassValues; }; +export type AnyClasses = { + [C in Classes[GPTString]]?: string | null; +}; + export type Classes = { [G in GPTString]: keyof typeof GAME_PT_CONFIGS[G]["classes"]; }; -export type ClassRecords = { - [C in Classes[G]]: integer; +export type ClassConfigs = { + [G in GPTString]: typeof GAME_PT_CONFIGS[G]["classes"]; }; export type ChartDocumentData = { @@ -300,7 +303,7 @@ export interface SpecificGamePTConfig { * or they may be provided by score imports, such as "dans", which cannot be * derived from a player's scores or profile ratings. */ - classes: Classes[GPT]; + classes: ClassConfigs[GPT]; /** * What's the default score rating algorithm for this GPT? diff --git a/common/src/webhooks.ts b/common/src/webhooks.ts index b5262902b..52f0fbb0a 100644 --- a/common/src/webhooks.ts +++ b/common/src/webhooks.ts @@ -9,8 +9,8 @@ export interface WebhookEventClassUpdateV1 { content: { userID: integer; set: Classes[GPTString]; - old: integer | null; - new: integer; + old: string | null; + new: string; game: Game; playtype: Playtypes[Game]; }; diff --git a/server/src/lib/score-import/framework/calculated-data/profile-classes.ts b/server/src/lib/score-import/framework/calculated-data/profile-classes.ts new file mode 100644 index 000000000..236abff78 --- /dev/null +++ b/server/src/lib/score-import/framework/calculated-data/profile-classes.ts @@ -0,0 +1,358 @@ +import type { ClassDeriver, GPTClassDerivers } from "./types"; +import type { ExtractedClasses, GPTString, UserGameStats } from "tachi-common"; + +function VF6ToClass(vf: number): UserGameStats<"sdvx:Single">["classes"]["vfClass"] { + // jesus christ man + if (vf >= 23) { + return "IMPERIAL_IV"; + } else if (vf >= 22) { + return "IMPERIAL_III"; + } else if (vf >= 21) { + return "IMPERIAL_II"; + } else if (vf >= 20) { + return "IMPERIAL_I"; + } else if (vf >= 19.75) { + return "CRIMSON_IV"; + } else if (vf > 19.5) { + return "CRIMSON_III"; + } else if (vf >= 19.25) { + return "CRIMSON_II"; + } else if (vf >= 19) { + return "CRIMSON_I"; + } else if (vf >= 18.75) { + return "ELDORA_IV"; + } else if (vf >= 18.5) { + return "ELDORA_III"; + } else if (vf >= 18.25) { + return "ELDORA_II"; + } else if (vf >= 18) { + return "ELDORA_I"; + } else if (vf >= 17.75) { + return "ARGENTO_IV"; + } else if (vf >= 17.5) { + return "ARGENTO_III"; + } else if (vf >= 17.25) { + return "ARGENTO_II"; + } else if (vf >= 17) { + return "ARGENTO_I"; + } else if (vf >= 16.75) { + return "CORAL_IV"; + } else if (vf >= 16.5) { + return "CORAL_III"; + } else if (vf >= 16.25) { + return "CORAL_II"; + } else if (vf >= 16) { + return "CORAL_I"; + } else if (vf >= 15.75) { + return "SCARLET_IV"; + } else if (vf >= 15.5) { + return "SCARLET_III"; + } else if (vf >= 15.25) { + return "SCARLET_II"; + } else if (vf >= 15) { + return "SCARLET_I"; + } else if (vf >= 14.75) { + return "CYAN_IV"; + } else if (vf >= 14.5) { + return "CYAN_III"; + } else if (vf >= 14.25) { + return "CYAN_II"; + } else if (vf >= 14) { + return "CYAN_I"; + } else if (vf >= 13.5) { + return "DANDELION_IV"; + } else if (vf >= 13) { + return "DANDELION_III"; + } else if (vf >= 12.5) { + return "DANDELION_II"; + } else if (vf >= 12) { + return "DANDELION_I"; + } else if (vf >= 11.5) { + return "COBALT_IV"; + } else if (vf >= 11) { + return "COBALT_III"; + } else if (vf >= 10.5) { + return "COBALT_II"; + } else if (vf >= 10) { + return "COBALT_I"; + } else if (vf >= 7.5) { + return "SIENNA_IV"; + } else if (vf >= 5) { + return "SIENNA_III"; + } else if (vf >= 2.5) { + return "SIENNA_II"; + } + + return "SIENNA_I"; +} + +function GitadoraSkillToColour(sk: number) { + if (sk >= 8500) { + return "RAINBOW"; + } else if (sk >= 8000) { + return "GOLD"; + } else if (sk >= 7500) { + return "SILVER"; + } else if (sk >= 7000) { + return "BRONZE"; + } else if (sk >= 6500) { + return "RED_GRD"; + } else if (sk >= 6000) { + return "RED"; + } else if (sk >= 5500) { + return "PURPLE_GRD"; + } else if (sk >= 5000) { + return "PURPLE"; + } else if (sk >= 4500) { + return "BLUE_GRD"; + } else if (sk >= 4000) { + return "BLUE"; + } else if (sk >= 3500) { + return "GREEN_GRD"; + } else if (sk >= 3000) { + return "GREEN"; + } else if (sk >= 2500) { + return "YELLOW_GRD"; + } else if (sk >= 2000) { + return "YELLOW"; + } else if (sk >= 1500) { + return "ORANGE_GRD"; + } else if (sk >= 1000) { + return "ORANGE"; + } + + return "WHITE"; +} + +export const CLASS_DERIVERS: GPTClassDerivers = { + "bms:14K": {}, + "bms:7K": {}, + "iidx:DP": {}, + "iidx:SP": {}, + "itg:Stamina": {}, + + "museca:Single": {}, + "pms:Controller": {}, + "pms:Keyboard": {}, + + "chunithm:Single": { + colour: (ratings) => { + const rating = ratings.naiveRating; + + if (rating === null) { + return null; + } + + if (rating >= 15) { + return "RAINBOW"; + } else if (rating >= 14.5) { + return "PLATINUM"; + } else if (rating >= 14) { + return "GOLD"; + } else if (rating >= 13) { + return "SILVER"; + } else if (rating >= 12) { + return "COPPER"; + } else if (rating >= 10) { + return "PURPLE"; + } else if (rating >= 7) { + return "RED"; + } else if (rating >= 4) { + return "ORANGE"; + } else if (rating >= 2) { + return "GREEN"; + } + + return "BLUE"; + }, + }, + "gitadora:Dora": { + colour: (ratings) => { + if (ratings.skill === null) { + return null; + } + + return GitadoraSkillToColour(ratings.skill); + }, + }, + "gitadora:Gita": { + colour: (ratings) => { + if (ratings.skill === null) { + return null; + } + + return GitadoraSkillToColour(ratings.skill); + }, + }, + + "jubeat:Single": { + colour: (ratings) => { + const jubility = ratings.jubility; + + if (jubility === null) { + return null; + } + + if (jubility >= 9500) { + return "GOLD"; + } else if (jubility >= 8500) { + return "ORANGE"; + } else if (jubility >= 7000) { + return "PINK"; + } else if (jubility >= 5500) { + return "PURPLE"; + } else if (jubility >= 4000) { + return "VIOLET"; + } else if (jubility >= 2500) { + return "BLUE"; + } else if (jubility >= 1500) { + return "LIGHT_BLUE"; + } else if (jubility >= 750) { + return "GREEN"; + } else if (jubility >= 250) { + return "YELLOW_GREEN"; + } + + return "BLACK"; + }, + }, + "maimaidx:Single": { + colour: (ratings) => { + const rate = ratings.rate; + + if (rate === null) { + return null; + } + + if (rate >= 15000) { + return "RAINBOW"; + } else if (rate >= 14500) { + return "PLATINUM"; + } else if (rate >= 14000) { + return "GOLD"; + } else if (rate >= 13000) { + return "SILVER"; + } else if (rate >= 12000) { + return "BRONZE"; + } else if (rate >= 10000) { + return "PURPLE"; + } else if (rate >= 7000) { + return "RED"; + } else if (rate >= 4000) { + return "YELLOW"; + } else if (rate >= 2000) { + return "GREEN"; + } else if (rate >= 1000) { + return "BLUE"; + } + + return "WHITE"; + }, + }, + + "popn:9B": { + class: (ratings) => { + const points = ratings.naiveClassPoints; + + if (points === null) { + return null; + } + + if (points < 21) { + return "KITTY"; + } else if (points < 34) { + return "STUDENT"; + } else if (points < 46) { + return "DELINQUENT"; + } else if (points < 59) { + return "DETECTIVE"; + } else if (points < 68) { + return "IDOL"; + } else if (points < 79) { + return "GENERAL"; + } else if (points < 91) { + return "HERMIT"; + } + + return "GOD"; + }, + }, + "sdvx:Single": { + vfClass: (ratings) => { + const vf6 = ratings.VF6; + + if (vf6 === null) { + return null; + } + + return VF6ToClass(vf6); + }, + }, + "usc:Controller": { + vfClass: (ratings) => { + const vf6 = ratings.VF6; + + if (vf6 === null) { + return null; + } + + return VF6ToClass(vf6); + }, + }, + "usc:Keyboard": { + vfClass: (ratings) => { + const vf6 = ratings.VF6; + + if (vf6 === null) { + return null; + } + + return VF6ToClass(vf6); + }, + }, + + "wacca:Single": { + colour: (ratings) => { + const rate = ratings.rate; + + if (rate === null) { + return null; + } + + if (rate >= 2500) { + return "RAINBOW"; + } else if (rate >= 2200) { + return "GOLD"; + } else if (rate >= 1900) { + return "SILVER"; + } else if (rate >= 1600) { + return "BLUE"; + } else if (rate >= 1300) { + return "PURPLE"; + } else if (rate >= 1000) { + return "RED"; + } else if (rate >= 600) { + return "YELLOW"; + } else if (rate >= 300) { + return "NAVY"; + } + + return "ASH"; + }, + }, +}; + +export function CalculateDerivedClasses( + gptString: GPT, + profileRatings: UserGameStats["ratings"] +) { + const derivedClasses: Record = {}; + + for (const [key, fn] of Object.entries(CLASS_DERIVERS[gptString])) { + const deriver = fn as ClassDeriver; + + derivedClasses[key] = deriver(profileRatings); + } + + return derivedClasses as Partial; +} diff --git a/server/src/lib/score-import/framework/calculated-data/profile.ts b/server/src/lib/score-import/framework/calculated-data/profile.ts index e69de29bb..2ce18212c 100644 --- a/server/src/lib/score-import/framework/calculated-data/profile.ts +++ b/server/src/lib/score-import/framework/calculated-data/profile.ts @@ -0,0 +1,460 @@ +import db from "external/mongo/db"; +import { GetGPTString } from "tachi-common"; +import type { GPTProfileCalculators } from "./types"; +import type { + GPTString, + Game, + PBScoreDocument, + Playtype, + ScoreRatingAlgorithms, + Versions, + integer, + UserGameStats, +} from "tachi-common"; + +/** + * Curries a function that returns the sum of N best ratings on `key`. + * + * @param key - What rating value to sum. + * @param n - The amount of rating values to pull. + * @param returnMean - Optionally, if true, return the sum of these values divided by N. + * @param nullIfNotEnoughScores - If true, return null if the total scores this user has is less than N. + * + * @returns - Number if the user has scores with that rating algorithm, null if they have + * no scores with this rating algorithm that are non-null. + */ +function CalcN( + key: ScoreRatingAlgorithms[GPT], + n: integer, + returnMean = false, + nullIfNotEnoughScores = false +) { + return async (game: Game, playtype: Playtype, userID: integer) => { + const sc = await db["personal-bests"].find( + { + game, + playtype, + userID, + isPrimary: true, + // @ts-expect-error what? this can't be a symbol. + [`calculatedData.${key}`]: { $type: "number" }, + }, + { + limit: n, + // @ts-expect-error what? this can't be a symbol. + sort: { [`calculatedData.${key}`]: -1 }, + } + ); + + if (sc.length === 0) { + return null; + } + + if (nullIfNotEnoughScores && sc.length < n) { + return null; + } + + let result = sc.reduce((a, e) => a + e.calculatedData[key]!, 0); + + if (returnMean) { + result = result / n; + } + + return result; + }; +} + +function SumBestN( + key: ScoreRatingAlgorithms[GPT], + n: integer, + nullIfNotEnoughScores = false +) { + return CalcN(key, n, false, nullIfNotEnoughScores); +} + +function AvgBestN( + key: ScoreRatingAlgorithms[GPT], + n: integer, + nullIfNotEnoughScores = false +) { + return CalcN(key, n, true, nullIfNotEnoughScores); +} + +// Wacca has a funny algorithm for rate involving gitadora-style latest chart bonuses, +async function CalculateWACCARate(game: Game, playtype: Playtype, userID: integer) { + const hotChartIDs = ( + await db.charts.wacca.find({ "data.isHot": true }, { projection: { chartID: 1 } }) + ).map((e) => e.chartID); + + const coldChartIDs = ( + await db.charts.wacca.find({ "data.isHot": false }, { projection: { chartID: 1 } }) + ).map((e) => e.chartID); + + const best15Hot = await db["personal-bests"].find( + { + game, + playtype, + userID, + chartID: { $in: hotChartIDs }, + "calculatedData.rate": { $type: "number" }, + }, + { + sort: { + "calculatedData.rate": -1, + }, + limit: 15, + projection: { + "calculatedData.rate": 1, + }, + } + ); + + const best35Cold = await db["personal-bests"].find( + { + game, + playtype, + userID, + chartID: { $in: coldChartIDs }, + "calculatedData.rate": { $type: "number" }, + }, + { + sort: { + "calculatedData.rate": -1, + }, + limit: 35, + projection: { + "calculatedData.rate": 1, + }, + } + ); + + if (best15Hot.length + best35Cold.length === 0) { + return null; + } + + return ( + best15Hot.reduce((a, e) => a + e.calculatedData.rate!, 0) + + best35Cold.reduce((a, e) => a + e.calculatedData.rate!, 0) + ); +} + +// basically the same as WACCA's. +async function CalculateMaimaiDXRate(game: Game, playtype: Playtype, userID: integer) { + const newChartIDs = ( + await db.charts.maimaidx.find({ "data.isLatest": true }, { projection: { chartID: 1 } }) + ).map((e) => e.chartID); + + const oldChartIDs = ( + await db.charts.maimaidx.find({ "data.isLatest": false }, { projection: { chartID: 1 } }) + ).map((e) => e.chartID); + + const best15New = await db["personal-bests"].find( + { + game, + playtype, + userID, + chartID: { $in: newChartIDs }, + "calculatedData.rate": { $type: "number" }, + }, + { + sort: { + "calculatedData.rate": -1, + }, + limit: 15, + projection: { + "calculatedData.rate": 1, + }, + } + ); + + const best35Old = await db["personal-bests"].find( + { + game, + playtype, + userID, + chartID: { $in: oldChartIDs }, + "calculatedData.rate": { $type: "number" }, + }, + { + sort: { + "calculatedData.rate": -1, + }, + limit: 35, + projection: { + "calculatedData.rate": 1, + }, + } + ); + + if (best15New.length + best35Old.length === 0) { + return null; + } + + return ( + best15New.reduce((a, e) => a + e.calculatedData.rate!, 0) + + best35Old.reduce((a, e) => a + e.calculatedData.rate!, 0) + ); +} + +async function GetBestRatingOnSongs( + songIDs: Array, + userID: integer, + game: Game, + playtype: Playtype, + ratingProp: "skill", + limit: integer +): Promise> { + const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([ + { + $match: { + game, + playtype, + userID, + songID: { $in: songIDs }, + }, + }, + { + $sort: { + [`calculatedData.${ratingProp}`]: -1, + }, + }, + { + $group: { + _id: "$songID", + doc: { $first: "$$ROOT" }, + }, + }, + + // for some godforsaken reason you have to sort twice. after a grouping + // the sort order becomes nondeterministic + { + $sort: { + [`doc.calculatedData.${ratingProp}`]: -1, + }, + }, + { + $limit: limit, + }, + ]); + + return r.map((e) => e.doc); +} + +async function GetBestJubilityOnSongs( + songIDs: Array, + userID: integer, + game: Game, + playtype: Playtype, + limit: integer +): Promise> { + const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([ + { + $match: { + game, + playtype, + userID, + songID: { $in: songIDs }, + }, + }, + { + // we need to do stuff dependent on the chart difficulty, + // so we need the chart difficulty + $lookup: { + from: "charts-jubeat", + localField: "chartID", + foreignField: "chartID", + as: "chart", + }, + }, + { + // "chart" is an array unless we unwind it. + $unwind: { + path: "$chart", + }, + }, + { + // sort on jubility (so we get the best score) + $sort: { + [`calculatedData.jubility`]: -1, + }, + }, + { + $group: { + _id: { + songID: "$songID", + + // Jubility is unique upon songID + difficulty. However, you + // cannot have a PB on both a HARD BSC and a BSC counted for + // jubility. This query is awkward. Sorry! + difficulty: { + $switch: { + branches: [ + { + case: { $in: ["$chart.difficulty", ["HARD BSC", "BSC"]] }, + then: "BSC", + }, + { + case: { $in: ["$chart.difficulty", ["HARD ADV", "ADV"]] }, + then: "ADV", + }, + { + case: { $in: ["$chart.difficulty", ["HARD EXT", "EXT"]] }, + then: "EXT", + }, + ], + }, + }, + }, + doc: { $first: "$$ROOT" }, + }, + }, + + // for some godforsaken reason you have to sort twice. after a grouping + // the sort order becomes nondeterministic + { + $sort: { + [`doc.calculatedData.jubility`]: -1, + }, + }, + { + $limit: limit, + }, + ]); + + return r.map((e) => e.doc); +} + +const CURRENT_JUBEAT_HOT_VERSION: Versions["jubeat:Single"] = "festo"; + +async function GetPBsForJubility(userID: integer) { + const hotSongs = await db.songs.jubeat.find( + { "data.displayVersion": CURRENT_JUBEAT_HOT_VERSION }, + { projection: { id: 1 } } + ); + + const hotSongIDs = hotSongs.map((e) => e.id); + + const coldSongs = await db.songs.jubeat.find( + { "data.displayVersion": { $ne: CURRENT_JUBEAT_HOT_VERSION } }, + { projection: { id: 1 } } + ); + + const coldSongIDs = coldSongs.map((e) => e.id); + + const [bestHotScores, bestScores] = await Promise.all([ + GetBestJubilityOnSongs(hotSongIDs, userID, "jubeat", "Single", 30), + GetBestJubilityOnSongs(coldSongIDs, userID, "jubeat", "Single", 30), + ]); + + return { bestHotScores, bestScores }; +} + +async function CalculateJubility(game: Game, playtype: Playtype, userID: integer): Promise { + const { bestHotScores, bestScores } = await GetPBsForJubility(userID); + + let jubility = 0; + + jubility = jubility + bestHotScores.reduce((a, e) => a + (e.calculatedData.jubility ?? 0), 0); + jubility = jubility + bestScores.reduce((a, e) => a + (e.calculatedData.jubility ?? 0), 0); + + return jubility; +} + +async function CalculateGitadoraSkill(game: Game, playtype: Playtype, userID: integer) { + const hotSongIDs = ( + await db.songs.gitadora.find({ "data.isHot": true }, { projection: { id: 1 } }) + ).map((e) => e.id); + + // get it + const coldSongIDs = ( + await db.songs.gitadora.find({ "data.isHot": false }, { projection: { id: 1 } }) + ).map((e) => e.id); + + const [bestHotScores, bestScores] = await Promise.all([ + GetBestRatingOnSongs(hotSongIDs, userID, game, playtype, "skill", 25), + GetBestRatingOnSongs(coldSongIDs, userID, game, playtype, "skill", 25), + ]); + + if (bestHotScores.length + bestScores.length === 0) { + return null; + } + + let skill = 0; + + skill = skill + bestHotScores.reduce((a, e) => a + e.calculatedData.skill!, 0); + skill = skill + bestScores.reduce((a, e) => a + e.calculatedData.skill!, 0); + + return skill; +} + +export const PROFILE_CALCULATORS: GPTProfileCalculators = { + "iidx:SP": { + BPI: AvgBestN("BPI", 20, true), + ktLampRating: AvgBestN("ktLampRating", 20), + }, + "iidx:DP": { + BPI: AvgBestN("BPI", 20, true), + ktLampRating: AvgBestN("ktLampRating", 20), + }, + + "bms:14K": { sieglinde: AvgBestN("sieglinde", 20) }, + "bms:7K": { sieglinde: AvgBestN("sieglinde", 20) }, + "pms:Controller": { sieglinde: AvgBestN("sieglinde", 20) }, + "pms:Keyboard": { sieglinde: AvgBestN("sieglinde", 20) }, + + "gitadora:Dora": { skill: CalculateGitadoraSkill, naiveSkill: SumBestN("skill", 50) }, + "gitadora:Gita": { skill: CalculateGitadoraSkill, naiveSkill: SumBestN("skill", 50) }, + + "chunithm:Single": { naiveRating: AvgBestN("rating", 20) }, + "itg:Stamina": { + // the sum of your 1 best blockrating/fastest32 is basically just + // picking your best blockrating/fastest32. neat. + highestBlock: SumBestN("blockRating", 1, true), + fastest32: SumBestN("fastest32", 1, true), + }, + "jubeat:Single": { + jubility: CalculateJubility, + naiveJubility: SumBestN("jubility", 60), + }, + "maimaidx:Single": { + rate: CalculateMaimaiDXRate, + naiveRate: SumBestN("rate", 50), + }, + "museca:Single": { + curatorSkill: SumBestN("curatorSkill", 20), + }, + "popn:9B": { + naiveClassPoints: SumBestN("classPoints", 20), + }, + "sdvx:Single": { + VF6: SumBestN("VF6", 50), + }, + "usc:Controller": { + VF6: SumBestN("VF6", 50), + }, + "usc:Keyboard": { + VF6: SumBestN("VF6", 50), + }, + "wacca:Single": { + rate: CalculateWACCARate, + naiveRate: SumBestN("rate", 50), + }, +}; + +/** + * Calculate profile ratings for this UGPT. + */ +export async function CalculateProfileRatings(game: Game, playtype: Playtype, userID: integer) { + const calculatedData: Record = {}; + + const gptString = GetGPTString(game, playtype); + + // Do this in parallel for performance. + await Promise.all( + Object.entries(PROFILE_CALCULATORS[gptString]).map(async ([key, fn]) => { + calculatedData[key] = await fn(game, playtype, userID); + }) + ); + + return calculatedData as UserGameStats["ratings"]; +} diff --git a/server/src/lib/score-import/framework/calculated-data/types.ts b/server/src/lib/score-import/framework/calculated-data/types.ts index dd30a1b9b..0833f8518 100644 --- a/server/src/lib/score-import/framework/calculated-data/types.ts +++ b/server/src/lib/score-import/framework/calculated-data/types.ts @@ -1,14 +1,19 @@ import type { DryScore } from "../common/types"; +import type { KtLogger } from "lib/logger/logger"; import type { - GPTString, ChartDocument, - ScoreRatingAlgorithms, - SessionRatingAlgorithms, - ScoreDocument, + ClassConfigs, + GPTString, GPTStringToGame, GPTStringToPlaytype, + ProfileRatingAlgorithms, + ScoreDocument, + ScoreRatingAlgorithms, + SessionRatingAlgorithms, + UserGameStats, integer, } from "tachi-common"; +import type { DerivedClassConfig, ProvidedClassConfig } from "tachi-common/types/game-config-utils"; export type ScoreCalculator = ( dryScore: DryScore, @@ -37,5 +42,43 @@ export type GPTSessionCalculators = { }; export type GPTProfileCalculators = { - [G in GPTString]: Record>; + [G in GPTString]: Record>; }; + +export type ClassDeriver = ( + profileRatings: UserGameStats["ratings"] +) => V | null; + +// absolutely stupid magic. + +// makes a record of all the classes that are derivable, +// and returns a ClassDeriver function for each: i.e. +// wacca has StageUP which is not derivable, and colour (which is) +// so this type will result in +// { +// colour: ClassDeriver<"YELLOW" | "asdf" ...> +// } +type RecordClassDeriver = { + [C in keyof ClassConfigs[GPT] as ClassConfigs[GPT][C] extends DerivedClassConfig + ? C + : never]: ClassConfigs[GPT][C] extends DerivedClassConfig + ? ClassDeriver + : never; +}; + +type RecordClassProvider = { + [C in keyof ClassConfigs[GPT] as ClassConfigs[GPT][C] extends DerivedClassConfig + ? C + : never]: ClassConfigs[GPT][C] extends ProvidedClassConfig ? V : never; +}; + +export type GPTClassDerivers = { + [GPT in GPTString]: RecordClassDeriver; +}; + +export type ClassProvider = ( + gptString: GPT, + userID: integer, + ratings: Record, + logger: KtLogger +) => Partial> | Promise>> | undefined; diff --git a/server/src/lib/score-import/framework/profile-calculated-data/builtin-class-handlers.test.ts b/server/src/lib/score-import/framework/profile-calculated-data/builtin-class-handlers.test.ts deleted file mode 100644 index 6488084d7..000000000 --- a/server/src/lib/score-import/framework/profile-calculated-data/builtin-class-handlers.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { - CalculateGitadoraColour, - GitadoraSkillToColour, - CalculateSDVXClass, - SDVXVF6ToClass, -} from "./builtin-class-handlers"; -import CreateLogCtx from "lib/logger/logger"; -import { GITADORA_COLOURS, SDVX_VF_CLASSES } from "tachi-common"; -import t from "tap"; - -const logger = CreateLogCtx(__filename); - -t.test("#CalculateGitadoraColour", (t) => { - t.strictSame( - CalculateGitadoraColour("gitadora", "Gita", 1, { skill: 1500 }), - { - colour: GITADORA_COLOURS.ORANGE_GRADIENT, - }, - "Should wrap the skill colour." - ); - - t.end(); -}); - -t.test("#GitadoraSkillToColour", (t) => { - t.equal(GitadoraSkillToColour(0), GITADORA_COLOURS.WHITE); - t.equal(GitadoraSkillToColour(999), GITADORA_COLOURS.WHITE); - t.equal(GitadoraSkillToColour(1000), GITADORA_COLOURS.ORANGE); - t.equal(GitadoraSkillToColour(1500), GITADORA_COLOURS.ORANGE_GRADIENT); - t.equal(GitadoraSkillToColour(2000), GITADORA_COLOURS.YELLOW); - t.equal(GitadoraSkillToColour(2500), GITADORA_COLOURS.YELLOW_GRADIENT); - t.equal(GitadoraSkillToColour(3000), GITADORA_COLOURS.GREEN); - t.equal(GitadoraSkillToColour(3500), GITADORA_COLOURS.GREEN_GRADIENT); - t.equal(GitadoraSkillToColour(4000), GITADORA_COLOURS.BLUE); - t.equal(GitadoraSkillToColour(4500), GITADORA_COLOURS.BLUE_GRADIENT); - t.equal(GitadoraSkillToColour(5000), GITADORA_COLOURS.PURPLE); - t.equal(GitadoraSkillToColour(5500), GITADORA_COLOURS.PURPLE_GRADIENT); - t.equal(GitadoraSkillToColour(6000), GITADORA_COLOURS.RED); - t.equal(GitadoraSkillToColour(6500), GITADORA_COLOURS.RED_GRADIENT); - t.equal(GitadoraSkillToColour(7000), GITADORA_COLOURS.BRONZE); - t.equal(GitadoraSkillToColour(7500), GITADORA_COLOURS.SILVER); - t.equal(GitadoraSkillToColour(8000), GITADORA_COLOURS.GOLD); - t.equal(GitadoraSkillToColour(8500), GITADORA_COLOURS.RAINBOW); - - t.end(); -}); - -t.test("#CalculateSDVXClass", (t) => { - t.strictSame(CalculateSDVXClass("sdvx", "Single", 1, { VF6: 10 }, logger), { - vfClass: SDVX_VF_CLASSES.COBALT_I, - }); - - t.end(); -}); - -t.test("#SDVXVF6ToClass", (t) => { - function f(vf: number) { - return SDVXVF6ToClass(vf, logger); - } - - t.equal(f(0), SDVX_VF_CLASSES.SIENNA_I); - t.equal(f(2.5), SDVX_VF_CLASSES.SIENNA_II); - t.equal(f(5), SDVX_VF_CLASSES.SIENNA_III); - t.equal(f(7.5), SDVX_VF_CLASSES.SIENNA_IV); - t.equal(f(10), SDVX_VF_CLASSES.COBALT_I); - t.equal(f(10.5), SDVX_VF_CLASSES.COBALT_II); - t.equal(f(11), SDVX_VF_CLASSES.COBALT_III); - t.equal(f(11.5), SDVX_VF_CLASSES.COBALT_IV); - t.equal(f(12), SDVX_VF_CLASSES.DANDELION_I); - t.equal(f(12.5), SDVX_VF_CLASSES.DANDELION_II); - t.equal(f(13), SDVX_VF_CLASSES.DANDELION_III); - t.equal(f(13.5), SDVX_VF_CLASSES.DANDELION_IV); - - t.equal(f(14), SDVX_VF_CLASSES.CYAN_I); - t.equal(f(14.25), SDVX_VF_CLASSES.CYAN_II); - t.equal(f(14.5), SDVX_VF_CLASSES.CYAN_III); - t.equal(f(14.75), SDVX_VF_CLASSES.CYAN_IV); - t.equal(f(15), SDVX_VF_CLASSES.SCARLET_I); - t.equal(f(15.25), SDVX_VF_CLASSES.SCARLET_II); - t.equal(f(15.5), SDVX_VF_CLASSES.SCARLET_III); - t.equal(f(15.75), SDVX_VF_CLASSES.SCARLET_IV); - t.equal(f(16), SDVX_VF_CLASSES.CORAL_I); - t.equal(f(16.25), SDVX_VF_CLASSES.CORAL_II); - t.equal(f(16.5), SDVX_VF_CLASSES.CORAL_III); - t.equal(f(16.75), SDVX_VF_CLASSES.CORAL_IV); - t.equal(f(17), SDVX_VF_CLASSES.ARGENTO_I); - t.equal(f(17.25), SDVX_VF_CLASSES.ARGENTO_II); - t.equal(f(17.5), SDVX_VF_CLASSES.ARGENTO_III); - t.equal(f(17.75), SDVX_VF_CLASSES.ARGENTO_IV); - t.equal(f(18), SDVX_VF_CLASSES.ELDORA_I); - t.equal(f(18.25), SDVX_VF_CLASSES.ELDORA_II); - t.equal(f(18.5), SDVX_VF_CLASSES.ELDORA_III); - t.equal(f(18.75), SDVX_VF_CLASSES.ELDORA_IV); - t.equal(f(19), SDVX_VF_CLASSES.CRIMSON_I); - t.equal(f(19.25), SDVX_VF_CLASSES.CRIMSON_II); - t.equal(f(19.5), SDVX_VF_CLASSES.CRIMSON_III); - t.equal(f(19.75), SDVX_VF_CLASSES.CRIMSON_IV); - - t.equal(f(20), SDVX_VF_CLASSES.IMPERIAL_I); - t.equal(f(21), SDVX_VF_CLASSES.IMPERIAL_II); - t.equal(f(22), SDVX_VF_CLASSES.IMPERIAL_III); - t.equal(f(23), SDVX_VF_CLASSES.IMPERIAL_IV); - t.equal(f(24), SDVX_VF_CLASSES.IMPERIAL_IV); - t.equal(f(1000), SDVX_VF_CLASSES.IMPERIAL_IV); - - t.end(); -}); diff --git a/server/src/lib/score-import/framework/profile-calculated-data/builtin-class-handlers.ts b/server/src/lib/score-import/framework/profile-calculated-data/builtin-class-handlers.ts deleted file mode 100644 index c09333f93..000000000 --- a/server/src/lib/score-import/framework/profile-calculated-data/builtin-class-handlers.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { - CHUNITHM_COLOURS, - GITADORA_COLOURS, - JUBEAT_COLOURS, - MAIMAIDX_COLOURS, - POPN_CLASSES, - SDVX_VF_CLASSES, - WACCA_COLOURS, -} from "tachi-common"; -import { IsNullish } from "utils/misc"; -import type { KtLogger } from "lib/logger/logger"; -import type { Game, integer, Playtype, ScoreCalculatedDataLookup } from "tachi-common"; -import type { ClassRecords } from "tachi-common/game-classes"; - -export function CalculateSDVXClass( - game: Game, - playtype: Playtype, - userID: integer, - ratings: Partial>, - logger: KtLogger -): Partial> { - if (ratings.VF6 === undefined) { - return {}; - } - - return { vfClass: SDVXVF6ToClass(ratings.VF6, logger) }; -} - -/** - * Converts a user's profile VF6 to a class. There are almost 40 classes, and writing - * if statements for all of them is a painful endeavour. This function returns an index - * derived from the gaps between classes, to avoid writing out all those statements. - * @see https://bemaniwiki.com/index.php?SOUND%20VOLTEX%20VIVID%20WAVE/VOLFORCE - */ -export function SDVXVF6ToClass(vf: number, logger: KtLogger) { - // This is impossible, but a failsafe regardless - if (vf >= 24) { - logger.warn(`User has excessive VF6 of ${vf}. Defaulting to Imperial IV.`); - return SDVX_VF_CLASSES.IMPERIAL_IV; - } else if (vf >= 20) { - // imperial i -> iv has gaps of 1 - return SDVX_VF_CLASSES.IMPERIAL_I + Math.floor(vf - 20); - } else if (vf >= 14) { - // cyan i -> crimson iv has gaps of 0.25 - return SDVX_VF_CLASSES.CYAN_I + Math.floor(4 * (vf - 14)); - } else if (vf >= 10) { - // cobalt i -> dandelion iv have gaps of 0.5 - return SDVX_VF_CLASSES.COBALT_I + Math.floor(2 * (vf - 10)); - } - - return Math.floor(vf / 2.5); -} - -export function CalculateGitadoraColour( - game: Game, - playtype: Playtype, - userID: integer, - ratings: Record -): Partial> { - if (IsNullish(ratings.skill)) { - return {}; - } - - const colour = GitadoraSkillToColour(ratings.skill); - - return { - colour, - }; -} - -export function GitadoraSkillToColour(sk: number) { - if (sk >= 8500) { - return GITADORA_COLOURS.RAINBOW; - } else if (sk >= 8000) { - return GITADORA_COLOURS.GOLD; - } else if (sk >= 7500) { - return GITADORA_COLOURS.SILVER; - } else if (sk >= 7000) { - return GITADORA_COLOURS.BRONZE; - } else if (sk >= 6500) { - return GITADORA_COLOURS.RED_GRADIENT; - } else if (sk >= 6000) { - return GITADORA_COLOURS.RED; - } else if (sk >= 5500) { - return GITADORA_COLOURS.PURPLE_GRADIENT; - } else if (sk >= 5000) { - return GITADORA_COLOURS.PURPLE; - } else if (sk >= 4500) { - return GITADORA_COLOURS.BLUE_GRADIENT; - } else if (sk >= 4000) { - return GITADORA_COLOURS.BLUE; - } else if (sk >= 3500) { - return GITADORA_COLOURS.GREEN_GRADIENT; - } else if (sk >= 3000) { - return GITADORA_COLOURS.GREEN; - } else if (sk >= 2500) { - return GITADORA_COLOURS.YELLOW_GRADIENT; - } else if (sk >= 2000) { - return GITADORA_COLOURS.YELLOW; - } else if (sk >= 1500) { - return GITADORA_COLOURS.ORANGE_GRADIENT; - } else if (sk >= 1000) { - return GITADORA_COLOURS.ORANGE; - } - - return GITADORA_COLOURS.WHITE; -} - -export function CalculateWACCAColour( - game: Game, - playtype: Playtype, - userID: integer, - ratings: Record -) { - if (IsNullish(ratings.rate)) { - return {}; - } - - const colour = WACCARateToColour(ratings.rate); - - return { colour }; -} - -function WACCARateToColour(rate: number) { - if (rate >= 2500) { - return WACCA_COLOURS.RAINBOW; - } else if (rate >= 2200) { - return WACCA_COLOURS.GOLD; - } else if (rate >= 1900) { - return WACCA_COLOURS.SILVER; - } else if (rate >= 1600) { - return WACCA_COLOURS.BLUE; - } else if (rate >= 1300) { - return WACCA_COLOURS.PURPLE; - } else if (rate >= 1000) { - return WACCA_COLOURS.RED; - } else if (rate >= 600) { - return WACCA_COLOURS.YELLOW; - } else if (rate >= 300) { - return WACCA_COLOURS.NAVY; - } - - return WACCA_COLOURS.ASH; -} - -export function CalculatePopnClass( - game: Game, - playtype: Playtype, - userID: integer, - ratings: Record -) { - if (IsNullish(ratings.naiveClassPoints)) { - return {}; - } - - const cls = PopnClassPointsToClass(ratings.naiveClassPoints); - - return { class: cls }; -} - -function PopnClassPointsToClass(points: number) { - if (points < 21) { - return POPN_CLASSES.KITTY; - } else if (points < 34) { - return POPN_CLASSES.GRADE_SCHOOL; - } else if (points < 46) { - return POPN_CLASSES.DELINQUENT; - } else if (points < 59) { - return POPN_CLASSES.DETECTIVE; - } else if (points < 68) { - return POPN_CLASSES.IDOL; - } else if (points < 79) { - return POPN_CLASSES.GENERAL; - } else if (points < 91) { - return POPN_CLASSES.HERMIT; - } - - return POPN_CLASSES.GOD; -} - -export function CalculateChunithmColour( - game: Game, - playtype: Playtype, - userID: integer, - ratings: Record -) { - if (IsNullish(ratings.naiveRating)) { - return {}; - } - - const colour = ChuniRatingToColour(ratings.naiveRating); - - return { colour }; -} - -function ChuniRatingToColour(rating: number) { - if (rating >= 15) { - return CHUNITHM_COLOURS.RAINBOW; - } else if (rating >= 14.5) { - return CHUNITHM_COLOURS.PLATINUM; - } else if (rating >= 14) { - return CHUNITHM_COLOURS.GOLD; - } else if (rating >= 13) { - return CHUNITHM_COLOURS.SILVER; - } else if (rating >= 12) { - return CHUNITHM_COLOURS.BRONZE; - } else if (rating >= 10) { - return CHUNITHM_COLOURS.PURPLE; - } else if (rating >= 7) { - return CHUNITHM_COLOURS.RED; - } else if (rating >= 4) { - return CHUNITHM_COLOURS.ORANGE; - } else if (rating >= 2) { - return CHUNITHM_COLOURS.GREEN; - } - - return CHUNITHM_COLOURS.BLUE; -} - -export function CalculateJubeatColour( - game: Game, - playtype: Playtype, - userID: integer, - ratings: Record -) { - if (IsNullish(ratings.jubility)) { - return {}; - } - - const colour = JubilityToColour(ratings.jubility); - - return { colour }; -} - -function JubilityToColour(jubility: number) { - if (jubility >= 9500) { - return JUBEAT_COLOURS.GOLD; - } else if (jubility >= 8500) { - return JUBEAT_COLOURS.ORANGE; - } else if (jubility >= 7000) { - return JUBEAT_COLOURS.PINK; - } else if (jubility >= 5500) { - return JUBEAT_COLOURS.PURPLE; - } else if (jubility >= 4000) { - return JUBEAT_COLOURS.VIOLET; - } else if (jubility >= 2500) { - return JUBEAT_COLOURS.BLUE; - } else if (jubility >= 1500) { - return JUBEAT_COLOURS.LIGHT_BLUE; - } else if (jubility >= 750) { - return JUBEAT_COLOURS.GREEN; - } else if (jubility >= 250) { - return JUBEAT_COLOURS.YELLOW_GREEN; - } - - return JUBEAT_COLOURS.BLACK; -} - -export function CalculateMaimaiDXColour( - game: Game, - playtype: Playtype, - userID: integer, - ratings: Record -) { - if (IsNullish(ratings.rate)) { - return {}; - } - - const colour = MaimaiDXRateToColour(ratings.rate); - - return { colour }; -} - -function MaimaiDXRateToColour(rate: number) { - if (rate >= 15000) { - return MAIMAIDX_COLOURS.RAINBOW; - } else if (rate >= 14500) { - return MAIMAIDX_COLOURS.PLATINUM; - } else if (rate >= 14000) { - return MAIMAIDX_COLOURS.GOLD; - } else if (rate >= 13000) { - return MAIMAIDX_COLOURS.SILVER; - } else if (rate >= 12000) { - return MAIMAIDX_COLOURS.BRONZE; - } else if (rate >= 10000) { - return MAIMAIDX_COLOURS.PURPLE; - } else if (rate >= 7000) { - return MAIMAIDX_COLOURS.RED; - } else if (rate >= 4000) { - return MAIMAIDX_COLOURS.YELLOW; - } else if (rate >= 2000) { - return MAIMAIDX_COLOURS.GREEN; - } else if (rate >= 1000) { - return MAIMAIDX_COLOURS.BLUE; - } - - return MAIMAIDX_COLOURS.WHITE; -} diff --git a/server/src/lib/score-import/framework/profile-calculated-data/classes.test.ts b/server/src/lib/score-import/framework/profile-calculated-data/classes.test.ts index 027892e1e..673e39d90 100644 --- a/server/src/lib/score-import/framework/profile-calculated-data/classes.test.ts +++ b/server/src/lib/score-import/framework/profile-calculated-data/classes.test.ts @@ -1,4 +1,4 @@ -import { ProcessClassDeltas, UpdateUGSClasses } from "./classes"; +import { ProcessClassDeltas, CalculateUGPTClasses } from "./classes"; import CreateLogCtx from "lib/logger/logger"; import { GITADORA_COLOURS } from "tachi-common"; import t from "tap"; @@ -9,7 +9,7 @@ const logger = CreateLogCtx(__filename); t.test("#UpdateUGSClasses", (t) => { t.test("Should produce an empty object by default", async (t) => { - const res = await UpdateUGSClasses("iidx", "SP", 1, {}, null, logger); + const res = await CalculateUGPTClasses("iidx", "SP", 1, {}, null, logger); t.strictSame(res, {}); @@ -17,7 +17,7 @@ t.test("#UpdateUGSClasses", (t) => { }); t.test("Should call and merge the ClassHandler", async (t) => { - const res = await UpdateUGSClasses("iidx", "SP", 1, {}, () => ({ dan: 2 }), logger); + const res = await CalculateUGPTClasses("iidx", "SP", 1, {}, () => ({ dan: 2 }), logger); t.strictSame(res, { dan: 2 }); @@ -25,7 +25,7 @@ t.test("#UpdateUGSClasses", (t) => { }); t.test("Should call static handlers if there is one", async (t) => { - const res = await UpdateUGSClasses( + const res = await CalculateUGPTClasses( "gitadora", "Dora", 1, diff --git a/server/src/lib/score-import/framework/profile-calculated-data/classes.ts b/server/src/lib/score-import/framework/profile-calculated-data/classes.ts index 727e5c4a4..fc40a3cf6 100644 --- a/server/src/lib/score-import/framework/profile-calculated-data/classes.ts +++ b/server/src/lib/score-import/framework/profile-calculated-data/classes.ts @@ -1,78 +1,23 @@ -import { - CalculateChunithmColour, - CalculateGitadoraColour, - CalculateJubeatColour, - CalculatePopnClass, - CalculateSDVXClass, - CalculateWACCAColour, - CalculateMaimaiDXColour, -} from "./builtin-class-handlers"; +import { CalculateDerivedClasses } from "../calculated-data/profile-classes"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; import { EmitWebhookEvent } from "lib/webhooks/webhooks"; -import { GetGamePTConfig } from "tachi-common"; +import { GetGPTString, GetGamePTConfig } from "tachi-common"; import { ReturnClassIfGreater } from "utils/class"; -import type { ClassHandler, ScoreClasses } from "./types"; +import type { ClassProvider } from "../calculated-data/types"; import type { KtLogger } from "lib/logger/logger"; import type { ClassDelta, - Game, - GPTString, - integer, - Playtype, - Playtypes, - UserGameStats, Classes, + GPTString, + Game, + Playtype, + UserGameStats, + integer, + ExtractedClasses, + AnyClasses, } from "tachi-common"; -type ClassHandlerMap = { - [G in Game]: - | { - [P in Playtypes[G]]: ClassHandler; - } - | null; -}; - -/** - * Describes the static class handlers for a game, I.E. ones that are - * always meant to be called when new scores are found. - * - * These can be calculated without any external (i.e. user/api-provided) data. - * - * A good example of this would be, say, jubeat's colours - the boundaries depend entirely - * on your profile skill level, which is known at the time this function is called. - */ -const STATIC_CLASS_HANDLERS: ClassHandlerMap = { - iidx: null, - bms: null, - chunithm: { - Single: CalculateChunithmColour, - }, - gitadora: { - Gita: CalculateGitadoraColour, - Dora: CalculateGitadoraColour, - }, - maimaidx: { - Single: CalculateMaimaiDXColour, - }, - museca: null, - popn: { - "9B": CalculatePopnClass, - }, - sdvx: { - Single: CalculateSDVXClass, - }, - usc: null, - wacca: { - Single: CalculateWACCAColour, - }, - jubeat: { - Single: CalculateJubeatColour, - }, - pms: null, - itg: null, -}; - /** * Calculates a User's Game Stats Classes. This function is rather complex, because the reality is rather complex. * @@ -90,36 +35,28 @@ const STATIC_CLASS_HANDLERS: ClassHandlerMap = { * @param ratings - A users ratings. This is calculated in rating.ts, and passed via update-ugs.ts. * We request this because we need it for things like gitadora's skill divisions - We don't need to calculate our skill * statistic twice if we just request it be passed to us! - * @param ClassHandler - The Custom Resolve Function that certain import types may pass to us as a means - * for retrieving information about a class. This returns the same thing as this function, and it is merged with the + * @param ClassProvider - The Custom Resolve Function that certain import types may pass to us as a means + * for providing information about a class. This returns the same thing as this function, and it is merged with the * defaults. */ -export async function UpdateUGSClasses( +export async function CalculateUGPTClasses( game: Game, playtype: Playtype, userID: integer, ratings: Record, - ClassHandler: ClassHandler | null, + ClassProvider: ClassProvider | null, logger: KtLogger -): Promise { - let classes: ScoreClasses = {}; +): Promise { + const gptString = GetGPTString(game, playtype); - // @ts-expect-error This one sucks - I need to look into a better way of representing these types - if (STATIC_CLASS_HANDLERS[game]?.[playtype] !== undefined) { - // @ts-expect-error see above - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call - classes = await STATIC_CLASS_HANDLERS[game][playtype]( - game, - playtype, - userID, - ratings, - logger - ); - } + // Derive all classes first. + let classes = CalculateDerivedClasses(gptString, ratings); - if (ClassHandler) { + // If this import method is providing us classes, merge those with the + // other classes we have. + if (ClassProvider) { logger.debug(`Calling custom class handler.`); - const customClasses = (await ClassHandler(game, playtype, userID, ratings, logger)) ?? {}; + const customClasses = (await ClassProvider(gptString, userID, ratings, logger)) ?? {}; classes = deepmerge(customClasses, classes); } @@ -131,17 +68,24 @@ export async function UpdateUGSClasses( * Calculates the class "deltas" for this users classes. * This is for calculating scenarios where a users class has improved (i.e. they have gone from 9th dan to 10th dan). * + * If a class is provided, we don't want to potentially downgrade users. I.e. + * if a user clears 7th dan while they're 10th dan, we don't want to downgrade them. + * + * For derived classes however, downgrading is fine. + * * Knowing this information allows us to attach it onto the import, and also emit things on webhooks. - * This function emits webhook events and inserts classachieved documents into the DB!** + * This function emits webhook events and inserts classachieved documents into the DB! */ export async function ProcessClassDeltas( game: Game, playtype: Playtype, - classes: ScoreClasses, + classes: AnyClasses, userGameStats: UserGameStats | null, userID: integer, logger: KtLogger ): Promise> { + const gptString = GetGPTString(game, playtype); + const deltas: Array = []; const achievementOps = []; @@ -152,7 +96,7 @@ export async function ProcessClassDeltas( const classSet = s as Classes[GPTString]; const classVal = classes[classSet]; - if (classVal === undefined) { + if (classVal === undefined || classVal === null) { logger.debug(`Skipped deltaing-class ${classSet}.`); continue; } @@ -160,11 +104,11 @@ export async function ProcessClassDeltas( const classConfig = gptConfig.classes[classSet]!; try { - const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats); + const isGreater = ReturnClassIfGreater(gptString, classSet, classVal, userGameStats); - // if this was worse, and this class isn't DERIVED (i.e. it's a dan) + // if this was worse, and this class is PROVIDED (i.e. it's a dan) // then don't do anything - if (isGreater === false && classConfig.type !== "DERIVED") { + if (isGreater === false && classConfig.type === "PROVIDED") { continue; } else { // otherwise, provide this as an update. diff --git a/server/src/lib/score-import/framework/profile-calculated-data/rating.test.ts b/server/src/lib/score-import/framework/profile-calculated-data/rating.test.ts deleted file mode 100644 index 616ef99e8..000000000 --- a/server/src/lib/score-import/framework/profile-calculated-data/rating.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { CalculateRatings } from "./rating"; -import db from "external/mongo/db"; -import CreateLogCtx from "lib/logger/logger"; -import t from "tap"; -import { mkFakePBIIDXSP, mkFakePBJubeat } from "test-utils/misc"; -import ResetDBState from "test-utils/resets"; - -const logger = CreateLogCtx(__filename); - -t.test("#CalculateRatings", (t) => { - t.beforeEach(ResetDBState); - - t.test("Should return BPI for IIDX", async (t) => { - const res = await CalculateRatings("iidx", "SP", 1, logger); - - t.strictSame(res, { BPI: null, ktLampRating: null }, "Should return BPI as a custom key."); - - const resDP = await CalculateRatings("iidx", "DP", 1, logger); - - t.strictSame( - resDP, - { BPI: null, ktLampRating: null }, - "Should return BPI as a custom key." - ); - - t.end(); - }); - - t.test("Should return VF6 for SDVX", async (t) => { - const res = await CalculateRatings("sdvx", "Single", 1, logger); - - t.strictSame(res, { VF6: null }, "Should return VF6 keys."); - - t.end(); - }); - - t.test("Should return VF6 for USC:Keyboard", async (t) => { - const res = await CalculateRatings("usc", "Keyboard", 1, logger); - - t.strictSame(res, { VF6: null }, "Should return VF6 keys."); - - t.end(); - }); - - t.test("Should return VF6 for USC:Controller", async (t) => { - const res = await CalculateRatings("usc", "Controller", 1, logger); - - t.strictSame(res, { VF6: null }, "Should return VF6 keys."); - - t.end(); - }); - - t.test("Should return skill for Gitadora", async (t) => { - const res = await CalculateRatings("gitadora", "Dora", 1, logger); - - t.strictSame(res, { skill: null, naiveSkill: null }, "Should return skill keys."); - - const res2 = await CalculateRatings("gitadora", "Gita", 1, logger); - - t.strictSame(res2, { skill: null, naiveSkill: null }, "Should return skill keys."); - - t.end(); - }); - - t.test("Should calculate jubility for Jubeat", async (t) => { - await db["personal-bests"].insert(mkFakePBJubeat()); - - const res = await CalculateRatings("jubeat", "Single", 1, logger); - - t.strictSame(res, { jubility: 5, naiveJubility: 5 }, "Should return jubility keys."); - - t.end(); - }); - - t.end(); -}); diff --git a/server/src/lib/score-import/framework/profile-calculated-data/rating.ts b/server/src/lib/score-import/framework/profile-calculated-data/rating.ts deleted file mode 100644 index 3518f67ef..000000000 --- a/server/src/lib/score-import/framework/profile-calculated-data/rating.ts +++ /dev/null @@ -1,477 +0,0 @@ -import db from "external/mongo/db"; -import type { KtLogger } from "lib/logger/logger"; -import type { - Game, - Versions, - GPTString, - integer, - PBScoreDocument, - Playtype, - UserGameStats, - ScoreRatingAlgorithms, -} from "tachi-common"; - -type CustomCalcNames = ScoreRatingAlgorithms[GPTString]; - -/** - * Curries a function that returns the sum of N best ratings on `key`. - * - * @param key - What rating value to sum. - * @param n - The amount of rating values to pull. - * @param returnMean - Optionally, if true, return the sum of these values divided by N. - * @param nullIfNotEnoughScores - If true, return null if the total scores this user has is less than N. - * - * @returns - Number if the user has scores with that rating algorithm, null if they have - * no scores with this rating algorithm that are non-null. - */ -function LazyCalcN( - key: CustomCalcNames, - n: integer, - returnMean = false, - nullIfNotEnoughScores = false -) { - return async (game: Game, playtype: Playtype, userID: integer) => { - const sc = await db["personal-bests"].find( - { - game, - playtype, - userID, - isPrimary: true, - [`calculatedData.${key}`]: { $type: "number" }, - }, - { - limit: n, - sort: { [`calculatedData.${key}`]: -1 }, - } - ); - - if (sc.length === 0) { - return null; - } - - if (nullIfNotEnoughScores && sc.length < n) { - return null; - } - - let result = sc.reduce((a, e) => a + e.calculatedData[key]!, 0); - - if (returnMean) { - result = result / n; - } - - return result; - }; -} - -const LazySumN = (key: CustomCalcNames, n: integer, nullIfNotEnoughScores = false) => - LazyCalcN(key, n, false, nullIfNotEnoughScores); -const LazyMeanN = (key: CustomCalcNames, n: integer, nullIfNotEnoughScores = false) => - LazyCalcN(key, n, true, nullIfNotEnoughScores); - -type RatingFunction = ( - game: Game, - playtype: Playtype, - userID: integer, - logger: KtLogger -) => Promise; - -function GetGPTRatingFunction(game: Game, playtype: Playtype): RatingFunction { - switch (`${game}:${playtype}` as GPTString) { - case "iidx:SP": - case "iidx:DP": - return async (g, p, u) => ({ - BPI: await LazyMeanN("BPI", 20, true)(g, p, u), - ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), - }); - case "sdvx:Single": - case "usc:Keyboard": - case "usc:Controller": - return async (g, p, u) => ({ - VF6: await LazySumN("VF6", 50)(g, p, u), - }); - case "gitadora:Dora": - case "gitadora:Gita": - return async (g, p, u, l) => ({ - skill: await CalculateGitadoraSkill(g, p, u, l), - naiveSkill: await LazySumN("skill", 50)(g, p, u), - }); - case "bms:7K": - case "bms:14K": - case "pms:Keyboard": - case "pms:Controller": - return async (g, p, u) => ({ - sieglinde: await LazyMeanN("sieglinde", 20)(g, p, u), - }); - case "chunithm:Single": - return async (g, p, u) => ({ - naiveRating: await LazyMeanN("rating", 20)(g, p, u), - }); - case "popn:9B": - return async (g, p, u) => ({ - naiveClassPoints: await LazyMeanN("classPoints", 20)(g, p, u), - }); - case "museca:Single": - return async (g, p, u) => ({ - curatorSkill: await LazySumN("curatorSkill", 20)(g, p, u), - }); - case "maimaidx:Single": - return async (g, p, u, l) => ({ - naiveRate: await LazySumN("rate", 50)(g, p, u), - rate: await CalculateMaimaiDXRate(g, p, u, l), - }); - case "wacca:Single": - return async (g, p, u, l) => ({ - naiveRate: await LazySumN("rate", 50)(g, p, u), - rate: await CalculateWACCARate(g, p, u, l), - }); - case "jubeat:Single": - return async (g, p, u, l) => ({ - jubility: await CalculateJubility(g, p, u, l), - naiveJubility: await LazySumN("jubility", 60)(g, p, u), - }); - case "itg:Stamina": - // HighestX is equivalent to summing 1. - return async (g, p, u) => ({ - highestBlock: await LazySumN("blockRating", 1, true)(g, p, u), - highest32: await LazySumN("fastest32", 1, true)(g, p, u), - }); - } -} - -export function CalculateRatings( - game: Game, - playtype: Playtype, - userID: integer, - logger: KtLogger -): Promise>> { - const RatingFunction = GetGPTRatingFunction(game, playtype); - - return RatingFunction(game, playtype, userID, logger); -} - -// Wacca has a funny algorithm for rate involving gitadora-style latest chart bonuses, -async function CalculateWACCARate( - game: Game, - playtype: Playtype, - userID: integer, - _logger: KtLogger -) { - const hotChartIDs = ( - await db.charts.wacca.find({ "data.isHot": true }, { projection: { chartID: 1 } }) - ).map((e) => e.chartID); - - const coldChartIDs = ( - await db.charts.wacca.find({ "data.isHot": false }, { projection: { chartID: 1 } }) - ).map((e) => e.chartID); - - const best15Hot = await db["personal-bests"].find( - { - game, - playtype, - userID, - chartID: { $in: hotChartIDs }, - "calculatedData.rate": { $type: "number" }, - }, - { - sort: { - "calculatedData.rate": -1, - }, - limit: 15, - projection: { - "calculatedData.rate": 1, - }, - } - ); - - const best35Cold = await db["personal-bests"].find( - { - game, - playtype, - userID, - chartID: { $in: coldChartIDs }, - "calculatedData.rate": { $type: "number" }, - }, - { - sort: { - "calculatedData.rate": -1, - }, - limit: 35, - projection: { - "calculatedData.rate": 1, - }, - } - ); - - if (best15Hot.length + best35Cold.length === 0) { - return null; - } - - return ( - best15Hot.reduce((a, e) => a + e.calculatedData.rate!, 0) + - best35Cold.reduce((a, e) => a + e.calculatedData.rate!, 0) - ); -} - -// basically the same as WACCA's. -async function CalculateMaimaiDXRate( - game: Game, - playtype: Playtype, - userID: integer, - _logger: KtLogger -) { - const newChartIDs = ( - await db.charts.maimaidx.find({ "data.isLatest": true }, { projection: { chartID: 1 } }) - ).map((e) => e.chartID); - - const oldChartIDs = ( - await db.charts.maimaidx.find({ "data.isLatest": false }, { projection: { chartID: 1 } }) - ).map((e) => e.chartID); - - const best15New = await db["personal-bests"].find( - { - game, - playtype, - userID, - chartID: { $in: newChartIDs }, - "calculatedData.rate": { $type: "number" }, - }, - { - sort: { - "calculatedData.rate": -1, - }, - limit: 15, - projection: { - "calculatedData.rate": 1, - }, - } - ); - - const best35Old = await db["personal-bests"].find( - { - game, - playtype, - userID, - chartID: { $in: oldChartIDs }, - "calculatedData.rate": { $type: "number" }, - }, - { - sort: { - "calculatedData.rate": -1, - }, - limit: 35, - projection: { - "calculatedData.rate": 1, - }, - } - ); - - if (best15New.length + best35Old.length === 0) { - return null; - } - - return ( - best15New.reduce((a, e) => a + e.calculatedData.rate!, 0) + - best35Old.reduce((a, e) => a + e.calculatedData.rate!, 0) - ); -} - -async function CalculateGitadoraSkill( - game: Game, - playtype: Playtype, - userID: integer, - _logger: KtLogger -) { - const hotSongIDs = ( - await db.songs.gitadora.find({ "data.isHot": true }, { projection: { id: 1 } }) - ).map((e) => e.id); - - // get it - const coldSongIDs = ( - await db.songs.gitadora.find({ "data.isHot": false }, { projection: { id: 1 } }) - ).map((e) => e.id); - - const [bestHotScores, bestScores] = await Promise.all([ - GetBestRatingOnSongs(hotSongIDs, userID, game, playtype, "skill", 25), - GetBestRatingOnSongs(coldSongIDs, userID, game, playtype, "skill", 25), - ]); - - if (bestHotScores.length + bestScores.length === 0) { - return null; - } - - let skill = 0; - - skill = skill + bestHotScores.reduce((a, e) => a + e.calculatedData.skill!, 0); - skill = skill + bestScores.reduce((a, e) => a + e.calculatedData.skill!, 0); - - return skill; -} - -export async function GetBestRatingOnSongs( - songIDs: Array, - userID: integer, - game: Game, - playtype: Playtype, - ratingProp: "skill", - limit: integer -): Promise> { - const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([ - { - $match: { - game, - playtype, - userID, - songID: { $in: songIDs }, - }, - }, - { - $sort: { - [`calculatedData.${ratingProp}`]: -1, - }, - }, - { - $group: { - _id: "$songID", - doc: { $first: "$$ROOT" }, - }, - }, - - // for some godforsaken reason you have to sort twice. after a grouping - // the sort order becomes nondeterministic - { - $sort: { - [`doc.calculatedData.${ratingProp}`]: -1, - }, - }, - { - $limit: limit, - }, - ]); - - return r.map((e) => e.doc); -} - -async function GetBestJubilityOnSongs( - songIDs: Array, - userID: integer, - game: Game, - playtype: Playtype, - limit: integer -): Promise> { - const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([ - { - $match: { - game, - playtype, - userID, - songID: { $in: songIDs }, - }, - }, - { - // we need to do stuff dependent on the chart difficulty, - // so we need the chart difficulty - $lookup: { - from: "charts-jubeat", - localField: "chartID", - foreignField: "chartID", - as: "chart", - }, - }, - { - // "chart" is an array unless we unwind it. - $unwind: { - path: "$chart", - }, - }, - { - // sort on jubility (so we get the best score) - $sort: { - [`calculatedData.jubility`]: -1, - }, - }, - { - $group: { - _id: { - songID: "$songID", - - // Jubility is unique upon songID + difficulty. However, you - // cannot have a PB on both a HARD BSC and a BSC counted for - // jubility. This query is awkward. Sorry! - difficulty: { - $switch: { - branches: [ - { - case: { $in: ["$chart.difficulty", ["HARD BSC", "BSC"]] }, - then: "BSC", - }, - { - case: { $in: ["$chart.difficulty", ["HARD ADV", "ADV"]] }, - then: "ADV", - }, - { - case: { $in: ["$chart.difficulty", ["HARD EXT", "EXT"]] }, - then: "EXT", - }, - ], - }, - }, - }, - doc: { $first: "$$ROOT" }, - }, - }, - - // for some godforsaken reason you have to sort twice. after a grouping - // the sort order becomes nondeterministic - { - $sort: { - [`doc.calculatedData.jubility`]: -1, - }, - }, - { - $limit: limit, - }, - ]); - - return r.map((e) => e.doc); -} - -const CURRENT_JUBEAT_HOT_VERSION: Versions["jubeat:Single"] = "festo"; - -export async function GetPBsForJubility(userID: integer) { - const hotSongs = await db.songs.jubeat.find( - { "data.displayVersion": CURRENT_JUBEAT_HOT_VERSION }, - { projection: { id: 1 } } - ); - - const hotSongIDs = hotSongs.map((e) => e.id); - - const coldSongs = await db.songs.jubeat.find( - { "data.displayVersion": { $ne: CURRENT_JUBEAT_HOT_VERSION } }, - { projection: { id: 1 } } - ); - - const coldSongIDs = coldSongs.map((e) => e.id); - - const [bestHotScores, bestScores] = await Promise.all([ - GetBestJubilityOnSongs(hotSongIDs, userID, "jubeat", "Single", 30), - GetBestJubilityOnSongs(coldSongIDs, userID, "jubeat", "Single", 30), - ]); - - return { bestHotScores, bestScores }; -} - -async function CalculateJubility( - game: Game, - playtype: Playtype, - userID: integer, - _logger: KtLogger -): Promise { - const { bestHotScores, bestScores } = await GetPBsForJubility(userID); - - let jubility = 0; - - jubility = jubility + bestHotScores.reduce((a, e) => a + (e.calculatedData.jubility ?? 0), 0); - jubility = jubility + bestScores.reduce((a, e) => a + (e.calculatedData.jubility ?? 0), 0); - - return jubility; -} diff --git a/server/src/lib/score-import/framework/profile-calculated-data/types.ts b/server/src/lib/score-import/framework/profile-calculated-data/types.ts deleted file mode 100644 index e23d7f908..000000000 --- a/server/src/lib/score-import/framework/profile-calculated-data/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { KtLogger } from "lib/logger/logger"; -import type { Game, GPTString, integer, Playtype } from "tachi-common"; -import type { ClassRecords } from "tachi-common/game-classes"; - -export type ScoreClasses = Partial>; - -export type ClassHandler = ( - game: Game, - playtype: Playtype, - userID: integer, - ratings: Record, - logger: KtLogger -) => Promise | ScoreClasses | undefined; diff --git a/server/src/lib/score-import/framework/score-importing/score-import-main.ts b/server/src/lib/score-import/framework/score-importing/score-import-main.ts index 5013b2a8e..88ae2f071 100644 --- a/server/src/lib/score-import/framework/score-importing/score-import-main.ts +++ b/server/src/lib/score-import/framework/score-importing/score-import-main.ts @@ -8,14 +8,14 @@ import { CheckAndSetOngoingImportLock, UnsetOngoingImportLock } from "../import- import { ProcessPBs } from "../pb/process-pbs"; import { UpdateUsersQuests } from "../quests/quests"; import { CreateSessions } from "../sessions/sessions"; -import { UpdateUsersGamePlaytypeStats } from "../profile-calculated-data/update-ugs"; +import { UpdateUsersGamePlaytypeStats } from "../ugpt-stats/update-ugpt-stats"; import db from "external/mongo/db"; import { GetGameConfig } from "tachi-common"; import { GetMillisecondsSince } from "utils/misc"; import { GetUserWithID } from "utils/user"; import type { ConverterFunction, ImportInputParser } from "../../import-types/common/types"; +import type { ClassProvider } from "../calculated-data/types"; import type { ChartIDPlaytypeMap, ScorePlaytypeMap } from "../common/types"; -import type { ClassHandler } from "../profile-calculated-data/types"; import type { KtLogger } from "lib/logger/logger"; import type { ScoreImportJob } from "lib/score-import/worker/types"; import type { @@ -87,7 +87,7 @@ export default async function ScoreImportMain( // We get an iterable from the provided parser function, alongside some context and a converter function. // This iterable does not have to be an array - it's anything that's iterable, like a generator. const parseTimeStart = process.hrtime.bigint(); - const { iterable, context, game, classHandler } = await InputParser(logger); + const { iterable, context, game, classProvider: classProvider } = await InputParser(logger); const parseTime = GetMillisecondsSince(parseTimeStart); @@ -172,7 +172,7 @@ export default async function ScoreImportMain( user, importType, game, - classHandler, + classProvider, logger, job ); @@ -260,7 +260,7 @@ export async function HandlePostImportSteps( user: UserDocument, importType: ImportTypes, game: Game, - classHandler: ClassHandler | null, + classProvider: ClassProvider | null, logger: KtLogger, job: ScoreImportJob | undefined ) { @@ -324,7 +324,7 @@ export async function HandlePostImportSteps( // --- 6. Game Stats --- // This function updates the users "stats" for this game - such as their profile rating or their classes. const ugsTimeStart = process.hrtime.bigint(); - const classDeltas = await UpdateUsersGameStats(game, playtypes, user.id, classHandler, logger); + const classDeltas = await UpdateUsersGameStats(game, playtypes, user.id, classProvider, logger); const ugsTime = GetMillisecondsSince(ugsTimeStart); @@ -384,20 +384,20 @@ async function UpdateUsersGameStats( game: Game, modifiedPlaytypes: Array, userID: integer, - classHandler: ClassHandler | null, + classProvider: ClassProvider | null, logger: KtLogger ) { const promises = []; - // Instead of using the provided playtypes, run the classHandler on all - // playtypes. This should only happen if a classHandler is provided, and is + // Instead of using the provided playtypes, run the classProvider on all + // playtypes. This should only happen if a classProvider is provided, and is // a hack fix for things like #480. const allPlaytypes = GetGameConfig(game).playtypes; - const playtypes = classHandler ? allPlaytypes : modifiedPlaytypes; + const playtypes = classProvider ? allPlaytypes : modifiedPlaytypes; for (const pt of playtypes) { - promises.push(UpdateUsersGamePlaytypeStats(game, pt, userID, classHandler, logger)); + promises.push(UpdateUsersGamePlaytypeStats(game, pt, userID, classProvider, logger)); } const r = await Promise.all(promises); diff --git a/server/src/lib/score-import/framework/profile-calculated-data/update-ugs.test.ts b/server/src/lib/score-import/framework/ugpt-stats/update-ugpt-stats.test.ts similarity index 98% rename from server/src/lib/score-import/framework/profile-calculated-data/update-ugs.test.ts rename to server/src/lib/score-import/framework/ugpt-stats/update-ugpt-stats.test.ts index 9b9aa3481..315698a4a 100644 --- a/server/src/lib/score-import/framework/profile-calculated-data/update-ugs.test.ts +++ b/server/src/lib/score-import/framework/ugpt-stats/update-ugpt-stats.test.ts @@ -1,4 +1,4 @@ -import { UpdateUsersGamePlaytypeStats } from "./update-ugs"; +import { UpdateUsersGamePlaytypeStats } from "./update-ugpt-stats"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; diff --git a/server/src/lib/score-import/framework/profile-calculated-data/update-ugs.ts b/server/src/lib/score-import/framework/ugpt-stats/update-ugpt-stats.ts similarity index 74% rename from server/src/lib/score-import/framework/profile-calculated-data/update-ugs.ts rename to server/src/lib/score-import/framework/ugpt-stats/update-ugpt-stats.ts index 0ea0c0342..a7cd1b10c 100644 --- a/server/src/lib/score-import/framework/profile-calculated-data/update-ugs.ts +++ b/server/src/lib/score-import/framework/ugpt-stats/update-ugpt-stats.ts @@ -1,12 +1,8 @@ -// "rating" refers to a user's profile statistics, as in their "rating" on a game. -// Some games have dedicated methods to calculate statistics like these, other games do not. -// That's about all there is to it! - -import { ProcessClassDeltas, UpdateUGSClasses } from "./classes"; -import { CalculateRatings } from "./rating"; +import { CalculateProfileRatings } from "../calculated-data/profile"; +import { CalculateUGPTClasses, ProcessClassDeltas } from "../profile-calculated-data/classes"; import db from "external/mongo/db"; import { CreateGameSettings } from "lib/game-settings/create-game-settings"; -import type { ClassHandler } from "./types"; +import type { ClassProvider } from "../calculated-data/types"; import type { KtLogger } from "lib/logger/logger"; import type { ClassDelta, Game, integer, Playtype, UserGameStats } from "tachi-common"; @@ -14,12 +10,12 @@ export async function UpdateUsersGamePlaytypeStats( game: Game, playtype: Playtype, userID: integer, - classHandler: ClassHandler | null, + classProvider: ClassProvider | null, logger: KtLogger ): Promise> { logger.debug(`Calculating Ratings...`); - const ratings = await CalculateRatings(game, playtype, userID, logger); + const ratings = await CalculateProfileRatings(game, playtype, userID); // Attempt to find a users game stats if one already exists. If one doesn't exist, // this is this players first import for this game! @@ -31,7 +27,14 @@ export async function UpdateUsersGamePlaytypeStats( logger.debug(`Calculating UGSClasses...`); - const classes = await UpdateUGSClasses(game, playtype, userID, ratings, classHandler, logger); + const classes = await CalculateUGPTClasses( + game, + playtype, + userID, + ratings, + classProvider, + logger + ); logger.debug(`Finished Calculating UGSClasses`); @@ -44,7 +47,7 @@ export async function UpdateUsersGamePlaytypeStats( if (userGameStats) { logger.debug(`Updated player gamestats for ${game} (${playtype})`); - const updateClasses: Record = {}; + const updateClasses: Record = {}; for (const delta of deltas) { updateClasses[`classes.${delta.set}`] = delta.new; diff --git a/server/src/lib/score-import/import-types/common/api-cg/parsers.ts b/server/src/lib/score-import/import-types/common/api-cg/parsers.ts index 9cb20b995..46dc496aa 100644 --- a/server/src/lib/score-import/import-types/common/api-cg/parsers.ts +++ b/server/src/lib/score-import/import-types/common/api-cg/parsers.ts @@ -134,7 +134,7 @@ export function CreateCGParser(cgGame: CGSupportedGames, service: CGServices) }, game: CGGameToTachiGame(cgGame), iterable: scores as Array, - classHandler: null, + classProvider: null, }; }; } diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.test.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.test.ts index ba9468b67..6357e6201 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.test.ts @@ -1,4 +1,4 @@ -import { CreateKaiIIDXClassHandler } from "./class-handler"; +import { CreateKaiIIDXClassProvider } from "./class-handler"; import { KaiTypeToBaseURL } from "../utils"; import CreateLogCtx from "lib/logger/logger"; import { IIDX_DANS } from "tachi-common"; @@ -8,10 +8,10 @@ import ResetDBState from "test-utils/resets"; const logger = CreateLogCtx(__filename); -t.test("#CreateKaiIIDXClassHandler", async (t) => { +t.test("#CreateKaiIIDXClassProvider", async (t) => { t.beforeEach(ResetDBState); - const fn = await CreateKaiIIDXClassHandler( + const fn = await CreateKaiIIDXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -46,7 +46,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => { }); t.test("Should return nothing if dan is not a number", async (t) => { - const fn = await CreateKaiIIDXClassHandler( + const fn = await CreateKaiIIDXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -74,7 +74,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => { }); t.test("Should return nothing if dan is too great", async (t) => { - const fn = await CreateKaiIIDXClassHandler( + const fn = await CreateKaiIIDXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -102,7 +102,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => { }); t.test("Should return nothing if dan is negative", async (t) => { - const fn = await CreateKaiIIDXClassHandler( + const fn = await CreateKaiIIDXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -130,7 +130,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => { }); t.test("Should gracefully handle negative API responses", async (t) => { - const fn = await CreateKaiIIDXClassHandler( + const fn = await CreateKaiIIDXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -149,7 +149,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => { t.test("Should call reauthFn if statusCode is 401", async (t) => { let pass = false; - const fn = await CreateKaiIIDXClassHandler( + const fn = await CreateKaiIIDXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -168,7 +168,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => { }); t.test("Should ignore null dans", async (t) => { - const fn = await CreateKaiIIDXClassHandler( + const fn = await CreateKaiIIDXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -196,7 +196,7 @@ t.test("#CreateKaiIIDXClassHandler", async (t) => { }); t.test("Should handle invalid playtypes", async (t) => { - const fn = await CreateKaiIIDXClassHandler( + const fn = await CreateKaiIIDXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts index 98e866ae1..f7f55b8c5 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts @@ -3,14 +3,14 @@ import { IIDX_DANS } from "tachi-common"; import nodeFetch from "utils/fetch"; import { IsRecord } from "utils/misc"; import type { KaiAPIReauthFunction } from "../traverse-api"; -import type { ClassHandler } from "lib/score-import/framework/profile-calculated-data/types"; +import type { ClassProvider } from "lib/score-import/framework/profile-calculated-data/types"; -export async function CreateKaiIIDXClassHandler( +export async function CreateKaiIIDXClassProvider( kaiType: "EAG" | "FLO", token: string, reauthFn: KaiAPIReauthFunction, fetch = nodeFetch -): Promise { +): Promise { let json: unknown; let err: unknown; const baseUrl = KaiTypeToBaseURL(kaiType); diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts index b97a19a7d..625f86e3b 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts @@ -1,4 +1,4 @@ -import { CreateKaiIIDXClassHandler } from "./class-handler"; +import { CreateKaiIIDXClassProvider } from "./class-handler"; import { CreateKaiReauthFunction } from "../reauth"; import { TraverseKaiAPI } from "../traverse-api"; import { KaiTypeToBaseURL } from "../utils"; @@ -30,7 +30,7 @@ export async function ParseKaiIIDX( context: { service, }, - classHandler: await CreateKaiIIDXClassHandler(service, authDoc.token, reauthFn, fetch), + classProvider: await CreateKaiIIDXClassProvider(service, authDoc.token, reauthFn, fetch), game: "iidx", }; } diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.test.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.test.ts index dfa62fbb4..894dccf6e 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.test.ts @@ -1,4 +1,4 @@ -import { CreateKaiSDVXClassHandler } from "./class-handler"; +import { CreateKaiSDVXClassProvider } from "./class-handler"; import { KaiTypeToBaseURL } from "../utils"; import CreateLogCtx from "lib/logger/logger"; import { SDVX_DANS } from "tachi-common"; @@ -8,10 +8,10 @@ import ResetDBState from "test-utils/resets"; const logger = CreateLogCtx(__filename); -t.test("#CreateKaiSDVXClassHandler", async (t) => { +t.test("#CreateKaiSDVXClassProvider", async (t) => { t.beforeEach(ResetDBState); - const fn = await CreateKaiSDVXClassHandler( + const fn = await CreateKaiSDVXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -45,7 +45,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => { }); t.test("Should return nothing if dan is not a number", async (t) => { - const fn = await CreateKaiSDVXClassHandler( + const fn = await CreateKaiSDVXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -72,7 +72,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => { }); t.test("Should return nothing if dan is too great", async (t) => { - const fn = await CreateKaiSDVXClassHandler( + const fn = await CreateKaiSDVXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -99,7 +99,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => { }); t.test("Should return nothing if dan is negative", async (t) => { - const fn = await CreateKaiSDVXClassHandler( + const fn = await CreateKaiSDVXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -126,7 +126,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => { }); t.test("Should gracefully handle negative API responses", async (t) => { - const fn = await CreateKaiSDVXClassHandler( + const fn = await CreateKaiSDVXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -145,7 +145,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => { t.test("Should call reauthFn if statusCode is 401", async (t) => { let pass = false; - const fn = await CreateKaiSDVXClassHandler( + const fn = await CreateKaiSDVXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await @@ -164,7 +164,7 @@ t.test("#CreateKaiSDVXClassHandler", async (t) => { }); t.test("Should ignore null dans", async (t) => { - const fn = await CreateKaiSDVXClassHandler( + const fn = await CreateKaiSDVXClassProvider( "FLO", "token", // eslint-disable-next-line @typescript-eslint/require-await diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.ts index 51bc32c1a..3f144e39c 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.ts @@ -3,14 +3,14 @@ import { SDVX_DANS } from "tachi-common"; import nodeFetch from "utils/fetch"; import { IsRecord } from "utils/misc"; import type { KaiAPIReauthFunction } from "../traverse-api"; -import type { ClassHandler } from "lib/score-import/framework/profile-calculated-data/types"; +import type { ClassProvider } from "lib/score-import/framework/profile-calculated-data/types"; -export async function CreateKaiSDVXClassHandler( +export async function CreateKaiSDVXClassProvider( kaiType: "EAG" | "FLO" | "MIN", token: string, reauthFn: KaiAPIReauthFunction, fetch = nodeFetch -): Promise { +): Promise { let json: unknown; let err: unknown; const baseUrl = KaiTypeToBaseURL(kaiType); diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts index f85c177ac..2386c0039 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts @@ -1,4 +1,4 @@ -import { CreateKaiSDVXClassHandler } from "./class-handler"; +import { CreateKaiSDVXClassProvider } from "./class-handler"; import { CreateKaiReauthFunction } from "../reauth"; import { TraverseKaiAPI } from "../traverse-api"; import { KaiTypeToBaseURL } from "../utils"; @@ -30,7 +30,7 @@ export async function ParseKaiSDVX( context: { service, }, - classHandler: await CreateKaiSDVXClassHandler(service, authDoc.token, reauthFn, fetch), + classProvider: await CreateKaiSDVXClassProvider(service, authDoc.token, reauthFn, fetch), game: "sdvx", }; } diff --git a/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts b/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts index aeaf380f4..cc4ecdc8b 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts @@ -1,12 +1,12 @@ import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; import { GetGamePTConfig } from "tachi-common"; -import type { ClassHandler } from "lib/score-import/framework/profile-calculated-data/types"; +import type { ClassProvider } from "lib/score-import/framework/profile-calculated-data/types"; import type { GamePTConfig, GPTString, ClassRecords, Classes } from "tachi-common"; // Note: This is tested by batch-manuals parser.test.ts. -export function CreateBatchManualClassHandler( +export function CreateBatchManualClassProvider( classes: Partial> -): ClassHandler { +): ClassProvider { return (game, playtype, userID, ratings, logger) => { const gptConfig = GetGamePTConfig(game, playtype); diff --git a/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts b/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts index 6aed5fc7b..9dd8218ee 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts @@ -336,9 +336,9 @@ t.test("#ParserFn", (t) => { logger ); - t.not(res.classHandler, null); + t.not(res.classProvider, null); - t.strictSame(res.classHandler!("iidx", "SP", 1, {}, logger), { dan: IIDX_DANS.KAIDEN }); + t.strictSame(res.classProvider!("iidx", "SP", 1, {}, logger), { dan: IIDX_DANS.KAIDEN }); t.end(); }); @@ -354,7 +354,7 @@ t.test("#ParserFn", (t) => { logger ); - t.equal(res.classHandler, null); + t.equal(res.classProvider, null); t.end(); }); diff --git a/server/src/lib/score-import/import-types/common/batch-manual/parser.ts b/server/src/lib/score-import/import-types/common/batch-manual/parser.ts index f9c1340b7..8c930d891 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/parser.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/parser.ts @@ -1,4 +1,4 @@ -import { CreateBatchManualClassHandler } from "./class-handler"; +import { CreateBatchManualClassProvider } from "./class-handler"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import { TachiConfig } from "lib/setup/config"; import { p } from "prudence"; @@ -104,8 +104,8 @@ export function ParseBatchManualFromObject( // if classes are provided, use those as a class handler. Otherwise, we // don't care. - classHandler: batchManual.classes - ? CreateBatchManualClassHandler(batchManual.classes) + classProvider: batchManual.classes + ? CreateBatchManualClassProvider(batchManual.classes) : null, }; } diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts index 0938fbbca..478d69522 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts @@ -245,7 +245,7 @@ function GenericParseEamIIDXCSV( iterable: iterableData, context, game: "iidx", - classHandler: null, + classProvider: null, }; } diff --git a/server/src/lib/score-import/import-types/common/types.ts b/server/src/lib/score-import/import-types/common/types.ts index 94c3a777e..308c4095d 100644 --- a/server/src/lib/score-import/import-types/common/types.ts +++ b/server/src/lib/score-import/import-types/common/types.ts @@ -1,6 +1,5 @@ import type { ConverterFailure } from "../../framework/common/converter-failures"; import type { DryScore } from "../../framework/common/types"; -import type { ClassHandler } from "../../framework/profile-calculated-data/types"; import type { SDVXEamusementCSVData } from "../file/eamusement-sdvx-csv/types"; import type { MerScore } from "../file/mer-iidx/types"; import type { S3Score } from "../file/solid-state-squad/types"; @@ -13,17 +12,12 @@ import type { KsHookSV6CStaticScore } from "../ir/kshook-sv6c-static/types"; import type { KsHookSV6CContext, KsHookSV6CScore } from "../ir/kshook-sv6c/types"; import type { LR2HookContext, LR2HookScore } from "../ir/lr2hook/types"; import type { IRUSCContext } from "../ir/usc/types"; -import type { - CGContext, - CGJubeatScore, - CGMusecaScore, - CGPopnScore, - CGSDVXScore, -} from "./api-cg/types"; +import type { CGContext, CGMusecaScore, CGPopnScore, CGSDVXScore } from "./api-cg/types"; import type { KaiContext } from "./api-kai/types"; import type { BatchManualContext } from "./batch-manual/types"; import type { IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./eamusement-iidx-csv/types"; import type { KtLogger } from "lib/logger/logger"; +import type { ClassProvider } from "lib/score-import/framework/calculated-data/types"; import type { USCClientScore } from "server/router/ir/usc/_playtype/types"; import type { BatchManualScore, @@ -31,7 +25,6 @@ import type { Game, ImportTypes, integer, - MongoDBDocument, SongDocument, } from "tachi-common"; import type { EmptyObject } from "utils/types"; @@ -113,7 +106,7 @@ export interface ImportTypeContextMap { "api/cg-prod-popn": CGContext; } -export interface OrphanScoreDocument extends MongoDBDocument { +export interface OrphanScoreDocument { importType: T; data: ImportTypeDataMap[T]; context: ImportTypeContextMap[T]; @@ -147,7 +140,7 @@ export interface ParserFunctionReturns { iterable: AsyncIterable | Iterable; context: C; game: Game; - classHandler: ClassHandler | null; + classProvider: ClassProvider | null; } export type ParserFunction> = ( diff --git a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.ts b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.ts index a071dec88..d1924d5e7 100644 --- a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.ts +++ b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.ts @@ -66,6 +66,6 @@ export default function ParseEamusementSDVXCSV( iterable, context: {}, game: "sdvx", - classHandler: null, + classProvider: null, }; } diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts b/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts index a314e87fc..8eff65d66 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts @@ -35,7 +35,7 @@ t.test("#ParseMerIIDX", (t) => { { game: "iidx", context: {}, - classHandler: null, + classProvider: null, iterable: [ { music_id: 3007, diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts b/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts index 1ac05703a..59b2e7716 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts @@ -53,7 +53,7 @@ export function ParseMerIIDX( } return { - classHandler: null, + classProvider: null, context: {}, iterable: jsonData as Array, game: "iidx", diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts index 52c17f7e9..ea6f01933 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts @@ -60,7 +60,7 @@ t.test("#ParseSolidStateXML", (t) => { "Should return the right scores in the iterable." ); t.equal(res.game, "iidx", "Should return IIDX as the game."); - t.equal(res.classHandler, null, "Should return no class handler."); + t.equal(res.classProvider, null, "Should return no class handler."); t.strictSame(res.context, {}, "Should return no context."); t.end(); @@ -97,7 +97,7 @@ t.test("#ParseSolidStateXML", (t) => { "Should return the right score in the iterable." ); t.equal(res.game, "iidx", "Should return IIDX as the game."); - t.equal(res.classHandler, null, "Should return no class handler."); + t.equal(res.classProvider, null, "Should return no class handler."); t.strictSame(res.context, {}, "Should return no context."); t.end(); @@ -134,7 +134,7 @@ t.test("#ParseSolidStateXML", (t) => { "Should return the right score in the iterable." ); t.equal(res.game, "iidx", "Should return IIDX as the game."); - t.equal(res.classHandler, null, "Should return no class handler."); + t.equal(res.classProvider, null, "Should return no class handler."); t.strictSame(res.context, {}, "Should return no context."); t.end(); diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts index a7318e847..d649e1745 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts @@ -132,7 +132,7 @@ export function ParseSolidStateXML( })); return { - classHandler: null, + classProvider: null, context: {}, iterable: scoreData as Array, game: "iidx", diff --git a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/class-handler.ts b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/class-handler.ts index 3a1087f28..15c91159b 100644 --- a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/class-handler.ts +++ b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/class-handler.ts @@ -1,6 +1,6 @@ import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import { WACCA_STAGEUPS } from "tachi-common"; -import type { ClassHandler } from "../../../framework/profile-calculated-data/types"; +import type { ClassProvider } from "../../../framework/ugpt-stats/types"; import type { MyPagePlayerStage } from "./types"; const STAGES: Record = { @@ -38,7 +38,7 @@ const STAGE_NAMES: Record = { 14: "XIV", }; -export function CreateMyPageScraperClassHandler(stage: MyPagePlayerStage): ClassHandler { +export function CreateMyPageScraperClassProvider(stage: MyPagePlayerStage): ClassProvider { return (_game, _playtype, _userID, _ratings, logger) => { const stageName = STAGE_NAMES[stage.id]; diff --git a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.test.ts b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.test.ts index 46d7ac969..ed6033ace 100644 --- a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.test.ts +++ b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.test.ts @@ -122,17 +122,17 @@ t.test("#ParseMyPageScraperPlayerCSV", (t) => { const file = MockMulterFile(buffer, "player.csv"); - const { iterable, game, classHandler } = ParseMyPageScraperPlayerCSV(file, {}, logger); + const { iterable, game, classProvider: classProvider } = ParseMyPageScraperPlayerCSV(file, {}, logger); t.equal(game, "wacca"); t.strictSame(iterable, [], "Should not have any score data."); - t.not(classHandler, null); + t.not(classProvider, null); - // There's no good way to test that the classHandler got a valid + // There's no good way to test that the classProvider got a valid // MyPagePlayerStage, so we just call it to see. - t.strictSame(classHandler!("wacca", "Single", 0, {}, logger), { + t.strictSame(classProvider!("wacca", "Single", 0, {}, logger), { stageUp: WACCA_STAGEUPS.XII, }); diff --git a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.ts b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.ts index 51771f32f..cb3fa0f21 100644 --- a/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.ts +++ b/server/src/lib/score-import/import-types/file/wacca-mypage-scraper/parser.ts @@ -1,4 +1,4 @@ -import { CreateMyPageScraperClassHandler } from "./class-handler"; +import { CreateMyPageScraperClassProvider } from "./class-handler"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import { parse, CsvError as CSVError } from "csv-parse/sync"; import { p } from "prudence"; @@ -111,7 +111,7 @@ export function ParseMyPageScraperRecordsCSV( iterable, context: {}, game: "wacca", - classHandler: null, + classProvider: null, }; } @@ -169,6 +169,6 @@ export function ParseMyPageScraperPlayerCSV( iterable: [], context: {}, game: "wacca", - classHandler: CreateMyPageScraperClassHandler(stage), + classProvider: CreateMyPageScraperClassProvider(stage), }; } diff --git a/server/src/lib/score-import/import-types/ir/barbatos/parser.ts b/server/src/lib/score-import/import-types/ir/barbatos/parser.ts index df7c6ba32..220188971 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/parser.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/parser.ts @@ -75,6 +75,6 @@ export function ParseBarbatosSingle( }, game: "sdvx", iterable: [body] as unknown as Array, - classHandler: null, + classProvider: null, }; } diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts index 37575933f..ac55c5948 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts @@ -126,6 +126,6 @@ export function ParseBeatorajaSingle( }, game: isPMS ? "pms" : "bms", iterable: [score], - classHandler: null, + classProvider: null, }; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts index 55bc6ae83..d6ff90791 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts @@ -1,4 +1,4 @@ -import { CreateFerStaticClassHandler } from "./class-handler"; +import { CreateFerStaticClassProvider } from "./class-handler"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import ResetDBState from "test-utils/resets"; @@ -6,11 +6,11 @@ import type { Playtype } from "tachi-common"; const logger = CreateLogCtx(__filename); -t.test("#FerStaticClassHandler", (t) => { +t.test("#FerStaticClassProvider", (t) => { t.beforeEach(ResetDBState); t.test("Should curry a function", (t) => { - const res = CreateFerStaticClassHandler({ sp_dan: 1 }); + const res = CreateFerStaticClassProvider({ sp_dan: 1 }); t.equal(typeof res, "function"); @@ -18,7 +18,7 @@ t.test("#FerStaticClassHandler", (t) => { }); t.test("Should work with no dans", (t) => { - const res = CreateFerStaticClassHandler({})("iidx", "SP", 1, {}, logger); + const res = CreateFerStaticClassProvider({})("iidx", "SP", 1, {}, logger); t.equal(res, undefined, "Should return nothing."); @@ -26,7 +26,7 @@ t.test("#FerStaticClassHandler", (t) => { }); t.test("Should update the same dan as the playtype.", (t) => { - const fn = CreateFerStaticClassHandler({ sp_dan: 5, dp_dan: 7 }); + const fn = CreateFerStaticClassProvider({ sp_dan: 5, dp_dan: 7 }); const res = fn("iidx", "SP", 1, {}, logger); t.strictSame(res, { dan: 5 }, "Should return SP dan's value."); @@ -39,7 +39,7 @@ t.test("#FerStaticClassHandler", (t) => { }); t.test("Should skip if dan is invalid.", (t) => { - const fn = CreateFerStaticClassHandler({ sp_dan: -1, dp_dan: 100 }); + const fn = CreateFerStaticClassProvider({ sp_dan: -1, dp_dan: 100 }); const res = fn("iidx", "SP", 1, {}, logger); t.equal(res, undefined, "Should skip SP dan's value."); @@ -52,7 +52,7 @@ t.test("#FerStaticClassHandler", (t) => { }); t.test("Should skip if playtype is invalid", (t) => { - const fn = CreateFerStaticClassHandler({ sp_dan: 5, dp_dan: 7 }); + const fn = CreateFerStaticClassProvider({ sp_dan: 5, dp_dan: 7 }); const res = fn("iidx", "INVALID" as Playtype, 1, {}, logger); t.equal(res, undefined, "Should skip over as a failsafe."); diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts index 5ebaa93cc..eb113f101 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts @@ -1,17 +1,17 @@ -import type { ClassHandler } from "../../../framework/profile-calculated-data/types"; +import type { ClassProvider } from "lib/score-import/framework/calculated-data/types"; import type { integer } from "tachi-common"; -export function CreateFerStaticClassHandler(body: Record): ClassHandler { - return (game, playtype, userID, ratings, logger) => { +export function CreateFerStaticClassProvider(body: Record): ClassProvider { + return (gptString, userID, ratings, logger) => { let index; - if (playtype === "SP") { + if (gptString === "iidx:SP") { index = body.sp_dan; - } else if (playtype === "DP") { + } else if (gptString === "iidx:DP") { index = body.dp_dan; } else { logger.warn( - `Invalid playtype ${playtype} passed to FerStaticClassHandler. Attempting to continue.` + `Invalid gptString ${gptString} passed to FerStaticClassProvider. Attempting to continue.` ); return; } @@ -21,7 +21,7 @@ export function CreateFerStaticClassHandler(body: Record): Clas } if (!Number.isInteger(index)) { - logger.info(`received invalid fer-static class of ${index} (${playtype}).`, { body }); + logger.info(`received invalid fer-static class of ${index} (${gptString}).`, { body }); return; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts index 165f18eea..7b5889b21 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts @@ -45,7 +45,7 @@ t.test("#ParseFervidexStatic", (t) => { game: "iidx", }); - t.type(res.classHandler, "function", "Should have a function ready for class handling"); + t.type(res.classProvider, "function", "Should have a function ready for class handling"); t.end(); }); @@ -65,7 +65,7 @@ t.test("#ParseFervidexStatic", (t) => { }); t.type( - res.classHandler, + res.classProvider, "function", "Should have a function ready for class handling, regardless of shouldImportScores being false" ); diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts index 3562c33b3..dbe3257f2 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts @@ -1,4 +1,4 @@ -import { CreateFerStaticClassHandler } from "./class-handler"; +import { CreateFerStaticClassProvider } from "./class-handler"; import { AssertStrAsPositiveInt } from "../../../framework/common/string-asserts"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import { SoftwareIDToVersion } from "../fervidex/parser"; @@ -22,7 +22,7 @@ export function ParseFervidexStatic( logger: KtLogger ): ParserFunctionReturns { const version = SoftwareIDToVersion(headers.model, logger); - const classHandler = CreateFerStaticClassHandler(body); + const classProvider = CreateFerStaticClassProvider(body); // if we shouldn't import scores, just sync up dans. if (!headers.shouldImportScores) { @@ -30,7 +30,7 @@ export function ParseFervidexStatic( context: { version }, game: "iidx", iterable: [], - classHandler, + classProvider, }; } @@ -90,6 +90,6 @@ export function ParseFervidexStatic( context: { version }, game: "iidx", iterable: scores, - classHandler, + classProvider, }; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex/parser.ts b/server/src/lib/score-import/import-types/ir/fervidex/parser.ts index 13d09e613..2aeba13af 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/parser.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/parser.ts @@ -150,6 +150,6 @@ export function ParseFervidexSingle( context: { version, timeReceived: Date.now(), userID }, game: "iidx", iterable: [score], - classHandler: null, + classProvider: null, }; } diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv6c-static/parser.ts b/server/src/lib/score-import/import-types/ir/kshook-sv6c-static/parser.ts index a542a0005..a031debc0 100644 --- a/server/src/lib/score-import/import-types/ir/kshook-sv6c-static/parser.ts +++ b/server/src/lib/score-import/import-types/ir/kshook-sv6c-static/parser.ts @@ -41,6 +41,6 @@ export function ParseKsHookSV6CStatic( game: "sdvx", iterable: data.scores, context: {}, - classHandler: null, + classProvider: null, }; } diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.ts b/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.ts index ea485e459..c874deebf 100644 --- a/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.ts +++ b/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.ts @@ -69,6 +69,6 @@ export function ParseKsHookSV6C( context: { timeReceived: Date.now(), }, - classHandler: null, + classProvider: null, }; } diff --git a/server/src/lib/score-import/import-types/ir/lr2hook/parser.test.ts b/server/src/lib/score-import/import-types/ir/lr2hook/parser.test.ts index f287ab87f..5ad89d576 100644 --- a/server/src/lib/score-import/import-types/ir/lr2hook/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/lr2hook/parser.test.ts @@ -22,7 +22,7 @@ t.test("#ParseLR2Hook", (t) => { t.equal(res.game, "bms"); t.type(res.context.timeReceived, "number"); t.ok(Array.isArray(res.iterable)); - t.equal(res.classHandler, null); + t.equal(res.classProvider, null); } catch (err) { t.fail(`[${message}] ${(err as Error).message}`); } diff --git a/server/src/lib/score-import/import-types/ir/lr2hook/parser.ts b/server/src/lib/score-import/import-types/ir/lr2hook/parser.ts index 8495042bd..6d9f89867 100644 --- a/server/src/lib/score-import/import-types/ir/lr2hook/parser.ts +++ b/server/src/lib/score-import/import-types/ir/lr2hook/parser.ts @@ -83,6 +83,6 @@ export function ParseLR2Hook( context: { timeReceived: Date.now(), }, - classHandler: null, + classProvider: null, }; } diff --git a/server/src/lib/score-import/import-types/ir/usc/parser.ts b/server/src/lib/score-import/import-types/ir/usc/parser.ts index 366a18cce..ac1ac2712 100644 --- a/server/src/lib/score-import/import-types/ir/usc/parser.ts +++ b/server/src/lib/score-import/import-types/ir/usc/parser.ts @@ -67,6 +67,6 @@ export function ParseIRUSC( }, game: "usc", iterable: [score] as Array, - classHandler: null, + classProvider: null, }; } diff --git a/server/src/lib/score-mutation/delete-scores.ts b/server/src/lib/score-mutation/delete-scores.ts index 5054ffc52..497f9c83d 100644 --- a/server/src/lib/score-mutation/delete-scores.ts +++ b/server/src/lib/score-mutation/delete-scores.ts @@ -5,7 +5,7 @@ import { GetAndUpdateUsersGoals } from "lib/score-import/framework/goals/goals"; import { UpdateChartRanking } from "lib/score-import/framework/pb/create-pb-doc"; import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs"; import { UpdateUsersQuests } from "lib/score-import/framework/quests/quests"; -import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/profile-calculated-data/update-ugs"; +import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/ugpt-stats/update-ugpt-stats"; import { RecalcSessions } from "utils/calculations/recalc-sessions"; import type { Game, Playtype, ScoreDocument } from "tachi-common"; diff --git a/server/src/lib/targets/goal-utils.ts b/server/src/lib/targets/goal-utils.ts index 7333a3191..69cef827d 100644 --- a/server/src/lib/targets/goal-utils.ts +++ b/server/src/lib/targets/goal-utils.ts @@ -1,10 +1,9 @@ import db from "external/mongo/db"; -import { GenericCalculatePercent } from "lib/score-import/framework/common/score-utils"; -import { FormatGame, GetGamePTConfig } from "tachi-common"; +import { FormatGame, GetGamePTConfig, GetSpecificGPTConfig } from "tachi-common"; import { GetFolderForIDGuaranteed, HumaniseChartID } from "utils/db"; import { GetFolderChartIDs } from "utils/folder"; import { FormatMaxDP, HumanisedJoinArray } from "utils/misc"; -import type { ChartDocument, Game, GoalDocument, Playtype } from "tachi-common"; +import type { ChartDocument, Game, GoalDocument, Playtype, GPTString } from "tachi-common"; export async function CreateGoalTitle( charts: GoalDocument["charts"], @@ -119,8 +118,19 @@ async function FormatCharts( } } -function FormatCriteria(criteria: GoalDocument["criteria"], game: Game, playtype: Playtype) { - const gptConfig = GetGamePTConfig(game, playtype); +function FormatCriteria( + criteria: GoalDocument["criteria"], + gptString: GPT +) { + const gptConfig = GetSpecificGPTConfig(gptString); + + switch (criteria.key) { + case "": + break; + + default: + break; + } switch (criteria.key) { case "scoreData.gradeIndex": diff --git a/server/src/scripts/state-sync/recalc-game-profiles.ts b/server/src/scripts/state-sync/recalc-game-profiles.ts index ecb035b41..4a51639ff 100644 --- a/server/src/scripts/state-sync/recalc-game-profiles.ts +++ b/server/src/scripts/state-sync/recalc-game-profiles.ts @@ -1,7 +1,7 @@ /* eslint-disable no-await-in-loop */ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/profile-calculated-data/update-ugs"; +import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/ugpt-stats/update-ugpt-stats"; import { WrapScriptPromise } from "utils/misc"; import { FormatUserDoc } from "utils/user"; import type { Game, Playtype, ScoreDocument } from "tachi-common"; diff --git a/server/src/server/router/ir/fervidex/router.ts b/server/src/server/router/ir/fervidex/router.ts index a5a81cc09..5dbfc431d 100644 --- a/server/src/server/router/ir/fervidex/router.ts +++ b/server/src/server/router/ir/fervidex/router.ts @@ -8,11 +8,12 @@ import { SoftwareIDToVersion } from "lib/score-import/import-types/ir/fervidex/p import { p } from "prudence"; import { RequirePermissions } from "server/middleware/auth"; import { PrudenceErrorFormatter } from "server/middleware/prudence-validate"; +import { GetGamePTConfig } from "tachi-common"; import { UpdateClassIfGreater } from "utils/class"; import { ParseEA3SoftID } from "utils/ea3id"; import { IsNullishOrEmptyStr } from "utils/misc"; import type { RequestHandler } from "express"; -import type { integer, Playtypes } from "tachi-common"; +import type { Playtypes, SpecificGamePTConfig, integer } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -316,12 +317,25 @@ router.post("/class/submit", ValidateModelHeader, async (req, res) => { // is 0 or 1. const playtype: Playtypes["iidx"] = body.play_style === 0 ? "SP" : "DP"; + const dans = ( + GetGamePTConfig("iidx", playtype) as unknown as SpecificGamePTConfig<"iidx:DP" | "iidx:SP"> + ).classes.dan.values; + + const dan = dans[body.course_id]; + + if (dan === undefined) { + return res.status(400).json({ + success: false, + description: `Unknown course_id ${body.course_id}.`, + }); + } + const r = await UpdateClassIfGreater( req[SYMBOL_TACHI_API_AUTH].userID!, "iidx", playtype, "dan", - body.course_id + dan.id ); return res.status(200).json({ diff --git a/server/src/utils/calculations/recalc-game-profiles.ts b/server/src/utils/calculations/recalc-game-profiles.ts index 6f3a2f98a..5c53eec32 100644 --- a/server/src/utils/calculations/recalc-game-profiles.ts +++ b/server/src/utils/calculations/recalc-game-profiles.ts @@ -2,7 +2,7 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/profile-calculated-data/update-ugs"; +import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/ugpt-stats/update-ugpt-stats"; import { FormatUserDoc } from "utils/user"; const logger = CreateLogCtx(__filename); diff --git a/server/src/utils/calculations/recalc-scores.ts b/server/src/utils/calculations/recalc-scores.ts index d72c24f8e..fcbda5d33 100644 --- a/server/src/utils/calculations/recalc-scores.ts +++ b/server/src/utils/calculations/recalc-scores.ts @@ -6,7 +6,7 @@ import { CreateCalculatedData } from "lib/score-import/framework/score-calculate import { GetAndUpdateUsersGoals } from "lib/score-import/framework/goals/goals"; import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs"; import { UpdateUsersQuests } from "lib/score-import/framework/quests/quests"; -import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/profile-calculated-data/update-ugs"; +import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/ugpt-stats/update-ugpt-stats"; import { TachiConfig } from "lib/setup/config"; import { GetGameConfig } from "tachi-common"; import { EfficientDBIterate } from "utils/efficient-db-iterate"; diff --git a/server/src/utils/class.ts b/server/src/utils/class.ts index 32af803b4..d9026b43d 100644 --- a/server/src/utils/class.ts +++ b/server/src/utils/class.ts @@ -2,8 +2,8 @@ import db from "external/mongo/db"; import { CreateGameSettings } from "lib/game-settings/create-game-settings"; import CreateLogCtx from "lib/logger/logger"; import { EmitWebhookEvent } from "lib/webhooks/webhooks"; -import type { Game, GPTString, integer, Playtype, UserGameStats } from "tachi-common"; -import type { Classes } from "tachi-common/game-classes"; +import { GetGPTConfig, GetGPTString } from "tachi-common"; +import type { Game, GPTString, integer, Playtype, UserGameStats, Classes } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -13,15 +13,61 @@ const logger = CreateLogCtx(__filename); * to compare to, and FALSE if it is worse or equal. */ export function ReturnClassIfGreater( + gptString: GPTString, classSet: Classes[GPTString], - classVal: integer, + classVal: string, userGameStats?: UserGameStats | null ) { - if (!userGameStats || userGameStats.classes[classSet] === undefined) { + const gptConfig = GetGPTConfig(gptString); + + const classInfo = gptConfig.classes[classSet]; + + if (!classInfo) { + logger.warn( + `Invalid ReturnClassIfGreater call. Attempted to index set '${classSet}' on ${gptString}. No such class is defined for this game.` + ); + return null; } - return classVal > userGameStats.classes[classSet]!; + if (!userGameStats) { + return null; + } + + const prevClass: string | null | undefined = userGameStats.classes[classSet]; + + if (prevClass === null || prevClass === undefined) { + return null; + } + + const previousClassIndex = ClassToIndex(gptString, classSet, prevClass); + const newClassIndex = ClassToIndex(gptString, classSet, classVal); + + return newClassIndex > previousClassIndex; +} + +export function ClassToIndex(gptString: GPTString, classSet: Classes[GPTString], classVal: string) { + const gptConfig = GetGPTConfig(gptString); + + const classInfo = gptConfig.classes[classSet]; + + if (!classInfo) { + logger.warn( + `Invalid ClassToIndex call. Attempted to index set '${classSet}' on ${gptString}. No such class is defined for this game. Returning -1.` + ); + + return -1; + } + + 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.` + ); + } + + return v; } /** @@ -37,10 +83,12 @@ export async function UpdateClassIfGreater( game: Game, playtype: Playtype, classSet: Classes[GPTString], - classVal: integer + classVal: string ) { + const gptString = GetGPTString(game, playtype); + const userGameStats = await db["game-stats"].findOne({ userID, game, playtype }); - const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats); + const isGreater = ReturnClassIfGreater(gptString, classSet, classVal, userGameStats); if (isGreater === false) { return false;