Merge pull request #742 from TNG-dev/zkldi/allow-class-downgrades

This commit is contained in:
zkldi
2022-11-04 15:52:35 +00:00
committed by GitHub
10 changed files with 237 additions and 9 deletions
+56
View File
@@ -81,6 +81,7 @@ interface BaseGamePTConfig<I extends IDStrings> {
clearLamp: Lamps[I];
classHumanisedFormat: Record<GameClassSets[I], Array<ClassInfo>>;
classProperties: Record<GameClassSets[I], { downgradable: boolean }>;
judgements: Array<JudgementLookup[I]>;
@@ -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"],
@@ -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
@@ -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: 9 },
{ classes: { vfClass: 10 } } as unknown as UserGameStats,
1,
logger
);
t.strictSame(res, [
{
game: "sdvx",
set: "vfClass",
playtype: "Single",
old: 10,
new: 9,
},
]);
t.end();
});
t.end();
});
@@ -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<IDStrings>;
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,
@@ -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<Array<ClassDelta>> {
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,
@@ -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<ScoreDocument>(TestingSDVXScore, {
scoreID: "scoreid_1",
scoreData: { score: 10_000_000 } as ScoreDocument["scoreData"],
chartID: "chart_1",
calculatedData: { VF6: 10 },
});
const score2 = deepmerge<ScoreDocument>(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();
});
@@ -120,7 +120,7 @@ export async function DeleteScore(
}
export async function DeleteMultipleScores(scores: Array<ScoreDocument>, 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);
+1 -2
View File
@@ -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!`
);
+17
View File
@@ -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<UserGameStats
modifant
);
}
export function mkFakeSDVXChart(
chartID: string,
modifant: Partial<ChartDocument<"sdvx:Single">> = {}
) {
return dmf(TestingSDVXAlbidaChart, {
chartID,
...modifant,
});
}
export function mkFakeSDVXPB(modifant: Partial<PBScoreDocument<"sdvx:Single">> = {}) {
return dmf(TestingSDVXPB, modifant);
}
+34
View File
@@ -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",