feat: sv6c static importing

This commit is contained in:
zkldi
2022-10-22 19:27:49 +01:00
parent b7ffa82f53
commit deb2329993
12 changed files with 301 additions and 5 deletions
@@ -8,6 +8,7 @@ import type { BarbatosContext, BarbatosScore, BarbatosSDVX6Score } from "../ir/b
import type { BeatorajaContext, BeatorajaScore } from "../ir/beatoraja/types";
import type { FervidexStaticContext, FervidexStaticScore } from "../ir/fervidex-static/types";
import type { FervidexContext, FervidexScore } from "../ir/fervidex/types";
import type { KsHookSV6CStaticScore } from "../ir/kshook-sv6c-static/types";
import type { KsHookSV6CContext, KsHookSV6CScore } from "../ir/kshook-sv6c/types";
import type { LR2HookContext, LR2HookScore } from "../ir/lr2hook/types";
import type { IRUSCContext } from "../ir/usc/types";
@@ -42,6 +43,7 @@ export interface ImportTypeDataMap {
"ir/beatoraja": BeatorajaScore;
"ir/usc": USCClientScore;
"ir/kshook-sv6c": KsHookSV6CScore;
"ir/kshook-sv6c-static": KsHookSV6CStaticScore;
"ir/lr2hook": LR2HookScore;
// These aren't placeholder values - the data is yielded in a way that
@@ -70,6 +72,7 @@ export interface ImportTypeContextMap {
"ir/beatoraja": BeatorajaContext;
"ir/usc": IRUSCContext;
"ir/kshook-sv6c": KsHookSV6CContext;
"ir/kshook-sv6c-static": EmptyObject;
"ir/lr2hook": LR2HookContext;
"api/arc-iidx": EmptyObject;
@@ -11,6 +11,7 @@ import { ConverterIRBarbatos } from "./ir/barbatos/converter";
import { ConverterIRBeatoraja } from "./ir/beatoraja/converter";
import { ConverterIRFervidexStatic } from "./ir/fervidex-static/converter";
import { ConverterIRFervidex } from "./ir/fervidex/converter";
import { ConverterKsHookSV6CStatic } from "./ir/kshook-sv6c-static/converter";
import { ConverterIRKsHookSV6C } from "./ir/kshook-sv6c/converter";
import { ConverterLR2Hook } from "./ir/lr2hook/converter";
import { ConverterIRUSC } from "./ir/usc/converter";
@@ -44,5 +45,6 @@ export const Converters: ConverterMap = {
"ir/direct-manual": ConverterBatchManual,
"ir/usc": ConverterIRUSC,
"ir/kshook-sv6c": ConverterIRKsHookSV6C,
"ir/kshook-sv6c-static": ConverterKsHookSV6CStatic,
"ir/lr2hook": ConverterLR2Hook,
};
@@ -0,0 +1,70 @@
import { ConverterKsHookSV6CStatic } from "./converter";
import CreateLogCtx from "lib/logger/logger";
import t from "tap";
import ResetDBState from "test-utils/resets";
import { TestingKsHookSV6CStaticScore } from "test-utils/test-data";
const logger = CreateLogCtx(__filename);
t.test("#ConverterKsHookSV6CStatic", (t) => {
t.beforeEach(ResetDBState);
t.test("Should match a score with its song and chart.", async (t) => {
const res = await ConverterKsHookSV6CStatic(
TestingKsHookSV6CStaticScore,
{},
"ir/kshook-sv6c-static",
logger
);
t.hasStrict(res, {
song: {
id: 1,
},
chart: {
data: {
inGameID: 1,
},
difficulty: "ADV",
},
dryScore: {
scoreData: {
score: 9_579_365,
grade: "AA+",
lamp: "EXCESSIVE CLEAR",
judgements: {
critical: 1184,
near: 46,
miss: 30,
},
hitMeta: {
maxCombo: 158,
exScore: 1334,
},
},
game: "sdvx",
importType: "ir/kshook-sv6c",
scoreMeta: {},
},
});
t.end();
});
t.test("Should throw an error if song or chart can't be found.", (t) => {
t.rejects(
() =>
ConverterKsHookSV6CStatic(
{ ...TestingKsHookSV6CStaticScore, music_id: 10000 },
{},
"ir/kshook-sv6c",
logger
),
"Should throw a KTDataNotFoundError if chart can't be found."
);
t.end();
});
t.end();
});
@@ -0,0 +1,61 @@
import { SV6CConvertDifficulty, SV6CConvertLamp } from "../kshook-sv6c/converter";
import {
InternalFailure,
KTDataNotFoundFailure,
} from "lib/score-import/framework/common/converter-failures";
import { GenericGetGradeAndPercent } from "lib/score-import/framework/common/score-utils";
import { FindSDVXChartOnInGameIDVersion } from "utils/queries/charts";
import { FindSongOnID } from "utils/queries/songs";
import type { ConverterFunction } from "../../common/types";
import type { KsHookSV6CStaticScore } from "./types";
import type { DryScore } from "lib/score-import/framework/common/types";
import type { EmptyObject } from "utils/types";
export const ConverterKsHookSV6CStatic: ConverterFunction<
KsHookSV6CStaticScore,
EmptyObject
> = async (data, context, importType, logger) => {
const diff = SV6CConvertDifficulty(data.difficulty);
const chart = await FindSDVXChartOnInGameIDVersion(data.music_id, diff, "konaste");
if (!chart) {
throw new KTDataNotFoundFailure(
`Could not find chart with songID ${data.music_id} (${diff} for Konaste).`,
importType,
data,
context
);
}
const song = await FindSongOnID("sdvx", chart.songID);
if (!song) {
logger.severe(`Song ${chart.songID} (sdvx) has no parent song?`);
throw new InternalFailure(`Song ${chart.songID} (sdvx) has no parent song?`);
}
const { percent, grade } = GenericGetGradeAndPercent("sdvx", data.score, chart);
const dryScore: DryScore<"sdvx:Single"> = {
game: "sdvx",
service: "kshook SV6C Static",
comment: null,
importType: "ir/kshook-sv6c",
timeAchieved: data.timestamp,
scoreData: {
score: data.score,
percent,
grade,
lamp: SV6CConvertLamp(data.clear),
judgements: {},
hitMeta: {
maxCombo: data.max_chain,
exScore: data.ex_score,
},
},
scoreMeta: {},
};
return { song, chart, dryScore };
};
@@ -0,0 +1,57 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ParseKsHookSV6CStatic } from "./parser";
import deepmerge from "deepmerge";
import CreateLogCtx from "lib/logger/logger";
import t from "tap";
import { TestingKsHookSV6CScore, TestingKsHookSV6CStaticScore } from "test-utils/test-data";
const logger = CreateLogCtx(__filename);
t.test("#ParseKsHookSV6CStatic", (t) => {
const assertFail = (data: any, message: string) => {
t.throws(() => ParseKsHookSV6CStatic(data, logger), message);
};
const assertSuccess = (data: any, message: string) => {
try {
t.doesNotThrow(() => ParseKsHookSV6CStatic(data, logger), message);
const res = ParseKsHookSV6CStatic(data, logger);
t.equal(res.game, "sdvx");
t.ok(Array.isArray(res.iterable));
} catch (err) {
t.fail(`[${message}] ${(err as Error).message}`);
}
};
const dm = (data: any) => ({ scores: [deepmerge(TestingKsHookSV6CStaticScore, data)] });
assertSuccess({ scores: [TestingKsHookSV6CStaticScore] }, "Should parse a valid score.");
assertSuccess(
dm({ unexpectedField: "foo" }),
"Should allow excess keys that we do not recognise."
);
assertFail({}, "Should reject an empty object");
assertFail({ scores: TestingKsHookSV6CStaticScore }, "Should reject non-array scores");
assertFail(dm({ clear: "invalid_clear" }), "Should reject invalid clears.");
assertFail(dm({ difficulty: "invalid_difficulty" }), "Should reject invalid difficulties.");
assertFail(dm({ grade: "invalid_grade" }), "Should reject invalid grades.");
assertFail(dm({ max_chain: -1 }), "Should reject negative max_chains.");
assertFail(dm({ max_chain: 100.5 }), "Should reject non-integer max_chains.");
assertFail(dm({ score: -1 }), "Should reject negative scores.");
assertFail(dm({ score: 10_000_001 }), "Should reject > 10m scores.");
assertSuccess(dm({ score: 10_000_000 }), "Should allow scores of 10m.");
assertSuccess(dm({ score: 0 }), "Should allow scores of 0.");
assertSuccess(
{ scores: [TestingKsHookSV6CStaticScore, TestingKsHookSV6CStaticScore] },
"Should allow multiple scores in the scores array."
);
t.end();
});
@@ -0,0 +1,46 @@
import { PR_KSHOOK_SV6C } from "../kshook-sv6c/parser";
import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error";
import p from "prudence";
import { FormatPrError } from "utils/prudence";
import type { ParserFunctionReturns } from "../../common/types";
import type { KsHookSV6CStaticBody, KsHookSV6CStaticScore } from "./types";
import type { KtLogger } from "lib/logger/logger";
import type { PrudenceSchema } from "prudence";
import type { EmptyObject } from "utils/types";
const PR_KSHOOK_SV6C_STATIC: PrudenceSchema = {
scores: [
{
score: PR_KSHOOK_SV6C.score!,
ex_score: PR_KSHOOK_SV6C.ex_score!,
clear: PR_KSHOOK_SV6C.clear!,
difficulty: PR_KSHOOK_SV6C.difficulty!,
grade: PR_KSHOOK_SV6C.grade!,
max_chain: PR_KSHOOK_SV6C.max_chain!,
music_id: PR_KSHOOK_SV6C.music_id!,
timestamp: p.isPositiveInteger,
},
],
};
export function ParseKsHookSV6CStatic(
body: Record<string, unknown>,
_logger: KtLogger
): ParserFunctionReturns<KsHookSV6CStaticScore, EmptyObject> {
// Ignore excess keys, as SV6C might add more features in the future.
const err = p(body, PR_KSHOOK_SV6C_STATIC, undefined, { allowExcessKeys: true });
if (err) {
throw new ScoreImportFatalError(400, FormatPrError(err));
}
const data = body as unknown as KsHookSV6CStaticBody;
return {
game: "sdvx",
iterable: data.scores,
context: {},
classHandler: null,
};
}
@@ -0,0 +1,17 @@
import type { KsHookSV6CScore } from "../kshook-sv6c/types";
export interface KsHookSV6CStaticScore {
clear: KsHookSV6CScore["clear"];
difficulty: KsHookSV6CScore["difficulty"];
grade: KsHookSV6CScore["grade"];
ex_score: number;
max_chain: number;
music_id: number;
score: number;
timestamp: number;
}
export interface KsHookSV6CStaticBody {
scores: Array<KsHookSV6CStaticScore>;
}
@@ -16,7 +16,7 @@ export const ConverterIRKsHookSV6C: ConverterFunction<KsHookSV6CScore, KsHookSV6
importType,
logger
) => {
const diff = ConvertDifficulty(data.difficulty);
const diff = SV6CConvertDifficulty(data.difficulty);
const chart = await FindSDVXChartOnInGameIDVersion(data.music_id, diff, "konaste");
@@ -48,7 +48,7 @@ export const ConverterIRKsHookSV6C: ConverterFunction<KsHookSV6CScore, KsHookSV6
score: data.score,
percent,
grade,
lamp: ConvertLamp(data.clear),
lamp: SV6CConvertLamp(data.clear),
judgements: {
critical: data.critical,
near: data.near,
@@ -66,7 +66,7 @@ export const ConverterIRKsHookSV6C: ConverterFunction<KsHookSV6CScore, KsHookSV6
return { song, chart, dryScore };
};
function ConvertLamp(clear: KsHookSV6CScore["clear"]): Lamps["sdvx:Single"] {
export function SV6CConvertLamp(clear: KsHookSV6CScore["clear"]): Lamps["sdvx:Single"] {
if (clear === "CLEAR_PLAYED") {
return "FAILED";
} else if (clear === "CLEAR_EFFECTIVE") {
@@ -80,7 +80,7 @@ function ConvertLamp(clear: KsHookSV6CScore["clear"]): Lamps["sdvx:Single"] {
return "PERFECT ULTIMATE CHAIN";
}
function ConvertDifficulty(
export function SV6CConvertDifficulty(
diff: KsHookSV6CScore["difficulty"]
): "ADV" | "ANY_INF" | "EXH" | "MXM" | "NOV" {
if (diff === "DIFFICULTY_NOVICE") {
@@ -6,7 +6,7 @@ import type { KsHookSV6CContext, KsHookSV6CScore } from "./types";
import type { KtLogger } from "lib/logger/logger";
import type { PrudenceSchema } from "prudence";
const PR_KSHOOK_SV6C: PrudenceSchema = {
export const PR_KSHOOK_SV6C: PrudenceSchema = {
clear: p.isIn(
"CLEAR_PLAYED",
"CLEAR_EFFECTIVE",
@@ -16,6 +16,7 @@ import { ParseBeatorajaSingle } from "./ir/beatoraja/parser";
import ParseDirectManual from "./ir/direct-manual/parser";
import { ParseFervidexStatic } from "./ir/fervidex-static/parser";
import { ParseFervidexSingle } from "./ir/fervidex/parser";
import { ParseKsHookSV6CStatic } from "./ir/kshook-sv6c-static/parser";
import { ParseKsHookSV6C } from "./ir/kshook-sv6c/parser";
import { ParseLR2Hook } from "./ir/lr2hook/parser";
import { ParseIRUSC } from "./ir/usc/parser";
@@ -43,5 +44,6 @@ export const Parsers = {
"ir/direct-manual": ParseDirectManual,
"ir/usc": ParseIRUSC,
"ir/kshook-sv6c": ParseKsHookSV6C,
"ir/kshook-sv6c-static": ParseKsHookSV6CStatic,
"ir/lr2hook": ParseLR2Hook,
};
@@ -99,4 +99,29 @@ router.post("/score/save", async (req, res) => {
return res.status(responseData.statusCode).json(responseData.body);
});
/**
* Imports statically from KsHook. Analogous to fervidex-static.
*
* @name POST /ir/kshook/sv6c/score/export
*/
router.post("/score/save", async (req, res) => {
const responseData = await ExpressWrappedScoreImportMain(
req[SYMBOL_TACHI_API_AUTH].userID!,
true,
"ir/kshook-sv6c-static",
[req.safeBody]
);
if (!responseData.body.success) {
// in-air rewrite description to error.
// @ts-expect-error Hack!
responseData.body.error = responseData.body.description;
// @ts-expect-error Hack!
delete responseData.body.description;
}
return res.status(responseData.statusCode).json(responseData.body);
});
export default router;
+13
View File
@@ -17,6 +17,7 @@ import type {
BarbatosSDVX6Score,
} from "lib/score-import/import-types/ir/barbatos/types";
import type { FervidexScore } from "lib/score-import/import-types/ir/fervidex/types";
import type { KsHookSV6CStaticScore } from "lib/score-import/import-types/ir/kshook-sv6c-static/types";
import type { KsHookSV6CScore } from "lib/score-import/import-types/ir/kshook-sv6c/types";
import type { LR2HookScore } from "lib/score-import/import-types/ir/lr2hook/types";
import type { USCClientScore } from "server/router/ir/usc/_playtype/types";
@@ -201,6 +202,18 @@ export const TestingKsHookSV6CScore: KsHookSV6CScore = {
ex_score: 1334,
retry_count: 0,
};
export const TestingKsHookSV6CStaticScore: KsHookSV6CStaticScore = {
clear: "CLEAR_EXCESSIVE",
difficulty: "DIFFICULTY_ADVANCED",
grade: "GRADE_AA_PLUS",
max_chain: 158,
music_id: 1,
score: 9579365,
ex_score: 1334,
timestamp: 1000,
};
export const TestingBMS7KScore: ScoreDocument<"bms:7K"> = {
calculatedData: {
sieglinde: 20,