mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-03 12:28:13 +03:00
Merge pull request #742 from TNG-dev/zkldi/allow-class-downgrades
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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!`
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user