diff --git a/seeds/collections/charts-maimaidx.json b/seeds/collections/charts-maimaidx.json index 67d109790..349c15144 100644 --- a/seeds/collections/charts-maimaidx.json +++ b/seeds/collections/charts-maimaidx.json @@ -11041,6 +11041,70 @@ "prismplus" ] }, + { + "chartID": "455372326d56ff36c9a817ad9e3db862a5f6c2f0", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 10187 + }, + "difficulty": "DX Advanced", + "isPrimary": true, + "level": "7+", + "levelNum": 7.8, + "playtype": "Single", + "songID": 95, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "591ea8f3a3f15a5f75c5a7cbb33719150b6a3828", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 10187 + }, + "difficulty": "DX Basic", + "isPrimary": true, + "level": "4", + "levelNum": 4, + "playtype": "Single", + "songID": 95, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "d598fd3a830ed70bdf76b681857296330cd88b3d", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 10187 + }, + "difficulty": "DX Expert", + "isPrimary": true, + "level": "10+", + "levelNum": 10.9, + "playtype": "Single", + "songID": 95, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "283fbbe68113da289059a81c7635a2f7d0dc6db9", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 10187 + }, + "difficulty": "DX Master", + "isPrimary": true, + "level": "13", + "levelNum": 13.3, + "playtype": "Single", + "songID": 95, + "versions": [ + "prismplus" + ] + }, { "chartID": "dbf525efab45d1c9147363da9836749f8a9402e5", "data": { @@ -150633,5 +150697,133 @@ "versions": [ "prismplus" ] + }, + { + "chartID": "798bddcca4ef4fc2a0403ba0a9bd120fa45bde4e", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 11864 + }, + "difficulty": "DX Advanced", + "isPrimary": true, + "level": "6", + "levelNum": 6.5, + "playtype": "Single", + "songID": 1527, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "1fa9ffa83add19468a408f727351b5fbff5d2704", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 11864 + }, + "difficulty": "DX Basic", + "isPrimary": true, + "level": "2", + "levelNum": 2, + "playtype": "Single", + "songID": 1527, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "3199545db1e72b754ef838d4993011ad9f0ee282", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 11864 + }, + "difficulty": "DX Expert", + "isPrimary": true, + "level": "9+", + "levelNum": 9.9, + "playtype": "Single", + "songID": 1527, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "134e15e7fdd0544b5b2f4928642e7572cc7c1804", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 11864 + }, + "difficulty": "DX Master", + "isPrimary": true, + "level": "12+", + "levelNum": 12.9, + "playtype": "Single", + "songID": 1527, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "0bd5f4cb68729b5b9dbac0ef91623ffcd341059b", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 11866 + }, + "difficulty": "DX Advanced", + "isPrimary": true, + "level": "6", + "levelNum": 6, + "playtype": "Single", + "songID": 1528, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "78f2c32e5fa920b8d1da1c4e6fce0030e049b00f", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 11866 + }, + "difficulty": "DX Basic", + "isPrimary": true, + "level": "2", + "levelNum": 2, + "playtype": "Single", + "songID": 1528, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "59a97f033d5d208b48b6972075b457201e7c7f22", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 11866 + }, + "difficulty": "DX Expert", + "isPrimary": true, + "level": "9", + "levelNum": 9.2, + "playtype": "Single", + "songID": 1528, + "versions": [ + "prismplus" + ] + }, + { + "chartID": "7d697ad800d479111da3a709a45e8e9264df8ac4", + "data": { + "displayVersion": "maimaiでらっくす PRiSM PLUS", + "inGameID": 11866 + }, + "difficulty": "DX Master", + "isPrimary": true, + "level": "13", + "levelNum": 13.3, + "playtype": "Single", + "songID": 1528, + "versions": [ + "prismplus" + ] } ] \ No newline at end of file diff --git a/seeds/collections/songs-maimaidx.json b/seeds/collections/songs-maimaidx.json index e2c8627be..be37ab048 100644 --- a/seeds/collections/songs-maimaidx.json +++ b/seeds/collections/songs-maimaidx.json @@ -19553,5 +19553,33 @@ "id": 1526, "searchTerms": [], "title": "Destined Marionette" + }, + { + "altTitles": [], + "artist": "みきとP", + "data": { + "duration": 159.6, + "genre": "niconico&ボーカロイド" + }, + "id": 1527, + "searchTerms": [ + "shoujo rei", + "ghost girl" + ], + "title": "少女レイ" + }, + { + "altTitles": [], + "artist": "R Sound Design", + "data": { + "duration": 163.022, + "genre": "niconico&ボーカロイド" + }, + "id": 1528, + "searchTerms": [ + "imperial girl", + "teikoku shoujou" + ], + "title": "帝国少女" } ] \ No newline at end of file diff --git a/seeds/scripts/rerunners/maimaidx/README.md b/seeds/scripts/rerunners/maimaidx/README.md index a04012db1..a67d34f93 100644 --- a/seeds/scripts/rerunners/maimaidx/README.md +++ b/seeds/scripts/rerunners/maimaidx/README.md @@ -25,3 +25,25 @@ ts-node merge-options.ts --version --input /path/to/maimai/App/Package 4. Manually add search terms in `seeds/collections/songs-maimaidx.json`. At a minimum, songs with Japanese titles should be romanized. 5. In the root project folder, run `just seeds test`. + +## Adding a new version + +1. Add a new version in `common/src/config/game-support/maimai-dx.ts`. + - Also, add an Omnimix variant for the previous version. +2. Create tables and folders for the new version (and the omnimix version). +See `seeds/scripts/rerunners/maimaidx/add-maimaidx-table-and-folders.js`. You will have +to adjust the constants for the version you're adding. +3. Deactivate old tables using `seeds/scripts/rerunners/toggle-table-inactive.js`: + +``` +node toggle-table-inactive.js -t maimaidx-Single--levels +``` + +Keep tables for the current and last (aka n-1) versions. + +4. Switch the new table to the default one by editing `seeds/collections/tables.json`. +Change to `"default": true` for the current version, and `"default": false` for all +other tables of the game. + +5. Verify you've done everything correctly by running `just seeds test` in the root project +folder. diff --git a/seeds/scripts/rerunners/maimaidx/blacklist.txt b/seeds/scripts/rerunners/maimaidx/blacklist.txt new file mode 100644 index 000000000..11fcbabc5 --- /dev/null +++ b/seeds/scripts/rerunners/maimaidx/blacklist.txt @@ -0,0 +1,23 @@ +# List of charts to ignore when parsing game data. +# Each line is a regex, and these regexes are matched against the strings: +# - S +# - C- +# Empty lines, or lines starting with # are ignored. + +# Ignore all UTAGEs (ID >=100000) since they're unsupported on Kamaitachi. +S1\d{5}$ + +# Ignore Xaleid◆scopiX (2): event chart used to implement the KALEIDXSCOPE event, +# not normally playable. +S11879$ + +S11856$ +S11857$ +S11858$ +S11859$ +S11861$ +S11874$ +S11875$ +S11876$ +S11877$ +S11878$ diff --git a/seeds/scripts/rerunners/maimaidx/merge-options.ts b/seeds/scripts/rerunners/maimaidx/merge-options.ts index b17a25371..49a3fba66 100644 --- a/seeds/scripts/rerunners/maimaidx/merge-options.ts +++ b/seeds/scripts/rerunners/maimaidx/merge-options.ts @@ -155,32 +155,37 @@ for (const chart of existingCharts) { songTitleArtistMap.set(`${song.title}-${song.artist}`, song.id); } -const parser = new XMLParser({ - numberParseOptions: { - hex: false, - leadingZeros: false, - // do not coerce any number-like strings to numbers, since song titles - // may also be numbers. we coerce anything we know to be a number later. - skipLike: /.*/u, - }, -}); +const blacklist = readFileSync(path.join(__dirname, "blacklist.txt"), "utf-8") + .split("\n") + .filter((e) => !e.startsWith("#") && e.trim() !== "") + .map((e) => new RegExp(e, "u")); -const newSongs: Array> = []; -const newCharts: Array> = []; - -const songIDGenerator = GetFreshSongIDGenerator("maimaidx"); - -for (const optionsDir of options.input) { - for (const option of readdirSync(optionsDir)) { - if (!option.match(/^[A-Z]\d{3}$/u)) { - continue; +function isInBlacklist(str: string) { + for (const regex of blacklist) { + if (regex.exec(str)) { + return true; } + } - const optionDir = path.join(optionsDir, option); - const musicsDir = path.join(optionDir, "music"); - const soundDataDir = path.join(optionDir, "SoundData"); + return false; +} + +// Read all audio files ahead of time so the correct file is used, in case of +// audio fixes +if (options.vgmsBinary) { + for (const optionsDir of options.input) { + for (const option of readdirSync(optionsDir)) { + if (!option.match(/^[A-Z]\d{3}$/u)) { + continue; + } + + const optionDir = path.join(optionsDir, option); + const soundDataDir = path.join(optionDir, "SoundData"); + + if (!existsSync(soundDataDir)) { + continue; + } - if (options.vgmsBinary && existsSync(soundDataDir)) { for (const cueFileName of readdirSync(soundDataDir)) { if (!cueFileName.match(/music\d+\.awb$/u)) { continue; @@ -211,6 +216,32 @@ for (const optionsDir of options.input) { } } } + } +} + +const parser = new XMLParser({ + numberParseOptions: { + hex: false, + leadingZeros: false, + // do not coerce any number-like strings to numbers, since song titles + // may also be numbers. we coerce anything we know to be a number later. + skipLike: /.*/u, + }, +}); + +const newSongs: Array> = []; +const newCharts: Array> = []; + +const songIDGenerator = GetFreshSongIDGenerator("maimaidx"); + +for (const optionsDir of options.input) { + for (const option of readdirSync(optionsDir)) { + if (!option.match(/^[A-Z]\d{3}$/u)) { + continue; + } + + const optionDir = path.join(optionsDir, option); + const musicsDir = path.join(optionDir, "music"); if (!existsSync(musicsDir)) { logger.warn(`Option at ${optionDir} does not have a "music" folder.`); @@ -241,10 +272,10 @@ for (const optionsDir of options.input) { const musicData = data.MusicData; const inGameID = Number(musicData.name.id); - if (inGameID >= 100000 || inGameID === 11879) { - // Ignore UTAGE charts, which are not supported by Tachi. - // Ignore Xaleid◆scopiX (2): event chart used to implement the KALEIDXSCOPE event, - // not normally playable. + if (isInBlacklist(`S${inGameID}`)) { + logger.verbose( + `Ignored ${musicData.artistName.str} - ${musicData.name.str} (inGameID ${inGameID}) as it is in the blacklist.` + ); continue; } @@ -322,7 +353,9 @@ for (const optionsDir of options.input) { songTitleArtistMap.set(`${songDoc.title}-${songDoc.artist}`, tachiSongID); songMap.set(tachiSongID, songDoc); - logger.info(`Added new song ${songDoc.artist} - ${songDoc.title}.`); + logger.info( + `Added new song ${songDoc.artist} - ${songDoc.title} (inGameID ${inGameID}, tachiSongID ${tachiSongID}).` + ); } else { const songDoc = songMap.get(tachiSongID)!; @@ -347,6 +380,13 @@ for (const optionsDir of options.input) { ); } + if (isInBlacklist(`C${inGameID}-${difficultyName}`)) { + logger.verbose( + `Ignoring ${musicData.artistName.str} - ${musicData.name.str} [${difficultyName}] (inGameID ${inGameID}) as it is in the blacklist.` + ); + continue; + } + if (inGameID > 10000) { difficultyName = `DX ${difficultyName}`; }