ir/beatoraja converters and parsers

This commit is contained in:
zkldi
2021-05-26 20:58:57 +01:00
parent e1bf593c96
commit 2d698d0083
9 changed files with 328 additions and 12 deletions
+3 -3
View File
@@ -61,7 +61,7 @@ dependencies:
express-session: 1.17.1
fast-json-stable-hash: 1.0.2
fast-xml-parser: 3.19.0
kamaitachi-common: github.com/zkldi/Kamaitachi-common/0398d77719606979c88833b37bce8020958cf058
kamaitachi-common: github.com/zkldi/Kamaitachi-common/99d4590a08602bf9229583282cdbe77bdf8e8d97
mongodb: 3.6.6
monk: 7.3.4
multer: 1.4.2
@@ -4344,8 +4344,8 @@ packages:
engines: {node: '>=6'}
dev: true
github.com/zkldi/Kamaitachi-common/0398d77719606979c88833b37bce8020958cf058:
resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/0398d77719606979c88833b37bce8020958cf058}
github.com/zkldi/Kamaitachi-common/99d4590a08602bf9229583282cdbe77bdf8e8d97:
resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/99d4590a08602bf9229583282cdbe77bdf8e8d97}
name: kamaitachi-common
version: 0.1.0
dependencies:
@@ -12,6 +12,8 @@ import { ImportTypes, Game, AnyChartDocument, AnySongDocument } from "kamaitachi
import { ConverterFailure } from "../../framework/common/converter-failures";
import { ClassHandler } from "../../framework/user-game-stats/classes";
import { DryScore } from "../../framework/common/types";
import { BeatorajaIRScoreFormat } from "../../../../server/router/ir/beatoraja/charts/convert-scores";
import { BeatorajaContext } from "../ir/beatoraja/types";
export interface ImportTypeDataMap {
"file/eamusement-iidx-csv": IIDXEamusementCSVData;
@@ -25,7 +27,7 @@ export interface ImportTypeDataMap {
"ir/fervidex": FervidexScore;
"ir/fervidex-static": FervidexStaticScore;
"ir/chunitachi": BatchManualScore;
"ir/beatoraja": EmptyObject;
"ir/beatoraja": BeatorajaIRScoreFormat;
"ir/usc": EmptyObject;
"api/arc-iidx": unknown;
@@ -49,7 +51,7 @@ export interface ImportTypeContextMap {
"ir/fervidex": FervidexContext;
"ir/fervidex-static": FervidexStaticContext;
"ir/chunitachi": BatchManualContext;
"ir/beatoraja": EmptyObject;
"ir/beatoraja": BeatorajaContext;
"ir/usc": EmptyObject;
"api/arc-iidx": EmptyObject;
@@ -0,0 +1,123 @@
import { FindChartOnSHA256 } from "../../../../../utils/queries/charts";
import { FindSongOnID } from "../../../../../utils/queries/songs";
import { InternalFailure } from "../../../framework/common/converter-failures";
import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils";
import { DryScore } from "../../../framework/common/types";
import { ConverterFunction } from "../../common/types";
import { BeatorajaContext, BeatorajaScore } from "./types";
import { ChartDocument } from "kamaitachi-common";
const LAMP_LOOKUP = {
NoPlay: "NO PLAY",
Failed: "FAILED",
LightAssistEasy: "ASSIST CLEAR",
Easy: "EASY CLEAR",
Normal: "CLEAR",
Hard: "HARD CLEAR",
ExHard: "EX HARD CLEAR",
FullCombo: "FULL COMBO",
Perfect: "FULL COMBO",
} as const;
const RANDOM_LOOKUP = {
0: "NONRAN",
1: "MIRROR",
2: "RANDOM",
3: "R-RANDOM",
4: "S-RANDOM",
} as const;
export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaContext> = async (
data,
context,
importType,
logger
) => {
const chart = (await FindChartOnSHA256("bms", data.sha256)) as ChartDocument<
"bms:7K" | "bms:14K"
>;
if (!chart) {
// @todo #141 Import charts into the Kamaitachi Database if the chart
// doesn't exist.
throw new Error("unimplemented");
}
const song = await FindSongOnID("bms", chart.songID);
if (!song) {
logger.severe(`Song-Chart Desync with BMS ${chart.chartID}.`);
throw new InternalFailure(`Song-Chart Desync with BMS ${chart.chartID}.`);
}
// if CN mode
if (data.lntype === 1) {
// Divide the EX score by the CN note count, then multiply it by the LN
// notecount. This converts CN EXScores to LN EXScores by routing
// through percent.
data.exscore = Math.floor((data.exscore / context.chart.notes) * chart.data.notecount);
}
const { grade, percent } = GenericGetGradeAndPercent("bms", data.exscore, chart);
const hitMeta: DryScore<"bms:7K" | "bms:14K">["scoreData"]["hitMeta"] = {
bp: data.minbp === -1 ? null : data.minbp,
gauge: data.gauge === -1 ? null : data.gauge,
};
let hitData: DryScore<"bms:7K" | "bms:14K">["scoreData"]["hitData"] = {};
// if NOT CN mode we can assign judgements properly
if (data.lntype === 0) {
for (const k of [
"ebd",
"lbd",
"egd",
"lgd",
"egr",
"lgr",
"epg",
"lpg",
"epr",
"lpr",
] as const) {
hitMeta[k] = data[k];
}
hitMeta.epr! += data.ems;
hitMeta.lpr! += data.lms;
hitData = {
pgreat: data.epg + data.lpg,
great: data.egr + data.lgr,
good: data.egd + data.lgd,
bad: data.ebd + data.lbd,
poor: data.epr + data.lpr + data.ems + data.lms,
};
}
const lamp = LAMP_LOOKUP[data.clear];
const dryScore: DryScore<"bms:7K" | "bms:14K"> = {
comment: null,
game: "bms",
importType,
scoreData: {
grade,
percent,
score: data.exscore,
lamp,
hitMeta,
hitData,
},
scoreMeta: {
client: context.client,
inputDevice: data.deviceType,
random: RANDOM_LOOKUP[data.option] ?? null,
},
timeAchieved: Date.now(),
service: "Beatoraja IR",
};
return { song, chart, dryScore };
};
@@ -0,0 +1,114 @@
import { KtLogger } from "../../../../logger/logger";
import p from "prudence";
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
import { FormatPrError } from "../../../../../utils/prudence";
import { BeatorajaChart, BeatorajaContext, BeatorajaScore } from "./types";
import { ConverterIRBeatoraja } from "./converter";
import { ParserFunctionReturnsSync } from "../../common/types";
const PR_BeatorajaScore = {
sha256: "string",
exscore: p.isPositiveInteger,
passnotes: p.isPositiveInteger,
gauge: p.or(p.isBetween(0, 100), p.is(-1)),
deviceType: p.isIn("BM_CONTROLLER", "KEYBOARD"),
minbp: p.or(p.isPositiveInteger, p.is(-1)),
option: p.isBoundedInteger(0, 4),
lntype: p.isIn(0, 1),
clear: p.isIn(
"NoPlay",
"Failed",
"LightAssistEasy",
"Easy",
"Normal",
"Hard",
"ExHard",
"FullCombo",
"Perfect"
),
assist: p.is(0),
maxcombo: p.isPositiveInteger,
epg: p.isPositiveInteger,
egr: p.isPositiveInteger,
egd: p.isPositiveInteger,
ebd: p.isPositiveInteger,
epr: p.isPositiveInteger,
lpg: p.isPositiveInteger,
lgr: p.isPositiveInteger,
lgd: p.isPositiveInteger,
lbd: p.isPositiveInteger,
lpr: p.isPositiveInteger,
ems: p.isPositiveInteger,
lms: p.isPositiveInteger,
};
const PR_BeatorajaChart = {
md5: "string",
sha256: "string",
title: "string",
subtitle: "string",
genre: "string",
artist: "string",
subartist: "string",
total: p.isPositive,
// currently only accepted playtypes.
mode: p.isIn("BEAT_7K", "BEAT_14K"),
judge: p.isPositive,
notes: p.isPositiveInteger,
hasUndefinedLN: p.is(false),
hasRandom: p.is(false),
};
export function ParseBeatorajaSingle(
body: Record<string, unknown>,
logger: KtLogger
): ParserFunctionReturnsSync<BeatorajaScore, BeatorajaContext> {
const err = p(
body.score,
PR_BeatorajaScore,
{},
{ allowExcessKeys: true, throwOnNonObject: false }
);
if (err) {
throw new ScoreImportFatalError(
400,
FormatPrError(err, "Invalid Beatoraja Import - Score")
);
}
const chartErr = p(
body.chart,
PR_BeatorajaChart,
{},
{ allowExcessKeys: true, throwOnNonObject: false }
);
if (chartErr) {
throw new ScoreImportFatalError(
400,
FormatPrError(chartErr, "Invalid Beatoraja Score - Chart")
);
}
const client = body.client;
if (client !== "beatoraja" && client !== "lr2oraja") {
throw new ScoreImportFatalError(400, `Unknown client ${client}`);
}
// asserted using prudence.
return {
context: {
client,
chart: body.chart as BeatorajaChart,
},
game: "bms",
iterable: ([body] as unknown) as BeatorajaScore[],
ConverterFunction: ConverterIRBeatoraja,
classHandler: null,
};
}
@@ -0,0 +1,58 @@
import { integer } from "kamaitachi-common";
export interface BeatorajaContext {
client: "beatoraja" | "lr2oraja";
chart: BeatorajaChart;
}
export interface BeatorajaChart {
md5: string;
sha256: string;
title: string;
subtitle: string;
genre: string;
artist: string;
subartist: string;
total: integer;
mode: "BEAT_7K" | "BEAT_14K";
judge: number;
notes: integer;
}
export interface BeatorajaScore {
sha256: string;
exscore: integer;
passnotes: integer;
gauge: number;
deviceType: "BM_CONTROLLER" | "KEYBOARD";
minbp: integer;
option: 0 | 1 | 2 | 3 | 4;
lntype: 0 | 1;
clear:
| "NoPlay"
| "Failed"
| "LightAssistEasy"
| "Easy"
| "Normal"
| "Hard"
| "ExHard"
| "FullCombo"
| "Perfect";
assist: 0;
maxcombo: integer;
epg: integer;
egr: integer;
egd: integer;
ebd: integer;
epr: integer;
lpg: integer;
lgr: integer;
lgd: integer;
lbd: integer;
lpr: integer;
ems: integer;
lms: integer;
}
@@ -34,7 +34,7 @@ type BeatorajaPartialScoreFormat = {
maxcombo: integer | null;
};
export type BeatorajaScoreFormat = BeatorajaPartialScoreFormat & BeatorajaScoreJudgements;
export type BeatorajaIRScoreFormat = BeatorajaPartialScoreFormat & BeatorajaScoreJudgements;
const logger = CreateLogCtx(__filename);
@@ -1,7 +1,14 @@
import { Router } from "express";
import db from "../../../../external/mongo/db";
import { SYMBOL_KtchiData } from "../../../../lib/constants/ktchi";
import { KtLogger } from "../../../../lib/logger/logger";
import { ExpressWrappedScoreImportMain } from "../../../../lib/score-import/framework/express-wrapper";
import { ParseBeatorajaSingle } from "../../../../lib/score-import/import-types/ir/beatoraja/parser";
import { Random20Hex } from "../../../../utils/misc";
import { PRIVATEINFO_GetUserCaseInsensitive } from "../../../../utils/user";
import {
GetUserWithIDGuaranteed,
PRIVATEINFO_GetUserCaseInsensitive,
} from "../../../../utils/user";
import prValidate from "../../../middleware/prudence-validate";
import { PasswordCompare } from "../../api/v1/auth/auth";
import { ValidateAuthToken, ValidateIRClientVersion } from "./auth";
@@ -66,7 +73,18 @@ router.use(ValidateAuthToken);
* store it as a new chart alongside the new score.
* @name POST /ir/beatoraja/submit-score
*/
router.post("/submit-score", async (req, res) => {});
router.post("/submit-score", async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_KtchiData]!.beatorajaAuthDoc!.userID);
const ParserFunction = (logger: KtLogger) => ParseBeatorajaSingle(req.body, logger);
const importRes = await ExpressWrappedScoreImportMain(
userDoc,
false,
"ir/beatoraja",
ParserFunction
);
});
/**
* Submits a course result to Kamaitachi. This only accepts a limited set of
+1 -1
View File
@@ -167,7 +167,7 @@ export async function CreatePOSTScoresResponseBody(
const usersRanking = scorePB.rankingData.rank;
// This returns N scores immediately ranked higher
// This returns immediately ranked higher
// than the current user.
const adjAbove = (await db["score-pbs"].find(
@@ -1,5 +1,5 @@
{
"song": {
"chart": {
"md5": "f8dcdfe070630bbb365323c662561a1a",
"sha256": "a6ed343ac9db07af0dd6eb8af3c80d79032aa777fd7ff2c28feb885717f47b89",
"title": "星の器〜STAR OF ANDROMEDA(ANOTHER)",
@@ -55,5 +55,6 @@
"judgeAlgorithm": "Combo",
"rule": "LR2",
"exscore": 1004
}
}
},
"client": "beatoraja"
}