import { Command } from "commander"; import { XMLParser } from "fast-xml-parser"; import { readFileSync } from "fs"; import { CreateChartID, MutateCollection, ReadCollection, WriteCollection } from "../../util"; import { ChartDocument, Difficulties, Playtypes, SongDocument, Versions } from "tachi-common"; import { GITADORA_GITA_CONF } from "tachi-common/config/game-support/gitadora"; const supportedVersions = Object.keys(GITADORA_GITA_CONF.versions); const program = new Command(); program.requiredOption("-i, --input "); program.requiredOption(`-v, --version <${supportedVersions.join("|")}>`); program.parse(process.argv); const options = program.opts(); const parser = new XMLParser({ ignoreAttributes: false }); const content = readFileSync(options.input, { encoding: "utf-8" }); // Genuinely surprised its utf-8 encoded const data = parser.parse(content); if (!supportedVersions.includes(options.version)) { console.error( `Invalid version ${options.version}. Expected any of ${supportedVersions.join(", ")}.` ); throw new Error(`Invalid version ${options.version}.`); } const version: Versions["gitadora:Gita" | "gitadora:Dora"] = options.version; interface XMLText { "#text": T; } // There's a lot of fields here that we are likely going to ignore, but it's here for reference interface Entry { music_id: XMLText; classics_diff_list: XMLText; xg_diff_list: XMLText; pad_diff: XMLText; seq_flag: XMLText; xg_seq_flag: XMLText; contain_stat: XMLText; first_ver: XMLText; first_classic_ver: XMLText; b_long: XMLText; // Boolean b_eemall: XMLText; // Boolean bpm: XMLText; bpm2: XMLText; title_ascii: XMLText; order_ascii: XMLText; order_kana: XMLText; category_kana: XMLText; artist_title_ascii: XMLText; artist_order_ascii: XMLText; artist_order_kana: XMLText; artist_category_kana: XMLText; secret: XMLText; xg_secret: XMLText; b_session: XMLText; // Boolean xg_b_session: XMLText; // Boolean speed: XMLText; life: XMLText; gf_ofst: XMLText; dm_ofst: XMLText; chart_list: XMLText; origin: XMLText; music_type: XMLText; genre: XMLText; xg_active_effect_type: XMLText; xg_movie_disp_type: XMLText; xg_movie_disp_id: XMLText; is_remaster: XMLText; // Not Boolean ? title_name: XMLText; license_disp: XMLText; default_music: XMLText; disable_area: XMLText; type_category: XMLText; data_ver: XMLText; seq_id: XMLText; is_classic_seq: XMLText; // Not Boolean ? } // Used for destructuring the difficulty byte array - see implementation for mapping const DIFFICULTIES: (Difficulties["gitadora:Gita" | "gitadora:Dora"] | null)[] = [ null, "BASIC", "ADVANCED", "EXTREME", "MASTER", null, "BASIC", "ADVANCED", "EXTREME", "MASTER", null, "BASS BASIC", "BASS ADVANCED", "BASS EXTREME", "BASS MASTER", ]; const PLAYTYPE_DIFFICULTY_MAP: (Playtypes["gitadora"] | null)[] = [ null, "Gita", "Gita", "Gita", "Gita", null, "Dora", "Dora", "Dora", "Dora", null, "Gita", "Gita", "Gita", "Gita", ]; function buildSong(entry: Entry): SongDocument<"gitadora"> { return { artist: entry.artist_title_ascii["#text"] || "", // Sometimes artist is just not there - this is valid! title: entry.title_name["#text"], id: entry.music_id["#text"], altTitles: [], searchTerms: entry.title_ascii["#text"] ? [entry.title_ascii["#text"]] : [], data: {}, }; } const buildChart = ( entry: Entry, difficulty: number, difficultyString: Difficulties["gitadora:Gita" | "gitadora:Dora"], playType: Playtypes["gitadora"] ): ChartDocument<"gitadora:Gita" | "gitadora:Dora"> => { // Comes in a 435, needs to become "4.35" const convertedDifficultyAsFloat = difficulty / 100; const convertedDifficultyAsString = convertedDifficultyAsFloat.toFixed(2); return { chartID: CreateChartID(), data: { inGameID: entry.music_id["#text"], }, difficulty: difficultyString, isPrimary: true, level: convertedDifficultyAsString, levelNum: convertedDifficultyAsFloat, playtype: playType, songID: entry.music_id["#text"], versions: [version], }; }; const songs = ReadCollection("songs-gitadora.json"); const existingChartDocs = ReadCollection("charts-gitadora.json"); const existingCharts = new Map>(); for (const chart of existingChartDocs) { existingCharts.set(`${chart.data.inGameID}-${chart.difficulty}-${chart.playtype}`, chart); } const newSongs: SongDocument<"gitadora">[] = []; const newCharts: ChartDocument<"gitadora:Gita" | "gitadora:Dora">[] = []; for (const entry of data.mdb.mdb_data as Entry[]) { // For logging purposes - song may not exist, not safe to access for logging const mid = entry.music_id["#text"]; const title = entry.title_name["#text"]; const artist = entry.artist_title_ascii["#text"] || ""; const song = songs.find((s: SongDocument<"gitadora">) => s.id === mid); if (!song) { const newSong = buildSong(entry); console.log(`New song: ${newSong.artist} - ${newSong.title} (id ${newSong.id})`); newSongs.push(newSong); } // Mode: | GUITAR | DRUMS | BASS // Diff: | BSC ADV EXT MAS | BSC ADV EXT MAS | BSC ADV EXT MAS // Array: [0, 100, 200, 300, 400, 0, 100, 200, 300, 400, 0, 100, 200, 300, 400] const splitDiff = entry.xg_diff_list["#text"].split(" "); for (let i = 0; i < 15; i++) { const diff = splitDiff[i]!; if (diff === "0") { continue; // There is no chart for this difficulty } const difficulty = DIFFICULTIES[i]; const playType = PLAYTYPE_DIFFICULTY_MAP[i]; if (!difficulty || !playType) { continue; // We're at the padding byte, and it somehow wasn't caught above - fixes types though! } const exists = existingCharts.get(`${mid}-${difficulty}-${playType}`); if (exists) { if (!exists.versions.includes(version)) { console.log( `Adding version ${version} to chart ${exists.playtype} ${exists.difficulty} of song ${song.artist} - ${song.title} (id ${mid})` ); // JS Pointer weirdness - copied from DDR code. // exists is a pointer to the existing chart, so we can mutate it. exists.versions.push(version); } } else { const newChart = buildChart(entry, parseInt(diff, 10), difficulty, playType); console.log( `New chart detected: ${newChart.playtype} ${newChart.difficulty} for song ${artist} - ${title} (id ${mid})` ); newCharts.push(newChart); } } } MutateCollection("songs-gitadora.json", (songs: Array>) => [ ...songs, ...newSongs, ]); // overwrite this collection instead of mutating it // we already know the existing chart docs and might have mutated them to // declare the new versions, or update chart constants. WriteCollection("charts-gitadora.json", [...existingChartDocs, ...newCharts]);