From 19e158333f7b7da3dab4a782480e2657da40f4b7 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 4 Nov 2022 14:40:56 +0000 Subject: [PATCH 1/3] feat: allow certain classes to be downgraded Things like class colours, volforce classes, etc. should be able to be downgraded if they fall below their requisites. --- common/src/config/config.ts | 56 +++++++++++++ .../user-game-stats/builtin-class-handlers.ts | 2 +- .../framework/user-game-stats/classes.test.ts | 25 +++++- .../framework/user-game-stats/classes.ts | 22 ++++- .../framework/user-game-stats/update-ugs.ts | 5 ++ .../lib/score-mutation/delete-scores.test.ts | 80 ++++++++++++++++++- .../src/lib/score-mutation/delete-scores.ts | 2 +- server/src/test-utils/misc.ts | 17 ++++ server/src/test-utils/test-data.ts | 34 ++++++++ 9 files changed, 236 insertions(+), 7 deletions(-) diff --git a/common/src/config/config.ts b/common/src/config/config.ts index 6964d1098..bfbd2cfb9 100644 --- a/common/src/config/config.ts +++ b/common/src/config/config.ts @@ -81,6 +81,7 @@ interface BaseGamePTConfig { clearLamp: Lamps[I]; classHumanisedFormat: Record>; + classProperties: Record; judgements: Array; @@ -356,6 +357,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { dan: IIDXDans, }, + classProperties: { + dan: { downgradable: false }, + }, supportsESD: true, judgementWindows: [ @@ -553,6 +557,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { dan: IIDXDans, }, + classProperties: { + dan: { downgradable: false }, + }, supportsESD: true, judgementWindows: [ @@ -677,6 +684,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { colour: CHUNITHMColours, }, + classProperties: { + colour: { downgradable: true }, + }, supportsESD: false, judgements: ["jcrit", "justice", "attack", "miss"], @@ -769,6 +779,10 @@ const GAME_PT_CONFIGS: GamePTConfigs = { dan: SDVXDans, vfClass: SDVXVFClasses, }, + classProperties: { + dan: { downgradable: false }, + vfClass: { downgradable: true }, + }, supportsESD: false, judgements: ["critical", "near", "miss"], @@ -855,6 +869,7 @@ const GAME_PT_CONFIGS: GamePTConfigs = { clearLamp: "CLEAR", classHumanisedFormat: {}, + classProperties: {}, supportsESD: false, judgements: ["critical", "near", "miss"], @@ -939,6 +954,7 @@ const GAME_PT_CONFIGS: GamePTConfigs = { clearLamp: "CLEAR", classHumanisedFormat: {}, + classProperties: {}, supportsESD: false, judgements: ["critical", "near", "miss"], @@ -1010,6 +1026,7 @@ const GAME_PT_CONFIGS: GamePTConfigs = { clearLamp: "CLEAR", classHumanisedFormat: {}, + classProperties: {}, supportsESD: true, judgementWindows: [ @@ -1122,6 +1139,12 @@ const GAME_PT_CONFIGS: GamePTConfigs = { lnDan: BMSLNDans, scratchDan: BMSScratchDans, }, + classProperties: { + genocideDan: { downgradable: false }, + stslDan: { downgradable: false }, + lnDan: { downgradable: false }, + scratchDan: { downgradable: false }, + }, supportsESD: false, judgements: ["pgreat", "great", "good", "bad", "poor"], @@ -1231,6 +1254,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { genocideDan: BMSGenocideDans, }, + classProperties: { + genocideDan: { downgradable: false }, + }, supportsESD: false, judgements: ["pgreat", "great", "good", "bad", "poor"], @@ -1358,6 +1384,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { dan: DDRDans, }, + classProperties: { + dan: { downgradable: false }, + }, supportsESD: true, judgementWindows: [ @@ -1487,6 +1516,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { dan: DDRDans, }, + classProperties: { + dan: { downgradable: false }, + }, supportsESD: true, judgementWindows: [ @@ -1585,6 +1617,7 @@ const GAME_PT_CONFIGS: GamePTConfigs = { clearLamp: "CLEAR", classHumanisedFormat: {}, + classProperties: {}, supportsESD: false, judgements: ["perfect", "great", "good", "miss"], @@ -1688,6 +1721,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { colour: GitadoraColours, }, + classProperties: { + colour: { downgradable: true }, + }, supportsESD: false, judgements: ["perfect", "great", "good", "ok", "miss"], @@ -1774,6 +1810,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { colour: GitadoraColours, }, + classProperties: { + colour: { downgradable: true }, + }, supportsESD: false, judgements: ["perfect", "great", "good", "ok", "miss"], @@ -1863,6 +1902,10 @@ const GAME_PT_CONFIGS: GamePTConfigs = { stageUp: WaccaStageUps, colour: WaccaColours, }, + classProperties: { + stageUp: { downgradable: false }, + colour: { downgradable: true }, + }, supportsESD: false, judgements: ["marvelous", "great", "good", "miss"], @@ -1947,6 +1990,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { class: PopnClasses, }, + classProperties: { + class: { downgradable: true }, + }, supportsESD: false, judgements: ["cool", "great", "good", "bad"], @@ -2034,6 +2080,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { colour: JubeatColours, }, + classProperties: { + colour: { downgradable: true }, + }, supportsESD: false, judgements: ["perfect", "great", "good", "poor", "miss"], @@ -2139,6 +2188,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { dan: PMSDans, }, + classProperties: { + dan: { downgradable: false }, + }, supportsESD: false, judgements: ["cool", "great", "good", "bad", "poor"], @@ -2247,6 +2299,9 @@ const GAME_PT_CONFIGS: GamePTConfigs = { classHumanisedFormat: { dan: PMSDans, }, + classProperties: { + dan: { downgradable: false }, + }, supportsESD: false, judgements: ["cool", "great", "good", "bad", "poor"], @@ -2350,6 +2405,7 @@ const GAME_PT_CONFIGS: GamePTConfigs = { clearLamp: "CLEAR", classHumanisedFormat: {}, + classProperties: {}, supportsESD: false, judgements: ["fantastic", "excellent", "great", "decent", "wayoff", "miss"], diff --git a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts index 390c9f696..6e987d466 100644 --- a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts +++ b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts @@ -34,7 +34,7 @@ export function CalculateSDVXClass( export function SDVXVF6ToClass(vf: number, logger: KtLogger) { // This is impossible, but a failsafe regardless if (vf >= 24) { - logger.warn(`User has excessive VF5 of ${vf}. Defaulting to Imperial IV.`); + logger.warn(`User has excessive VF6 of ${vf}. Defaulting to Imperial IV.`); return SDVXVFClasses.IMPERIAL_IV; } else if (vf >= 20) { // imperial i -> iv has gaps of 1 diff --git a/server/src/lib/score-import/framework/user-game-stats/classes.test.ts b/server/src/lib/score-import/framework/user-game-stats/classes.test.ts index d27ce5947..f98c466c7 100644 --- a/server/src/lib/score-import/framework/user-game-stats/classes.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/classes.test.ts @@ -124,7 +124,7 @@ t.test("#ProcessClassDeltas", (t) => { t.end(); }); - t.test("Should not return worse classes", async (t) => { + t.test("Should not return worse classes if the class isn't downgradable", async (t) => { const res = await ProcessClassDeltas( "iidx", "SP", @@ -139,5 +139,28 @@ t.test("#ProcessClassDeltas", (t) => { t.end(); }); + t.test("Should return worse classes if the class is downgradable", async (t) => { + const res = await ProcessClassDeltas( + "sdvx", + "Single", + { vfClass: 10 }, + { classes: { vfClass: 9 } } as unknown as UserGameStats, + 1, + logger + ); + + t.strictSame(res, [ + { + game: "sdvx", + set: "vfClass", + playtype: "Single", + old: 10, + new: 9, + }, + ]); + + t.end(); + }); + t.end(); }); diff --git a/server/src/lib/score-import/framework/user-game-stats/classes.ts b/server/src/lib/score-import/framework/user-game-stats/classes.ts index 074abf869..6a9cb91a7 100644 --- a/server/src/lib/score-import/framework/user-game-stats/classes.ts +++ b/server/src/lib/score-import/framework/user-game-stats/classes.ts @@ -9,6 +9,7 @@ import { import deepmerge from "deepmerge"; import db from "external/mongo/db"; import { EmitWebhookEvent } from "lib/webhooks/webhooks"; +import { GetGamePTConfig } from "tachi-common"; import { ReturnClassIfGreater } from "utils/class"; import type { ClassHandler, ScoreClasses } from "./types"; import type { KtLogger } from "lib/logger/logger"; @@ -87,7 +88,7 @@ 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 ImportTypeClassResolveFn - The Custom Resolve Function that certain import types may pass to us as a means + * @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 * defaults. */ @@ -143,6 +144,8 @@ export async function ProcessClassDeltas( const achievementOps = []; + const gptConfig = GetGamePTConfig(game, playtype); + for (const s of Object.keys(classes)) { const classSet = s as keyof GameClasses; const classVal = classes[classSet]; @@ -152,12 +155,19 @@ export async function ProcessClassDeltas( continue; } + const classConfig = gptConfig.classProperties[classSet]; + try { const isGreater = ReturnClassIfGreater(classSet, classVal, userGameStats); - if (isGreater === false) { + // if this was worse, and this class isn't downgradable (i.e. it's a dan) + // then don't do anything + if (isGreater === false && !classConfig.downgradable) { continue; } else { + // otherwise, provide this as an update. + // This *may* be negative in the case where the user downgraded a + // downgradable class (i.e. deleted scores, chart re-rates). let delta: ClassDelta; if (isGreater === null) { @@ -178,7 +188,13 @@ export async function ProcessClassDeltas( }; } - void EmitWebhookEvent({ type: "class-update/v1", content: { userID, ...delta } }); + // if this wasn't a downgrade + if (isGreater !== false) { + void EmitWebhookEvent({ + type: "class-update/v1", + content: { userID, ...delta }, + }); + } achievementOps.push({ userID, diff --git a/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts b/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts index 6b0033297..b887c187c 100644 --- a/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts +++ b/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts @@ -10,6 +10,9 @@ import type { ClassHandler } from "./types"; import type { KtLogger } from "lib/logger/logger"; import type { ClassDelta, Game, integer, Playtype, UserGameStats } from "tachi-common"; +/** + * @param allowDowngrades - If passed, this will allow metrics to be downgraded. + */ export async function UpdateUsersGamePlaytypeStats( game: Game, playtype: Playtype, @@ -17,6 +20,8 @@ export async function UpdateUsersGamePlaytypeStats( classHandler: ClassHandler | null, logger: KtLogger ): Promise> { + logger.debug(`Calculating Ratings...`); + const ratings = await CalculateRatings(game, playtype, userID, logger); // Attempt to find a users game stats if one already exists. If one doesn't exist, diff --git a/server/src/lib/score-mutation/delete-scores.test.ts b/server/src/lib/score-mutation/delete-scores.test.ts index 9ac2a35e9..3fa32a2e5 100644 --- a/server/src/lib/score-mutation/delete-scores.test.ts +++ b/server/src/lib/score-mutation/delete-scores.test.ts @@ -1,11 +1,16 @@ import { DeleteScore } from "./delete-scores"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; +import { SDVXVFClasses } from "lib/constants/classes"; +import CreateLogCtx from "lib/logger/logger"; import t from "tap"; +import { mkFakeSDVXChart, mkFakeSDVXPB } from "test-utils/misc"; import ResetDBState from "test-utils/resets"; -import { TestingIIDXSPScore } from "test-utils/test-data"; +import { TestingIIDXSPScore, TestingSDVXScore } from "test-utils/test-data"; import type { ImportDocument, ScoreDocument, SessionDocument } from "tachi-common"; +const logger = CreateLogCtx(__filename); + const mockImportDocument: ImportDocument = { userID: 1, userIntent: false, @@ -173,5 +178,78 @@ t.test("#DeleteScore", (t) => { t.end(); }); + t.test("Should update classes if the user's classes should need to change.", async (t) => { + await db.charts.sdvx.insert( + mkFakeSDVXChart("chart_1", { + difficulty: "EXH", + }) + ); + await db.charts.sdvx.insert( + mkFakeSDVXChart("chart_2", { + difficulty: "NOV", + }) + ); + + const score = deepmerge(TestingSDVXScore, { + scoreID: "scoreid_1", + scoreData: { score: 10_000_000 } as ScoreDocument["scoreData"], + chartID: "chart_1", + calculatedData: { VF6: 10 }, + }); + const score2 = deepmerge(TestingSDVXScore, { + scoreID: "scoreid_2", + scoreData: { score: 9_000_000 } as ScoreDocument["scoreData"], + chartID: "chart_2", + calculatedData: { VF6: 4 }, + }); + + await db.scores.insert([score, score2]); + await db["personal-bests"].insert([ + mkFakeSDVXPB({ + chartID: "chart_1", + calculatedData: { + VF6: 10, + }, + }), + mkFakeSDVXPB({ + chartID: "chart_2", + calculatedData: { + VF6: 4, + }, + }), + ]); + + await db["game-stats"].insert({ + userID: 1, + game: "sdvx", + playtype: "Single", + classes: { + vfClass: SDVXVFClasses.CYAN_I, + }, + ratings: { + VF6: 14, + }, + }); + + await DeleteScore(score); + + const res = await db["game-stats"].findOne({ + userID: 1, + game: "sdvx", + playtype: "Single", + }); + + t.hasStrict(res, { + classes: { + vfClass: SDVXVFClasses.SIENNA_II, + }, + ratings: { + VF6: 4, + }, + }); + + t.end(); + }); + t.end(); }); diff --git a/server/src/lib/score-mutation/delete-scores.ts b/server/src/lib/score-mutation/delete-scores.ts index 038c36f0a..9cf52013d 100644 --- a/server/src/lib/score-mutation/delete-scores.ts +++ b/server/src/lib/score-mutation/delete-scores.ts @@ -120,7 +120,7 @@ export async function DeleteScore( } export async function DeleteMultipleScores(scores: Array, blacklist = false) { - logger.info(`received request to delete ${scores.length} (Blacklist: ${blacklist}).`); + logger.info(`Received request to delete ${scores.length} (Blacklist: ${blacklist}).`); const scoreIDs = scores.map((e) => e.scoreID); diff --git a/server/src/test-utils/misc.ts b/server/src/test-utils/misc.ts index 08f3e27f8..5a63b5043 100644 --- a/server/src/test-utils/misc.ts +++ b/server/src/test-utils/misc.ts @@ -7,10 +7,13 @@ import { HC511UserGoal, TestingIIDXSPScore, TestingIIDXSPScorePB, + TestingSDVXAlbidaChart, + TestingSDVXPB, TestingSDVXScore, } from "./test-data"; import deepmerge from "deepmerge"; import type { + ChartDocument, Game, GoalDocument, GoalSubscriptionDocument, @@ -117,3 +120,17 @@ export function mkFakeGameStats(userID: integer, modifant: Partial> = {} +) { + return dmf(TestingSDVXAlbidaChart, { + chartID, + ...modifant, + }); +} + +export function mkFakeSDVXPB(modifant: Partial> = {}) { + return dmf(TestingSDVXPB, modifant); +} diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index 3cc504d4f..276b01735 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -169,6 +169,40 @@ export const TestingSDVXScore: ScoreDocument<"sdvx:Single"> = { importType: "ir/direct-manual", }; +export const TestingSDVXPB: PBScoreDocument<"sdvx:Single"> = { + game: "sdvx", + playtype: "Single", + userID: 1, + scoreData: { + score: 786, + lamp: "CLEAR", + percent: 50, + grade: "C", + esd: 30, + gradeIndex: 3, + lampIndex: 4, + judgements: {}, + hitMeta: {}, + }, + calculatedData: {}, + timeAchieved: 1619454485988, + songID: 1, + + // albida adv + chartID: "5088a4d0e1ee9d0cc2f625934306e45b1a60699b", + highlight: false, + isPrimary: true, + composedFrom: { + lampPB: "score_1", + scorePB: "score_1", + }, + rankingData: { + outOf: 1, + rank: 1, + rivalRank: null, + }, +}; + export const TestingSDVXAlbidaChart: ChartDocument<"sdvx:Single"> = { rgcID: null, chartID: "5088a4d0e1ee9d0cc2f625934306e45b1a60699b", From 2c8ac579d4b5f340335839ddb9fa5256928984d9 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 4 Nov 2022 14:41:21 +0000 Subject: [PATCH 2/3] fix: prevent tests from sometimes not running this is some weird circular import issue, not sure how to resolve it but tearing out the import and replacing the function works so, that's what we're rolling w for now. --- server/src/lib/setup/config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/lib/setup/config.ts b/server/src/lib/setup/config.ts index 19c44443e..49c1fcee4 100644 --- a/server/src/lib/setup/config.ts +++ b/server/src/lib/setup/config.ts @@ -4,7 +4,6 @@ import dotenv from "dotenv"; import JSON5 from "json5"; import p from "prudence"; import { StaticConfig } from "tachi-common"; -import { IsNullishOrEmptyStr } from "utils/misc"; import { FormatPrError } from "utils/prudence"; import fs from "fs"; import { URL } from "url"; @@ -264,7 +263,7 @@ if (!mongoUrl) { const seqUrl = process.env.SEQ_URL ?? ""; -if (!seqUrl && !IsNullishOrEmptyStr(tachiServerConfig.LOGGER_CONFIG.SEQ_API_KEY)) { +if (!seqUrl && tachiServerConfig.LOGGER_CONFIG.SEQ_API_KEY) { logger.warn( `No SEQ_URL specified in environment, yet LOGGER_CONFIG.SEQ_API_KEY was defined. No logs will be sent to Seq!` ); From ef1a892f820a22cb9429cc88f27e54090e65e8dd Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 4 Nov 2022 14:42:34 +0000 Subject: [PATCH 3/3] fix: regression test for downgrades --- .../score-import/framework/user-game-stats/classes.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/lib/score-import/framework/user-game-stats/classes.test.ts b/server/src/lib/score-import/framework/user-game-stats/classes.test.ts index f98c466c7..c2e4c96c0 100644 --- a/server/src/lib/score-import/framework/user-game-stats/classes.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/classes.test.ts @@ -143,8 +143,8 @@ t.test("#ProcessClassDeltas", (t) => { const res = await ProcessClassDeltas( "sdvx", "Single", - { vfClass: 10 }, - { classes: { vfClass: 9 } } as unknown as UserGameStats, + { vfClass: 9 }, + { classes: { vfClass: 10 } } as unknown as UserGameStats, 1, logger );