diff --git a/server/.eslintrc b/server/.eslintrc index 3a1317fc3..3e301ca99 100644 --- a/server/.eslintrc +++ b/server/.eslintrc @@ -22,15 +22,18 @@ "plugin:import/errors" ], "rules": { - "prettier/prettier": ["error", { - "printWidth": 100, - "tabWidth": 4, - "semi": true, - "singleQuote": false, - "trailingComma": "es5", - "bracketSpacing": true, - "endOfLine": "auto" - }], + "prettier/prettier": [ + "error", + { + "printWidth": 100, + "tabWidth": 4, + "semi": true, + "singleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "endOfLine": "auto" + } + ], "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-non-null-assertion": "off", "require-await": "warn", @@ -42,7 +45,10 @@ "no-eval": "error", "no-var": "error", "no-new-object": "error", - "quote-props": ["error", "as-needed"], + "quote-props": [ + "error", + "as-needed" + ], "no-array-constructor": "error", "prefer-template": "error", "prefer-promise-reject-errors": "error", @@ -73,9 +79,14 @@ "no-throw-literal": "error", "no-unmodified-loop-condition": "error", "no-unused-expressions": "error", + // "no-unused-vars": [ + // "warn", + // { + // "match": "logger" + // } + // ], "no-useless-call": "error", "no-useless-return": "error", - "require-unicode-regexp": "error", - + "require-unicode-regexp": "error" } } \ No newline at end of file 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 new file mode 100644 index 000000000..29f9637f5 --- /dev/null +++ b/server/src/lib/score-import/import-types/ir/usc/converter.test.ts @@ -0,0 +1,125 @@ +import t from "tap"; +import db, { CloseMongoConnection } from "../../../../../external/mongo/db"; +import { ConverterIRUSC, DeriveLamp, DeriveNoteMod } from "./converter"; +import d from "deepmerge"; +import { uscChart, uscScore } from "../../../../../test-utils/test-data"; +import CreateLogCtx from "../../../../logger/logger"; +import ResetDBState from "../../../../../test-utils/reset-db-state"; +import { USCClientScore } from "../../../../../server/router/ir/usc/usc"; + +const logger = CreateLogCtx(__filename); + +t.test("#DeriveLamp", (t) => { + t.equal(DeriveLamp(uscScore, logger), "EXCESSIVE CLEAR"); + + t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 0 }, gauge: 50 }), logger), "FAILED"); + + t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 0 }, gauge: 70 }), logger), "CLEAR"); + + t.equal( + DeriveLamp(d(uscScore, { options: { gaugeType: 1 }, gauge: 0.1 }), logger), + "EXCESSIVE CLEAR" + ); + + t.equal(DeriveLamp(d(uscScore, { options: { gaugeType: 1 }, gauge: 0 }), logger), "FAILED"); + + t.equal( + DeriveLamp( + d(uscScore, { score: 10_000_000, options: { gaugeType: 1 }, gauge: 0.1 }), + logger + ), + "PERFECT ULTIMATE CHAIN" + ); + + // error: 0 should not take priority over score 10million + t.equal( + DeriveLamp( + d(uscScore, { + score: 10_000_000, + options: { gaugeType: 1 }, + gauge: 0.1, + error: 0, + }), + logger + ), + "PERFECT ULTIMATE CHAIN" + ); + + t.equal( + DeriveLamp( + d(uscScore, { + score: 9_000_000, + options: { gaugeType: 0 }, + gauge: 15, + error: 0, + }), + logger + ), + "ULTIMATE CHAIN" + ); + + t.throws(() => DeriveLamp(d(uscScore, { options: { gaugeType: 2 } }), logger)); + + t.end(); +}); + +t.test("#DeriveNoteMod", (t) => { + t.equal(DeriveNoteMod(uscScore), "MIRROR"); + + t.equal(DeriveNoteMod(d(uscScore, { options: { random: true } })), "MIR-RAN"); + + t.equal(DeriveNoteMod(d(uscScore, { options: { random: false, mirror: false } })), "NORMAL"); + t.equal(DeriveNoteMod(d(uscScore, { options: { random: true, mirror: false } })), "RANDOM"); + + t.end(); +}); + +const dm = (p: Partial | any) => + ConverterIRUSC(d(uscScore, p), { chart: uscChart }, "ir/usc", logger); + +t.test("#ConverterIRUSC", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should convert a score", async (t) => { + const res = await dm({}); + + t.hasStrict(res, { + song: { + id: 1, + }, + chart: { + chartID: uscChart.chartID, + }, + dryScore: {}, + } as any); + + t.end(); + }); + + t.test("Should reject scores with invalid hit windows", (t) => { + t.rejects(dm({ windows: { perfect: 0 } })); + t.rejects(dm({ windows: { good: 0 } })); + t.rejects(dm({ windows: { hold: 0 } })); + t.rejects(dm({ windows: { miss: 0 } })); + t.rejects(dm({ windows: { slam: 0 } })); + + t.end(); + }); + + t.test("Should reject scores with invalid autoflags", (t) => { + t.rejects(dm({ options: { autoFlags: 1 } })); + + t.end(); + }); + + t.test("Should throw an InternalFailure on song-chart desync", async (t) => { + await db.songs.usc.remove({}); + t.rejects(dm({})); + + t.end(); + }); + + t.end(); +}); + +t.teardown(CloseMongoConnection); 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 01bf40506..8b1befddc 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 @@ -5,7 +5,7 @@ import { InternalFailure, InvalidScoreFailure } from "../../../framework/common/ import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import { IRUSCContext } from "./types"; import { Lamps } from "kamaitachi-common"; -import uuid from "uuid"; +import { v4 as UUIDv4 } from "uuid"; import { ConverterFunction } from "../../common/types"; import { DryScore } from "../../../framework/common/types"; import { @@ -16,7 +16,10 @@ import { USC_DEFAULT_SLAM, } from "../../../../constants/usc-ir"; -function DeriveNoteMod(data: USCClientScore): "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN" { +/** + * Interprets the "note mod" used based on the USC score. + */ +export function DeriveNoteMod(data: USCClientScore): "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN" { if (data.options.mirror && data.options.random) { return "MIR-RAN"; } else if (data.options.mirror) { @@ -28,7 +31,10 @@ function DeriveNoteMod(data: USCClientScore): "NORMAL" | "MIRROR" | "RANDOM" | " return "NORMAL"; } -function DeriveLamp(scoreDoc: USCClientScore, logger: KtLogger): Lamps["usc:Single"] { +/** + * Determines the lamp of a USC score. + */ +export function DeriveLamp(scoreDoc: USCClientScore, logger: KtLogger): Lamps["usc:Single"] { if (scoreDoc.score === 10_000_000) { return "PERFECT ULTIMATE CHAIN"; } else if (scoreDoc.error === 0) { @@ -98,7 +104,7 @@ export const ConverterIRUSC: ConverterFunction = a scoreMeta: { gaugeMod: data.options.gaugeOpt === 0 ? "NORMAL" : "HARD", noteMod: DeriveNoteMod(data), - replayID: uuid.v4(), + replayID: UUIDv4(), }, }; 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 new file mode 100644 index 000000000..df5724401 --- /dev/null +++ b/server/src/lib/score-import/import-types/ir/usc/parser.test.ts @@ -0,0 +1,31 @@ +import t from "tap"; +import { CloseMongoConnection } from "../../../../../external/mongo/db"; +import { uscChart, uscScore } from "../../../../../test-utils/test-data"; +import CreateLogCtx from "../../../../logger/logger"; +import { ParseIRUSC } from "./parser"; + +const logger = CreateLogCtx(__filename); + +t.test("#ParseIRUSC", (t) => { + t.test("Should validate and convert a score into an iterable", (t) => { + const res = ParseIRUSC( + ({ score: uscScore } as unknown) as Record, + uscChart, + logger + ); + + t.hasStrict(res, { + game: "usc", + context: { + chart: uscChart, + }, + iterable: [uscScore], + } as any); + + t.end(); + }); + + t.end(); +}); + +t.teardown(CloseMongoConnection); diff --git a/server/src/test-utils/mock-db/songs-usc.json b/server/src/test-utils/mock-db/songs-usc.json new file mode 100644 index 000000000..c92a9d07d --- /dev/null +++ b/server/src/test-utils/mock-db/songs-usc.json @@ -0,0 +1,13 @@ +[ + { + "title": "ALBIDA Powerless Mix", + "artist": "無力P", + "id": 1, + "firstVersion": null, + "alt-titles": [], + "search-titles": [], + "data": { + "sdvxEquiv": null + } + } +] \ No newline at end of file diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index c6d81098a..62f650c8b 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -11,8 +11,16 @@ import { } from "kamaitachi-common"; import path from "path"; import db from "../external/mongo/db"; +import { + USC_DEFAULT_HOLD, + USC_DEFAULT_MISS, + USC_DEFAULT_NEAR, + USC_DEFAULT_PERFECT, + USC_DEFAULT_SLAM, +} 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/usc"; const file = (name: string) => path.join(__dirname, "/test-data", name); @@ -184,3 +192,44 @@ export const barbScore: BarbatosScore = { score: 9_000_000, song_id: 1, }; + +export const uscChart: ChartDocument<"usc:Single"> = { + rgcID: null, + chartID: "USC_CHART_ID", + difficulty: "NOV", + songID: 1, + playtype: "Single", + levelNum: 1, + level: "1", + flags: { + CUSTOM: true, + }, + data: { + hashSHA256: "USC_CHART_HASH", + }, + isPrimary: true, + versions: [], +}; + +export const uscScore: USCClientScore = { + crit: 100, + error: 5, + near: 50, + gauge: 80, + options: { + autoFlags: 0, + gaugeOpt: 0, + gaugeType: 1, + mirror: true, + random: false, + }, + score: 9_000_000, + timestamp: 0, + windows: { + good: USC_DEFAULT_NEAR, + perfect: USC_DEFAULT_PERFECT, + hold: USC_DEFAULT_HOLD, + miss: USC_DEFAULT_MISS, + slam: USC_DEFAULT_SLAM, + }, +};