Completely refactor USC support to use Controller/Keyboard playtypes.

This commit is contained in:
zkldi
2021-11-13 08:51:34 +00:00
parent 4d16d24da6
commit eaad1d1cf2
22 changed files with 243 additions and 127 deletions
+10 -6
View File
@@ -831,12 +831,16 @@ export const DatabaseSchemas: Record<Databases, ValidatorFunction> = {
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,
@@ -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",
@@ -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 };
}
@@ -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 {
@@ -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();
});
@@ -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),
}),
},
@@ -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;
@@ -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<USCClientScore>) =>
ConverterIRUSC(
d(uscScore, p),
{ chartHash: uscChart.data.hashSHA1 as string },
{ chartHash: uscChart.data.hashSHA1 as string, playtype: "Controller" },
"ir/usc",
logger
);
@@ -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<USCClientScore, IRUSCContext> = 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<USCClientScore, IRUSCContext> = 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,
@@ -10,6 +10,7 @@ t.test("#ParseIRUSC", (t) => {
const res = ParseIRUSC(
{ score: uscScore } as unknown as Record<string, unknown>,
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,
});
@@ -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<string, unknown>,
chartHash: string,
playtype: Playtypes["usc"],
logger: KtLogger
): ParserFunctionReturns<USCClientScore, IRUSCContext> {
const err = p(
@@ -40,6 +42,7 @@ export function ParseIRUSC(
return {
context: {
chartHash,
playtype,
},
game: "usc",
iterable: [body.score] as USCClientScore[],
@@ -1,3 +1,6 @@
import { Playtypes } from "tachi-common";
export interface IRUSCContext {
chartHash: string;
playtype: Playtypes["usc"];
}
@@ -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";
@@ -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, {
@@ -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,
});
@@ -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"
);
@@ -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<USCServerScore> {
// @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<POSTScoresResponseBody> {
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];
}
-11
View File
@@ -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;
@@ -4,7 +4,7 @@
"chartID": "USC_CHART_ID",
"difficulty": "NOV",
"songID": 1,
"playtype": "Single",
"playtype": "Controller",
"levelNum": 1,
"level": "1",
"flags": {
+3 -3
View File
@@ -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: {
+2 -2
View File
@@ -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;
}
+1 -1
View File
@@ -47,7 +47,7 @@ export type EmptyObject = Record<string, never>;
* 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">;