mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-01 03:18:06 +03:00
feat: sv6c static importing
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user