mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-11 00:38:28 +03:00
Support PMS score importing.
This commit is contained in:
Vendored
+1
@@ -49,6 +49,7 @@
|
||||
"wacca",
|
||||
"popn",
|
||||
"jubeat",
|
||||
"pms",
|
||||
],
|
||||
IMPORT_TYPES: [
|
||||
"file/eamusement-iidx-csv",
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
Generated
+4
-4
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
+1
-1
@@ -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,
|
||||
+14
-4
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user