mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-22 23:18:05 +03:00
feat: CG IIDX API import method (#1715)
* wip * yay no errors * refactor a bit, support ghostGauge * missCount is null from the api, not -1, if player died * improve ghostGuage parsing * add tests * I knew I was missing something with nullable misscount * add new import methods to .env.example
This commit is contained in:
@@ -19,9 +19,11 @@ const choices: Array<[string, string]> = (
|
||||
["CG DEV MUSECA", "api/cg-dev-museca"],
|
||||
["CG DEV Pop'n", "api/cg-dev-popn"],
|
||||
["CG DEV Jubeat", "api/cg-dev-jubeat"],
|
||||
["CG DEV IIDX", "api/cg-dev-iidx"],
|
||||
["CG SDVX", "api/cg-prod-sdvx"],
|
||||
["CG MUSECA", "api/cg-prod-museca"],
|
||||
["CG Pop'n", "api/cg-prod-popn"],
|
||||
["CG IIDX", "api/cg-prod-iidx"],
|
||||
["MYT CHUNITHM", "api/myt-chunithm"],
|
||||
["MYT MAIMAI DX", "api/myt-maimaidx"],
|
||||
["MYT ONGEKI", "api/myt-ongeki"],
|
||||
|
||||
@@ -164,6 +164,9 @@ function ImportInfoDisplayer({ game }: { game: GameGroup }) {
|
||||
/>,
|
||||
<ImportTypeInfoCard importType="api/flo-iidx" key="api/flo-iidx" />,
|
||||
<ImportTypeInfoCard importType="api/eag-iidx" key="api/eag-iidx" />,
|
||||
<ImportTypeInfoCard importType="api/cg-gan-iidx" key="api/cg-gan-iidx" />,
|
||||
<ImportTypeInfoCard importType="api/cg-nag-iidx" key="api/cg-nag-iidx" />,
|
||||
<ImportTypeInfoCard importType="api/cg-dev-iidx" key="api/cg-dev-iidx" />,
|
||||
<ImportInfoCard
|
||||
desc="Use your data from a CG instance (Dev/GAN/NAG)."
|
||||
href="kt-cg-iidx-importer"
|
||||
@@ -695,6 +698,36 @@ function ImportTypeInfoCard({
|
||||
name="CG GAN Integration"
|
||||
/>
|
||||
);
|
||||
case "api/cg-dev-iidx":
|
||||
return (
|
||||
<ImportInfoCard
|
||||
desc="Pull your IIDX scores from the CG Dev Network."
|
||||
href="cg-dev-iidx"
|
||||
key="cg-dev-iidx"
|
||||
moreInfo="Note: All networks are reduced to their first three letters for anonymity reasons."
|
||||
name="CG Dev Integration"
|
||||
/>
|
||||
);
|
||||
case "api/cg-nag-iidx":
|
||||
return (
|
||||
<ImportInfoCard
|
||||
desc="Pull your IIDX scores from the NAG Network."
|
||||
href="cg-nag-iidx"
|
||||
key="cg-nag-iidx"
|
||||
moreInfo="Note: All networks are reduced to their first three letters for anonymity reasons."
|
||||
name="CG NAG Integration"
|
||||
/>
|
||||
);
|
||||
case "api/cg-gan-iidx":
|
||||
return (
|
||||
<ImportInfoCard
|
||||
desc="Pull your IIDX scores from the GAN Network."
|
||||
href="cg-gan-iidx"
|
||||
key="cg-gan-iidx"
|
||||
moreInfo="Note: All networks are reduced to their first three letters for anonymity reasons."
|
||||
name="CG GAN Integration"
|
||||
/>
|
||||
);
|
||||
case "api/myt-chunithm":
|
||||
return (
|
||||
<ImportInfoCard
|
||||
|
||||
@@ -213,6 +213,9 @@ export default function ImportRoutes() {
|
||||
<Route exact path="/import/cg-dev-jubeat">
|
||||
<CGIntegrationPage cgType="dev" game="jubeat" />
|
||||
</Route>
|
||||
<Route exact path="/import/cg-dev-iidx">
|
||||
<CGIntegrationPage cgType="dev" game="iidx" />
|
||||
</Route>
|
||||
|
||||
<Route exact path="/import/cg-nag-sdvx">
|
||||
<CGIntegrationPage cgType="nag" game="sdvx" />
|
||||
@@ -226,6 +229,9 @@ export default function ImportRoutes() {
|
||||
<Route exact path="/import/cg-nag-jubeat">
|
||||
<CGIntegrationPage cgType="nag" game="jubeat" />
|
||||
</Route>
|
||||
<Route exact path="/import/cg-nag-iidx">
|
||||
<CGIntegrationPage cgType="nag" game="iidx" />
|
||||
</Route>
|
||||
|
||||
<Route exact path="/import/cg-gan-sdvx">
|
||||
<CGIntegrationPage cgType="gan" game="sdvx" />
|
||||
@@ -239,6 +245,9 @@ export default function ImportRoutes() {
|
||||
<Route exact path="/import/cg-gan-jubeat">
|
||||
<CGIntegrationPage cgType="gan" game="jubeat" />
|
||||
</Route>
|
||||
<Route exact path="/import/cg-gan-iidx">
|
||||
<CGIntegrationPage cgType="gan" game="iidx" />
|
||||
</Route>
|
||||
|
||||
<Route exact path="/import/kt-cg-iidx-importer">
|
||||
<IIDXCGSiteImportPage />
|
||||
|
||||
@@ -18,7 +18,7 @@ import ImportStateRenderer from "./ImportStateRenderer";
|
||||
|
||||
interface Props {
|
||||
cgType: "dev" | "gan" | "nag";
|
||||
game: "jubeat" | "museca" | "popn" | "sdvx";
|
||||
game: "iidx" | "jubeat" | "museca" | "popn" | "sdvx";
|
||||
}
|
||||
|
||||
export default function CGIntegrationPage({ cgType, game }: Props) {
|
||||
|
||||
@@ -39,14 +39,17 @@ const API_IMPORT_TYPES: Record<APIImportTypes, true> = {
|
||||
"api/myt-maimaidx": true,
|
||||
"api/myt-ongeki": true,
|
||||
"api/myt-wacca": true,
|
||||
"api/cg-dev-iidx": true,
|
||||
"api/cg-dev-jubeat": true,
|
||||
"api/cg-dev-museca": true,
|
||||
"api/cg-dev-popn": true,
|
||||
"api/cg-dev-sdvx": true,
|
||||
"api/cg-nag-iidx": true,
|
||||
"api/cg-nag-jubeat": true,
|
||||
"api/cg-nag-museca": true,
|
||||
"api/cg-nag-popn": true,
|
||||
"api/cg-nag-sdvx": true,
|
||||
"api/cg-gan-iidx": true,
|
||||
"api/cg-gan-jubeat": true,
|
||||
"api/cg-gan-museca": true,
|
||||
"api/cg-gan-popn": true,
|
||||
|
||||
@@ -20,9 +20,9 @@ export type APIImportTypes =
|
||||
| "api/myt-wacca"
|
||||
|
||||
// cg has dev and prod supported
|
||||
// with four games.
|
||||
// with five games.
|
||||
// this typescript feature for stringliteral expansion is kinda neat.
|
||||
| `api/cg-${"dev" | "gan" | "nag"}-${"jubeat" | "museca" | "popn" | "sdvx"}`;
|
||||
| `api/cg-${"dev" | "gan" | "nag"}-${"iidx" | "jubeat" | "museca" | "popn" | "sdvx"}`;
|
||||
|
||||
export type IRImportTypes =
|
||||
| "ir/barbatos"
|
||||
|
||||
@@ -3,46 +3,49 @@
|
||||
|
||||
/** Represents the enum public.import_type */
|
||||
type ImportType =
|
||||
| 'file/batch-manual'
|
||||
| 'file/eamusement-iidx-csv'
|
||||
| 'file/eamusement-sdvx-csv'
|
||||
| 'file/mypagescraper-player-csv'
|
||||
| 'file/mypagescraper-records-csv'
|
||||
| 'file/pli-iidx-csv'
|
||||
| 'file/solid-state-squad'
|
||||
| 'api/arc-iidx'
|
||||
| 'api/arc-sdvx'
|
||||
| 'api/mer-iidx'
|
||||
| 'api/cg-dev-iidx'
|
||||
| 'api/cg-dev-jubeat'
|
||||
| 'api/cg-dev-museca'
|
||||
| 'api/cg-dev-popn'
|
||||
| 'api/cg-dev-sdvx'
|
||||
| 'api/cg-gan-iidx'
|
||||
| 'api/cg-gan-jubeat'
|
||||
| 'api/cg-gan-museca'
|
||||
| 'api/cg-gan-popn'
|
||||
| 'api/cg-gan-sdvx'
|
||||
| 'api/cg-nag-iidx'
|
||||
| 'api/cg-nag-jubeat'
|
||||
| 'api/cg-nag-museca'
|
||||
| 'api/cg-nag-popn'
|
||||
| 'api/cg-nag-sdvx'
|
||||
| 'api/eag-iidx'
|
||||
| 'api/eag-sdvx'
|
||||
| 'api/flo-iidx'
|
||||
| 'api/flo-sdvx'
|
||||
| 'api/mer-iidx'
|
||||
| 'api/min-sdvx'
|
||||
| 'api/myt-chunithm'
|
||||
| 'api/myt-maimaidx'
|
||||
| 'api/myt-ongeki'
|
||||
| 'api/myt-wacca'
|
||||
| 'api/cg-dev-jubeat'
|
||||
| 'api/cg-dev-museca'
|
||||
| 'api/cg-dev-popn'
|
||||
| 'api/cg-dev-sdvx'
|
||||
| 'api/cg-gan-jubeat'
|
||||
| 'api/cg-gan-museca'
|
||||
| 'api/cg-gan-popn'
|
||||
| 'api/cg-gan-sdvx'
|
||||
| 'api/cg-nag-jubeat'
|
||||
| 'api/cg-nag-museca'
|
||||
| 'api/cg-nag-popn'
|
||||
| 'api/cg-nag-sdvx'
|
||||
| 'file/batch-manual'
|
||||
| 'file/eamusement-iidx-csv'
|
||||
| 'file/eamusement-sdvx-csv'
|
||||
| 'file/import-class'
|
||||
| 'file/mypagescraper-player-csv'
|
||||
| 'file/mypagescraper-records-csv'
|
||||
| 'file/pli-iidx-csv'
|
||||
| 'file/solid-state-squad'
|
||||
| 'ir/barbatos'
|
||||
| 'ir/beatoraja'
|
||||
| 'ir/direct-manual'
|
||||
| 'ir/fervidex-static'
|
||||
| 'ir/fervidex'
|
||||
| 'ir/kshook-sv6c-static'
|
||||
| 'ir/fervidex-static'
|
||||
| 'ir/kshook-sv6c'
|
||||
| 'ir/kshook-sv6c-static'
|
||||
| 'ir/lr2hook'
|
||||
| 'ir/usc'
|
||||
| 'file/import-class';
|
||||
| 'ir/usc';
|
||||
|
||||
export default ImportType;
|
||||
|
||||
@@ -49,7 +49,7 @@ TACHI_CDN_SAVE_LOCATION_PRIVATE_REGION=us-east-1
|
||||
TACHI_NAME=Tachi Local Dev
|
||||
TACHI_TYPE=omni
|
||||
TACHI_GAME_GROUPS=iidx,museca,sdvx,bms,chunithm,usc,wacca,popn,jubeat,pms,maimai,maimaidx,arcaea,ongeki,ddr
|
||||
TACHI_IMPORT_TYPES=file/eamusement-iidx-csv,file/batch-manual,file/import-class,file/solid-state-squad,file/pli-iidx-csv,ir/direct-manual,ir/barbatos,ir/fervidex,ir/fervidex-static,ir/beatoraja,ir/usc,ir/kshook-sv6c,api/eag-iidx,api/eag-sdvx,api/flo-iidx,api/flo-sdvx,api/min-sdvx
|
||||
TACHI_IMPORT_TYPES=file/eamusement-iidx-csv,file/batch-manual,file/import-class,file/solid-state-squad,file/pli-iidx-csv,ir/direct-manual,ir/barbatos,ir/fervidex,ir/fervidex-static,ir/beatoraja,ir/usc,ir/kshook-sv6c,api/cg-dev-iidx,api/cg-nag-iidx,api/cg-gan-iidx,api/eag-iidx,api/eag-sdvx,api/flo-iidx,api/flo-sdvx,api/min-sdvx
|
||||
|
||||
# TACHI_GITHUB_APP_ID=
|
||||
# TACHI_GITHUB_APP_BASE64_PRIVATE_KEY=
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import { log } from "#lib/log/log";
|
||||
import DB from "#services/pg/db";
|
||||
import { Testing511Song, Testing511SPA } from "#test-utils/test-data";
|
||||
import deepmerge from "deepmerge";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { CGContext, CGIIDXScore } from "../types";
|
||||
|
||||
import { ConverterAPICGIIDX } from "./converter";
|
||||
|
||||
const LEG_SONG_ID = "s_cg_iidx_leg_test";
|
||||
const LEG_CHART_ID = "c_cg_iidx_leg_test";
|
||||
|
||||
const iidxScore: CGIIDXScore = {
|
||||
version: 33,
|
||||
internalId: 1000,
|
||||
difficulty: "SPA",
|
||||
exScore: 1464,
|
||||
clearType: 4,
|
||||
perfectCount: 686,
|
||||
greatCount: 92,
|
||||
missCount: 8,
|
||||
dead: 0,
|
||||
ghost: "131316191818171a171717181817191617191718151913131917131717171817191818191818181918181a18181719171419171810141814191211161a131519",
|
||||
ghostGauge:
|
||||
"4c044c044c044c044c044c049a0402056a05060688060a075c071208940816096409b209000a340a9c0ad00a380bba0b560c8a0cbe0c0c0d740dc20d100e440eac0e140f620fb00ffe0f4c109a100211501191116112c9127f138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388138813881388135813721388138813881388138813881388138813de128813881388138813881388138813cc114e121113881388138813881388138813881388138813881388138813881388138813881300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
option1: 72584948351232,
|
||||
option2: 0,
|
||||
dateTime: "2025-11-29 14:07:09",
|
||||
};
|
||||
|
||||
const context: CGContext = {
|
||||
service: "dev",
|
||||
userID: 1,
|
||||
};
|
||||
|
||||
async function seed511Another() {
|
||||
await DB.insertInto("song")
|
||||
.values({
|
||||
id: Testing511Song.id,
|
||||
legacy_id: 1,
|
||||
game_group: "iidx",
|
||||
title: Testing511Song.title,
|
||||
artist: Testing511Song.artist,
|
||||
search_terms: Testing511Song.searchTerms,
|
||||
alt_titles: Testing511Song.altTitles,
|
||||
data: Testing511Song.data,
|
||||
fts_document: "",
|
||||
})
|
||||
.execute();
|
||||
|
||||
await DB.insertInto("chart")
|
||||
.values({
|
||||
id: Testing511SPA.chartID,
|
||||
legacy_id: Testing511SPA.chartID,
|
||||
game: "iidx-sp",
|
||||
song_id: Testing511Song.id,
|
||||
difficulty: "ANOTHER",
|
||||
level: Testing511SPA.level,
|
||||
level_num: Testing511SPA.levelNum,
|
||||
is_primary: Testing511SPA.isPrimary,
|
||||
versions: Testing511SPA.versions,
|
||||
data: Testing511SPA.data,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function seedLeggendariaFixture() {
|
||||
await DB.insertInto("song")
|
||||
.values({
|
||||
id: LEG_SONG_ID,
|
||||
legacy_id: 24_011,
|
||||
game_group: "iidx",
|
||||
title: "冬椿 ft. Kanae Asaba",
|
||||
artist: "artist",
|
||||
search_terms: [],
|
||||
alt_titles: [],
|
||||
data: { displayVersion: "1", genre: "TEST" },
|
||||
fts_document: "",
|
||||
})
|
||||
.execute();
|
||||
|
||||
await DB.insertInto("chart")
|
||||
.values({
|
||||
id: LEG_CHART_ID,
|
||||
legacy_id: LEG_CHART_ID,
|
||||
game: "iidx-sp",
|
||||
song_id: LEG_SONG_ID,
|
||||
difficulty: "LEGGENDARIA",
|
||||
level: "12",
|
||||
level_num: 12,
|
||||
is_primary: true,
|
||||
versions: ["33"],
|
||||
data: {
|
||||
inGameID: 24_011,
|
||||
notecount: 100,
|
||||
"2dxtraSet": null,
|
||||
},
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
describe("ConverterAPICGIIDX", () => {
|
||||
it("returns song, chart, and dryScore for valid input", async () => {
|
||||
await seed511Another();
|
||||
|
||||
const res = await ConverterAPICGIIDX(iidxScore, context, "api/cg-dev-iidx", log);
|
||||
|
||||
expect(res.song).toMatchObject(Testing511Song);
|
||||
expect(res.chart).toMatchObject({
|
||||
chartID: Testing511SPA.chartID,
|
||||
difficulty: "ANOTHER",
|
||||
});
|
||||
expect(res.dryScore).toStrictEqual({
|
||||
comment: null,
|
||||
game: "iidx-sp",
|
||||
importType: "api/cg-dev-iidx",
|
||||
timeAchieved: 1764425229000,
|
||||
service: "CG Dev",
|
||||
scoreData: {
|
||||
score: 1464,
|
||||
lamp: "CLEAR",
|
||||
judgements: {
|
||||
pgreat: 686,
|
||||
great: 92,
|
||||
},
|
||||
optional: {
|
||||
bp: 8,
|
||||
gaugeHistory: [
|
||||
22, 22, 22, 22, 22, 22, 23.56, 25.64, 27.72, 30.84, 33.44, 36.04, 37.68,
|
||||
41.32, 43.92, 46.52, 48.08, 49.64, 51.2, 52.24, 54.32, 55.36, 57.44, 60.04,
|
||||
63.16, 64.2, 65.24, 66.8, 68.88, 70.44, 72, 73.04, 75.12, 77.2, 78.76,
|
||||
80.32, 81.88, 83.44, 85, 87.08, 88.64, 89.94, 94.1, 96.18, 99.82, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 99.04,
|
||||
99.56, 100, 100, 100, 100, 100, 100, 100, 100, 96.6, 100, 100, 100, 100,
|
||||
100, 100, 100, 91.12, 93.72, 97.62, 100, 100, 100, 100, 100, 100, 100, 100,
|
||||
100, 100, 100, 100, 100, 100, 100, 100,
|
||||
],
|
||||
},
|
||||
},
|
||||
scoreMeta: {
|
||||
random: "S-RANDOM",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("converts legacy Leggendaria music_id via lookup table", async () => {
|
||||
await seed511Another();
|
||||
await seedLeggendariaFixture();
|
||||
|
||||
const res = await ConverterAPICGIIDX(
|
||||
deepmerge(iidxScore, { internalId: 24101 } as Partial<CGIIDXScore>),
|
||||
context,
|
||||
"api/cg-dev-iidx",
|
||||
log,
|
||||
);
|
||||
|
||||
expect(res.song).toMatchObject({ title: "冬椿 ft. Kanae Asaba" });
|
||||
expect(res.chart).toMatchObject({
|
||||
difficulty: "LEGGENDARIA",
|
||||
data: {
|
||||
inGameID: 24_011,
|
||||
},
|
||||
});
|
||||
expect(res.dryScore.game).toBe("iidx-sp");
|
||||
const sd = res.dryScore.scoreData as { lamp: string; score: number };
|
||||
expect(sd.score).toBe(1464);
|
||||
expect(sd.lamp).toBe("CLEAR");
|
||||
});
|
||||
|
||||
it("throws when chart is missing", async () => {
|
||||
await seed511Another();
|
||||
|
||||
await expect(
|
||||
ConverterAPICGIIDX(
|
||||
deepmerge(iidxScore, { internalId: 0 } as Partial<CGIIDXScore>),
|
||||
context,
|
||||
"api/cg-dev-iidx",
|
||||
log,
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
failureType: "SongOrChartNotFound",
|
||||
message: /Could not find chart with songID 0 \(iidx-sp ANOTHER - Version 33\)/u,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid music_id type", async () => {
|
||||
await seed511Another();
|
||||
|
||||
await expect(
|
||||
ConverterAPICGIIDX(
|
||||
deepmerge(iidxScore, { internalId: "foo" }),
|
||||
context,
|
||||
"api/cg-dev-iidx",
|
||||
log,
|
||||
),
|
||||
).rejects.toMatchObject({
|
||||
message: expect.stringMatching(
|
||||
/.*invalid input syntax for type integer.*foo/iu,
|
||||
) as string,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,436 @@
|
||||
import type { DryScore } from "#lib/score-import/framework/common/types";
|
||||
import type { ConverterFunction } from "#lib/score-import/import-types/common/types";
|
||||
import type { Difficulties, integer, ScoreMeta, Versions } from "tachi-common";
|
||||
import type { GetEnumValue } from "tachi-common/types/metrics";
|
||||
|
||||
import {
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
SkipScoreFailure,
|
||||
SongOrChartNotFoundFailure,
|
||||
} from "#lib/score-import/framework/common/converter-failures";
|
||||
import { ParseDateFromString } from "#lib/score-import/framework/common/score-utils";
|
||||
import { FindIIDXChartOnInGameIDVersion } from "#utils/queries/charts";
|
||||
import { FindSongOnID } from "#utils/queries/songs";
|
||||
|
||||
import type { CGContext, CGIIDXScore } from "../types";
|
||||
|
||||
import { FormatCGService } from "../util";
|
||||
|
||||
export const ConverterAPICGIIDX: ConverterFunction<CGIIDXScore, CGContext> = async (
|
||||
data,
|
||||
processContext,
|
||||
importType,
|
||||
log,
|
||||
) => {
|
||||
const version = ConvertVersion(data.version);
|
||||
const playtype = data.difficulty.startsWith("S") ? "SP" : "DP";
|
||||
const game = playtype === "SP" ? "iidx-sp" : "iidx-dp";
|
||||
|
||||
let musicID = data.internalId;
|
||||
|
||||
if (data.difficulty === "SPB") {
|
||||
throw new SkipScoreFailure(`We don't support BEGINNER charts. Sorry!`);
|
||||
}
|
||||
|
||||
let difficulty = ConvertDifficulty(data.difficulty);
|
||||
|
||||
if (OldLeggendariaConversionTable[musicID] !== undefined) {
|
||||
musicID = OldLeggendariaConversionTable[musicID]!;
|
||||
|
||||
// This is now definitely a leggendaria.
|
||||
difficulty = "LEGGENDARIA";
|
||||
}
|
||||
|
||||
const chart = await FindIIDXChartOnInGameIDVersion(game, musicID, difficulty, version);
|
||||
|
||||
if (!chart) {
|
||||
throw new SongOrChartNotFoundFailure(
|
||||
`Could not find chart with songID ${musicID} (${playtype} ${difficulty} - Version ${version})`,
|
||||
importType,
|
||||
data,
|
||||
processContext,
|
||||
);
|
||||
}
|
||||
|
||||
const song = await FindSongOnID("iidx", chart.song.id);
|
||||
|
||||
if (!song) {
|
||||
log.fatal(`Song-Chart desync with song ID ${chart.song.id} (iidx).`);
|
||||
throw new InternalFailure(`Song-Chart desync with song ID ${chart.song.id} (iidx).`);
|
||||
}
|
||||
|
||||
const dryScore: DryScore<"iidx-dp" | "iidx-sp"> = {
|
||||
comment: null,
|
||||
game: game,
|
||||
importType,
|
||||
timeAchieved: ParseDateFromString(data.dateTime),
|
||||
service: FormatCGService(processContext.service),
|
||||
scoreData: {
|
||||
score: data.exScore,
|
||||
lamp: ConvertLamp(data.clearType),
|
||||
judgements: {
|
||||
pgreat: data.perfectCount,
|
||||
great: data.greatCount,
|
||||
},
|
||||
optional: {
|
||||
bp: data.missCount,
|
||||
gaugeHistory: ConvertGhostGauge(data.ghostGauge),
|
||||
},
|
||||
},
|
||||
scoreMeta: ConvertOptions(data),
|
||||
};
|
||||
|
||||
return { song, chart, dryScore };
|
||||
};
|
||||
|
||||
// LEGGENDARIAs got turned into a real difficulty as of HEROIC VERSE.
|
||||
// This service still sends the old songIDs when old scores are requested,
|
||||
// so we need to convert it up.
|
||||
const OldLeggendariaConversionTable: Record<integer, integer> = {
|
||||
// RUGGED ASH† -> RUGGED ASH
|
||||
1100: 1017,
|
||||
|
||||
// Clione† -> Clione
|
||||
4100: 4005,
|
||||
|
||||
// ABSOLUTE† -> ABSOLUTE
|
||||
4101: 4001,
|
||||
|
||||
// RIDE ON THE LIGHT (HI GREAT MIX) † -> RIDE ON THE LIGHT (HI GREAT MIX)
|
||||
5100: 5014,
|
||||
|
||||
// RED ZONE† -> RED ZONE
|
||||
11100: 11032,
|
||||
|
||||
// spiral galaxy† -> spiral galaxy
|
||||
11101: 11012,
|
||||
|
||||
// Little Little Princess† -> Little Little Princess
|
||||
12100: 12002,
|
||||
|
||||
// CONTRACT† -> CONTRACT
|
||||
13100: 13010,
|
||||
|
||||
// VANESSA† -> VANESSA
|
||||
14100: 14009,
|
||||
|
||||
// KAMAITACHI† -> KAMAITACHI
|
||||
14101: 14046,
|
||||
|
||||
// ICARUS† -> ICARUS
|
||||
15101: 15023,
|
||||
|
||||
// THE DEEP STRIKER† -> THE DEEP STRIKER
|
||||
15102: 15007,
|
||||
|
||||
// Blue Rain† -> Blue Rain
|
||||
15104: 15004,
|
||||
|
||||
// Wanna Party?† -> Wanna Party?
|
||||
15105: 15045,
|
||||
|
||||
// 凛として咲く花の如く† -> 凛として咲く花の如く
|
||||
16101: 16050,
|
||||
|
||||
// THANK YOU FOR PLAYING† -> THANK YOU FOR PLAYING
|
||||
16102: 16045,
|
||||
|
||||
// naughty girl@Queen's Palace† -> naughty girl@Queen's Palace
|
||||
16103: 16031,
|
||||
|
||||
// Kung-fu Empire† -> Kung-fu Empire
|
||||
16104: 16015,
|
||||
|
||||
// SOLID STATE SQUAD† -> SOLID STATE SQUAD
|
||||
17101: 17060,
|
||||
|
||||
// Golden Palms† -> Golden Palms
|
||||
18100: 18025,
|
||||
|
||||
// おおきなこえで† -> おおきなこえで
|
||||
18103: 18011,
|
||||
|
||||
// QUANTUM TELEPORTATION† -> QUANTUM TELEPORTATION
|
||||
19100: 19063,
|
||||
|
||||
// 朧† -> 朧
|
||||
20103: 20100,
|
||||
|
||||
// 仮想空間の旅人たち† -> 仮想空間の旅人たち
|
||||
20104: 20039,
|
||||
|
||||
// LUV CAN SAVE U† -> LUV CAN SAVE U
|
||||
20105: 20068,
|
||||
|
||||
// Howling† -> Howling
|
||||
20106: 20024,
|
||||
|
||||
// 龍と少女とデコヒーレンス† -> 龍と少女とデコヒーレンス
|
||||
20107: 20019,
|
||||
|
||||
// Close the World feat.a☆ru†LEGGENDARIA -> Close the World feat. a☆ru
|
||||
21100: 21012,
|
||||
|
||||
// Sigmund†LEGGENDARIA -> Sigmund
|
||||
21101: 21059,
|
||||
|
||||
// Ancient Scapes†LEGGENDARIA -> Ancient Scapes
|
||||
21102: 21069,
|
||||
|
||||
// invoker†LEGGENDARIA -> invoker
|
||||
21103: 21073,
|
||||
|
||||
// Feel The Beat†LEGGENDARIA -> Feel The Beat
|
||||
21104: 21052,
|
||||
|
||||
// 疾風迅雷†LEGGENDARIA -> 疾風迅雷
|
||||
21105: 21048,
|
||||
|
||||
// Verflucht†LEGGENDARIA -> Verflucht
|
||||
21106: 21050,
|
||||
|
||||
// 廿† -> 廿
|
||||
21107: 21029,
|
||||
|
||||
// CHRONO DIVER -NORNIR-† -> CHRONO DIVER -NORNIR-
|
||||
22101: 22008,
|
||||
|
||||
// chrono diver -fragment-† -> chrono diver -fragment-
|
||||
22102: 22013,
|
||||
|
||||
// 恋は白帯、サンシロー† -> 恋は白帯、サンシロー
|
||||
22103: 22024,
|
||||
|
||||
// Beat Radiance† -> Beat Radiance
|
||||
22104: 22027,
|
||||
|
||||
// 超青少年ノ為ノ超多幸ナ超古典的超舞曲† -> 超青少年ノ為ノ超多幸ナ超古典的超舞曲
|
||||
22105: 22031,
|
||||
|
||||
// EBONY & IVORY† -> EBONY & IVORY
|
||||
22106: 22089,
|
||||
|
||||
// Cosmic Cat† -> Cosmic Cat
|
||||
22107: 22006,
|
||||
|
||||
// Damage Per Second† -> Damage Per Second
|
||||
23100: 23054,
|
||||
|
||||
// STARLIGHT DANCEHALL† -> STARLIGHT DANCEHALL
|
||||
23101: 23031,
|
||||
|
||||
// Amazing Mirage† -> Amazing Mirage
|
||||
24100: 24041,
|
||||
|
||||
// 冬椿 ft. Kanae Asaba† -> 冬椿 ft. Kanae Asaba
|
||||
24101: 24011,
|
||||
};
|
||||
|
||||
function ConvertOptions(data: CGIIDXScore): ScoreMeta["iidx-dp"] | ScoreMeta["iidx-sp"] {
|
||||
if (data.version < 23) {
|
||||
return {}; // Unknown if bits are the same in older versions
|
||||
}
|
||||
|
||||
const opt1 = BigInt(data.option1);
|
||||
const opt2 = BigInt(data.option2);
|
||||
const isModern = data.version >= 27;
|
||||
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
type XOR<T, U> = (T | Without<U, T>) & (U | Without<T, U>);
|
||||
|
||||
type randoms = NonNullable<ScoreMeta["iidx-sp"]["random"]>;
|
||||
type gauges = NonNullable<ScoreMeta["iidx-sp"]["gauge"]>;
|
||||
|
||||
const RANDOM_MASKS: Partial<Record<randoms, bigint>> = isModern
|
||||
? {
|
||||
RANDOM: 1n << 39n,
|
||||
"R-RANDOM": 1n << 40n,
|
||||
"S-RANDOM": 1n << 41n,
|
||||
MIRROR: 1n << 42n,
|
||||
}
|
||||
: {
|
||||
RANDOM: 1n << 18n,
|
||||
"R-RANDOM": 1n << 19n,
|
||||
"S-RANDOM": 1n << 20n,
|
||||
MIRROR: 1n << 21n,
|
||||
};
|
||||
|
||||
const GAUGE_MASKS: Partial<Record<gauges, bigint>> = isModern
|
||||
? {
|
||||
"ASSISTED EASY": 1n << 35n,
|
||||
EASY: 1n << 36n,
|
||||
HARD: 1n << 37n,
|
||||
"EX-HARD": 1n << 38n,
|
||||
}
|
||||
: {
|
||||
"ASSISTED EASY": 1n << 14n,
|
||||
EASY: 1n << 15n,
|
||||
HARD: 1n << 16n,
|
||||
"EX-HARD": 1n << 17n,
|
||||
};
|
||||
|
||||
// TODO: find bits for AUTO SCRATCH and LEGACY NOTE
|
||||
|
||||
// Helper to find which string keys match the bitmask
|
||||
const getActiveOptions = (
|
||||
val: bigint,
|
||||
masks: Record<string, bigint>,
|
||||
): Array<XOR<gauges, randoms>> => {
|
||||
const options = [];
|
||||
|
||||
for (const [name, mask] of Object.entries(masks)) {
|
||||
if ((val & mask) !== 0n) {
|
||||
options.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
const scoreMeta: Partial<ScoreMeta["iidx-dp" | "iidx-sp"]> = {};
|
||||
|
||||
// for Random options, if no options are selected we know NONRAN is used.
|
||||
// and we know only one option can be selected.
|
||||
|
||||
scoreMeta.random = "NONRAN";
|
||||
|
||||
const randomOptions = getActiveOptions(opt1, RANDOM_MASKS);
|
||||
|
||||
if (randomOptions.length > 0) {
|
||||
scoreMeta.random = randomOptions[0] as randoms;
|
||||
}
|
||||
|
||||
// For DP random is an array [p1 side, p2 side]
|
||||
if (data.difficulty.startsWith("D")) {
|
||||
scoreMeta.random = [scoreMeta.random, "NONRAN"];
|
||||
const rightSide = getActiveOptions(opt2, RANDOM_MASKS);
|
||||
|
||||
if (rightSide.length > 0) {
|
||||
scoreMeta.random[1] = rightSide[0] as randoms;
|
||||
}
|
||||
}
|
||||
|
||||
// for Gauge options, if no options are selected we do NOT know the gauge type
|
||||
// like Random only one Gauge can be selected
|
||||
|
||||
const gaugeOptions = getActiveOptions(opt1, GAUGE_MASKS);
|
||||
|
||||
if (gaugeOptions.length > 0) {
|
||||
scoreMeta.gauge = gaugeOptions[0] as gauges;
|
||||
}
|
||||
|
||||
return scoreMeta;
|
||||
}
|
||||
|
||||
function ConvertGhostGauge(rawHex: string): Array<number> | null {
|
||||
// Sanity check, all ghostGauge data that we have seen is 1,536 characters.
|
||||
if (rawHex.length !== 1536) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const graph = [];
|
||||
const MAX_VAL = 5000;
|
||||
let failed = false;
|
||||
|
||||
for (let i = 0; i < rawHex.length; i += 4) {
|
||||
const chunk = rawHex.slice(i, i + 4);
|
||||
const bigEndianHex = chunk.slice(2, 4) + chunk.slice(0, 2);
|
||||
const rawValue = parseInt(bigEndianHex, 16);
|
||||
|
||||
if (rawValue > MAX_VAL) {
|
||||
failed = true; // We've encountered a status flag seemingly indicating death.
|
||||
graph.push(0.0);
|
||||
break;
|
||||
}
|
||||
const percentage = (rawValue / MAX_VAL) * 100;
|
||||
graph.push(Number(percentage.toFixed(2)));
|
||||
}
|
||||
|
||||
// Remove padding 0s at the end of a normal play.
|
||||
if (!failed) {
|
||||
let lastDataIndex = -1;
|
||||
for (let i = graph.length - 1; i >= 0; i--) {
|
||||
if (graph[i] > 0) {
|
||||
lastDataIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return lastDataIndex === -1 ? null : graph.slice(0, lastDataIndex + 1);
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
function ConvertDifficulty(difficulty: string): Difficulties["iidx-dp" | "iidx-sp"] {
|
||||
switch (difficulty.charAt(2)) {
|
||||
case "N":
|
||||
return "NORMAL";
|
||||
case "H":
|
||||
return "HYPER";
|
||||
case "A":
|
||||
return "ANOTHER";
|
||||
case "L":
|
||||
return "LEGGENDARIA";
|
||||
}
|
||||
|
||||
throw new InvalidScoreFailure(`Unknown difficulty ${difficulty}.`);
|
||||
}
|
||||
|
||||
function ConvertLamp(lamp: integer): GetEnumValue<"iidx-dp" | "iidx-sp", "lamp"> {
|
||||
switch (lamp) {
|
||||
case 0:
|
||||
return "NO PLAY";
|
||||
case 1:
|
||||
return "FAILED";
|
||||
case 2:
|
||||
return "ASSIST CLEAR";
|
||||
case 3:
|
||||
return "EASY CLEAR";
|
||||
case 4:
|
||||
return "CLEAR";
|
||||
case 5:
|
||||
return "HARD CLEAR";
|
||||
case 6:
|
||||
return "EX HARD CLEAR";
|
||||
case 7:
|
||||
return "FULL COMBO";
|
||||
}
|
||||
|
||||
throw new InvalidScoreFailure(`Unknown lamp value ${lamp}.`);
|
||||
}
|
||||
|
||||
function ConvertVersion(version: integer): Versions["iidx-dp" | "iidx-sp"] {
|
||||
switch (version) {
|
||||
case 20:
|
||||
return "20";
|
||||
case 21:
|
||||
return "21";
|
||||
case 22:
|
||||
return "22";
|
||||
case 23:
|
||||
return "23";
|
||||
case 24:
|
||||
return "24";
|
||||
case 25:
|
||||
return "25";
|
||||
case 26:
|
||||
return "26";
|
||||
case 27:
|
||||
return "27-omni";
|
||||
case 28:
|
||||
return "28-omni";
|
||||
case 29:
|
||||
return "29-omni";
|
||||
case 30:
|
||||
return "30-omni";
|
||||
case 31:
|
||||
return "31-omni";
|
||||
case 32:
|
||||
return "32-omni";
|
||||
case 33:
|
||||
return "33";
|
||||
}
|
||||
|
||||
throw new InvalidScoreFailure(`Unsupported version ${version}.`);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { type APIImportTypes, FormatPrError, type integer, type V3Game } from "t
|
||||
import type { ParserFunctionReturns } from "../types";
|
||||
import type {
|
||||
CGContext,
|
||||
CGIIDXScore,
|
||||
CGJubeatScore,
|
||||
CGMusecaScore,
|
||||
CGPopnScore,
|
||||
@@ -22,6 +23,29 @@ import type {
|
||||
import { FetchCGScores } from "./traverse-api";
|
||||
import { CGGameToTachiGame, FormatCGService } from "./util";
|
||||
|
||||
const PR_CG_IIDX = {
|
||||
internalId: p.isPositiveInteger,
|
||||
difficulty: "string",
|
||||
version: p.isPositiveInteger,
|
||||
exScore: p.isPositiveInteger,
|
||||
clearType: p.isPositiveInteger,
|
||||
perfectCount: p.isPositiveInteger,
|
||||
greatCount: p.isPositiveInteger,
|
||||
|
||||
// missCount is null when it's not recorded, so we have to allow that.
|
||||
missCount: p.or(p.isPositiveInteger, p.is(null)),
|
||||
|
||||
// unused
|
||||
dead: p.any,
|
||||
|
||||
option1: p.isPositiveInteger,
|
||||
option2: p.isPositiveInteger,
|
||||
ghost: p.any,
|
||||
ghostGauge: p.any,
|
||||
|
||||
dateTime: "string",
|
||||
};
|
||||
|
||||
const PR_CG_JUBEAT = {
|
||||
internalId: p.isPositiveInteger,
|
||||
difficulty: p.isPositiveInteger,
|
||||
@@ -97,6 +121,7 @@ const PR_CG_POPN = {
|
||||
|
||||
// given a CG game, what should the returned data look like?
|
||||
const CG_SCHEMAS: Record<CGSupportedGames, PrudenceSchema> = {
|
||||
iidx: PR_CG_IIDX,
|
||||
jb: PR_CG_JUBEAT,
|
||||
msc: PR_CG_MUSECA,
|
||||
sdvx: PR_CG_SDVX,
|
||||
@@ -169,16 +194,19 @@ export function CreateCGParser<T extends { dateTime: string }>(
|
||||
};
|
||||
}
|
||||
|
||||
export const ParseCGDevIIDX = CreateCGParser<CGIIDXScore>("iidx", "dev", "api/cg-dev-iidx");
|
||||
export const ParseCGDevMuseca = CreateCGParser<CGMusecaScore>("msc", "dev", "api/cg-dev-museca");
|
||||
export const ParseCGDevSDVX = CreateCGParser<CGSDVXScore>("sdvx", "dev", "api/cg-dev-sdvx");
|
||||
export const ParseCGDevJubeat = CreateCGParser<CGJubeatScore>("jb", "dev", "api/cg-dev-jubeat");
|
||||
export const ParseCGDevPopn = CreateCGParser<CGPopnScore>("popn", "dev", "api/cg-dev-popn");
|
||||
|
||||
export const ParseCGGanIIDX = CreateCGParser<CGIIDXScore>("iidx", "dev", "api/cg-gan-iidx");
|
||||
export const ParseCGGanMuseca = CreateCGParser<CGMusecaScore>("msc", "gan", "api/cg-gan-museca");
|
||||
export const ParseCGGanSDVX = CreateCGParser<CGSDVXScore>("sdvx", "gan", "api/cg-gan-sdvx");
|
||||
export const ParseCGGanJubeat = CreateCGParser<CGJubeatScore>("jb", "gan", "api/cg-gan-jubeat");
|
||||
export const ParseCGGanPopn = CreateCGParser<CGPopnScore>("popn", "gan", "api/cg-gan-popn");
|
||||
|
||||
export const ParseCGNagIIDX = CreateCGParser<CGIIDXScore>("iidx", "dev", "api/cg-nag-iidx");
|
||||
export const ParseCGNagMuseca = CreateCGParser<CGMusecaScore>("msc", "nag", "api/cg-nag-museca");
|
||||
export const ParseCGNagSDVX = CreateCGParser<CGSDVXScore>("sdvx", "nag", "api/cg-nag-sdvx");
|
||||
export const ParseCGNagJubeat = CreateCGParser<CGJubeatScore>("jb", "nag", "api/cg-nag-jubeat");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { integer } from "tachi-common";
|
||||
|
||||
export type CGSupportedGames =
|
||||
| "iidx"
|
||||
| "jb" // jubeat
|
||||
| "msc" // museca
|
||||
| "popn"
|
||||
@@ -24,6 +25,23 @@ export interface CGErrorResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface CGIIDXScore {
|
||||
version: integer;
|
||||
internalId: integer;
|
||||
difficulty: string;
|
||||
exScore: integer;
|
||||
clearType: integer;
|
||||
perfectCount: integer;
|
||||
greatCount: integer;
|
||||
missCount: integer | null;
|
||||
dead: integer;
|
||||
ghost: string;
|
||||
ghostGauge: string;
|
||||
option1: integer;
|
||||
option2: integer;
|
||||
dateTime: string;
|
||||
}
|
||||
|
||||
export interface CGMusecaScore {
|
||||
internalId: integer;
|
||||
difficulty: integer;
|
||||
|
||||
@@ -8,6 +8,7 @@ export function CGGameToTachiGame(cgGame: CGSupportedGames): GameGroup {
|
||||
return "jubeat";
|
||||
case "msc":
|
||||
return "museca";
|
||||
case "iidx":
|
||||
case "popn":
|
||||
case "sdvx":
|
||||
return cgGame;
|
||||
|
||||
@@ -32,6 +32,7 @@ import type { LR2HookContext, LR2HookScore } from "../ir/lr2hook/types";
|
||||
import type { IRUSCContext } from "../ir/usc/types";
|
||||
import type {
|
||||
CGContext,
|
||||
CGIIDXScore,
|
||||
CGJubeatScore,
|
||||
CGMusecaScore,
|
||||
CGPopnScore,
|
||||
@@ -78,16 +79,19 @@ export interface ImportTypeDataMap {
|
||||
"api/cg-dev-museca": CGMusecaScore;
|
||||
"api/cg-dev-popn": CGPopnScore;
|
||||
"api/cg-dev-jubeat": CGJubeatScore;
|
||||
"api/cg-dev-iidx": CGIIDXScore;
|
||||
|
||||
"api/cg-nag-sdvx": CGSDVXScore;
|
||||
"api/cg-nag-museca": CGMusecaScore;
|
||||
"api/cg-nag-popn": CGPopnScore;
|
||||
"api/cg-nag-jubeat": CGJubeatScore;
|
||||
"api/cg-nag-iidx": CGIIDXScore;
|
||||
|
||||
"api/cg-gan-sdvx": CGSDVXScore;
|
||||
"api/cg-gan-museca": CGMusecaScore;
|
||||
"api/cg-gan-popn": CGPopnScore;
|
||||
"api/cg-gan-jubeat": CGJubeatScore;
|
||||
"api/cg-gan-iidx": CGIIDXScore;
|
||||
}
|
||||
|
||||
export interface ImportTypeContextMap {
|
||||
@@ -124,14 +128,17 @@ export interface ImportTypeContextMap {
|
||||
"api/cg-dev-sdvx": CGContext;
|
||||
"api/cg-dev-museca": CGContext;
|
||||
"api/cg-dev-popn": CGContext;
|
||||
"api/cg-dev-iidx": CGContext;
|
||||
"api/cg-nag-jubeat": CGContext;
|
||||
"api/cg-nag-sdvx": CGContext;
|
||||
"api/cg-nag-museca": CGContext;
|
||||
"api/cg-nag-popn": CGContext;
|
||||
"api/cg-nag-iidx": CGContext;
|
||||
"api/cg-gan-jubeat": CGContext;
|
||||
"api/cg-gan-sdvx": CGContext;
|
||||
"api/cg-gan-museca": CGContext;
|
||||
"api/cg-gan-popn": CGContext;
|
||||
"api/cg-gan-iidx": CGContext;
|
||||
}
|
||||
|
||||
export interface OrphanScoreDocument<T extends ImportTypes = ImportTypes> {
|
||||
|
||||
@@ -8,6 +8,7 @@ import ConvertAPIMytChunithm from "./api/myt-chunithm/converter";
|
||||
import ConvertAPIMytMaimaiDx from "./api/myt-maimaidx/converter";
|
||||
import ConvertAPIMytOngeki from "./api/myt-ongeki/converter";
|
||||
import ConvertAPIMytWACCA from "./api/myt-wacca/converter";
|
||||
import { ConverterAPICGIIDX } from "./common/api-cg/iidx/converter";
|
||||
import { ConverterAPICGJubeat } from "./common/api-cg/jb/converter";
|
||||
import { ConverterAPICGMuseca } from "./common/api-cg/museca/converter";
|
||||
import { ConverterAPICGPopn } from "./common/api-cg/popn/converter";
|
||||
@@ -70,12 +71,15 @@ export const Converters: ConverterMap = {
|
||||
"api/cg-dev-museca": ConverterAPICGMuseca,
|
||||
"api/cg-dev-popn": ConverterAPICGPopn,
|
||||
"api/cg-dev-jubeat": ConverterAPICGJubeat,
|
||||
"api/cg-dev-iidx": ConverterAPICGIIDX,
|
||||
"api/cg-nag-sdvx": ConverterAPICGSDVX,
|
||||
"api/cg-nag-museca": ConverterAPICGMuseca,
|
||||
"api/cg-nag-popn": ConverterAPICGPopn,
|
||||
"api/cg-nag-jubeat": ConverterAPICGJubeat,
|
||||
"api/cg-nag-iidx": ConverterAPICGIIDX,
|
||||
"api/cg-gan-sdvx": ConverterAPICGSDVX,
|
||||
"api/cg-gan-museca": ConverterAPICGMuseca,
|
||||
"api/cg-gan-popn": ConverterAPICGPopn,
|
||||
"api/cg-gan-jubeat": ConverterAPICGJubeat,
|
||||
"api/cg-gan-iidx": ConverterAPICGIIDX,
|
||||
};
|
||||
|
||||
@@ -8,14 +8,17 @@ import ParseMytMaimaiDx from "./api/myt-maimaidx/parser";
|
||||
import ParseMytOngeki from "./api/myt-ongeki/parser";
|
||||
import ParseMytWACCA from "./api/myt-wacca/parser";
|
||||
import {
|
||||
ParseCGDevIIDX,
|
||||
ParseCGDevJubeat,
|
||||
ParseCGDevMuseca,
|
||||
ParseCGDevPopn,
|
||||
ParseCGDevSDVX,
|
||||
ParseCGGanIIDX,
|
||||
ParseCGGanJubeat,
|
||||
ParseCGGanMuseca,
|
||||
ParseCGGanPopn,
|
||||
ParseCGGanSDVX,
|
||||
ParseCGNagIIDX,
|
||||
ParseCGNagJubeat,
|
||||
ParseCGNagMuseca,
|
||||
ParseCGNagPopn,
|
||||
@@ -61,16 +64,19 @@ export const Parsers = {
|
||||
"api/cg-dev-popn": ParseCGDevPopn,
|
||||
"api/cg-dev-museca": ParseCGDevMuseca,
|
||||
"api/cg-dev-jubeat": ParseCGDevJubeat,
|
||||
"api/cg-dev-iidx": ParseCGDevIIDX,
|
||||
|
||||
"api/cg-nag-sdvx": ParseCGNagSDVX,
|
||||
"api/cg-nag-popn": ParseCGNagPopn,
|
||||
"api/cg-nag-museca": ParseCGNagMuseca,
|
||||
"api/cg-nag-jubeat": ParseCGNagJubeat,
|
||||
"api/cg-nag-iidx": ParseCGNagIIDX,
|
||||
|
||||
"api/cg-gan-sdvx": ParseCGGanSDVX,
|
||||
"api/cg-gan-popn": ParseCGGanPopn,
|
||||
"api/cg-gan-museca": ParseCGGanMuseca,
|
||||
"api/cg-gan-jubeat": ParseCGGanJubeat,
|
||||
"api/cg-gan-iidx": ParseCGGanIIDX,
|
||||
|
||||
"api/myt-chunithm": ParseMytChunithm,
|
||||
"api/myt-maimaidx": ParseMytMaimaiDx,
|
||||
|
||||
@@ -636,6 +636,12 @@ export const Testing511SPA: ChartDocument<"iidx-sp"> = {
|
||||
"26-omni",
|
||||
"27",
|
||||
"26",
|
||||
"25",
|
||||
"24",
|
||||
"23",
|
||||
"22",
|
||||
"21",
|
||||
"20",
|
||||
"inf",
|
||||
"16-cs",
|
||||
"12-cs",
|
||||
@@ -643,6 +649,21 @@ export const Testing511SPA: ChartDocument<"iidx-sp"> = {
|
||||
"8-cs",
|
||||
"7-cs",
|
||||
"bmus",
|
||||
"28",
|
||||
"28-omni",
|
||||
"29",
|
||||
"27-2dxtra",
|
||||
"28-2dxtra",
|
||||
"30",
|
||||
"29-omni",
|
||||
"30-omni",
|
||||
"31",
|
||||
"30-2dxtra",
|
||||
"31-omni",
|
||||
"32",
|
||||
"31-2dxtra",
|
||||
"33",
|
||||
"32-omni",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user