add ea3softid parsing

This commit is contained in:
zkldi
2021-05-17 06:49:22 +01:00
parent 0c076e3b69
commit 9f4acd454b
13 changed files with 220 additions and 22 deletions
@@ -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")
+42 -5
View File
@@ -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();
+49 -1
View File
@@ -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],
};
}
+7
View File
@@ -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";
+3
View File
@@ -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,