mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-04 12:58:11 +03:00
add ea3softid parsing
This commit is contained in:
@@ -101,6 +101,26 @@ t.test("POST /api/ir/fervidex/score/submit", async (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should import a valid score with 2dx-gsm", async (t) => {
|
||||
let res = await mockApi
|
||||
.post("/api/ir/fervidex/score/submit")
|
||||
.set("Cookie", cookie)
|
||||
.set("X-Software-Model", "LDJ:J:B:A:2020092900")
|
||||
.send(GetKTDataJSON("./fervidex/2dxgsm.json"));
|
||||
|
||||
t.equal(res.body.success, true, "Should be successful");
|
||||
|
||||
t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores.");
|
||||
|
||||
let scores = await db.scores.count({
|
||||
service: "Fervidex",
|
||||
});
|
||||
|
||||
t.equal(scores, 1, "Should import 1 score.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should reject an invalid body", async (t) => {
|
||||
let res = await mockApi
|
||||
.post("/api/ir/fervidex/score/submit")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Router, RequestHandler } from "express";
|
||||
import { GetUserWithIDGuaranteed } from "../../../common/user";
|
||||
import { ParseEA3SoftID } from "../../../common/util";
|
||||
import { IIDX_AC_MODEL, INF2_MODEL, REV_2DXBMS } from "../../../constants/ea3id";
|
||||
import { RequireLoggedIn } from "../../../middleware/require-logged-in";
|
||||
import { ExpressWrappedScoreImportMain } from "../../../score-import/framework/express-wrapper";
|
||||
import { ParseFervidexStatic } from "../../../score-import/import-types/ir/fervidex-static/parser";
|
||||
@@ -10,10 +12,26 @@ const router: Router = Router({ mergeParams: true });
|
||||
const RequireInf2ModelHeader: RequestHandler = async (req, res, next) => {
|
||||
let swModel = req.header("X-Software-Model");
|
||||
|
||||
if (!swModel || !swModel.startsWith("P2D:J:B:A")) {
|
||||
return res.status(400).send({
|
||||
if (!swModel) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: "This endpoint is only available for INF2 clients.",
|
||||
description: `Invalid X-Software-Model.`,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let softID = ParseEA3SoftID(swModel);
|
||||
|
||||
if (softID.model !== INF2_MODEL) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
description: "This endpoint is only available for INF2 clients.",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid X-Software-Model.`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,8 +41,27 @@ const RequireInf2ModelHeader: RequestHandler = async (req, res, next) => {
|
||||
const ValidateModelHeader: RequestHandler = async (req, res, next) => {
|
||||
let swModel = req.header("X-Software-Model");
|
||||
|
||||
if (!swModel || swModel.startsWith("LDJ:J:B:X") || swModel.startsWith("LDJ:J:B:Z")) {
|
||||
return res.status(400).send({ success: false, description: "Invalid X-Software-Model." });
|
||||
if (!swModel) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid X-Software-Model.`,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
let softID = ParseEA3SoftID(swModel);
|
||||
|
||||
if (softID.rev !== REV_2DXBMS) {
|
||||
return res.status(400).send({
|
||||
success: false,
|
||||
description: "2DX_BMS is not supported.",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid X-Software-Model.`,
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { version } from "commander";
|
||||
import { Difficulties, Game, integer, Playtypes, IDStrings } from "kamaitachi-common";
|
||||
import db from "../../db/db";
|
||||
|
||||
@@ -96,6 +95,46 @@ export function FindChartOnInGameID(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a non-custom chart on its in-game-ID, playtype and difficulty.
|
||||
* This explicitly ignores 2dxtra charts, and is necessary to use for iidx to disambiguate.
|
||||
*/
|
||||
export function FindIIDXChartOnInGameID(
|
||||
inGameID: number,
|
||||
playtype: Playtypes[Game],
|
||||
difficulty: Difficulties[IDStrings]
|
||||
) {
|
||||
// @todo throw an error if this is called with a game that doesn't
|
||||
// support InGameID.
|
||||
return db.charts.iidx.findOne({
|
||||
"data.inGameID": inGameID,
|
||||
"flags.2dxtra": false,
|
||||
playtype,
|
||||
difficulty,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a non-custom chart on its in-game-ID, playtype and difficulty.
|
||||
* This explicitly ignores 2dxtra charts, and is necessary to use for iidx to disambiguate.
|
||||
*/
|
||||
export function FindIIDXChartOnInGameIDVersion(
|
||||
inGameID: number,
|
||||
playtype: Playtypes[Game],
|
||||
difficulty: Difficulties[IDStrings],
|
||||
version: string
|
||||
) {
|
||||
// @todo throw an error if this is called with a game that doesn't
|
||||
// support InGameID.
|
||||
return db.charts.iidx.findOne({
|
||||
"data.inGameID": inGameID,
|
||||
"flags.2dxtra": false,
|
||||
playtype,
|
||||
difficulty,
|
||||
versions: version,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a chart on its in-game-ID, playtype, difficulty and version.
|
||||
*/
|
||||
@@ -114,6 +153,15 @@ export function FindChartOnInGameIDVersion(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an IIDX chart on its 2dxtra hash, which is the sha256 of the .1 buffer.
|
||||
*/
|
||||
export function FindIIDXChartWith2DXtraHash(hash: string) {
|
||||
return db.charts.iidx.findOne({
|
||||
"data.hashSHA256": hash,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an SDVX Chart on its in game ID. This exists to handle
|
||||
* oddities with SDVX difficulties - If "ANY_INF" is sent, it actually
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// which really really messes with a lot of the ecosystem.
|
||||
// shim.
|
||||
|
||||
export default function EscapeStringRegexp(string: string) {
|
||||
export function EscapeStringRegexp(string: string) {
|
||||
if (typeof string !== "string") {
|
||||
throw new TypeError("Expected a string");
|
||||
}
|
||||
@@ -12,3 +12,23 @@ export default function EscapeStringRegexp(string: string) {
|
||||
// Use a simple backslash escape when it's always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns' stricter grammar.
|
||||
return string.replace(/[|\\{}()[\]^$+*?.]/gu, "\\$&").replace(/-/gu, "\\x2d");
|
||||
}
|
||||
|
||||
export function ParseEA3SoftID(ver: string) {
|
||||
let a = ver.split(":");
|
||||
|
||||
if (a.length !== 5) {
|
||||
throw new Error(`Invalid Version Code. Had ${a.length} components.`);
|
||||
}
|
||||
|
||||
if (!ver.match(/^[A-Z0-9]{3}:[A-Z]:[A-Z]:[A-Z]:[0-9]{10}$/u)) {
|
||||
throw new Error(`Invalid Version Code.`);
|
||||
}
|
||||
|
||||
return {
|
||||
model: a[0],
|
||||
dest: a[1], // region
|
||||
spec: a[2],
|
||||
rev: a[3],
|
||||
ext: a[4],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export const INF2_MODEL = "P2D";
|
||||
export const IIDX_AC_MODEL = "LDJ";
|
||||
export const IIDX_AC_LIGHTNING_MODEL = "TDJ";
|
||||
|
||||
export const REV_2DXTRA = "E";
|
||||
export const REV_2DXBMS = "Z";
|
||||
export const REV_OMNIMIX = "X";
|
||||
@@ -1,4 +1,5 @@
|
||||
import t from "tap";
|
||||
import { CloseMongoConnection } from "./db/db";
|
||||
import CreateLogCtx, { Transports } from "./logger";
|
||||
|
||||
t.test("Logger Tests", (t) => {
|
||||
@@ -20,3 +21,5 @@ t.test("Logger Tests", (t) => {
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseMongoConnection);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import t from "tap";
|
||||
import { CreateImportLoggerAndID } from "./import-logger";
|
||||
import { PublicUserDocument } from "kamaitachi-common";
|
||||
import { CloseMongoConnection } from "../../../db/db";
|
||||
|
||||
t.test("#CreateImportLoggerAndID", (t) => {
|
||||
let { importID, logger } = CreateImportLoggerAndID(
|
||||
@@ -17,3 +18,5 @@ t.test("#CreateImportLoggerAndID", (t) => {
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseMongoConnection);
|
||||
|
||||
@@ -4,7 +4,7 @@ import CreateLogCtx from "../../../../logger";
|
||||
import { Testing511Song, Testing511SPA } from "../../../../test-utils/test-data";
|
||||
import { ResolveChartFromSong, ResolveMatchTypeToKTData, ConverterBatchManual } from "./converter";
|
||||
import deepmerge from "deepmerge";
|
||||
import EscapeStringRegexp from "../../../../common/escape-string-regexp";
|
||||
import EscapeStringRegexp from "../../../../common/util";
|
||||
import { CloseMongoConnection } from "../../../../db/db";
|
||||
import { Game } from "kamaitachi-common";
|
||||
import ResetDBState from "../../../../test-utils/reset-db-state";
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
} from "../../../framework/score-importing/converter-failures";
|
||||
import { FervidexContext, FervidexScore } from "./types";
|
||||
import { Lamps, Grades, Difficulties, Playtypes } from "kamaitachi-common";
|
||||
import { FindChartOnInGameIDVersion } from "../../../../common/database-lookup/chart";
|
||||
import {
|
||||
FindIIDXChartOnInGameIDVersion,
|
||||
FindIIDXChartWith2DXtraHash,
|
||||
} from "../../../../common/database-lookup/chart";
|
||||
|
||||
export const FERVIDEX_LAMP_LOOKUP = {
|
||||
0: "NO PLAY",
|
||||
@@ -139,13 +142,17 @@ export const ConverterIRFervidex: ConverterFunction<FervidexScore, FervidexConte
|
||||
) => {
|
||||
let { difficulty, playtype } = SplitFervidexChartRef(data.chart);
|
||||
|
||||
let chart = await FindChartOnInGameIDVersion(
|
||||
"iidx",
|
||||
data.entry_id,
|
||||
playtype,
|
||||
difficulty,
|
||||
context.version
|
||||
);
|
||||
let chart;
|
||||
if (data.custom) {
|
||||
chart = await FindIIDXChartWith2DXtraHash(data.chart_sha256);
|
||||
} else {
|
||||
chart = await FindIIDXChartOnInGameIDVersion(
|
||||
data.entry_id,
|
||||
playtype,
|
||||
difficulty,
|
||||
context.version
|
||||
);
|
||||
}
|
||||
|
||||
if (!chart) {
|
||||
throw new KTDataNotFoundFailure(
|
||||
@@ -204,7 +211,7 @@ export const ConverterIRFervidex: ConverterFunction<FervidexScore, FervidexConte
|
||||
gaugeHistory,
|
||||
gauge,
|
||||
bp: data.bad + data.poor,
|
||||
comboBreak: null,
|
||||
comboBreak: data.combo_break,
|
||||
gsm: data["2dx-gsm"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { KtLogger, ParserFunctionReturnsSync } from "../../../../types";
|
||||
import p, { PrudenceSchema } from "prudence";
|
||||
import p, { PrudenceSchema, ValidSchemaValue } from "prudence";
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { FormatPrError, optNull } from "../../../../common/prudence";
|
||||
import { FervidexContext, FervidexScore } from "./types";
|
||||
@@ -8,6 +8,8 @@ import { ConverterIRFervidex } from "./converter";
|
||||
const PR_Fervidex: PrudenceSchema = {
|
||||
chart: p.isIn("spb", "spn", "dpn", "sph", "dph", "spa", "dpa", "spl", "dpl"),
|
||||
entry_id: p.isPositiveInteger,
|
||||
chart_sha256: "string",
|
||||
custom: "boolean",
|
||||
|
||||
pgreat: p.isPositiveInteger,
|
||||
great: p.isPositiveInteger,
|
||||
@@ -18,16 +20,18 @@ const PR_Fervidex: PrudenceSchema = {
|
||||
slow: p.isPositiveInteger,
|
||||
fast: p.isPositiveInteger,
|
||||
max_combo: p.isPositiveInteger,
|
||||
combo_break: p.isPositiveInteger,
|
||||
|
||||
ex_score: p.isPositiveInteger,
|
||||
clear_type: p.isBoundedInteger(0, 7),
|
||||
|
||||
gauge: [p.isBoundedInteger(0, 255)],
|
||||
ghost: [p.isBoundedInteger(0, 100)],
|
||||
|
||||
dead: {
|
||||
dead: p.optional(({
|
||||
measure: optNull(p.isPositiveInteger),
|
||||
note: optNull(p.isPositiveInteger),
|
||||
},
|
||||
} as unknown) as ValidSchemaValue),
|
||||
|
||||
option: {
|
||||
gauge: optNull(p.isIn("ASSISTED_EASY", "EASY", "HARD", "EX_HARD")),
|
||||
@@ -39,7 +43,7 @@ const PR_Fervidex: PrudenceSchema = {
|
||||
},
|
||||
|
||||
// we dont use it and we dont care.
|
||||
pacemaker: p.optional(p.any),
|
||||
pacemaker: p.any,
|
||||
|
||||
"2dx-gsm": p.optional({
|
||||
// @ts-expect-error recursive types error
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface FervidexScore {
|
||||
gauge: [integer];
|
||||
ghost: [integer];
|
||||
|
||||
dead: {
|
||||
dead?: {
|
||||
measure?: integer | null;
|
||||
note?: integer | null;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"bad": 0,
|
||||
"chart": "spa",
|
||||
"clear_type": 1,
|
||||
"combo_break": 6,
|
||||
"dead": {
|
||||
"measure": 18,
|
||||
"note": 40
|
||||
},
|
||||
"entry_id": 1000,
|
||||
"ex_score": 68,
|
||||
"fast": 0,
|
||||
"gauge": [
|
||||
100,
|
||||
50
|
||||
],
|
||||
"ghost": [
|
||||
0,
|
||||
2
|
||||
],
|
||||
"good": 0,
|
||||
"great": 0,
|
||||
"max_combo": 34,
|
||||
"option": {
|
||||
"gauge": "HARD",
|
||||
"range": "SUDDEN_PLUS",
|
||||
"style": "RANDOM"
|
||||
},
|
||||
"pacemaker": {
|
||||
"name": "",
|
||||
"score": 363,
|
||||
"type": "PACEMAKER_A"
|
||||
},
|
||||
"pgreat": 34,
|
||||
"poor": 6,
|
||||
"slow": 0,
|
||||
"2dx-gsm": {
|
||||
"EASY": [0, 10],
|
||||
"NORMAL": [0, 20],
|
||||
"HARD": [20, 0],
|
||||
"EX_HARD": [10, 0]
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
"measure": 18,
|
||||
"note": 40
|
||||
},
|
||||
"custom": false,
|
||||
"chart_sha256": "asdfasdf",
|
||||
"entry_id": 1000,
|
||||
"ex_score": 68,
|
||||
"fast": 0,
|
||||
@@ -14,6 +16,10 @@
|
||||
100,
|
||||
50
|
||||
],
|
||||
"ghost": [
|
||||
0,
|
||||
2
|
||||
],
|
||||
"good": 0,
|
||||
"great": 0,
|
||||
"max_combo": 34,
|
||||
|
||||
Reference in New Issue
Block a user