From 0b7de99583a7be2dd9e4002c5e12a3d3ce4ed784 Mon Sep 17 00:00:00 2001 From: Daniel THIRION Date: Sat, 19 Apr 2025 00:04:28 +0200 Subject: [PATCH] fix(api-cg/jb): use clearFlag bitfield to generate Lamp (#1276) --- .../common/api-cg/jb/converter.test.ts | 259 ++++++++++++------ .../common/api-cg/jb/converter.ts | 30 +- .../import-types/common/api-cg/types.ts | 2 +- 3 files changed, 197 insertions(+), 94 deletions(-) diff --git a/server/src/lib/score-import/import-types/common/api-cg/jb/converter.test.ts b/server/src/lib/score-import/import-types/common/api-cg/jb/converter.test.ts index 225954b97..9a43fd560 100644 --- a/server/src/lib/score-import/import-types/common/api-cg/jb/converter.test.ts +++ b/server/src/lib/score-import/import-types/common/api-cg/jb/converter.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-bitwise */ import { ConverterAPICGJubeat } from "./converter"; import CreateLogCtx from "lib/logger/logger"; import { ParseDateFromString } from "lib/score-import/framework/common/score-utils"; @@ -8,6 +9,11 @@ import type { DryScore } from "lib/score-import/framework/common/types"; const logger = CreateLogCtx(__filename); +const BIT_FAILED = 1 << 0; +const BIT_CLEAR = 1 << 1; +const BIT_FULL_COMBO = 1 << 2; +const BIT_EXCELLENT = 1 << 3; + function mkInput(modifant: Partial = {}) { const validInput: CGJubeatScore = { internalId: 10000001, @@ -22,7 +28,7 @@ function mkInput(modifant: Partial = {}) { poorCount: 0, missCount: 0, musicRate: 952, - clearFlag: 0, // unknown, unused + clearFlag: BIT_FULL_COMBO | BIT_CLEAR | BIT_FAILED, }; return dmf(validInput, modifant); @@ -34,9 +40,6 @@ function mkOutput(modifant: any = {}): DryScore<"jubeat:Single"> { comment: null, game: "jubeat", importType: "api/cg-dev-jubeat", - - // we handle it like this because -- with no timezone info - // this will fail in CI; it has a different timezone there! timeAchieved: ParseDateFromString("2019-06-06 08:14:22"), service: "CG Dev", scoreData: { @@ -47,6 +50,7 @@ function mkOutput(modifant: any = {}): DryScore<"jubeat:Single"> { great: 50, good: 25, poor: 0, + miss: 0, }, musicRate: 95.2, optional: {}, @@ -62,7 +66,6 @@ t.test("#ConverterAPICGJubeat", (t) => { service: "dev", userID: 1, }; - const convert = (modifant: Partial = {}) => ConverterAPICGJubeat(mkInput(modifant), context, "api/cg-dev-jubeat", logger); @@ -85,81 +88,181 @@ t.test("#ConverterAPICGJubeat", (t) => { t.end(); }); - t.test("Lamps", async (t) => { - t.hasStrict( - await convert({ - score: 690_000, - missCount: 1, // at least a miss otherwise it's an FC - }), - { - dryScore: mkOutput({ - scoreData: { - lamp: "FAILED", - score: 690_000, - judgements: { - miss: 1, - }, - }, - }), - } - ); - t.hasStrict( - await convert({ - score: 700_000, - missCount: 1, - }), - { - dryScore: mkOutput({ - scoreData: { - lamp: "CLEAR", - score: 700_000, - judgements: { - miss: 1, - }, - }, - }), - } - ); - t.hasStrict( - await convert({ - missCount: 0, - poorCount: 0, - }), - { - dryScore: mkOutput({ - scoreData: { - lamp: "FULL COMBO", - judgements: { - poor: 0, - miss: 0, - }, - }, - }), - } - ); - t.hasStrict( - await convert({ - missCount: 0, - poorCount: 0, - goodCount: 0, + t.test("Lamp Interpretation from Bitfield", (t) => { + t.test("should interpret EXCELLENT bitfield correctly", async (t) => { + const clearFlag = BIT_EXCELLENT | BIT_FULL_COMBO | BIT_CLEAR | BIT_FAILED; + const score = 1_000_000; + const judgements = { + perfectCount: 200, greatCount: 0, - score: 1_000_000, - }), - { - dryScore: mkOutput({ - scoreData: { - lamp: "EXCELLENT", - score: 1_000_000, - judgements: { - great: 0, - good: 0, - poor: 0, - miss: 0, - }, - }, + goodCount: 0, + poorCount: 0, + missCount: 0, + }; + + t.hasStrict( + await convert({ + clearFlag, + score, + ...judgements, }), - } - ); + { + dryScore: mkOutput({ + scoreData: { + lamp: "EXCELLENT", + score, + judgements: { + perfect: 200, + great: 0, + good: 0, + poor: 0, + miss: 0, + }, + }, + }), + } + ); + }); + + t.test("should interpret FULL COMBO bitfield correctly", async (t) => { + const clearFlag = BIT_FULL_COMBO | BIT_CLEAR | BIT_FAILED; + const judgements = { + perfectCount: 100, + greatCount: 50, + goodCount: 25, + poorCount: 0, + missCount: 0, + }; + const score = 947_184; + + t.hasStrict( + await convert({ + clearFlag, + score, + ...judgements, + }), + { + dryScore: mkOutput({ + scoreData: { + lamp: "FULL COMBO", + score, + judgements: { + perfect: 100, + great: 50, + good: 25, + poor: 0, + miss: 0, + }, + }, + }), + } + ); + }); + + t.test("should interpret CLEAR bitfield correctly", async (t) => { + const clearFlag = BIT_CLEAR | BIT_FAILED; + const score = 750_000; + const judgements = { + perfectCount: 90, + greatCount: 40, + goodCount: 20, + poorCount: 5, + missCount: 1, + }; + + t.hasStrict( + await convert({ + clearFlag, + score, + ...judgements, + }), + { + dryScore: mkOutput({ + scoreData: { + lamp: "CLEAR", + score, + judgements: { + perfect: 90, + great: 40, + good: 20, + poor: 5, + miss: 1, + }, + }, + }), + } + ); + }); + + t.test("should interpret FAILED bitfield correctly (standard)", async (t) => { + const clearFlag = BIT_FAILED; + const score = 650_000; + const judgements = { + perfectCount: 80, + greatCount: 30, + goodCount: 15, + poorCount: 10, + missCount: 5, + }; + + t.hasStrict( + await convert({ + clearFlag, + score, + ...judgements, + }), + { + dryScore: mkOutput({ + scoreData: { + lamp: "FAILED", + score, + judgements: { + perfect: 80, + great: 30, + good: 15, + poor: 10, + miss: 5, + }, + }, + }), + } + ); + }); + + t.test("should interpret FAILED bitfield (case of challenge fail)", async (t) => { + const clearFlag = BIT_FAILED; + const score = 920_000; + const judgements = { + perfectCount: 110, + greatCount: 55, + goodCount: 30, + poorCount: 0, + missCount: 0, + }; + + t.hasStrict( + await convert({ + clearFlag, + score, + ...judgements, + }), + { + dryScore: mkOutput({ + scoreData: { + lamp: "FAILED", + score, + judgements: { + perfect: 110, + great: 55, + good: 30, + poor: 0, + miss: 0, + }, + }, + }), + } + ); + }); t.end(); }); diff --git a/server/src/lib/score-import/import-types/common/api-cg/jb/converter.ts b/server/src/lib/score-import/import-types/common/api-cg/jb/converter.ts index bbe6a29ff..e28602690 100644 --- a/server/src/lib/score-import/import-types/common/api-cg/jb/converter.ts +++ b/server/src/lib/score-import/import-types/common/api-cg/jb/converter.ts @@ -28,7 +28,7 @@ export const ConverterAPICGJubeat: ConverterFunction = poor: data.poorCount, miss: data.missCount, }; - const lamp = GuessLamp(judgements, data.score); + const lamp = GetLamp(data.clearFlag); const chart = await FindChartOnInGameID("jubeat", data.internalId, "Single", difficulty); @@ -125,23 +125,23 @@ function ConvertMusicRate(rate: number): number { return rate / 10; } -function GuessLamp(judgments: Record, score: number) { - if ( - judgments.good === 0 && - judgments.great === 0 && - judgments.miss === 0 && - judgments.poor === 0 - ) { - if (score !== 1_000_000) { - throw new InvalidScoreFailure(`Score is an EXC, yet score !== 1_000_000.`); - } - +/* eslint-disable no-bitwise */ +function GetLamp(clearFlag: integer) { + if ((clearFlag & (1 << 3)) !== 0) { return "EXCELLENT"; - } else if (judgments.miss === 0 && judgments.poor === 0) { + } + + if ((clearFlag & (1 << 2)) !== 0) { return "FULL COMBO"; - } else if (score >= 700_000) { + } + + if ((clearFlag & (1 << 1)) !== 0) { return "CLEAR"; } - return "FAILED"; + if ((clearFlag & (1 << 0)) !== 0) { + return "FAILED"; + } + + throw new InvalidScoreFailure("Failed to decode Lamp using bitfield clearFlag."); } diff --git a/server/src/lib/score-import/import-types/common/api-cg/types.ts b/server/src/lib/score-import/import-types/common/api-cg/types.ts index 8b97e5264..0aedb51b4 100644 --- a/server/src/lib/score-import/import-types/common/api-cg/types.ts +++ b/server/src/lib/score-import/import-types/common/api-cg/types.ts @@ -71,7 +71,7 @@ export interface CGJubeatScore { internalId: integer; difficulty: integer; version: integer; - clearFlag: unknown; // Weird bitwise field, unused. + clearFlag: integer; // Weird bitwise field, unused. score: integer; hardMode: boolean;