diff --git a/bemani/format/iidxmusicdb.py b/bemani/format/iidxmusicdb.py index 37e7810..69c3949 100644 --- a/bemani/format/iidxmusicdb.py +++ b/bemani/format/iidxmusicdb.py @@ -17,6 +17,8 @@ class IIDXSong: """ Initialize a IIDX Song. Everything is self-explanatory except difficulties, which is a list of integers representing the difficulty for SPN, SPH, SPA, DPN, DPH, DPA. + For IIDX 27 and above, there are 4 additional charts in the difficulties list for + B7, L7, B14 and L14. """ self.id = songid self.title = title @@ -31,7 +33,7 @@ class IIDXMusicDB: def __init__(self, data: bytes) -> None: self.__songs: Dict[int, Tuple[IIDXSong, int]] = {} self.__data = data - self.__parse_db(data) + self.__version = self.__parse_db(data) def get_new_db(self) -> bytes: # Write out a new music DB based on any possible changes to songs @@ -53,86 +55,115 @@ class IIDXMusicDB: data = copy_over(data, format_string(song.genre), offset, 128) data = copy_over(data, format_string(song.artist), offset, 192) data = copy_over(data, bytes([song.folder]), offset, 280) - data = copy_over(data, bytes(song.difficulties), offset, 288) + if self.__version < 27: + # This is easy. + data = copy_over(data, bytes(song.difficulties), offset, 288) + elif self.__version >= 27: + # This is gross, but I'm too lazy to do it right. + data = copy_over(data, bytes([song.difficulties[6]]), offset, 288) + data = copy_over(data, bytes(song.difficulties[0:3]), offset, 289) + data = copy_over(data, bytes(song.difficulties[7:9]), offset, 292) + data = copy_over(data, bytes(song.difficulties[3:6]), offset, 294) + data = copy_over(data, bytes([song.difficulties[9]]), offset, 297) + return data - def __parse_db(self, data: bytes) -> None: + def __parse_string(self, string: bytes) -> str: + for i in range(len(string)): + if string[i] == 0: + string = string[:i] + break + + return string.decode("shift-jis") + + def __parse_db(self, data: bytes) -> int: # Verify the signature - sig = struct.unpack_from( - "<4s", + magic, gameversion, songcount, indexcount = struct.unpack_from( + "<4sBxxxHHxxxx", data, 0, ) - # Offset and difference lookup (not sure this is always right) - gameversion = data[4] - if gameversion == 20: - offset = 0xA420 - leap = 0x320 - elif gameversion == 21: - offset = 0xABF0 - leap = 0x320 - elif gameversion == 22: - offset = 0xB3C0 - leap = 0x340 - elif gameversion == 23: - offset = 0xBB90 - leap = 0x340 - elif gameversion == 24: - offset = 0xC360 - leap = 0x340 - elif gameversion == 25: - offset = 0xCB30 - leap = 0x340 - elif gameversion == 26: - offset = 0xD300 - leap = 0x344 - else: + + if magic != b"IIDX": + raise Exception(f"Invalid signature '{magic}' found!") + + # Stride lookup, which appears unfortunately hardcoded in the game DLL. + leap = { + 20: 0x320, + 21: 0x320, + 22: 0x340, + 23: 0x340, + 24: 0x340, + 25: 0x340, + 26: 0x344, + 27: 0x52C, + 28: 0x52C, + }.get(gameversion) + if leap is None: raise Exception(f"Unsupported game version {gameversion} found!") - if sig[0] != b"IIDX": - raise Exception(f"Invalid signature '{sig[0]}' found!") - - def parse_string(string: bytes) -> str: - for i in range(len(string)): - if string[i] == 0: - string = string[:i] - break - - return string.decode("shift-jis") + # Skip past index nodes, which are all 16-bit integers, and past 16 byte header. + offset = (indexcount * 2) + 0x10 # Load songs - while True: - try: + for songid in range(songcount): + songoffset = offset + (songid * leap) + if gameversion < 27: songdata = struct.unpack_from( "<64s64s64s64s24xB7xBBBBBB162xH", data, - offset, + songoffset, + ) + song = IIDXSong( + songid=songdata[11], + title=self.__parse_string(songdata[0]), + english_title=self.__parse_string(songdata[1]), + genre=self.__parse_string(songdata[2]), + artist=self.__parse_string(songdata[3]), + difficulties=[ + songdata[5], + songdata[6], + songdata[7], + songdata[8], + songdata[9], + songdata[10], + ], + folder=songdata[4], + ) + elif gameversion >= 27: + # Heroic Verse and above have a completely different structure for song entries + songdata = struct.unpack_from( + "<64s64s64s64s24xB7x10B646xH", + data, + songoffset, + ) + song = IIDXSong( + songid=songdata[15], + title=self.__parse_string(songdata[0]), + english_title=self.__parse_string(songdata[1]), + genre=self.__parse_string(songdata[2]), + artist=self.__parse_string(songdata[3]), + difficulties=[ + songdata[6], + songdata[7], + songdata[8], + songdata[11], + songdata[12], + songdata[13], + songdata[5], + songdata[9], + songdata[10], + songdata[14], + ], + folder=songdata[4], ) - except struct.error: - # Out of input! - break - songoffset = offset - offset = offset + leap - song = IIDXSong( - songdata[11], - parse_string(songdata[0]), - parse_string(songdata[1]), - parse_string(songdata[2]), - parse_string(songdata[3]), - [ - songdata[5], - songdata[6], - songdata[7], - songdata[8], - songdata[9], - songdata[10], - ], - songdata[4], - ) if song.artist == "event_data" and song.genre == "event_data": continue - self.__songs[songdata[11]] = (song, songoffset) + + self.__songs[song.id] = (song, songoffset) + + return gameversion @property def songs(self) -> List[IIDXSong]: