From eaad1d1cf2755591696c6fce8a7bbce4ca97fa25 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Sat, 13 Nov 2021 08:51:34 +0000 Subject: [PATCH] Completely refactor USC support to use Controller/Keyboard playtypes. --- server/src/external/mongo/schemas.ts | 16 ++-- .../calculated-data/calculated-data.test.ts | 43 ++++++++- .../calculated-data/calculated-data.ts | 10 +- .../framework/sessions/calculated-data.ts | 10 +- .../framework/user-game-stats/rating.test.ts | 18 +++- .../framework/user-game-stats/rating.ts | 5 +- .../score-import/import-types/common/types.ts | 2 +- .../import-types/ir/usc/converter.test.ts | 5 +- .../import-types/ir/usc/converter.ts | 36 +++---- .../import-types/ir/usc/parser.test.ts | 3 +- .../import-types/ir/usc/parser.ts | 5 +- .../score-import/import-types/ir/usc/types.ts | 3 + .../lib/score-import/import-types/parsers.ts | 1 - .../router/ir/usc/_playtype/router.test.ts | 95 ++++++++++++------- .../server/router/ir/usc/_playtype/router.ts | 38 ++++++-- .../router/ir/usc/_playtype/usc.test.ts | 10 +- .../src/server/router/ir/usc/_playtype/usc.ts | 45 +++++---- server/src/server/router/ir/usc/router.ts | 11 --- server/src/test-utils/mock-db/charts-usc.json | 2 +- server/src/test-utils/test-data.ts | 6 +- server/src/utils/queries/pbs.ts | 4 +- server/src/utils/types.ts | 2 +- 22 files changed, 243 insertions(+), 127 deletions(-) diff --git a/server/src/external/mongo/schemas.ts b/server/src/external/mongo/schemas.ts index 031618551..1569d0f14 100644 --- a/server/src/external/mongo/schemas.ts +++ b/server/src/external/mongo/schemas.ts @@ -831,12 +831,16 @@ export const DatabaseSchemas: Record = { arcChartID: "?string", }) ), - "charts-usc": prSchemaify( - PR_ChartDocument("usc", "Single", { - hashSHA1: p.or("string", ["string"]), - isOfficial: "boolean", - }) - ), + "charts-usc": (self) => { + const playtype = getPlaytype("usc", self); + + return prSchemaify( + PR_ChartDocument("usc", playtype, { + hashSHA1: p.or("string", ["string"]), + isOfficial: "boolean", + }) + )(self); + }, goals: prSchemaify({ game: p.isIn(games), playtype: isValidPlaytype, diff --git a/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts b/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts index 9cdfb4ca0..63ab1c96f 100644 --- a/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts +++ b/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts @@ -43,14 +43,27 @@ t.test("#CreateCalculatedData", async (t) => { ); const uscRes = await CreateCalculatedData( - { game: "usc", playtype: "Single" } as ScoreDocument, - { data: { isOfficial: false }, playtype: "Single" } as ChartDocument, + { game: "usc", playtype: "Controller" } as ScoreDocument, + { data: { isOfficial: false }, playtype: "Controller" } as ChartDocument, null, logger ); t.strictSame(uscRes, { VF6: null }, "Should return null if chart was not an official."); + const uscKbRes = await CreateCalculatedData( + { game: "usc", playtype: "Keyboard" } as ScoreDocument, + { data: { isOfficial: false }, playtype: "Keyboard" } as ChartDocument, + null, + logger + ); + + t.strictSame( + uscKbRes, + { VF6: null }, + "Should return null if chart was not an official (Keyboard)." + ); + t.end(); }); @@ -318,10 +331,10 @@ t.test("#CalculateDataForGamePT", (t) => { t.end(); }); - t.test("usc:Single", async (t) => { + t.test("usc:Controller", async (t) => { const res = await CalculateDataForGamePT( "usc", - "Single", + "Controller", Testing511SPA, TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing null, @@ -333,12 +346,32 @@ t.test("#CalculateDataForGamePT", (t) => { { VF6: "?number", }, - "Response should contain nulled keys for usc:Single" + "Response should contain nulled keys for usc:Controller" ); t.end(); }); + t.test("usc:Keyboard", async (t) => { + const res = await CalculateDataForGamePT( + "usc", + "Keyboard", + Testing511SPA, + TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + null, + logger + ); + + prAssert( + res, + { + VF6: "?number", + }, + "Response should contain nulled keys for usc:Keyboard" + ); + + t.end(); + }); // t.test("jubeat:Single", async (t) => { // const res = await CalculateDataForGamePT( // "jubeat", diff --git a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts index 72c66c146..6e1e24072 100644 --- a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts +++ b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts @@ -88,7 +88,8 @@ const CalculatedDataFunctions: CalculatedDataFunctions = { // Single: CalculateDataJubeat, // }, usc: { - Single: CalculateDataSDVXorUSC, + Controller: CalculateDataSDVXorUSC, + Keyboard: CalculateDataSDVXorUSC, }, }; @@ -187,9 +188,12 @@ function CalculateDataSDVXorUSC( dryScore: DryScore, chart: ChartDocument, logger: KtLogger -): CalculatedData<"sdvx:Single" | "usc:Single"> { +): CalculatedData<"sdvx:Single" | "usc:Keyboard" | "usc:Controller"> { // for usc, unofficial charts currently have no VF6 value. - if (dryScore.game === "usc" && !(chart as ChartDocument<"usc:Single">).data.isOfficial) { + if ( + dryScore.game === "usc" && + !(chart as ChartDocument<"usc:Controller" | "usc:Keyboard">).data.isOfficial + ) { return { VF6: null }; } diff --git a/server/src/lib/score-import/framework/sessions/calculated-data.ts b/server/src/lib/score-import/framework/sessions/calculated-data.ts index 77608acd7..0868984a3 100644 --- a/server/src/lib/score-import/framework/sessions/calculated-data.ts +++ b/server/src/lib/score-import/framework/sessions/calculated-data.ts @@ -112,7 +112,15 @@ const CalculatedDataFunctions: CalculatedDataFunctions = { // }), // }, usc: { - Single: (scd) => { + Controller: (scd) => { + const VF6 = AvgBest10Map(scd, "VF6"); + + return { + VF6, + ProfileVF6: VF6 === null ? null : VF6 * 50, + }; + }, + Keyboard: (scd) => { const VF6 = AvgBest10Map(scd, "VF6"); return { diff --git a/server/src/lib/score-import/framework/user-game-stats/rating.test.ts b/server/src/lib/score-import/framework/user-game-stats/rating.test.ts index 14076517c..d1ab7b7d3 100644 --- a/server/src/lib/score-import/framework/user-game-stats/rating.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/rating.test.ts @@ -28,18 +28,26 @@ t.test("#CalculateRatings", (t) => { t.end(); }); - t.test("Should return VF4 and VF5 for SDVX", async (t) => { + t.test("Should return VF6 for SDVX", async (t) => { const res = await CalculateRatings("sdvx", "Single", 1, logger); - t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys."); + t.strictSame(res, { VF6: 0 }, "Should return VF6 keys."); t.end(); }); - t.test("Should return VF4 and VF5 for USC", async (t) => { - const res = await CalculateRatings("usc", "Single", 1, logger); + t.test("Should return VF6 for USC:Keyboard", async (t) => { + const res = await CalculateRatings("usc", "Keyboard", 1, logger); - t.strictSame(res, { VF6: 0 }, "Should return VF4 and VF5 keys."); + t.strictSame(res, { VF6: 0 }, "Should return VF6 keys."); + + t.end(); + }); + + t.test("Should return VF6 for USC:Controller", async (t) => { + const res = await CalculateRatings("usc", "Controller", 1, logger); + + t.strictSame(res, { VF6: 0 }, "Should return VF6 keys."); t.end(); }); diff --git a/server/src/lib/score-import/framework/user-game-stats/rating.ts b/server/src/lib/score-import/framework/user-game-stats/rating.ts index 362b9a3eb..69bf6963f 100644 --- a/server/src/lib/score-import/framework/user-game-stats/rating.ts +++ b/server/src/lib/score-import/framework/user-game-stats/rating.ts @@ -87,7 +87,10 @@ const RatingFunctions: RatingFunctions = { }), }, usc: { - Single: async (g, p, u) => ({ + Keyboard: async (g, p, u) => ({ + VF6: await LazySumN("VF6", 50)(g, p, u), + }), + Controller: async (g, p, u) => ({ VF6: await LazySumN("VF6", 50)(g, p, u), }), }, diff --git a/server/src/lib/score-import/import-types/common/types.ts b/server/src/lib/score-import/import-types/common/types.ts index 6cf8b4d87..dcb178c7b 100644 --- a/server/src/lib/score-import/import-types/common/types.ts +++ b/server/src/lib/score-import/import-types/common/types.ts @@ -20,10 +20,10 @@ import { import { ConverterFailure } from "../../framework/common/converter-failures"; import { DryScore } from "../../framework/common/types"; import { BeatorajaContext, BeatorajaScore } from "../ir/beatoraja/types"; -import { USCClientScore } from "server/router/ir/usc/types"; import { IRUSCContext } from "../ir/usc/types"; import { ClassHandler } from "../../framework/user-game-stats/types"; import { KsHookSV3CScore } from "../ir/kshook-sv3c/types"; +import { USCClientScore } from "server/router/ir/usc/_playtype/types"; export interface ImportTypeDataMap { "file/eamusement-iidx-csv": IIDXEamusementCSVData; diff --git a/server/src/lib/score-import/import-types/ir/usc/converter.test.ts b/server/src/lib/score-import/import-types/ir/usc/converter.test.ts index 6e5921b55..4465a6653 100644 --- a/server/src/lib/score-import/import-types/ir/usc/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/usc/converter.test.ts @@ -5,8 +5,7 @@ import d from "deepmerge"; import { uscChart, uscScore } from "test-utils/test-data"; import CreateLogCtx from "lib/logger/logger"; import ResetDBState from "test-utils/resets"; - -import { USCClientScore } from "server/router/ir/usc/types"; +import { USCClientScore } from "server/router/ir/usc/_playtype/types"; const logger = CreateLogCtx(__filename); @@ -78,7 +77,7 @@ t.test("#DeriveNoteMod", (t) => { const dm = (p: Partial) => ConverterIRUSC( d(uscScore, p), - { chartHash: uscChart.data.hashSHA1 as string }, + { chartHash: uscChart.data.hashSHA1 as string, playtype: "Controller" }, "ir/usc", logger ); diff --git a/server/src/lib/score-import/import-types/ir/usc/converter.ts b/server/src/lib/score-import/import-types/ir/usc/converter.ts index 0da32dad0..bab72ed73 100644 --- a/server/src/lib/score-import/import-types/ir/usc/converter.ts +++ b/server/src/lib/score-import/import-types/ir/usc/converter.ts @@ -1,16 +1,4 @@ -import { USCClientScore } from "server/router/ir/usc/types"; -import { FindSongOnID } from "utils/queries/songs"; -import { KtLogger } from "lib/logger/logger"; -import { - InternalFailure, - InvalidScoreFailure, - KTDataNotFoundFailure, -} from "../../../framework/common/converter-failures"; -import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; -import { IRUSCContext } from "./types"; -import { Lamps } from "tachi-common"; -import { ConverterFunction } from "../../common/types"; -import { DryScore } from "../../../framework/common/types"; +import db from "external/mongo/db"; import { USC_DEFAULT_HOLD, USC_DEFAULT_MISS, @@ -18,7 +6,19 @@ import { USC_DEFAULT_PERFECT, USC_DEFAULT_SLAM, } from "lib/constants/usc-ir"; -import db from "external/mongo/db"; +import { KtLogger } from "lib/logger/logger"; +import { USCClientScore } from "server/router/ir/usc/_playtype/types"; +import { Lamps } from "tachi-common"; +import { FindSongOnID } from "utils/queries/songs"; +import { + InternalFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, +} from "../../../framework/common/converter-failures"; +import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; +import { DryScore } from "../../../framework/common/types"; +import { ConverterFunction } from "../../common/types"; +import { IRUSCContext } from "./types"; /** * Interprets the "note mod" used based on the USC score. @@ -38,7 +38,10 @@ export function DeriveNoteMod(data: USCClientScore): "NORMAL" | "MIRROR" | "RAND /** * Determines the lamp of a USC score. */ -export function DeriveLamp(scoreDoc: USCClientScore, logger: KtLogger): Lamps["usc:Single"] { +export function DeriveLamp( + scoreDoc: USCClientScore, + logger: KtLogger +): Lamps["usc:Keyboard" | "usc:Controller"] { if (scoreDoc.score === 10_000_000) { return "PERFECT ULTIMATE CHAIN"; } else if (scoreDoc.error === 0) { @@ -78,6 +81,7 @@ export const ConverterIRUSC: ConverterFunction = a const chartDoc = await db.charts.usc.findOne({ "data.hashSHA1": context.chartHash, + playtype: context.playtype, }); if (!chartDoc) { @@ -98,7 +102,7 @@ export const ConverterIRUSC: ConverterFunction = a const { grade, percent } = GenericGetGradeAndPercent("usc", data.score, chartDoc); - const dryScore: DryScore<"usc:Single"> = { + const dryScore: DryScore<"usc:Controller" | "usc:Keyboard"> = { comment: null, game: "usc", importType, diff --git a/server/src/lib/score-import/import-types/ir/usc/parser.test.ts b/server/src/lib/score-import/import-types/ir/usc/parser.test.ts index 4ca67475e..0d5f99485 100644 --- a/server/src/lib/score-import/import-types/ir/usc/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/usc/parser.test.ts @@ -10,6 +10,7 @@ t.test("#ParseIRUSC", (t) => { const res = ParseIRUSC( { score: uscScore } as unknown as Record, uscChart.data.hashSHA1 as string, + "Controller", logger ); @@ -25,7 +26,7 @@ t.test("#ParseIRUSC", (t) => { }); t.test("Should reject empty bodies", (t) => { - t.throws(() => ParseIRUSC({}, uscChart.data.hashSHA1 as string, logger), { + t.throws(() => ParseIRUSC({}, uscChart.data.hashSHA1 as string, "Controller", logger), { statusCode: 400, message: /invalid usc score/iu, }); diff --git a/server/src/lib/score-import/import-types/ir/usc/parser.ts b/server/src/lib/score-import/import-types/ir/usc/parser.ts index bd3b39e0a..43baa5821 100644 --- a/server/src/lib/score-import/import-types/ir/usc/parser.ts +++ b/server/src/lib/score-import/import-types/ir/usc/parser.ts @@ -1,10 +1,11 @@ import { KtLogger } from "lib/logger/logger"; import p, { PrudenceSchema } from "prudence"; -import { USCClientScore } from "server/router/ir/usc/types"; import { FormatPrError } from "utils/prudence"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import { ParserFunctionReturns } from "../../common/types"; import { IRUSCContext } from "./types"; +import { Playtypes } from "tachi-common"; +import { USCClientScore } from "server/router/ir/usc/_playtype/types"; const PR_USCIRScore: PrudenceSchema = { score: p.isBoundedInteger(0, 10_000_000), @@ -24,6 +25,7 @@ const PR_USCIRScore: PrudenceSchema = { export function ParseIRUSC( body: Record, chartHash: string, + playtype: Playtypes["usc"], logger: KtLogger ): ParserFunctionReturns { const err = p( @@ -40,6 +42,7 @@ export function ParseIRUSC( return { context: { chartHash, + playtype, }, game: "usc", iterable: [body.score] as USCClientScore[], diff --git a/server/src/lib/score-import/import-types/ir/usc/types.ts b/server/src/lib/score-import/import-types/ir/usc/types.ts index 16e168d0c..badada65d 100644 --- a/server/src/lib/score-import/import-types/ir/usc/types.ts +++ b/server/src/lib/score-import/import-types/ir/usc/types.ts @@ -1,3 +1,6 @@ +import { Playtypes } from "tachi-common"; + export interface IRUSCContext { chartHash: string; + playtype: Playtypes["usc"]; } diff --git a/server/src/lib/score-import/import-types/parsers.ts b/server/src/lib/score-import/import-types/parsers.ts index bcd3bb61d..d5db685c3 100644 --- a/server/src/lib/score-import/import-types/parsers.ts +++ b/server/src/lib/score-import/import-types/parsers.ts @@ -5,7 +5,6 @@ import { ParseEagSDVX } from "./api/eag-sdvx/parser"; import { ParseFloIIDX } from "./api/flo-iidx/parser"; import { ParseFloSDVX } from "./api/flo-sdvx/parser"; import { ParseMinSDVX } from "./api/min-sdvx/parser"; -import { ParseBatchManualFromObject } from "./common/batch-manual/parser"; import ParseBatchManual from "./file/batch-manual/parser"; import ParseEamusementIIDXCSV from "./file/eamusement-iidx-csv/parser"; import { ParseMerIIDX } from "./file/mer-iidx/parser"; diff --git a/server/src/server/router/ir/usc/_playtype/router.test.ts b/server/src/server/router/ir/usc/_playtype/router.test.ts index e66cb764b..9142e5ae3 100644 --- a/server/src/server/router/ir/usc/_playtype/router.test.ts +++ b/server/src/server/router/ir/usc/_playtype/router.test.ts @@ -39,22 +39,23 @@ function TestAuth(url: string) { }); } -t.test("GET /ir/usc", async (t) => { +// Due to how this works, as long as these tests pass, the two IRs work identically. +t.test("GET /ir/usc/Keyboard", async (t) => { + t.beforeEach(ResetDBState); + await db["api-tokens"].insert({ userID: 1, identifier: "USC Token", permissions: { submit_score: true, }, - token: "foo", + token: "bar", fromAPIClient: null, }); - t.beforeEach(ResetDBState); + TestAuth("/ir/usc/Controller"); - TestAuth("/ir/usc"); - - const res = await mockApi.get("/ir/usc").set("Authorization", "Bearer foo"); + const res = await mockApi.get("/ir/usc/Controller").set("Authorization", "Bearer bar"); t.equal(res.body.statusCode, 20, "Should return 20"); t.match( @@ -69,13 +70,43 @@ t.test("GET /ir/usc", async (t) => { t.end(); }); -t.test("GET /ir/usc/charts/:chartHash", (t) => { +t.test("GET /ir/usc/Controller", async (t) => { + await db["api-tokens"].insert({ + userID: 1, + identifier: "USC Token", + permissions: { + submit_score: true, + }, + token: "foo", + fromAPIClient: null, + }); + + t.beforeEach(ResetDBState); + + TestAuth("/ir/usc/Controller"); + + const res = await mockApi.get("/ir/usc/Controller").set("Authorization", "Bearer foo"); + + t.equal(res.body.statusCode, 20, "Should return 20"); + t.match( + res.body.body, + { + serverName: /tachi/iu, + irVersion: /^[0-9]\.[0-9]\.[0-9](-a)?$/iu, + }, + "Should return the right body." + ); + + t.end(); +}); + +t.test("GET /ir/usc/Controller/charts/:chartHash", (t) => { t.beforeEach(ResetDBState); t.beforeEach(InsertFakeUSCAuth); t.test("Should return 20 if the chartHash matches a chart.", async (t) => { const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH") + .get("/ir/usc/Controller/charts/USC_CHART_HASH") .set("Authorization", "Bearer foo"); t.equal(res.body.statusCode, 20, "Should return 20"); @@ -85,7 +116,7 @@ t.test("GET /ir/usc/charts/:chartHash", (t) => { t.test("Should return 44 if the chartHash doesn't match a chart.", async (t) => { const res = await mockApi - .get("/ir/usc/charts/INVALID_HASH") + .get("/ir/usc/Controller/charts/INVALID_HASH") .set("Authorization", "Bearer foo"); t.equal(res.body.statusCode, 44, "Should return 44"); @@ -105,7 +136,7 @@ const USC_SCORE_PB: PBScoreDocument = { songID: 1, userID: 1, timeAchieved: 0, - playtype: "Single", + playtype: "Controller", game: "usc", highlight: false, composedFrom: { @@ -135,14 +166,14 @@ const USC_SCORE_PB: PBScoreDocument = { }, }; -t.test("GET /ir/usc/:chartHash/record", (t) => { +t.test("GET /ir/usc/Controller/:chartHash/record", (t) => { t.beforeEach(ResetDBState); t.beforeEach(InsertFakeUSCAuth); - TestAuth("/ir/usc/:chartHash/record"); + TestAuth("/ir/usc/Controller/:chartHash/record"); t.test("Should return 44 if the chartHash doesn't match a chart.", async (t) => { const res = await mockApi - .get("/ir/usc/charts/INVALID_HASH/record") + .get("/ir/usc/Controller/charts/INVALID_HASH/record") .set("Authorization", "Bearer foo"); t.equal(res.body.statusCode, 44, "Should return 44"); @@ -152,7 +183,7 @@ t.test("GET /ir/usc/:chartHash/record", (t) => { t.test("Should return 44 if there are no scores on the chart.", async (t) => { const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/record") + .get("/ir/usc/Controller/charts/USC_CHART_HASH/record") .set("Authorization", "Bearer foo"); t.equal(res.body.statusCode, 44, "Should return 44"); @@ -175,7 +206,7 @@ t.test("GET /ir/usc/:chartHash/record", (t) => { } as ScoreDocument); const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/record") + .get("/ir/usc/Controller/charts/USC_CHART_HASH/record") .set("Authorization", "Bearer foo"); t.equal(res.body.statusCode, 20, "Should return 20"); @@ -206,17 +237,17 @@ t.test("GET /ir/usc/:chartHash/record", (t) => { t.test("GET /charts/:chartHash/leaderboard", (t) => { t.beforeEach(ResetDBState); t.beforeEach(InsertFakeUSCAuth); - TestAuth("/ir/usc/:chartHash/leaderboard"); + TestAuth("/ir/usc/Controller/:chartHash/leaderboard"); t.test("Should return 40 if mode is invalid", async (t) => { const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard") + .get("/ir/usc/Controller/charts/USC_CHART_HASH/leaderboard") .set("Authorization", "Bearer foo"); t.equal(res.body.statusCode, 40); const res2 = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=invalid") + .get("/ir/usc/Controller/charts/USC_CHART_HASH/leaderboard?mode=invalid") .set("Authorization", "Bearer foo"); t.equal(res2.body.statusCode, 40); @@ -226,13 +257,13 @@ t.test("GET /charts/:chartHash/leaderboard", (t) => { t.test("Should return 40 if N is invalid", async (t) => { const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best") + .get("/ir/usc/Controller/charts/USC_CHART_HASH/leaderboard?mode=best") .set("Authorization", "Bearer foo"); t.equal(res.body.statusCode, 40); const res2 = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=foo") + .get("/ir/usc/Controller/charts/USC_CHART_HASH/leaderboard?mode=best&n=foo") .set("Authorization", "Bearer foo"); t.equal(res2.body.statusCode, 40); @@ -242,7 +273,7 @@ t.test("GET /charts/:chartHash/leaderboard", (t) => { t.test("Should return empty arr for mode = best if no scores", async (t) => { const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=5") + .get("/ir/usc/Controller/charts/USC_CHART_HASH/leaderboard?mode=best&n=5") .set("Authorization", "Bearer foo"); t.equal(res.body.statusCode, 20); @@ -282,7 +313,7 @@ t.test("GET /charts/:chartHash/leaderboard", (t) => { ] as ScoreDocument[]); const res = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=2") + .get("/ir/usc/Controller/charts/USC_CHART_HASH/leaderboard?mode=best&n=2") .set("Authorization", "Bearer foo"); t.equal(res.body.statusCode, 20); @@ -318,7 +349,7 @@ t.test("GET /charts/:chartHash/leaderboard", (t) => { ); const res2 = await mockApi - .get("/ir/usc/charts/USC_CHART_HASH/leaderboard?mode=best&n=1") + .get("/ir/usc/Controller/charts/USC_CHART_HASH/leaderboard?mode=best&n=1") .set("Authorization", "Bearer foo"); t.equal(res2.body.statusCode, 20); @@ -362,7 +393,7 @@ t.test("POST /replays", (t) => { const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); const res = await mockApi - .post("/ir/usc/replays") + .post("/ir/usc/Controller/replays") .field("identifier", "MOCK_IDENTIFIER") .attach("replay", replayFile, "replay.urf") .set("Authorization", "Bearer foo"); @@ -392,7 +423,7 @@ t.test("POST /replays", (t) => { const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); const res = await mockApi - .post("/ir/usc/replays") + .post("/ir/usc/Controller/replays") .attach("replay", replayFile, "replay.urf") .set("Authorization", "Bearer foo"); @@ -416,7 +447,7 @@ t.test("POST /replays", (t) => { // const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); const res = await mockApi - .post("/ir/usc/replays") + .post("/ir/usc/Controller/replays") .field("identifier", "MOCK_IDENTIFIER") // .attach("replay", replayFile, "replay.urf") .set("Authorization", "Bearer foo"); @@ -441,7 +472,7 @@ t.test("POST /replays", (t) => { const replayFile = GetKTDataBuffer("./usc/replayfile.urf"); const res = await mockApi - .post("/ir/usc/replays") + .post("/ir/usc/Controller/replays") .field("identifier", "INVALID_IDENTIFIER") .attach("replay", replayFile, "replay.urf") .set("Authorization", "Bearer foo"); @@ -508,7 +539,7 @@ t.test("POST /scores", (t) => { t.test("Should submit a score from a valid request.", async (t) => { const res = await mockApi - .post("/ir/usc/scores") + .post("/ir/usc/Controller/scores") .set("Authorization", "Bearer token") .send(validRequest); @@ -535,7 +566,7 @@ t.test("POST /scores", (t) => { t.test("Should orphan a score and return 22 if chart has never been seen.", async (t) => { const res = await mockApi - .post("/ir/usc/scores") + .post("/ir/usc/Controller/scores") .set("Authorization", "Bearer token") .send( deepmerge(validRequest, { @@ -614,7 +645,7 @@ t.test("POST /scores", (t) => { ] as PublicUserDocument[]); const res = await mockApi - .post("/ir/usc/scores") + .post("/ir/usc/Controller/scores") .set("Authorization", "Bearer token") .send( deepmerge(validRequest, { @@ -627,7 +658,7 @@ t.test("POST /scores", (t) => { t.equal(res.body.statusCode, 22); const res2 = await mockApi - .post("/ir/usc/scores") + .post("/ir/usc/Controller/scores") .set("Authorization", "Bearer token2") .send( deepmerge(validRequest, { @@ -646,7 +677,7 @@ t.test("POST /scores", (t) => { t.strictSame(orphanData?.userIDs, [1, 2]); const res3 = await mockApi - .post("/ir/usc/scores") + .post("/ir/usc/Controller/scores") .set("Authorization", "Bearer token3") .send( deepmerge(validRequest, { diff --git a/server/src/server/router/ir/usc/_playtype/router.ts b/server/src/server/router/ir/usc/_playtype/router.ts index 10a5920c6..7d1c12c35 100644 --- a/server/src/server/router/ir/usc/_playtype/router.ts +++ b/server/src/server/router/ir/usc/_playtype/router.ts @@ -19,6 +19,7 @@ import { ImportDocument, PBScoreDocument, SuccessfulAPIResponse, + Playtypes, } from "tachi-common"; import { FormatPrError } from "utils/prudence"; import { GetBlacklist } from "utils/queries/blacklist"; @@ -30,7 +31,6 @@ import { TachiScoreToServerScore, USCChartIndexToDiff, } from "./usc"; - const logger = CreateLogCtx(__filename); const router: Router = Router({ mergeParams: true }); @@ -81,6 +81,17 @@ const ValidateUSCRequest: RequestHandler = async (req, res, next) => { return next(); }; +router.use((req, res, next) => { + if (req.params.playtype !== "Keyboard" && req.params.playtype !== "Controller") { + return res.status(400).json({ + success: false, + description: "Invalid playtype. Expected Keyboard or Controller.", + }); + } + + return next(); +}); + router.use(ValidateUSCRequest); // This is an implementation of the USCIR spec as per https://uscir.readthedocs.io. // This specification always returns 200 OK, regardless of whether the result was okay @@ -105,10 +116,15 @@ router.get("/", (req, res) => ); const RetrieveChart: RequestHandler = async (req, res, next) => { - const chart = await db.charts.usc.findOne({ "data.hashSHA1": req.params.chartHash }); + const chart = await db.charts.usc.findOne({ + "data.hashSHA1": req.params.chartHash, + playtype: req.params.playtype as Playtypes["usc"], + }); AssignToReqTachiData(req, { - uscChartDoc: (chart ?? undefined) as ChartDocument<"usc:Single"> | undefined, + uscChartDoc: (chart ?? undefined) as + | ChartDocument<"usc:Controller" | "usc:Keyboard"> + | undefined, }); return next(); @@ -157,7 +173,7 @@ router.get("/charts/:chartHash/record", RetrieveChart, async (req, res) => { const serverRecord = (await db["personal-bests"].findOne({ chartID: chart.chartID, "rankingData.rank": 1, - })) as PBScoreDocument<"usc:Single"> | null; + })) as PBScoreDocument<"usc:Controller" | "usc:Keyboard"> | null; if (!serverRecord) { return res.status(200).json({ @@ -231,7 +247,7 @@ router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) => }, limit: n, } - )) as PBScoreDocument<"usc:Single">[]; + )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; const serverScores = await Promise.all(bestScores.map(TachiScoreToServerScore)); @@ -259,6 +275,8 @@ const PR_USCIRChartDoc = { * @name POST /ir/usc/:playtype/scores */ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => { + const playtype = req.params.playtype as Playtypes["usc"]; + const chartErr = p( req.body.chart, PR_USCIRChartDoc, @@ -280,21 +298,22 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => { let chartDoc = (await db.charts.usc.findOne({ "data.hashSHA1": uscChart.chartHash, - })) as ChartDocument<"usc:Single"> | null; + playtype, + })) as ChartDocument<"usc:Controller" | "usc:Keyboard"> | null; // If the chart doesn't exist, call HandleOrphanQueue. // If this chart has never been seen before, orphan it. // If this chart is already orphaned, increase its unique player // playcount. if (!chartDoc) { - const { song, chart } = ConvertUSCChart(uscChart); + const { song, chart } = ConvertUSCChart(uscChart, playtype); const uscChartName = `${uscChart.artist} - ${uscChart.title} (${USCChartIndexToDiff( uscChart.difficulty )})`; chartDoc = await HandleOrphanQueue( - "usc:Single", + `usc:${playtype}` as const, "usc", chart, song, @@ -310,6 +329,7 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => { const blacklist = await GetBlacklist(); const scoresToDeorphan = await db["orphan-scores"].find({ "context.chartHash": chartDoc.data.hashSHA1, + "context.playtype": playtype, }); await Promise.all( @@ -323,6 +343,7 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => { const importRes = await ExpressWrappedScoreImportMain(userID, false, "ir/usc", [ req.body, uscChart.chartHash, + playtype, ]); // If this was an orphan chart request, return ACCEPTED, @@ -391,7 +412,6 @@ router.post( const correspondingScore = await db.scores.findOne({ userID: req[SYMBOL_TachiAPIAuth]!.userID!, - game: "usc", scoreID: req.body.identifier, }); diff --git a/server/src/server/router/ir/usc/_playtype/usc.test.ts b/server/src/server/router/ir/usc/_playtype/usc.test.ts index bf89e0e87..92376118d 100644 --- a/server/src/server/router/ir/usc/_playtype/usc.test.ts +++ b/server/src/server/router/ir/usc/_playtype/usc.test.ts @@ -5,7 +5,7 @@ import t from "tap"; import ResetDBState from "test-utils/resets"; import { CreatePOSTScoresResponseBody, TachiScoreToServerScore } from "./usc"; -const mockScorePB: PBScoreDocument<"usc:Single"> = { +const mockScorePB: PBScoreDocument<"usc:Controller" | "usc:Keyboard"> = { chartID: "USC_CHART_ID", calculatedData: { VF6: 0, @@ -17,7 +17,7 @@ const mockScorePB: PBScoreDocument<"usc:Single"> = { game: "usc", highlight: false, isPrimary: true, - playtype: "Single", + playtype: "Controller", rankingData: { outOf: 2, rank: 1, @@ -240,7 +240,9 @@ const mockUserDocs = [2, 3, 4, 5, 6, 7, 8, 9, 10].map((e) => ({ t.test("#CreatePOSTScoresResponseBody", async (t) => { t.beforeEach(ResetDBState); - const chartDoc = (await db.charts.usc.findOne()) as ChartDocument<"usc:Single">; + const chartDoc = (await db.charts.usc.findOne()) as ChartDocument< + "usc:Controller" | "usc:Keyboard" + >; t.test("Should correctly return POSTScoresResponseBody", async (t) => { await db.scores.insert(mockScoreDocument); @@ -257,7 +259,7 @@ t.test("#CreatePOSTScoresResponseBody", async (t) => { const res = await CreatePOSTScoresResponseBody( 1, - chartDoc as ChartDocument<"usc:Single">, + chartDoc as ChartDocument<"usc:Controller" | "usc:Keyboard">, "USER_1_SCORE_PB" ); diff --git a/server/src/server/router/ir/usc/_playtype/usc.ts b/server/src/server/router/ir/usc/_playtype/usc.ts index 17f34a2ae..94f6515e8 100644 --- a/server/src/server/router/ir/usc/_playtype/usc.ts +++ b/server/src/server/router/ir/usc/_playtype/usc.ts @@ -1,15 +1,21 @@ import db from "external/mongo/db"; import { USCIR_ADJACENT_SCORE_N } from "lib/constants/usc-ir"; import CreateLogCtx from "lib/logger/logger"; -import { ChartDocument, integer, PBScoreDocument, ScoreDocument, SongDocument } from "tachi-common"; +import { + ChartDocument, + integer, + PBScoreDocument, + Playtypes, + ScoreDocument, + SongDocument, +} from "tachi-common"; import { MStoS, Random20Hex } from "utils/misc"; import { GetPBOnChart, GetServerRecordOnChart } from "utils/scores"; import { USCClientChart, USCServerScore } from "./types"; - const logger = CreateLogCtx(__filename); export const TACHI_LAMP_TO_USC: Record< - PBScoreDocument<"usc:Single">["scoreData"]["lamp"], + PBScoreDocument<"usc:Controller" | "usc:Keyboard">["scoreData"]["lamp"], USCServerScore["lamp"] > = { // we don't do NO PLAY, so its not handled. @@ -27,7 +33,7 @@ export const TACHI_LAMP_TO_USC: Record< * fields are null. */ export async function TachiScoreToServerScore( - tachiScore: PBScoreDocument<"usc:Single"> + tachiScore: PBScoreDocument<"usc:Controller" | "usc:Keyboard"> ): Promise { // @optimisable // Repeated calls to this may pre-emptively provide usernames @@ -54,7 +60,7 @@ export async function TachiScoreToServerScore( const scorePB = (await db.scores.findOne({ scoreID: tachiScore.composedFrom.scorePB, - })) as ScoreDocument<"usc:Single"> | null; + })) as ScoreDocument<"usc:Controller" | "usc:Keyboard"> | null; if (!scorePB) { logger.severe( @@ -82,13 +88,12 @@ export async function TachiScoreToServerScore( export async function CreatePOSTScoresResponseBody( userID: integer, - chartDoc: ChartDocument<"usc:Single">, + chartDoc: ChartDocument<"usc:Controller" | "usc:Keyboard">, scoreID: string ): Promise { - const scorePB = (await GetPBOnChart( - userID, - chartDoc.chartID - )) as PBScoreDocument<"usc:Single"> | null; + const scorePB = (await GetPBOnChart(userID, chartDoc.chartID)) as PBScoreDocument< + "usc:Controller" | "usc:Keyboard" + > | null; if (!scorePB) { logger.severe(`Score was imported for chart, but no ScorePB was available on this chart?`, { @@ -100,9 +105,9 @@ export async function CreatePOSTScoresResponseBody( ); } - const ktServerRecord = (await GetServerRecordOnChart( - chartDoc.chartID - )) as PBScoreDocument<"usc:Single"> | null; + const ktServerRecord = (await GetServerRecordOnChart(chartDoc.chartID)) as PBScoreDocument< + "usc:Controller" | "usc:Keyboard" + > | null; // this is impossible to trigger without making a race-condition. /* istanbul ignore next */ @@ -133,7 +138,7 @@ export async function CreatePOSTScoresResponseBody( limit: USCIR_ADJACENT_SCORE_N, sort: { "rankingData.rank": -1 }, } - )) as PBScoreDocument<"usc:Single">[]; + )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; // The specification enforces that we return them in // ascending order, though, so we reverse this after @@ -159,7 +164,7 @@ export async function CreatePOSTScoresResponseBody( limit: USCIR_ADJACENT_SCORE_N, sort: { "rankingData.rank": 1 }, } - )) as PBScoreDocument<"usc:Single">[]; + )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; const [score, serverRecord, adjacentAbove, adjacentBelow] = await Promise.all([ TachiScoreToServerScore(scorePB), @@ -170,7 +175,7 @@ export async function CreatePOSTScoresResponseBody( const originalScore = (await db.scores.findOne({ scoreID, - })) as ScoreDocument<"usc:Single">; + })) as ScoreDocument<"usc:Controller" | "usc:Keyboard">; if (!originalScore) { logger.severe( @@ -202,14 +207,14 @@ export interface POSTScoresResponseBody { sendReplay: string; } -export function ConvertUSCChart(uscChartDoc: USCClientChart) { - const chart: ChartDocument<"usc:Single"> = { +export function ConvertUSCChart(uscChartDoc: USCClientChart, playtype: Playtypes["usc"]) { + const chart: ChartDocument<"usc:Controller" | "usc:Keyboard"> = { chartID: Random20Hex(), difficulty: USCChartIndexToDiff(uscChartDoc.difficulty), isPrimary: true, level: "?", levelNum: 0, - playtype: "Single", + playtype, rgcID: null, songID: 0, versions: [], @@ -234,6 +239,6 @@ export function ConvertUSCChart(uscChartDoc: USCClientChart) { export function USCChartIndexToDiff( index: 0 | 1 | 2 | 3 -): ChartDocument<"usc:Single">["difficulty"] { +): ChartDocument<"usc:Controller" | "usc:Keyboard">["difficulty"] { return (["NOV", "ADV", "EXH", "INF"] as const)[index]; } diff --git a/server/src/server/router/ir/usc/router.ts b/server/src/server/router/ir/usc/router.ts index 6d85a0198..274a7e4a2 100644 --- a/server/src/server/router/ir/usc/router.ts +++ b/server/src/server/router/ir/usc/router.ts @@ -3,17 +3,6 @@ import uscirRouter from "./_playtype/router"; const router: Router = Router({ mergeParams: true }); -router.use((req, res, next) => { - if (req.params.playtype !== "Keyboard" && req.params.playtype !== "Controller") { - return res.status(400).json({ - success: false, - description: "Invalid playtype. Expected Keyboard or Controller.", - }); - } - - return next(); -}); - router.use("/:playtype", uscirRouter); export default router; diff --git a/server/src/test-utils/mock-db/charts-usc.json b/server/src/test-utils/mock-db/charts-usc.json index 115af8eab..d820d5dae 100644 --- a/server/src/test-utils/mock-db/charts-usc.json +++ b/server/src/test-utils/mock-db/charts-usc.json @@ -4,7 +4,7 @@ "chartID": "USC_CHART_ID", "difficulty": "NOV", "songID": 1, - "playtype": "Single", + "playtype": "Controller", "levelNum": 1, "level": "1", "flags": { diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index 26fe5c7ec..9de13f098 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -20,8 +20,8 @@ import { } from "lib/constants/usc-ir"; import { DryScore } from "lib/score-import/framework/common/types"; import { BarbatosScore } from "lib/score-import/import-types/ir/barbatos/types"; -import { USCClientScore } from "../server/router/ir/usc/types"; import { KsHookSV3CScore } from "lib/score-import/import-types/ir/kshook-sv3c/types"; +import { USCClientScore } from "server/router/ir/usc/_playtype/types"; const file = (name: string) => path.join(__dirname, "/test-data", name); @@ -200,12 +200,12 @@ export const barbScore: BarbatosScore = { song_id: 1, }; -export const uscChart: ChartDocument<"usc:Single"> = { +export const uscChart: ChartDocument<"usc:Controller" | "usc:Keyboard"> = { rgcID: null, chartID: "USC_CHART_ID", difficulty: "NOV", songID: 1, - playtype: "Single", + playtype: "Keyboard", levelNum: 1, level: "1", data: { diff --git a/server/src/utils/queries/pbs.ts b/server/src/utils/queries/pbs.ts index 6e8ed50b9..1c27afb31 100644 --- a/server/src/utils/queries/pbs.ts +++ b/server/src/utils/queries/pbs.ts @@ -11,7 +11,7 @@ export async function GetAdjacentAbove(userPB: PBScoreDocument, size = 5) { limit: size, sort: { "rankingData.rank": -1 }, } - )) as PBScoreDocument<"usc:Single">[]; + )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; return adjAbove; } @@ -26,7 +26,7 @@ export async function GetAdjacentBelow(userPB: PBScoreDocument, size = 5) { limit: size, sort: { "rankingData.rank": 1 }, } - )) as PBScoreDocument<"usc:Single">[]; + )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; return adjAbove; } diff --git a/server/src/utils/types.ts b/server/src/utils/types.ts index 3e02aa619..748e79584 100644 --- a/server/src/utils/types.ts +++ b/server/src/utils/types.ts @@ -47,7 +47,7 @@ export type EmptyObject = Record; * Data that may be monkey-patched onto req.tachi. This holds things such as middleware results. */ export interface TachiRequestData { - uscChartDoc?: ChartDocument<"usc:Single">; + uscChartDoc?: ChartDocument<"usc:Controller" | "usc:Keyboard">; beatorajaChartDoc?: ChartDocument<"bms:7K" | "bms:14K">;