Merge pull request #552 from TNG-dev/release-v2.0.2

This commit is contained in:
zkldi
2021-12-07 22:06:19 +00:00
committed by GitHub
43 changed files with 996 additions and 117 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ local-cdn
test-cdn
# Weird Redis Dumps
dump.rdb
*.rdb
scripts/quick-insert/insert.ts
scripts/bpi-poyashi-data/output.json
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "tachi-server",
"version": "2.0.1",
"version": "2.0.2",
"description": "A score tracking server.",
"main": "js/index.js",
"private": true,
@@ -51,6 +51,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.41.0",
"@aws-sdk/signature-v4-crt": "^3.41.0",
"@types/mocha": "^9.0.0",
"@valuabletouch/winston-seq": "^1.2.0",
"bcryptjs": "^2.4.3",
@@ -82,7 +83,7 @@
"rimraf": "3.0.2",
"safe-json-stringify": "1.2.0",
"seq-logging": "^1.1.1",
"tachi-common": "0.3.10",
"tachi-common": "0.3.13",
"ts-node": "10.0.0",
"typescript": "4.3.4",
"winston": "3.3.3",
+599 -22
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -209,7 +209,7 @@ const GetScoreMeta = (game: Game, playtype: Playtypes[Game]): PrudenceSchema =>
} else if (game === "usc") {
return {
noteMod: optNull(p.isIn("NORMAL", "MIRROR", "RANDOM", "MIR-RAN")),
gaugeMod: optNull(p.isIn("NORMAL", "HARD")),
gaugeMod: optNull(p.isIn("NORMAL", "HARD", "PERMISSIVE")),
};
} else if (game === "bms" && playtype === "7K") {
return {
@@ -771,7 +771,7 @@ export const DatabaseSchemas: Record<Databases, ValidatorFunction> = {
return prSchemaify(
PR_ChartDocument("iidx", playtype, {
notecount: p.isPositiveNonZeroInteger,
inGameID: p.isPositiveNonZeroInteger,
inGameID: p.or(p.isPositiveNonZeroInteger, [p.isPositiveNonZeroInteger]),
arcChartID: "?string",
hashSHA256: "?string",
"2dxtraSet": "?string",
+1
View File
@@ -6,6 +6,7 @@ export const MODEL_IIDX_LIGHTNING = "TDJ";
export const REV_2DXTRA = "E";
export const REV_2DXBMS = "Z";
export const REV_OMNIMIX = "X";
export const REV_NORMAL = "A";
export const EXT_HEROIC_VERSE = "2020092900";
export const EXT_BISTROVER = "2021091500";
+1 -1
View File
@@ -4,7 +4,7 @@
const MAJOR = 2;
const MINOR = 0;
const PATCH = 1;
const PATCH = 2;
// As is with all front-facing zkldi projects, the version names for tachi-server
// are from an album I like. In this case, the album is Portishead - Dummy.
+4 -3
View File
@@ -10,6 +10,7 @@ import {
SongDocument,
} from "tachi-common";
import { GetNextCounterValue } from "utils/db";
import { DedupeArr } from "utils/misc";
const logger = CreateLogCtx(__filename);
@@ -35,7 +36,7 @@ export async function HandleOrphanQueue<I extends IDStrings>(
userID: integer,
chartName: string
) {
logger.debug(`received orphanqueue request for ${chartName}.`);
logger.debug(`Received orphanqueue request for ${chartName}.`);
const orphanChart = await db["orphan-chart-queue"].findOne(
Object.assign({ idString }, orphanMatchCriteria),
@@ -45,7 +46,7 @@ export async function HandleOrphanQueue<I extends IDStrings>(
);
if (!orphanChart) {
logger.verbose(`received unknown chart ${chartName}, orphaning.`);
logger.verbose(`Received unknown chart ${chartName}, orphaning.`);
await db["orphan-chart-queue"].insert({
idString,
@@ -59,7 +60,7 @@ export async function HandleOrphanQueue<I extends IDStrings>(
orphanChart.userIDs.push(userID);
const uniqueUsersArr = [...new Set(orphanChart.userIDs)];
const uniqueUsersArr = DedupeArr(orphanChart.userIDs);
const playcount = uniqueUsersArr.length;
// If N or more people have played this chart while orphaned, unorphan
@@ -45,7 +45,7 @@ export async function CreateKaiIIDXClassHandler(
return {};
}
if (iidxDan === null) {
if (iidxDan === null || iidxDan === undefined) {
return {};
}
@@ -36,7 +36,7 @@ export async function CreateKaiSDVXClassHandler(
const sdvxDan: number | null = json.skill_level - 1;
if (sdvxDan === null || sdvxDan === undefined) {
if (json.skill_level === null || json.skill_level === undefined) {
return {};
}
@@ -65,7 +65,7 @@ const PR_ScoreMeta = (game: Game, playtype: Playtypes[Game]): PrudenceSchema =>
} else if (game === "usc") {
return {
noteMod: optNull(p.isIn("NORMAL", "MIRROR", "RANDOM", "MIR-RAN")),
gaugeMod: optNull(p.isIn("NORMAL", "HARD")),
gaugeMod: optNull(p.isIn("NORMAL", "HARD", "PERMISSIVE")),
};
} else if (game === "sdvx") {
return {
@@ -16,11 +16,11 @@ import { ClassHandler } from "../../framework/user-game-stats/types";
import { SDVXEamusementCSVData } from "../file/eamusement-sdvx-csv/types";
import { MerScore } from "../file/mer-iidx/types";
import { S3Score } from "../file/solid-state-squad/types";
import { BarbatosScore } from "../ir/barbatos/types";
import { BarbatosContext, BarbatosScore } from "../ir/barbatos/types";
import { BeatorajaContext, BeatorajaScore } from "../ir/beatoraja/types";
import { FervidexStaticContext, FervidexStaticScore } from "../ir/fervidex-static/types";
import { FervidexContext, FervidexScore } from "../ir/fervidex/types";
import { KsHookSV3CScore } from "../ir/kshook-sv3c/types";
import { KsHookSV3CContext, KsHookSV3CScore } from "../ir/kshook-sv3c/types";
import { IRUSCContext } from "../ir/usc/types";
import { KaiContext } from "./api-kai/types";
import { BatchManualContext } from "./batch-manual/types";
@@ -62,12 +62,12 @@ export interface ImportTypeContextMap {
"file/pli-iidx-csv": IIDXEamusementCSVContext;
"ir/direct-manual": BatchManualContext;
"ir/barbatos": EmptyObject;
"ir/barbatos": BarbatosContext;
"ir/fervidex": FervidexContext;
"ir/fervidex-static": FervidexStaticContext;
"ir/beatoraja": BeatorajaContext;
"ir/usc": IRUSCContext;
"ir/kshook-sv3c": EmptyObject;
"ir/kshook-sv3c": KsHookSV3CContext;
"api/arc-iidx": EmptyObject;
"api/arc-sdvx": EmptyObject;
@@ -66,7 +66,6 @@ export function ParseSolidStateXML(
}
if (!parsedXML?.s3data?.scoredata?.song) {
logger.crit("foo", { data: fileData.buffer });
throw new ScoreImportFatalError(400, `Invalid S3 XML, no s3data -> scoredata -> song?`);
}
@@ -40,7 +40,12 @@ t.test("#ConverterIRBarbatos", (t) => {
};
t.test("Should convert a BarbatosScore into a Dry Score", async (t) => {
const res = await ConverterIRBarbatos(barbScore, {}, "ir/barbatos", logger);
const res = await ConverterIRBarbatos(
barbScore,
{ timeReceived: 10 },
"ir/barbatos",
logger
);
t.hasStrict(res, {
song: albidaSong,
@@ -82,7 +87,7 @@ t.test("#ConverterIRBarbatos", (t) => {
() =>
ConverterIRBarbatos(
deepmerge(barbScore, { song_id: 1000 }) as BarbatosScore,
{},
{ timeReceived: 10 },
"ir/barbatos",
logger
),
@@ -97,9 +102,12 @@ t.test("#ConverterIRBarbatos", (t) => {
t.test("Should throw InternalFailure if song-chart desync.", async (t) => {
await db.songs.sdvx.remove({ id: 1 }); // force a song-chart desync
t.rejects(() => ConverterIRBarbatos(barbScore, {}, "ir/barbatos", logger), {
message: /Song 1 \(sdvx\) has no parent song/u,
});
t.rejects(
() => ConverterIRBarbatos(barbScore, { timeReceived: 10 }, "ir/barbatos", logger),
{
message: /Song 1 \(sdvx\) has no parent song/u,
}
);
t.end();
});
@@ -1,7 +1,6 @@
import { Lamps } from "tachi-common";
import { FindSDVXChartOnInGameID } from "utils/queries/charts";
import { FindSongOnID } from "utils/queries/songs";
import { EmptyObject } from "utils/types";
import {
InternalFailure,
KTDataNotFoundFailure,
@@ -9,7 +8,7 @@ import {
import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils";
import { DryScore } from "../../../framework/common/types";
import { ConverterFunction } from "../../common/types";
import { BarbatosScore } from "./types";
import { BarbatosContext, BarbatosScore } from "./types";
const LAMP_LOOKUP = {
1: "FAILED",
@@ -27,7 +26,7 @@ const DIFFICULTY_LOOKUP = {
4: "MXM",
};
export const ConverterIRBarbatos: ConverterFunction<BarbatosScore, EmptyObject> = async (
export const ConverterIRBarbatos: ConverterFunction<BarbatosScore, BarbatosContext> = async (
data,
context,
importType,
@@ -65,7 +64,7 @@ export const ConverterIRBarbatos: ConverterFunction<BarbatosScore, EmptyObject>
service: "Barbatos",
comment: null,
importType: "ir/barbatos",
timeAchieved: Date.now(),
timeAchieved: context.timeReceived,
scoreData: {
score: data.score,
percent,
@@ -84,7 +83,7 @@ export const ConverterIRBarbatos: ConverterFunction<BarbatosScore, EmptyObject>
},
},
scoreMeta: {
inSkillAnalyser: data.is_skill_analyser,
inSkillAnalyser: data.is_skill_analyzer,
},
};
@@ -21,7 +21,7 @@ const PR_Barbatos = {
did_fail: "boolean",
clear_type: p.isIn(1, 2, 3, 4, 5),
gauge_type: p.isIn(0, 1, 2, 3),
is_skill_analyser: "boolean",
is_skill_analyzer: "boolean",
};
export function ParseBarbatosSingle(
@@ -15,5 +15,9 @@ export interface BarbatosScore {
did_fail: boolean;
clear_type: 1 | 2 | 3 | 4 | 5;
gauge_type: 1 | 2;
is_skill_analyser: boolean;
is_skill_analyzer: boolean;
}
export interface BarbatosContext {
timeReceived: number;
}
@@ -182,7 +182,7 @@ export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaCo
inputDevice: data.deviceType,
random,
},
timeAchieved: Date.now(),
timeAchieved: context.timeReceived,
service: "Beatoraja IR",
};
@@ -110,12 +110,13 @@ export function ParseBeatorajaSingle(
throw new ScoreImportFatalError(400, `Unsupported client ${client}`);
}
// asserted using prudence.
return {
context: {
client: "lr2oraja",
// asserted using prudence.
chart: body.chart as BeatorajaChart,
userID,
timeReceived: Date.now(),
},
game: "bms",
iterable: [body.score] as unknown as BeatorajaScore[],
@@ -4,6 +4,7 @@ export interface BeatorajaContext {
client: "lr2oraja";
chart: BeatorajaChart;
userID: integer; // unexpectedly necessary for orphan code!
timeReceived: number;
}
export interface BeatorajaChart {
@@ -125,7 +125,7 @@ t.test("#ConverterIRFervidex", (t) => {
t.test("Should convert a valid fervidex score into a dry score.", async (t) => {
const res = await ConverterIRFervidex(
baseFervidexScore,
{ version: "27" },
{ version: "27", timeReceived: 10 },
"ir/fervidex",
logger
);
@@ -159,7 +159,7 @@ t.test("#ConverterIRFervidex", (t) => {
const res = await ConverterIRFervidex(
deepmerge(baseFervidexScore, { option: { style_2p: "R_RANDOM" }, chart: "dpa" }),
{ version: "27" },
{ version: "27", timeReceived: 10 },
"ir/fervidex",
logger
);
@@ -182,7 +182,7 @@ t.test("#ConverterIRFervidex", (t) => {
t.test("Should null BP if the user died pre-emptively.", async (t) => {
const res = await ConverterIRFervidex(
deepmerge(baseFervidexScore, { dead: { measure: 1, note: 10 } }),
{ version: "27" },
{ version: "27", timeReceived: 10 },
"ir/fervidex",
logger
);
@@ -204,7 +204,7 @@ t.test("#ConverterIRFervidex", (t) => {
t.rejects(
ConverterIRFervidex(
deepmerge(baseFervidexScore, { chart: "spl" }),
{ version: "27" },
{ version: "27", timeReceived: 10 },
"ir/fervidex",
logger
),
@@ -218,7 +218,12 @@ t.test("#ConverterIRFervidex", (t) => {
await db.songs.iidx.remove({}); // this forces desync
t.rejects(
ConverterIRFervidex(baseFervidexScore, { version: "27" }, "ir/fervidex", logger),
ConverterIRFervidex(
baseFervidexScore,
{ version: "27", timeReceived: 10 },
"ir/fervidex",
logger
),
{ message: /Song 1 \(iidx\) has no parent song/giu }
);
@@ -258,7 +263,7 @@ t.test("#ConverterIRFervidex", (t) => {
t.test("Should convert undeflow gauge to null.", async (t) => {
const res = await ConverterIRFervidex(
deepmerge(baseFervidexScore, { gauge: [10, 5, 249, 248] }),
{ version: "27" },
{ version: "27", timeReceived: 10 },
"ir/fervidex",
logger
);
@@ -23,7 +23,7 @@ export const FERVIDEX_LAMP_LOOKUP = {
};
export function TachifyAssist(
assist: FervidexScore["option"]["assist"]
assist: Required<FervidexScore>["option"]["assist"]
): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["assist"] {
switch (assist) {
case "FULL_ASSIST":
@@ -40,7 +40,7 @@ export function TachifyAssist(
}
export function TachifyGauge(
gauge: FervidexScore["option"]["gauge"]
gauge: Required<FervidexScore>["option"]["gauge"]
): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["gauge"] {
switch (gauge) {
case "ASSISTED_EASY":
@@ -58,7 +58,7 @@ export function TachifyGauge(
}
export function TachifyRange(
gauge: FervidexScore["option"]["range"]
gauge: Required<FervidexScore>["option"]["range"]
): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["range"] {
switch (gauge) {
case "HIDDEN_PLUS":
@@ -77,7 +77,7 @@ export function TachifyRange(
}
}
export function TachifyRandom(gauge: FervidexScore["option"]["style"]) {
export function TachifyRandom(gauge: Required<FervidexScore>["option"]["style"]) {
switch (gauge) {
case "RANDOM":
return "RANDOM";
@@ -186,7 +186,7 @@ export const ConverterIRFervidex: ConverterFunction<FervidexScore, FervidexConte
service: "Fervidex",
comment: null,
importType: "ir/fervidex",
timeAchieved: Date.now(),
timeAchieved: context.timeReceived,
scoreData: {
score: data.ex_score,
percent,
@@ -212,15 +212,15 @@ export const ConverterIRFervidex: ConverterFunction<FervidexScore, FervidexConte
},
},
scoreMeta: {
assist: TachifyAssist(data.option.assist),
gauge: TachifyGauge(data.option.gauge),
assist: TachifyAssist(data.option?.assist),
gauge: TachifyGauge(data.option?.gauge),
// @ts-expect-error Awkward expansion of iidx:SP|DP strings here causes this
// to complain that [x,x] is not assignable to SP randoms. This is a lazy ignore!
random:
chart.playtype === "SP"
? TachifyRandom(data.option.style)
: [TachifyRandom(data.option.style), TachifyRandom(data.option.style_2p)],
range: TachifyRange(data.option.range),
? TachifyRandom(data.option?.style)
: [TachifyRandom(data.option?.style), TachifyRandom(data.option?.style_2p)],
range: TachifyRange(data.option?.range),
},
};
@@ -0,0 +1,33 @@
import CreateLogCtx from "lib/logger/logger";
import t from "tap";
import { SoftwareIDToVersion } from "./parser";
const logger = CreateLogCtx(__filename);
t.test("#SoftwareIDToVersion", (t) => {
const f = (sid: string) => SoftwareIDToVersion(sid, logger);
t.throws(() => f("a"), {}, "Should throw on invalid string.");
t.throws(() => f("LDJ:J:B:Q:2020092900"), {}, "Should throw on unknown HV revision.");
t.throws(() => f("LDJ:J:B:Q:2021091500"), {}, "Should throw on unknown BST revision.");
t.throws(() => f("XDJ:J:B:A:2020092900"), {}, "Should throw on unknown model.");
t.throws(() => f("LDJ:J:B:A:2099092900"), {}, "Should throw on unknown ext.");
t.equal(f("LDJ:J:B:A:2020092900"), "27");
t.equal(f("LDJ:J:B:X:2020092900"), "27-omni");
t.equal(f("LDJ:J:B:E:2020092900"), "27-2dxtra");
t.equal(f("LDJ:J:B:A:2021091500"), "28");
t.equal(f("LDJ:J:B:X:2021091500"), "28-omni");
t.equal(f("LDJ:J:B:E:2021091500"), "28-2dxtra");
t.equal(f("TDJ:J:B:A:2020092900"), "27");
t.equal(f("TDJ:J:B:X:2020092900"), "27-omni");
t.equal(f("TDJ:J:B:E:2020092900"), "27-2dxtra");
t.equal(f("TDJ:J:B:A:2021091500"), "28");
t.equal(f("TDJ:J:B:X:2021091500"), "28-omni");
t.equal(f("TDJ:J:B:E:2021091500"), "28-2dxtra");
t.equal(f("P2D:J:B:A:2020092900"), "inf");
t.end();
});
@@ -2,8 +2,10 @@ import {
EXT_BISTROVER,
EXT_HEROIC_VERSE,
MODEL_IIDX,
MODEL_IIDX_LIGHTNING,
MODEL_INFINITAS_2,
REV_2DXTRA,
REV_NORMAL,
REV_OMNIMIX,
} from "lib/constants/ea3id";
import { KtLogger } from "lib/logger/logger";
@@ -42,7 +44,7 @@ const PR_Fervidex: PrudenceSchema = {
note: optNull(p.isPositiveInteger),
} as unknown as ValidSchemaValue),
option: {
option: p.optional({
gauge: optNull(p.isIn("ASSISTED_EASY", "EASY", "HARD", "EX_HARD")),
range: optNull(
p.isIn("SUDDEN_PLUS", "HIDDEN_PLUS", "SUD_PLUS_HID_PLUS", "LIFT", "LIFT_SUD_PLUS")
@@ -50,7 +52,7 @@ const PR_Fervidex: PrudenceSchema = {
style: optNull(p.isIn("RANDOM", "R_RANDOM", "S_RANDOM", "MIRROR")),
style_2p: optNull(p.isIn("RANDOM", "R_RANDOM", "S_RANDOM", "MIRROR")),
assist: optNull(p.isIn("AUTO_SCRATCH", "LEGACY_NOTE", "ASCR_LEGACY", "FULL_ASSIST")),
},
}),
// we dont use it and we dont care.
pacemaker: p.any,
@@ -73,24 +75,25 @@ export function SoftwareIDToVersion(model: string, logger: KtLogger) {
if (data.model === MODEL_INFINITAS_2) {
return "inf";
} else if (data.model === MODEL_IIDX) {
// only HV. everything else is disabled deliberately.
} else if (data.model === MODEL_IIDX || data.model === MODEL_IIDX_LIGHTNING) {
// pretty icky yandere-dev tier if statements, but hey
// that's just how it works...
if (data.ext === EXT_HEROIC_VERSE) {
if (data.rev === REV_OMNIMIX) {
return "27-omni";
}
if (data.rev === REV_2DXTRA) {
} else if (data.rev === REV_2DXTRA) {
return "27-2dxtra";
} else if (data.rev === REV_NORMAL) {
return "27";
}
return "27";
} else if (data.ext === EXT_BISTROVER) {
if (data.rev === REV_OMNIMIX) {
return "28-omni";
} else if (data.rev === REV_2DXTRA) {
return "28-2dxtra";
} else if (data.rev === REV_NORMAL) {
return "28";
}
return "28";
}
}
@@ -122,7 +125,7 @@ export function ParseFervidexSingle(
// asserted using prudence.
return {
context: { version },
context: { version, timeReceived: Date.now() },
game: "iidx",
iterable: [body] as unknown as FervidexScore[],
classHandler: null,
@@ -30,7 +30,7 @@ export interface FervidexScore {
note?: integer | null;
};
option: {
option?: {
gauge?: "ASSISTED_EASY" | "EASY" | "HARD" | "EX_HARD" | null;
range?:
| "SUDDEN_PLUS"
@@ -60,4 +60,5 @@ export interface FervidexScore {
export interface FervidexContext {
version: GPTSupportedVersions["iidx:SP" | "iidx:DP"];
timeReceived: number;
}
@@ -12,7 +12,7 @@ t.test("#ConverterIRKsHookSV3C", (t) => {
t.test("Should match a score with its song and chart.", async (t) => {
const res = await ConverterIRKsHookSV3C(
TestingKsHookSV3CScore,
{},
{ timeReceived: 10 },
"ir/kshook-sv3c",
logger
);
@@ -57,7 +57,7 @@ t.test("#ConverterIRKsHookSV3C", (t) => {
() =>
ConverterIRKsHookSV3C(
Object.assign({}, TestingKsHookSV3CScore, { music_id: 10000 }),
{},
{ timeReceived: 10 },
"ir/kshook-sv3c",
logger
),
@@ -7,11 +7,10 @@ import { DryScore } from "lib/score-import/framework/common/types";
import { Lamps } from "tachi-common";
import { FindSDVXChartOnInGameIDVersion } from "utils/queries/charts";
import { FindSongOnID } from "utils/queries/songs";
import { EmptyObject } from "utils/types";
import { ConverterFunction } from "../../common/types";
import { KsHookSV3CScore } from "./types";
import { KsHookSV3CContext, KsHookSV3CScore } from "./types";
export const ConverterIRKsHookSV3C: ConverterFunction<KsHookSV3CScore, EmptyObject> = async (
export const ConverterIRKsHookSV3C: ConverterFunction<KsHookSV3CScore, KsHookSV3CContext> = async (
data,
context,
importType,
@@ -44,7 +43,7 @@ export const ConverterIRKsHookSV3C: ConverterFunction<KsHookSV3CScore, EmptyObje
service: "kshook SV3C",
comment: null,
importType: "ir/kshook-sv3c",
timeAchieved: Date.now(),
timeAchieved: context.timeReceived,
scoreData: {
score: data.score,
percent,
@@ -38,7 +38,7 @@ t.test("#ParseKsHookSV3C", (t) => {
const res = ParseKsHookSV3C(data, logger);
t.equal(res.game, "sdvx");
t.strictSame(res.context, {});
t.type(res.context.timeReceived, "number");
t.ok(Array.isArray(res.iterable));
t.type(res.classHandler, "function");
} catch (err) {
@@ -3,9 +3,8 @@ import { KtLogger } from "lib/logger/logger";
import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error";
import p, { PrudenceSchema } from "prudence";
import { FormatPrError } from "utils/prudence";
import { EmptyObject } from "utils/types";
import { ParserFunctionReturns } from "../../common/types";
import { KsHookSV3CScore } from "./types";
import { KsHookSV3CContext, KsHookSV3CScore } from "./types";
const PR_KsHookSV3C: PrudenceSchema = {
appeal_id: p.isPositiveInteger,
@@ -75,7 +74,7 @@ const PR_KsHookSV3C: PrudenceSchema = {
export function ParseKsHookSV3C(
body: Record<string, unknown>,
logger: KtLogger
): ParserFunctionReturns<KsHookSV3CScore, EmptyObject> {
): ParserFunctionReturns<KsHookSV3CScore, KsHookSV3CContext> {
// Ignore excess keys, as SV3C might add more features in the future.
const err = p(body, PR_KsHookSV3C, undefined, { allowExcessKeys: true });
@@ -88,7 +87,9 @@ export function ParseKsHookSV3C(
return {
game: "sdvx",
iterable: [score],
context: {},
context: {
timeReceived: Date.now(),
},
classHandler: () => {
const skillLevel = ConvertSkillLevel(score.skill_level);
@@ -56,3 +56,7 @@ export interface KsHookSV3CScore {
skill_name: integer;
track_no: integer;
}
export interface KsHookSV3CContext {
timeReceived: number;
}
@@ -16,6 +16,17 @@ t.test("#DeriveLamp", (t) => {
t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 0 }, gauge: 0.7 }), logger), "CLEAR");
t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 2 }, gauge: 1 }), logger), "FAILED");
t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 2 }, gauge: 0 }), logger), "FAILED");
t.equal(
DeriveLamp(d(uscScore, { options: { gaugeType: 2 }, gauge: 1, score: 10_000_000 }), logger),
"PERFECT ULTIMATE CHAIN"
);
t.equal(
DeriveLamp(d(uscScore, { options: { gaugeType: 2 }, gauge: 1, error: 0 }), logger),
"ULTIMATE CHAIN"
);
t.equal(
DeriveLamp(d(uscScore, { options: { gaugeType: 1 }, gauge: 0.1 }), logger),
"EXCESSIVE CLEAR"
@@ -58,7 +69,7 @@ t.test("#DeriveLamp", (t) => {
"ULTIMATE CHAIN"
);
t.throws(() => DeriveLamp(d(uscScore, { options: { gaugeType: 2 } }), logger));
t.throws(() => DeriveLamp(d(uscScore, { options: { gaugeType: 3 } }), logger));
t.end();
});
@@ -77,7 +88,7 @@ t.test("#DeriveNoteMod", (t) => {
const dm = (p: Partial<USCClientScore>) =>
ConverterIRUSC(
d(uscScore, p),
{ chartHash: uscChart.data.hashSHA1 as string, playtype: "Controller" },
{ chartHash: uscChart.data.hashSHA1 as string, playtype: "Controller", timeReceived: 10 },
"ir/usc",
logger
);
@@ -50,6 +50,8 @@ export function DeriveLamp(
return scoreDoc.gauge >= 0.7 ? "CLEAR" : "FAILED";
} else if (scoreDoc.options.gaugeType === 1) {
return scoreDoc.gauge > 0 ? "EXCESSIVE CLEAR" : "FAILED";
} else if (scoreDoc.options.gaugeType === 2) {
return "FAILED";
}
logger.error(`Could not derive Lamp from Score Document`, { scoreDoc });
@@ -106,7 +108,7 @@ export const ConverterIRUSC: ConverterFunction<USCClientScore, IRUSCContext> = a
comment: null,
game: "usc",
importType,
timeAchieved: Date.now(),
timeAchieved: context.timeReceived,
service: "USC-IR",
scoreData: {
grade,
@@ -126,7 +128,12 @@ export const ConverterIRUSC: ConverterFunction<USCClientScore, IRUSCContext> = a
},
},
scoreMeta: {
gaugeMod: data.options.gaugeType === 0 ? "NORMAL" : "HARD",
gaugeMod:
data.options.gaugeType === 0
? "NORMAL"
: data.options.gaugeType === 1
? "HARD"
: "PERMISSIVE",
noteMod: DeriveNoteMod(data),
},
};
@@ -18,7 +18,7 @@ const PR_USCIRScore: PrudenceSchema = {
late: p.optional(p.isPositiveInteger),
combo: p.optional(p.isPositiveInteger),
options: {
gaugeType: p.isIn(0, 1),
gaugeType: p.isIn(0, 1, 2),
mirror: "boolean",
random: "boolean",
autoFlags: p.isInteger,
@@ -62,6 +62,7 @@ export function ParseIRUSC(
context: {
chartHash,
playtype,
timeReceived: Date.now(),
},
game: "usc",
iterable: [score] as USCClientScore[],
@@ -3,4 +3,5 @@ import { Playtypes } from "tachi-common";
export interface IRUSCContext {
chartHash: string;
playtype: Playtypes["usc"];
timeReceived: number;
}
@@ -12,6 +12,7 @@ import { Game, UserAuthLevels } from "tachi-common";
import db from "external/mongo/db";
import { DeleteScore } from "lib/delete-scores/delete-scores";
import { UpdateAllPBs } from "utils/calculations/recalc-scores";
import DestroyUserGamePlaytypeData from "utils/reset-state/destroy-ugpt";
const logger = CreateLogCtx(__filename);
@@ -260,6 +261,44 @@ router.post("/delete-score", prValidate({ scoreID: "string" }), async (req, res)
});
});
/**
* Destroys a users UGPT profile and forces a site recalc.
*
* @POST /api/v1/admin/destroy-ugpt
*/
router.post(
"/destroy-ugpt",
prValidate({
userID: p.isInteger,
game: p.isIn(TachiConfig.GAMES),
playtype: "string", // lazy
}),
async (req, res) => {
const { userID, game, playtype } = req.body;
const ugpt = await db["game-stats"].findOne({
userID,
game,
playtype,
});
if (!ugpt) {
return res.status(404).json({
success: false,
description: `No stats for ${userID} (${game} ${playtype}) exist.`,
});
}
await DestroyUserGamePlaytypeData(userID, game, playtype);
return res.status(200).json({
success: true,
description: `Completely destroyed UGPT for ${userID} (${game} ${playtype}).`,
body: {},
});
}
);
/**
* Destroy a chart and all of its scores (and sessions).
*
@@ -5,6 +5,7 @@ import mockApi from "test-utils/mock-api";
import ResetDBState from "test-utils/resets";
import { GetKTDataJSON } from "test-utils/test-data";
import { Sleep } from "utils/misc";
import deepmerge from "deepmerge";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function TestHeaders(url: string, data: any) {
@@ -301,6 +302,27 @@ t.test("POST /ir/fervidex/score/submit", (t) => {
t.end();
});
t.test("Should import a valid score with undefined options", async (t) => {
const res = await mockApi
.post("/ir/fervidex/score/submit")
.set("Authorization", "Bearer mock_token")
.set("User-Agent", "fervidex/1.3.0")
.set("X-Software-Model", "LDJ:J:B:A:2020092900")
.send(deepmerge(GetKTDataJSON("./fervidex/base.json"), { option: undefined }));
t.equal(res.body.success, true, "Should be successful");
t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores.");
const scores = await db.scores.count({
service: "Fervidex",
});
t.equal(scores, 1, "Should import 1 score.");
t.end();
});
t.test("Should import a valid score with 2dx-gsm", async (t) => {
const res = await mockApi
.post("/ir/fervidex/score/submit")
@@ -60,7 +60,7 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => {
// version.minor
if (versions[1] < 3) {
logger.debug(
`Rejected outdated fervidex client from user ${req[SYMBOL_TachiAPIAuth].userID!}.`
`Rejected outdated fervidex client from user ${req[SYMBOL_TachiAPIAuth].userID}.`
);
return res.status(400).json({
success: false,
@@ -582,6 +582,12 @@ t.test("POST /scores", (t) => {
t.equal(res.body.statusCode, 22);
t.match(
res.body.description,
"(1/3)",
"Should contain (required/outOf) in the response description."
);
const dbScore = await db.scores.findOne({
game: "usc",
});
@@ -21,6 +21,7 @@ import {
Playtypes,
SuccessfulAPIResponse,
} from "tachi-common";
import { DedupeArr } from "utils/misc";
import { FormatPrError } from "utils/prudence";
import { GetBlacklist } from "utils/queries/blacklist";
import { AssignToReqTachiData } from "utils/req-tachi-data";
@@ -319,6 +320,7 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
song,
{
"chartDoc.data.hashSHA1": uscChart.chartHash,
"chartDoc.playtype": playtype,
},
ServerConfig.USC_QUEUE_SIZE,
req[SYMBOL_TachiAPIAuth].userID!,
@@ -332,9 +334,12 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
"context.playtype": playtype,
});
await Promise.all(
scoresToDeorphan.map((score) => ReprocessOrphan(score, blacklist, logger))
);
for (const score of scoresToDeorphan) {
// Has to be like this to avoid race conditions where two
// orphans resolve to the same scoreID and collide in mid-air!
// eslint-disable-next-line no-await-in-loop
await ReprocessOrphan(score, blacklist, logger);
}
}
}
@@ -346,16 +351,6 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
playtype,
]);
// If this was an orphan chart request, return ACCEPTED,
// since it may be unorphaned in the future
if (!chartDoc) {
return res.status(200).json({
statusCode: STATUS_CODES.ACCEPTED,
description:
"This score has been accepted, but is waiting for more players before its parent chart is accepted.",
});
}
if (importRes.statusCode === 500) {
return res.status(200).json({
statusCode: STATUS_CODES.SERVER_ERROR,
@@ -370,6 +365,71 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
const importDoc = (importRes.body as SuccessfulAPIResponse).body as ImportDocument;
// If the import failed, AND the import failure WAS NOT that the chart didnt exist
// report that error instead.
if (importDoc.errors[0]?.type && importDoc.errors[0].type !== "KTDataNotFound") {
logger.info(`USC Import Failed ${importDoc.errors[0].message}`, {
importDoc,
userID,
});
return res.status(200).json({
statusCode: STATUS_CODES.BAD_REQ,
description: `${importDoc.errors[0].type} ${importDoc.errors[0].message}`,
});
}
// If this was an orphan chart request, return ACCEPTED,
// since it may be unorphaned in the future
if (!chartDoc) {
const orphanChart = await db["orphan-chart-queue"].findOne({
"chartDoc.data.hashSHA1": uscChart.chartHash,
"chartDoc.playtype": playtype,
});
if (!orphanChart) {
logger.severe(
`No orphan chart for USC ${uscChart.chartHash} (${playtype})? One was expected.`,
{ uscChart, playtype }
);
return res.status(200).json({
statusCode: STATUS_CODES.SERVER_ERROR,
description: `An internal server error has occured.`,
});
}
const players = DedupeArr([...orphanChart.userIDs, userID]);
return res.status(200).json({
statusCode: STATUS_CODES.ACCEPTED,
description: `This score has been accepted, but is waiting for more players before its parent chart is accepted. (${players.length}/${ServerConfig.USC_QUEUE_SIZE})`,
});
}
// If the chartDoc exists, any error is a failure here.
if (importDoc.errors[0]) {
logger.info(`USC Import Failed ${importDoc.errors[0].message}`, {
importDoc,
userID,
});
return res.status(200).json({
statusCode: STATUS_CODES.BAD_REQ,
description: `${importDoc.errors[0].type} ${importDoc.errors[0].message}`,
});
}
if (!importDoc.scoreIDs[0]) {
logger.warn(`No scoreID, but import was successful?`, {
importDoc,
userID,
});
return res.status(200).json({
statusCode: STATUS_CODES.BAD_REQ,
description: "No score was imported.",
});
}
if (importDoc.errors[0]) {
logger.info(`USC Import Failed ${importDoc.errors[0].message}`, {
importDoc,
@@ -8,7 +8,7 @@ export interface USCServerScore {
near: integer;
error: integer;
ranking: integer;
gaugeMod: "NORMAL" | "HARD";
gaugeMod: "NORMAL" | "HARD" | "PERMISSIVE";
noteMod: "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN";
username: string;
}
@@ -24,7 +24,7 @@ export interface USCClientScore {
late: integer | null;
combo: integer | null;
options: {
gaugeType: 0 | 1;
gaugeType: 0 | 1 | 2;
gaugeOpt: integer;
mirror: boolean;
random: boolean;
+1 -1
View File
@@ -195,7 +195,7 @@ export const barbScore: BarbatosScore = {
near_fast: 40,
near_slow: 10,
gauge_type: 1,
is_skill_analyser: false,
is_skill_analyzer: false,
level: 10,
max_chain: 100,
percent: 90,
@@ -13,5 +13,5 @@
"did_fail": false,
"clear_type": 2,
"gauge_type": 0,
"is_skill_analyser": false
"is_skill_analyzer": false
}
+1 -1
View File
@@ -59,7 +59,7 @@ export async function ResolveFolderToCharts(
const folderDataTransposed: Record<string, unknown> = {};
for (const key in folder.data) {
folderDataTransposed[key.replace(/¬/gu, ".").replace("~", "$")] = folder.data[key];
folderDataTransposed[key.replace(/¬/gu, ".").replace(/~/gu, "$")] = folder.data[key];
}
const fx = deepmerge.all([filter, { playtype: folder.playtype }, folderDataTransposed]);
@@ -0,0 +1,94 @@
import db from "external/mongo/db";
import { integer, Game, Playtypes } from "tachi-common";
import { UpdateAllPBs } from "utils/calculations/recalc-scores";
/**
* Completely resets a UGPT profile.
*
* This function is dangerous! Should only be ran by admins.
*/
export default async function DestroyUserGamePlaytypeData(
userID: integer,
game: Game,
playtype: Playtypes[Game]
) {
await db["game-stats-snapshots"].remove({
userID,
game,
playtype,
});
await db.scores.remove({
userID,
game,
playtype,
});
const chartIDs = (
await db["personal-bests"].find(
{
userID,
game,
playtype,
},
{
projection: {
chartID: 1,
},
}
)
).map((e) => e.chartID);
await db["personal-bests"].remove({
userID,
game,
playtype,
});
await UpdateAllPBs(undefined, {
chartID: { $in: chartIDs },
});
const sessionIDs = (
await db.sessions.find(
{
userID,
game,
playtype,
},
{
projection: {
sessionID: 1,
},
}
)
).map((e) => e.sessionID);
await db["session-view-cache"].remove({
sessionID: { $in: sessionIDs },
});
await db.sessions.remove({
userID,
game,
playtype,
});
await db.imports.remove({
userID,
game,
playtype,
});
await db["game-settings"].remove({
userID,
game,
playtype,
});
await db["game-stats"].remove({
userID,
game,
playtype,
});
}