diff --git a/server/.github/test.conf.json5 b/server/.github/test.conf.json5 index d195bb78c..1c5471e15 100644 --- a/server/.github/test.conf.json5 +++ b/server/.github/test.conf.json5 @@ -49,6 +49,7 @@ "wacca", "popn", "jubeat", + "pms", ], IMPORT_TYPES: [ "file/eamusement-iidx-csv", diff --git a/server/package.json b/server/package.json index 94e61beaa..f1a3e0386 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 753d2025b..a9a1c0ce4 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -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: diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts index a3ccd606d..85ea59e0d 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts @@ -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 = async ( data, context, @@ -109,24 +142,39 @@ export const ConverterIRBeatoraja: ConverterFunction | 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 = { + 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 = { +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, diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts index 7ca9671c5..0a77352ce 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts @@ -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, 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, }; } diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/types.ts b/server/src/lib/score-import/import-types/ir/beatoraja/types.ts index 3d2dbc7dd..4a20948a5 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/types.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/types.ts @@ -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; diff --git a/server/src/lib/setup/config.ts b/server/src/lib/setup/config.ts index d1240d2b6..2a88d8904 100644 --- a/server/src/lib/setup/config.ts +++ b/server/src/lib/setup/config.ts @@ -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 = { diff --git a/server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts similarity index 100% rename from server/src/server/router/ir/beatoraja/charts/convert-scores.test.ts rename to server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts diff --git a/server/src/server/router/ir/beatoraja/charts/convert-scores.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts similarity index 97% rename from server/src/server/router/ir/beatoraja/charts/convert-scores.ts rename to server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts index 710fe6c46..1a09dcda3 100644 --- a/server/src/server/router/ir/beatoraja/charts/convert-scores.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts @@ -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, diff --git a/server/src/server/router/ir/beatoraja/charts/router.test.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts similarity index 100% rename from server/src/server/router/ir/beatoraja/charts/router.test.ts rename to server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts diff --git a/server/src/server/router/ir/beatoraja/charts/router.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.ts similarity index 74% rename from server/src/server/router/ir/beatoraja/charts/router.ts rename to server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.ts index 590569ed9..391138cf5 100644 --- a/server/src/server/router/ir/beatoraja/charts/router.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.ts @@ -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( { diff --git a/server/src/server/router/ir/beatoraja/router.test.ts b/server/src/server/router/ir/beatoraja/router.test.ts index b0a177887..c10ae5f46 100644 --- a/server/src/server/router/ir/beatoraja/router.test.ts +++ b/server/src/server/router/ir/beatoraja/router.test.ts @@ -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(); }); diff --git a/server/src/server/router/ir/beatoraja/router.ts b/server/src/server/router/ir/beatoraja/router.ts index 326ed42be..817356b7a 100644 --- a/server/src/server/router/ir/beatoraja/router.ts +++ b/server/src/server/router/ir/beatoraja/router.ts @@ -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); diff --git a/server/src/utils/queries/charts.ts b/server/src/utils/queries/charts.ts index 615f9e52b..412ac396f 100644 --- a/server/src/utils/queries/charts.ts +++ b/server/src/utils/queries/charts.ts @@ -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, diff --git a/server/src/utils/types.ts b/server/src/utils/types.ts index f168d1b02..094d6407d 100644 --- a/server/src/utils/types.ts +++ b/server/src/utils/types.ts @@ -49,7 +49,7 @@ export type EmptyObject = Record; 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;