mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-28 18:08:15 +03:00
LR2Hook Converter and Parser definitions
This commit is contained in:
+1
-1
@@ -83,7 +83,7 @@
|
||||
"rimraf": "3.0.2",
|
||||
"safe-json-stringify": "1.2.0",
|
||||
"seq-logging": "^1.1.1",
|
||||
"tachi-common": "0.3.16",
|
||||
"tachi-common": "0.3.17",
|
||||
"ts-node": "10.0.0",
|
||||
"typescript": "4.3.4",
|
||||
"winston": "3.3.3",
|
||||
|
||||
Generated
+4
-4
@@ -63,7 +63,7 @@ specifiers:
|
||||
safe-json-stringify: 1.2.0
|
||||
seq-logging: ^1.1.1
|
||||
supertest: 6.1.3
|
||||
tachi-common: 0.3.16
|
||||
tachi-common: 0.3.17
|
||||
tap: 15.0.9
|
||||
ts-node: 10.0.0
|
||||
tsconfig-paths: 3.10.1
|
||||
@@ -107,7 +107,7 @@ dependencies:
|
||||
rimraf: 3.0.2
|
||||
safe-json-stringify: 1.2.0
|
||||
seq-logging: 1.1.1
|
||||
tachi-common: 0.3.16
|
||||
tachi-common: 0.3.17
|
||||
ts-node: 10.0.0_83f53b0a0c5616d3fa00ed4e30b9ce1b
|
||||
typescript: 4.3.4
|
||||
winston: 3.3.3
|
||||
@@ -5387,8 +5387,8 @@ packages:
|
||||
strip-ansi: 6.0.0
|
||||
dev: true
|
||||
|
||||
/tachi-common/0.3.16:
|
||||
resolution: {integrity: sha512-cCTD+gKWRvOuQAUEf9MNkIAt961JKu/pHOF/Sw+OoMNxAQfiJ9qwjsIF9YXBX16r0q+Olh1Ps+ImaP3j5/PLRQ==}
|
||||
/tachi-common/0.3.17:
|
||||
resolution: {integrity: sha512-YL5UlTT5qHSaAzIbSgz2UzagTE9HV++C0XVAopFg8wYwUei2b1R5shwXgzyO0RtZgqVT0ivz2ug+jx+RN37pCw==}
|
||||
dependencies:
|
||||
monk: 7.3.4
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -21,6 +21,7 @@ import { BeatorajaContext, BeatorajaScore } from "../ir/beatoraja/types";
|
||||
import { FervidexStaticContext, FervidexStaticScore } from "../ir/fervidex-static/types";
|
||||
import { FervidexContext, FervidexScore } from "../ir/fervidex/types";
|
||||
import { KsHookSV3CContext, KsHookSV3CScore } from "../ir/kshook-sv3c/types";
|
||||
import { LR2HookContext, LR2HookScore } from "../ir/lr2hook/types";
|
||||
import { IRUSCContext } from "../ir/usc/types";
|
||||
import { KaiContext } from "./api-kai/types";
|
||||
import { BatchManualContext } from "./batch-manual/types";
|
||||
@@ -41,6 +42,7 @@ export interface ImportTypeDataMap {
|
||||
"ir/beatoraja": BeatorajaScore;
|
||||
"ir/usc": USCClientScore;
|
||||
"ir/kshook-sv3c": KsHookSV3CScore;
|
||||
"ir/lr2hook": LR2HookScore;
|
||||
|
||||
// These aren't placeholder values - the data is yielded in a way that
|
||||
// the value of these is legitimately unknown at convert time.
|
||||
@@ -68,6 +70,7 @@ export interface ImportTypeContextMap {
|
||||
"ir/beatoraja": BeatorajaContext;
|
||||
"ir/usc": IRUSCContext;
|
||||
"ir/kshook-sv3c": KsHookSV3CContext;
|
||||
"ir/lr2hook": LR2HookContext;
|
||||
|
||||
"api/arc-iidx": EmptyObject;
|
||||
"api/arc-sdvx": EmptyObject;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ConverterIRBeatoraja } from "./ir/beatoraja/converter";
|
||||
import { ConverterIRFervidexStatic } from "./ir/fervidex-static/converter";
|
||||
import { ConverterIRFervidex } from "./ir/fervidex/converter";
|
||||
import { ConverterIRKsHookSV3C } from "./ir/kshook-sv3c/converter";
|
||||
import { ConverterLR2Hook } from "./ir/lr2hook/converter";
|
||||
import { ConverterIRUSC } from "./ir/usc/converter";
|
||||
|
||||
export type ConverterMap = {
|
||||
@@ -43,4 +44,5 @@ export const Converters: ConverterMap = {
|
||||
"ir/direct-manual": ConverterBatchManual,
|
||||
"ir/usc": ConverterIRUSC,
|
||||
"ir/kshook-sv3c": ConverterIRKsHookSV3C,
|
||||
"ir/lr2hook": ConverterLR2Hook,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { TestingLR2HookScore } from "test-utils/test-data";
|
||||
import { ConverterLR2Hook } from "./converter";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#ConverterLR2Hook", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should match a score with its song and chart.", async (t) => {
|
||||
const res = await ConverterLR2Hook(
|
||||
TestingLR2HookScore,
|
||||
{ timeReceived: 10 },
|
||||
"ir/lr2hook",
|
||||
logger
|
||||
);
|
||||
|
||||
t.hasStrict(res, {
|
||||
song: {
|
||||
id: 27339,
|
||||
},
|
||||
chart: {
|
||||
chartID: "88eb6cc5683e2740cbd07f588a5f3db1db8d467b",
|
||||
data: {
|
||||
hashMD5: TestingLR2HookScore.md5,
|
||||
},
|
||||
},
|
||||
dryScore: {
|
||||
scoreData: {
|
||||
score: TestingLR2HookScore.scoreData.exScore,
|
||||
},
|
||||
game: "bms",
|
||||
importType: "ir/lr2hook",
|
||||
scoreMeta: {
|
||||
client: "LR2",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should throw an error if song or chart can't be found.", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConverterLR2Hook(
|
||||
Object.assign({}, TestingLR2HookScore, { md5: "nonsense_md5" }),
|
||||
{ timeReceived: 10 },
|
||||
"ir/lr2hook",
|
||||
logger
|
||||
),
|
||||
"Should throw a KTDataNotFoundError if chart can't be found."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Lamps } from ".pnpm/tachi-common@0.3.16/node_modules/tachi-common";
|
||||
import {
|
||||
InternalFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "lib/score-import/framework/common/converter-failures";
|
||||
import { GenericGetGradeAndPercent } from "lib/score-import/framework/common/score-utils";
|
||||
import { DryScore } from "lib/score-import/framework/common/types";
|
||||
import { FindBMSChartOnHash } from "utils/queries/charts";
|
||||
import { FindSongOnID } from "utils/queries/songs";
|
||||
import { ConverterFunction } from "../../common/types";
|
||||
import { LR2HookContext, LR2HookScore } from "./types";
|
||||
|
||||
export const ConverterLR2Hook: ConverterFunction<LR2HookScore, LR2HookContext> = async (
|
||||
data,
|
||||
context,
|
||||
importType,
|
||||
logger
|
||||
) => {
|
||||
const chart = await FindBMSChartOnHash(data.md5);
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
`Could not find chart with md5 ${data.md5}.`,
|
||||
importType,
|
||||
data,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnID("bms", chart.songID);
|
||||
|
||||
if (!song) {
|
||||
logger.severe(`Song ${chart.songID} (bms) has no parent song?`);
|
||||
throw new InternalFailure(`Song ${chart.songID} (bms) has no parent song?`);
|
||||
}
|
||||
|
||||
const { percent, grade } = GenericGetGradeAndPercent("bms", data.scoreData.exScore, chart);
|
||||
|
||||
const dryScore: DryScore<"bms:7K" | "bms:14K"> = {
|
||||
game: "bms",
|
||||
service: "LR2Hook",
|
||||
comment: null,
|
||||
importType: "ir/lr2hook",
|
||||
timeAchieved: context.timeReceived,
|
||||
scoreData: {
|
||||
score: data.scoreData.exScore,
|
||||
percent,
|
||||
grade,
|
||||
lamp: ConvertLamp(data.scoreData.lamp),
|
||||
judgements: {
|
||||
pgreat: data.scoreData.pgreat,
|
||||
great: data.scoreData.great,
|
||||
good: data.scoreData.good,
|
||||
bad: data.scoreData.bad,
|
||||
poor: data.scoreData.poor,
|
||||
},
|
||||
hitMeta: {
|
||||
bp: data.scoreData.bad + data.scoreData.poor,
|
||||
maxCombo: data.scoreData.maxCombo,
|
||||
gauge: data.scoreData.hpGraph[999] ?? 0,
|
||||
},
|
||||
},
|
||||
scoreMeta: {
|
||||
client: "LR2",
|
||||
},
|
||||
};
|
||||
|
||||
return { song, chart, dryScore };
|
||||
};
|
||||
|
||||
function ConvertLamp(lamp: LR2HookScore["scoreData"]["lamp"]): Lamps["bms:7K" | "bms:14K"] {
|
||||
switch (lamp) {
|
||||
case "EASY":
|
||||
return "EASY CLEAR";
|
||||
case "FAIL":
|
||||
return "FAILED";
|
||||
case "FULL COMBO":
|
||||
return "FULL COMBO";
|
||||
case "HARD":
|
||||
return "HARD CLEAR";
|
||||
case "NO PLAY":
|
||||
return "NO PLAY";
|
||||
case "NORMAL":
|
||||
return "CLEAR";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import { ParseLR2Hook } from "./parser";
|
||||
import deepmerge from "deepmerge";
|
||||
import { TestingLR2HookScore } from "test-utils/test-data";
|
||||
import { ApplyNTimes } from "utils/misc";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#ParseLR2Hook", (t) => {
|
||||
const assertFail = (data: any, message: string) => {
|
||||
t.throws(() => ParseLR2Hook(data, logger), message);
|
||||
};
|
||||
|
||||
const assertSuccess = (data: any, message: string) => {
|
||||
try {
|
||||
t.doesNotThrow(() => ParseLR2Hook(data, logger), message);
|
||||
|
||||
const res = ParseLR2Hook(data, logger);
|
||||
|
||||
t.equal(res.game, "bms");
|
||||
t.type(res.context.timeReceived, "number");
|
||||
t.ok(Array.isArray(res.iterable));
|
||||
t.equal(res.classHandler, null);
|
||||
} catch (err) {
|
||||
t.fail(`[${message}] ${err.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const dm = (data: any) => deepmerge(TestingLR2HookScore, data);
|
||||
const dms = (data: any) =>
|
||||
deepmerge(
|
||||
TestingLR2HookScore,
|
||||
{ scoreData: data },
|
||||
{
|
||||
arrayMerge: (a, b) => b,
|
||||
}
|
||||
);
|
||||
|
||||
assertSuccess(TestingLR2HookScore, "Should parse a valid score.");
|
||||
assertSuccess(
|
||||
dm({ unexpectedField: "foo" }),
|
||||
"Should allow excess keys that we do not recognise."
|
||||
);
|
||||
assertSuccess(
|
||||
dm({ scoreData: { unexpectedField: "foo" } }),
|
||||
"Should allow excess keys inside scoreData that we do not recognise."
|
||||
);
|
||||
|
||||
assertFail({}, "Should reject an empty object");
|
||||
|
||||
for (const key of [
|
||||
"pgreat",
|
||||
"good",
|
||||
"bad",
|
||||
"poor",
|
||||
"great",
|
||||
"maxCombo",
|
||||
"exScore",
|
||||
"notesTotal",
|
||||
"notesPlayed",
|
||||
]) {
|
||||
assertFail(dms({ [key]: -1 }), `Should reject negative ${key}`);
|
||||
assertFail(dms({ [key]: 0.5 }), `Should reject decimal ${key}`);
|
||||
assertFail(dms({ [key]: "0" }), `Should reject string ${key}`);
|
||||
assertFail(dms({ [key]: null }), `Should reject nonsense ${key}`);
|
||||
}
|
||||
|
||||
assertFail(dms({ lamp: "UNKNOWN_LAMP" }), "Should reject unknown lamp.");
|
||||
assertFail(dms({ lamp: null }), "Should reject null lamp.");
|
||||
assertFail(dms({ lamp: undefined }), "Should reject no lamp.");
|
||||
|
||||
assertFail(
|
||||
dms({ hpGraph: ApplyNTimes(999, () => 50) }),
|
||||
"Should disallow hp graph with <1000 elements."
|
||||
);
|
||||
assertFail(dms({ hpGraph: [] }), "Should disallow hp graph with 0 elements.");
|
||||
assertFail(
|
||||
dms({ hpGraph: ApplyNTimes(1001, () => 50) }),
|
||||
"Should disallow hp graph with >1000 elements."
|
||||
);
|
||||
assertFail(
|
||||
dms({ hpGraph: ApplyNTimes(1000, () => 101) }),
|
||||
"Should disallow hp graph with elements larger than 100."
|
||||
);
|
||||
assertFail(
|
||||
dms({ hpGraph: ApplyNTimes(1000, () => -1) }),
|
||||
"Should disallow hp graph with negative element values."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import { ParserFunctionReturns } from "../../common/types";
|
||||
import { LR2HookContext, LR2HookScore } from "./types";
|
||||
import p, { PrudenceSchema } from "prudence";
|
||||
import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error";
|
||||
import { FormatPrError } from "utils/prudence";
|
||||
|
||||
const PR_LR2Hook: PrudenceSchema = {
|
||||
md5: "string",
|
||||
scoreData: {
|
||||
pgreat: p.isPositiveInteger,
|
||||
great: p.isPositiveInteger,
|
||||
good: p.isPositiveInteger,
|
||||
bad: p.isPositiveInteger,
|
||||
poor: p.isPositiveInteger,
|
||||
maxCombo: p.isPositiveInteger,
|
||||
exScore: p.isPositiveInteger,
|
||||
moneyScore: p.isPositiveInteger,
|
||||
notesTotal: p.isPositiveInteger,
|
||||
notesPlayed: p.isPositiveInteger,
|
||||
lamp: p.isIn("NO PLAY", "FAIL", "EASY", "NORMAL", "HARD", "FULL COMBO"),
|
||||
hpGraph: (self) => {
|
||||
if (!Array.isArray(self)) {
|
||||
return "Expected an array";
|
||||
}
|
||||
if (self.length !== 1000) {
|
||||
return "Expected an array with length 1000.";
|
||||
}
|
||||
if (self.some((x) => x < 0 || x > 100 || !Number.isInteger(x))) {
|
||||
return "Expected an array of 1000 integers between 0 and 100.";
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function ParseLR2Hook(
|
||||
body: Record<string, unknown>,
|
||||
logger: KtLogger
|
||||
): ParserFunctionReturns<LR2HookScore, LR2HookContext> {
|
||||
// Ignore excess keys, as lr2hook is likely to add more features in the future.
|
||||
const err = p(body, PR_LR2Hook, undefined, { allowExcessKeys: true });
|
||||
|
||||
if (err) {
|
||||
throw new ScoreImportFatalError(400, FormatPrError(err));
|
||||
}
|
||||
|
||||
const score = body as unknown as LR2HookScore;
|
||||
|
||||
return {
|
||||
game: "bms",
|
||||
iterable: [score],
|
||||
context: {
|
||||
timeReceived: Date.now(),
|
||||
},
|
||||
classHandler: null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { integer } from "tachi-common";
|
||||
|
||||
export interface LR2HookScore {
|
||||
md5: string;
|
||||
scoreData: {
|
||||
pgreat: integer;
|
||||
great: integer;
|
||||
good: integer;
|
||||
bad: integer;
|
||||
poor: integer;
|
||||
maxCombo: integer;
|
||||
exScore: integer;
|
||||
moneyScore: integer;
|
||||
notesTotal: integer;
|
||||
notesPlayed: integer;
|
||||
lamp: "NO PLAY" | "FAIL" | "EASY" | "NORMAL" | "HARD" | "FULL COMBO";
|
||||
hpGraph: integer[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface LR2HookContext {
|
||||
timeReceived: number;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import ParseDirectManual from "./ir/direct-manual/parser";
|
||||
import { ParseFervidexStatic } from "./ir/fervidex-static/parser";
|
||||
import { ParseFervidexSingle } from "./ir/fervidex/parser";
|
||||
import { ParseKsHookSV3C } from "./ir/kshook-sv3c/parser";
|
||||
import { ParseLR2Hook } from "./ir/lr2hook/parser";
|
||||
import { ParseIRUSC } from "./ir/usc/parser";
|
||||
|
||||
export const Parsers = {
|
||||
@@ -42,4 +43,5 @@ export const Parsers = {
|
||||
"ir/direct-manual": ParseDirectManual,
|
||||
"ir/usc": ParseIRUSC,
|
||||
"ir/kshook-sv3c": ParseKsHookSV3C,
|
||||
"ir/lr2hook": ParseLR2Hook,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { DryScore } from "lib/score-import/framework/common/types";
|
||||
import { BarbatosScore } from "lib/score-import/import-types/ir/barbatos/types";
|
||||
import { KsHookSV3CScore } from "lib/score-import/import-types/ir/kshook-sv3c/types";
|
||||
import { LR2HookScore } from "lib/score-import/import-types/ir/lr2hook/types";
|
||||
import path from "path";
|
||||
import { USCClientScore } from "server/router/ir/usc/_playtype/types";
|
||||
import {
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
SongDocument,
|
||||
UserGoalDocument,
|
||||
} from "tachi-common";
|
||||
import { ApplyNTimes, RFA } from "utils/misc";
|
||||
|
||||
const file = (name: string) => path.join(__dirname, "/test-data", name);
|
||||
|
||||
@@ -41,6 +43,24 @@ export const TestingIIDXSPScore = GetKTDataJSON(
|
||||
|
||||
export const TestingKsHookSV3CScore = GetKTDataJSON("./kshook-sv3c/base.json") as KsHookSV3CScore;
|
||||
|
||||
export const TestingLR2HookScore: LR2HookScore = {
|
||||
md5: "38616b85332037cc12924f2ae2840262",
|
||||
scoreData: {
|
||||
pgreat: 1000,
|
||||
great: 500,
|
||||
good: 100,
|
||||
bad: 50,
|
||||
poor: 25,
|
||||
exScore: 2500,
|
||||
maxCombo: 50,
|
||||
moneyScore: 150_000,
|
||||
notesTotal: 2256,
|
||||
notesPlayed: 2256,
|
||||
lamp: "HARD",
|
||||
hpGraph: ApplyNTimes(1000, () => RFA([100, 50, 80, 0])),
|
||||
},
|
||||
};
|
||||
|
||||
export const Testing511SPA = GetKTDataJSON("./tachi/iidx-511spa.json") as ChartDocument<"iidx:SP">;
|
||||
|
||||
export const Testing511Song = GetKTDataJSON("./tachi/iidx-511-song.json") as SongDocument<"iidx">;
|
||||
|
||||
@@ -37,7 +37,7 @@ export function MStoS(ms: number) {
|
||||
/**
|
||||
* Random From Array - Selects a random value from an array.
|
||||
*/
|
||||
export function RFA(arr: unknown[]) {
|
||||
export function RFA<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
@@ -133,3 +133,12 @@ export function Sleep(ms: number) {
|
||||
export function GetTimeXHoursAgo(hours: integer) {
|
||||
return Date.now() - ONE_HOUR * hours;
|
||||
}
|
||||
|
||||
export function ApplyNTimes<T>(n: integer, fn: (i: integer) => T): T[] {
|
||||
const arr = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
arr.push(fn(i));
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user