Support PMS score importing.

This commit is contained in:
zkldi
2022-01-19 17:55:24 +00:00
parent d5f03b0d60
commit ae5d5bc9f5
15 changed files with 160 additions and 60 deletions
+1
View File
@@ -49,6 +49,7 @@
"wacca",
"popn",
"jubeat",
"pms",
],
IMPORT_TYPES: [
"file/eamusement-iidx-csv",
+1 -1
View File
@@ -82,7 +82,7 @@
"rimraf": "3.0.2",
"safe-json-stringify": "1.2.0",
"seq-logging": "^1.1.1",
"tachi-common": "0.5.1",
"tachi-common": "0.5.4",
"ts-node": "10.0.0",
"tsconfig-paths": "3.10.1",
"typescript": "4.3.4",
+4 -4
View File
@@ -63,7 +63,7 @@ specifiers:
safe-json-stringify: 1.2.0
seq-logging: ^1.1.1
supertest: 6.1.3
tachi-common: 0.5.1
tachi-common: 0.5.4
tap: 15.0.9
ts-node: 10.0.0
tsconfig-paths: 3.10.1
@@ -107,7 +107,7 @@ dependencies:
rimraf: 3.0.2
safe-json-stringify: 1.2.0
seq-logging: 1.1.1
tachi-common: 0.5.1
tachi-common: 0.5.4
ts-node: 10.0.0_83f53b0a0c5616d3fa00ed4e30b9ce1b
tsconfig-paths: 3.10.1
typescript: 4.3.4
@@ -5386,8 +5386,8 @@ packages:
strip-ansi: 6.0.0
dev: true
/tachi-common/0.5.1:
resolution: {integrity: sha512-jAjBDYPaBDvn2YgAs1H2Q+WaeULS3AWgcdF2yIodcQYTcnjsWE0Kc1I7o55G3UjP82sGAFC35OijdPbPooo5Wg==}
/tachi-common/0.5.4:
resolution: {integrity: sha512-6QDpdvyGB2JOzPcW+7Hm/jXCKXXrIB6KgTKmvVq86771Iyali/oA2iigSb+oeVUQb8zNX2IM/s9hRD9F+80xTw==}
dependencies:
monk: 7.3.4
transitivePeerDependencies:
@@ -3,10 +3,10 @@ import { KtLogger } from "lib/logger/logger";
import { HandleOrphanQueue } from "lib/orphan-queue/orphan-queue";
import { ReprocessOrphan } from "lib/score-import/framework/orphans/orphans";
import { ServerConfig, TachiConfig } from "lib/setup/config";
import { ChartDocument, SongDocument } from "tachi-common";
import { ChartDocument, SongDocument, Playtypes } from "tachi-common";
import { Random20Hex } from "utils/misc";
import { GetBlacklist } from "utils/queries/blacklist";
import { FindChartOnSHA256 } from "utils/queries/charts";
import { FindChartOnSHA256, FindChartOnSHA256Playtype } from "utils/queries/charts";
import { FindSongOnID } from "utils/queries/songs";
import {
InternalFailure,
@@ -17,7 +17,6 @@ import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils
import { DryScore } from "../../../framework/common/types";
import { ConverterFunction } from "../../common/types";
import { BeatorajaChart, BeatorajaContext, BeatorajaScore } from "./types";
const LAMP_LOOKUP = {
NoPlay: "NO PLAY",
Failed: "FAILED",
@@ -39,6 +38,7 @@ const RANDOM_LOOKUP = {
} as const;
async function HandleOrphanChartProcess(
game: "bms" | "pms",
data: BeatorajaScore,
context: BeatorajaContext,
logger: KtLogger
@@ -53,22 +53,54 @@ async function HandleOrphanChartProcess(
throw new InvalidScoreFailure(`${TachiConfig.NAME} will not support #RANDOM charts.`);
}
const idString = context.chart.mode === "BEAT_7K" ? "bms:7K" : "bms:14K";
let chart;
let criteria;
const { chartDoc, songDoc } = ConvertBeatorajaChartToTachi(context.chart);
const chart = await HandleOrphanQueue(
idString,
"bms",
chartDoc,
songDoc,
{
if (game === "bms") {
criteria = {
"chartDoc.data.hashSHA256": context.chart.sha256,
},
ServerConfig.BEATORAJA_QUEUE_SIZE,
context.userID,
chartName
);
};
const idString = context.chart.mode === "BEAT_7K" ? "bms:7K" : "bms:14K";
const { chartDoc, songDoc } = ConvertBeatorajaChartToTachi(
context.chart,
context.chart.mode === "BEAT_7K" ? "7K" : "14K"
);
chart = await HandleOrphanQueue(
idString,
"bms",
chartDoc,
songDoc,
criteria,
ServerConfig.BEATORAJA_QUEUE_SIZE,
context.userID,
chartName
);
} else {
const playtype = data.deviceType === "BM_CONTROLLER" ? "Controller" : "Keyboard";
criteria = {
"chartDoc.data.hashSHA256": context.chart.sha256,
playtype,
};
const idString = playtype === "Controller" ? "pms:Controller" : "pms:Keyboard";
const { chartDoc, songDoc } = ConvertBeatorajaChartToTachi(context.chart, playtype);
chart = await HandleOrphanQueue(
idString,
"pms",
chartDoc,
songDoc,
criteria,
ServerConfig.BEATORAJA_QUEUE_SIZE,
context.userID,
chartName
);
}
// If chart wasn't unorphaned as a result of this request
// orphan this score and return ktdnf
@@ -82,15 +114,16 @@ async function HandleOrphanChartProcess(
}
const blacklist = await GetBlacklist();
const scoresToDeorphan = await db["orphan-scores"].find({
"data.sha256": chartDoc.data.hashSHA256,
});
const scoresToDeorphan = await db["orphan-scores"].find(criteria);
await Promise.all(scoresToDeorphan.map((e) => ReprocessOrphan(e, blacklist, logger)));
return chart;
}
// NOTE: This converter handles both PMS and BMS scores. The two are very similar,
// infact, beatoraja barely does anything different between the two. PMS is essentially
// BMS but with the columns set to 9.
export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaContext> = async (
data,
context,
@@ -109,24 +142,39 @@ export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaCo
);
}
let chart = (await FindChartOnSHA256("bms", data.sha256)) as ChartDocument<
"bms:7K" | "bms:14K"
> | null;
const game = context.chart.mode === "POPN_9K" ? "pms" : "bms";
let chart;
if (game === "bms") {
chart = (await FindChartOnSHA256(game, data.sha256)) as ChartDocument<
"bms:7K" | "bms:14K"
> | null;
} else {
// It's still called BM_CONTROLLER even though its popn!
const playtype = data.deviceType === "BM_CONTROLLER" ? "Controller" : "Keyboard";
chart = (await FindChartOnSHA256Playtype(game, data.sha256, playtype)) as ChartDocument<
"pms:Controller" | "pms:Keyboard"
> | null;
}
if (!chart) {
chart = await HandleOrphanChartProcess(data, context, logger);
chart = await HandleOrphanChartProcess(game, data, context, logger);
}
const song = await FindSongOnID("bms", chart.songID);
const song = await FindSongOnID(game, chart.songID);
if (!song) {
logger.severe(`Song-Chart Desync with BMS ${chart.chartID}.`);
throw new InternalFailure(`Song-Chart Desync with BMS ${chart.chartID}.`);
logger.severe(`Song-Chart Desync with ${game} ${chart.chartID}.`);
throw new InternalFailure(`Song-Chart Desync with ${game} ${chart.chartID}.`);
}
const { grade, percent } = GenericGetGradeAndPercent("bms", data.exscore, chart);
const { grade, percent } = GenericGetGradeAndPercent(game, data.exscore, chart);
const hitMeta: DryScore<"bms:7K" | "bms:14K">["scoreData"]["hitMeta"] = {
const hitMeta: DryScore<
"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard"
>["scoreData"]["hitMeta"] = {
bp: data.minbp === -1 ? null : data.minbp,
gauge: data.gauge === -1 ? null : data.gauge,
};
@@ -159,15 +207,17 @@ export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaCo
let random = null;
if (chart.playtype === "7K") {
// pms and bms are fine using this randomlookup, except for 14k, which is
// broken in beatoraja.
if (chart.playtype !== "14K") {
random = RANDOM_LOOKUP[data.option];
}
const lamp = LAMP_LOOKUP[data.clear];
const dryScore: DryScore<"bms:7K" | "bms:14K"> = {
const dryScore: DryScore<"bms:7K" | "bms:14K" | "pms:Keyboard" | "pms:Controller"> = {
comment: null,
game: "bms",
game,
importType,
scoreData: {
grade,
@@ -189,14 +239,14 @@ export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaCo
return { song, chart, dryScore };
};
function ConvertBeatorajaChartToTachi(chart: BeatorajaChart) {
const chartDoc: ChartDocument<"bms:14K" | "bms:7K"> = {
function ConvertBeatorajaChartToTachi(chart: BeatorajaChart, playtype: Playtypes["bms" | "pms"]) {
const chartDoc: ChartDocument<"bms:14K" | "bms:7K" | "pms:Controller" | "pms:Keyboard"> = {
chartID: Random20Hex(),
difficulty: "CHART",
isPrimary: true,
level: "?",
levelNum: 0,
playtype: chart.mode === "BEAT_7K" ? "7K" : "14K",
playtype,
rgcID: null,
songID: 0,
versions: [],
@@ -209,7 +259,7 @@ function ConvertBeatorajaChartToTachi(chart: BeatorajaChart) {
},
};
const songDoc: SongDocument<"bms"> = {
const songDoc: SongDocument<"bms" | "pms"> = {
artist: chart.artist,
title: chart.title,
id: 0,
@@ -56,14 +56,15 @@ const PR_BeatorajaChart = {
lntype: p.isIn(-1, 0, 1, 2),
// currently only accepted playtypes.
mode: p.isIn("BEAT_7K", "BEAT_14K"),
mode: p.isIn("BEAT_7K", "BEAT_14K", "POPN_9K"),
judge: p.isPositive,
notes: p.isPositiveInteger,
hasUndefinedLN: "boolean",
hasRandom: "boolean",
};
const SUPPORTED_BEATORAJA_CLIENTS = [
const SUPPORTED_BMS_CLIENTS = [
"LR2oraja 0.8.4",
"LR2oraja 0.8.3",
"LR2oraja 0.8.2",
"LR2oraja 0.8.1",
@@ -74,6 +75,14 @@ const SUPPORTED_BEATORAJA_CLIENTS = [
"LR2oraja(rekidai.info) 0.8.0",
];
const SUPPORTED_PMS_CLIENTS = [
"Beatoraja 0.8.4",
"Beatoraja 0.8.3",
"Beatoraja 0.8.2",
"Beatoraja 0.8.1",
"Beatoraja 0.8.0",
];
export function ParseBeatorajaSingle(
body: Record<string, unknown>,
userID: integer,
@@ -110,20 +119,31 @@ export function ParseBeatorajaSingle(
// Force stringify this, since it's not validated by prudence.
const client = `${body.client}`;
if (!SUPPORTED_BEATORAJA_CLIENTS.includes(client)) {
throw new ScoreImportFatalError(400, `Unsupported client ${client}`);
const score = body.score as BeatorajaScore;
const chart = body.chart as BeatorajaChart;
if (chart.mode === "BEAT_14K" || chart.mode === "BEAT_7K") {
if (!SUPPORTED_BMS_CLIENTS.includes(client)) {
throw new ScoreImportFatalError(400, `Unsupported BMS client ${client}`);
}
} else if (chart.mode === "POPN_9K") {
if (!SUPPORTED_PMS_CLIENTS.includes(client)) {
throw new ScoreImportFatalError(400, `Unsupported PMS client ${client}`);
}
}
const isPMS = chart.mode === "POPN_9K";
return {
context: {
client: "lr2oraja",
client: isPMS ? "beatoraja" : "lr2oraja",
// asserted using prudence.
chart: body.chart as BeatorajaChart,
chart,
userID,
timeReceived: Date.now(),
},
game: "bms",
iterable: [body.score] as unknown as BeatorajaScore[],
game: isPMS ? "pms" : "bms",
iterable: [score],
classHandler: null,
};
}
@@ -1,7 +1,7 @@
import { integer } from "tachi-common";
export interface BeatorajaContext {
client: "lr2oraja";
client: "lr2oraja" | "beatoraja";
chart: BeatorajaChart;
userID: integer; // unexpectedly necessary for orphan code!
timeReceived: number;
@@ -17,7 +17,7 @@ export interface BeatorajaChart {
subartist: string;
total: integer;
mode: "BEAT_7K" | "BEAT_14K";
mode: "BEAT_7K" | "BEAT_14K" | "POPN_9K";
judge: number;
notes: integer;
hasRandom: boolean;
+8
View File
@@ -262,6 +262,14 @@ if (!["dev", "production", "staging", "test"].includes(nodeEnv)) {
process.exit(1);
}
// if (bms XOR popn) is enabled
if (TachiConfig.GAMES.includes("bms") !== TachiConfig.GAMES.includes("pms")) {
logger.error(
`BMS and PMS MUST be enabled at the same time, due to how the beatoraja IR works.`
);
process.exit(1);
}
const replicaIdentity = process.env.REPLICA_IDENTITY;
export const Environment = {
@@ -45,7 +45,7 @@ export type BeatorajaIRScoreFormat = BeatorajaPartialScoreFormat & BeatorajaScor
* @returns A Beatoraja Score Document.
*/
export function TachiScoreDataToBeatorajaFormat(
pbScore: PBScoreDocument<"bms:7K" | "bms:14K">,
pbScore: PBScoreDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard">,
sha256: string,
username: string,
notecount: integer,
@@ -8,10 +8,20 @@ import { TachiScoreDataToBeatorajaFormat } from "./convert-scores";
const router: Router = Router({ mergeParams: true });
const GetChartDocument: RequestHandler = async (req, res, next) => {
const chart = (await db.charts.bms.findOne({
"data.hashSHA256": req.params.chartSHA256,
})) as ChartDocument<"bms:7K" | "bms:14K"> | null;
let chart: ChartDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard"> | null =
(await db.charts.bms.findOne({
"data.hashSHA256": req.params.chartSHA256,
})) as ChartDocument<"bms:7K" | "bms:14K"> | null;
// if we dont find the chart in bms,
// it's probably a pms chart.
if (!chart) {
chart = (await db.charts.pms.findOne({
"data.hashSHA256": req.params.chartSHA256,
})) as ChartDocument<"pms:Controller" | "pms:Keyboard"> | null;
}
// if we still haven't found it, we've got nothin.
if (!chart) {
return res.status(404).json({
success: false,
@@ -36,7 +46,7 @@ router.get("/scores", async (req, res) => {
const scores = (await db["personal-bests"].find({
chartID: chart.chartID,
})) as PBScoreDocument<"bms:7K" | "bms:14K">[];
})) as PBScoreDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard">[];
const userDocs = await db.users.find(
{
@@ -69,7 +69,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
t.equal(res.status, 400);
t.equal(res.body.success, false);
t.match(res.body.description, /Unsupported client/u);
t.match(res.body.description, /Unsupported BMS client/u);
t.end();
});
@@ -7,7 +7,7 @@ import { ServerConfig } from "lib/setup/config";
import { RequireNotGuest } from "server/middleware/auth";
import { UpdateClassIfGreater } from "utils/class";
import { ValidateIRClientVersion } from "./auth";
import chartsRouter from "./charts/router";
import chartsRouter from "./charts/_chartSHA256/router";
const logger = CreateLogCtx(__filename);
+12 -1
View File
@@ -234,7 +234,7 @@ export function FindSDVXChartOnDFVersion(
}
export function FindChartOnSHA256(game: Game, hash: string) {
if (game !== "bms" && game !== "usc" && game !== "iidx") {
if (game !== "bms" && game !== "usc" && game !== "iidx" && game !== "pms") {
throw new Error(`Cannot call FindChartOnSHA256 for game ${game}.`);
}
@@ -243,6 +243,17 @@ export function FindChartOnSHA256(game: Game, hash: string) {
});
}
export function FindChartOnSHA256Playtype(game: Game, hash: string, playtype: Playtypes[Game]) {
if (game !== "bms" && game !== "usc" && game !== "iidx" && game !== "pms") {
throw new Error(`Cannot call FindChartOnSHA256 for game ${game}.`);
}
return db.charts[game].findOne({
"data.hashSHA256": hash,
playtype,
});
}
export function FindChartOnARCID(game: "iidx" | "ddr" | "jubeat" | "sdvx", arcID: string) {
return db.charts[game].findOne({
"data.arcChartID": arcID,
+1 -1
View File
@@ -49,7 +49,7 @@ export type EmptyObject = Record<string, never>;
export interface TachiRequestData {
uscChartDoc?: ChartDocument<"usc:Controller" | "usc:Keyboard">;
beatorajaChartDoc?: ChartDocument<"bms:7K" | "bms:14K">;
beatorajaChartDoc?: ChartDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard">;
requestedUser?: PublicUserDocument;
requestedUserGameStats?: UserGameStats;