def srams_for_game(self, filename: str) -> List[str]: with self.__lock: # First, see if we already cached this file. if filename in self.__cache: return self.__cache[filename] valid_srams: List[str] = [] with open(filename, "rb") as fp: # First, grab the file size, see if there are any srams at all for this file. data = FileBytes(fp) # If it's a Naomi ROM, so SRAMs must be 32kb in size. rom = NaomiRom(data) if rom.valid: # Grab currently known SRAMs srams: List[str] = [] for directory in self.__directories: srams.extend( os.path.join(directory, f) for f in os.listdir(directory)) # Figure out which of these is valid for this ROM type. for sram in srams: try: size = os.path.getsize(sram) except Exception: size = 0 if size == NaomiSettingsPatcher.SRAM_SIZE: valid_srams.append(sram) self.__cache[filename] = valid_srams return valid_srams
def eeprom_info(self) -> Optional[NaomiSettingsInfo]: # Parse the ROM header so we can narrow our search. naomi = self.__rom or NaomiRom(self.__data) self.__rom = naomi # Only look at main executables. executable = naomi.main_executable for sec in executable.sections: # Constrain the search to the section that we jump to, since that will always # be where our trojan is. if executable.entrypoint >= sec.load_address and executable.entrypoint < ( sec.load_address + sec.length): if sec.length > NaomiSettingsPatcher.MAX_TROJAN_SIZE: # This can't possibly be the trojan, just skip it for speed. continue try: # Grab the old entrypoint from the existing modification since the ROM header # entrypoint will be the old trojan EXE. _, _, debug, date = get_config(self.__data, start=sec.offset, end=sec.offset + sec.length) return NaomiSettingsInfo(debug, date) except Exception: continue return None
def has_sram(self) -> bool: if self.__has_sram is None: # Need to see if there is an attached SRAM section in this exe. naomi = self.__rom or NaomiRom(self.__data) self.__rom = naomi for section in naomi.main_executable.sections: if section.load_address == self.SRAM_LOCATION and section.length == self.SRAM_SIZE: self.__has_sram = True break else: self.__has_sram = False return self.__has_sram
def get_naomi_settings( self, filename: str, settingsdata: Optional[bytes], region: NaomiRomRegionEnum = NaomiRomRegionEnum.REGION_JAPAN, patches: Optional[List[str]] = None, ) -> Tuple[Optional[NaomiSettingsWrapper], bool]: settings: Optional[NaomiSettingsWrapper] = None patches = patches or [] with open(filename, "rb") as fp: data = FileBytes(fp) # Check to make sure its not already got an SRAM section. If it # does, disallow the following section from being created. patcher = NaomiSettingsPatcher(data, get_default_trojan()) if patcher.rom.valid: # First, try to load any previously configured EEPROM. if settingsdata is not None: if len(settingsdata) != NaomiSettingsPatcher.EEPROM_SIZE: raise Exception("We don't support non-EEPROM settings!") settings = self.__naomi_manager.from_eeprom(settingsdata) else: # Second, if we didn't configure one, see if there's a previously configured # one in the ROM itself. settingsdata = patcher.get_eeprom() if settingsdata is not None: if len(settingsdata) != NaomiSettingsPatcher.EEPROM_SIZE: raise Exception("We don't support non-EEPROM settings!") settings = self.__naomi_manager.from_eeprom(settingsdata) else: # Finally, attempt to patch with any patches that fit in the first # chunk, so the defaults we get below match any force settings # patches we did to the header. for patch in patches: with open(patch, "r") as pp: differences = pp.readlines() differences = [d.strip() for d in differences if d.strip()] try: data = BinaryDiff.patch(data, differences, ignore_size_differences=True) except BinaryDiffException: # Patch was for something not in the header. pass rom = NaomiRom(data) if rom.valid: settings = self.__naomi_manager.from_rom(rom, region) return settings, settingsdata is not None
def settings_for_game(self, filename: str) -> List[str]: with self.__lock: # First, see if we already cached this file. if filename in self.__cache: return self.__cache[filename] valid_settings: List[str] = [] # Now, try to treat it as a Naomi ROM with open(filename, "rb") as fp: data = FileBytes(fp) rom = NaomiRom(data) if rom.valid: valid_settings = sorted([os.path.join(self.__naomi_directory, f) for f, _ in self.__naomi_manager.files_for_rom(rom).items()]) self.__cache[filename] = valid_settings return valid_settings
def get_sram(self) -> Optional[bytes]: # Parse the ROM header so we can narrow our search. naomi = self.__rom or NaomiRom(self.__data) self.__rom = naomi # Only look at main executables. executable = naomi.main_executable for sec in executable.sections: # Check and see if it is a SRAM chunk. if sec.load_address == self.SRAM_LOCATION and sec.length == self.SRAM_SIZE: # It is! self.__has_sram = True return self.__data[sec.offset:(sec.offset + sec.length)] # Couldn't find a section that matched. self.__has_sram = False return None
def put_sram(self, sram: bytes, *, verbose: bool = False) -> None: # First, parse the ROM we were given. naomi = self.__rom or NaomiRom(self.__data) # Now make sure the SRAM is valid. if len(sram) != self.SRAM_SIZE: raise NaomiSettingsPatcherException( "Invalid SRAM size to attach to a Naomi ROM!") # Patch the section directly onto the ROM. self.__has_sram = True self.__data = add_or_update_section(self.__data, self.SRAM_LOCATION, sram, header=naomi, verbose=verbose) # Also, write back the new ROM. self.__rom = naomi
def game_name(self, filename: str, region: CabinetRegionEnum) -> str: with self.__lock: local_key = f"{region.value}-{filename}" if local_key in self.__names: return self.__names[local_key] # Grab enough of the header for a match with open(filename, "rb") as fp: data = fp.read(0x1000) length = os.fstat(fp.fileno()).st_size # Now, check and see if we have a checksum match crc = zlib.crc32(data, 0) checksum = f"{region.value}-{crc}-{length}" if checksum in self.__checksums: self.__names[local_key] = self.__checksums[checksum] return self.__names[local_key] # Now, see if we can figure out from the header rom = NaomiRom(data) if rom.valid: # Arbitrarily choose USA region as default naomi_region = { CabinetRegionEnum.REGION_JAPAN: NaomiRomRegionEnum.REGION_JAPAN, CabinetRegionEnum.REGION_USA: NaomiRomRegionEnum.REGION_USA, CabinetRegionEnum.REGION_EXPORT: NaomiRomRegionEnum.REGION_EXPORT, CabinetRegionEnum.REGION_KOREA: NaomiRomRegionEnum.REGION_KOREA, CabinetRegionEnum.REGION_AUSTRALIA: NaomiRomRegionEnum.REGION_AUSTRALIA, }.get(region, NaomiRomRegionEnum.REGION_JAPAN) self.__names[local_key] = rom.names[naomi_region] self.__checksums[checksum] = self.__names[local_key] return self.__names[local_key] # Finally, fall back to filename, getting rid of extensions and underscores self.__names[local_key] = os.path.splitext( os.path.basename(filename))[0].replace('_', ' ') self.__checksums[checksum] = self.__names[local_key] return self.__names[local_key]
def put_eeprom(self, eeprom: bytes, *, enable_debugging: bool = False, verbose: bool = False) -> None: # First, parse the ROM we were given. naomi = self.__rom or NaomiRom(self.__data) # Now make sure the EEPROM is valid. if len(eeprom) == self.EEPROM_SIZE: # First, we need to modify the settings trojan with this ROM's load address and # the EEPROM we want to add. Make sure the EEPRom we were given is valid. if not NaomiEEPRom.validate(eeprom, serial=naomi.serial): raise NaomiSettingsPatcherException( "EEPROM is incorrectly formed!") if naomi.serial != eeprom[3:7] or naomi.serial != eeprom[21:25]: raise NaomiSettingsPatcherException( "EEPROM is not for this game!") else: raise NaomiSettingsPatcherException( "Invalid EEPROM size to attach to a Naomi ROM!") # Now we need to add an EXE init section to the ROM. if self.__trojan is None or not self.__trojan: raise NaomiSettingsPatcherException( "Cannot have an empty trojan when attaching EEPROM settings!") # Patch the trojan onto the ROM, updating the settings in the trojan accordingly. self.__data = add_or_update_trojan( self.__data, self.__trojan, 1 if enable_debugging else 0, 0, datachunk=eeprom, header=naomi, verbose=verbose, ) # Also, write back the new ROM. self.__rom = naomi self.__has_eeprom = True
def get_eeprom(self) -> Optional[bytes]: # Parse the ROM header so we can narrow our search. naomi = self.__rom or NaomiRom(self.__data) self.__rom = naomi # Only look at main executables. executable = naomi.main_executable for sec in executable.sections: # Constrain the search to the section that we jump to, since that will always # be where our trojan is. if executable.entrypoint >= sec.load_address and executable.entrypoint < ( sec.load_address + sec.length): if sec.length > NaomiSettingsPatcher.MAX_TROJAN_SIZE: # This can't possibly be the trojan, just skip it for speed. continue try: # Grab the old entrypoint from the existing modification since the ROM header # entrypoint will be the old trojan EXE. get_config(self.__data, start=sec.offset, end=sec.offset + sec.length) # Returns the requested EEPRom that should be written prior to the game starting. for i in range( sec.offset, sec.offset + sec.length - (self.EEPROM_SIZE - 1)): if NaomiEEPRom.validate( self.__data[i:(i + self.EEPROM_SIZE)], serial=naomi.serial): self.__has_eeprom = True return self.__data[i:(i + self.EEPROM_SIZE)] except Exception: pass # Couldn't find a section that matched. self.__has_eeprom = False return None
def game_name(self, filename: str, region: str) -> str: with self.__lock: if filename in self.__names: return self.__names[filename] # Grab enough of the header for a match with open(filename, "rb") as fp: data = fp.read(0x1000) length = os.fstat(fp.fileno()).st_size # Now, check and see if we have a checksum match crc = zlib.crc32(data, 0) checksum = f"{crc}-{length}" if checksum in self.__checksums: self.__names[filename] = self.__checksums[checksum] return self.__names[filename] # Now, see if we can figure out from the header rom = NaomiRom(data) if rom.valid: # Arbitrarily choose USA region as default naomi_region = { 'japan': NaomiRom.REGION_JAPAN, 'usa': NaomiRom.REGION_USA, 'export': NaomiRom.REGION_EXPORT, 'korea': NaomiRom.REGION_KOREA, 'australia': NaomiRom.REGION_AUSTRALIA, }.get(region.lower(), NaomiRom.REGION_USA) self.__names[filename] = rom.names[naomi_region] self.__checksums[checksum] = self.__names[filename] return self.__names[filename] # Finally, fall back to filename, getting rid of extensions and underscores self.__names[filename] = os.path.splitext(os.path.basename(filename))[0].replace('_', ' ') self.__checksums[checksum] = self.__names[filename] return self.__names[filename]
def main() -> int: # Create the argument parser parser = argparse.ArgumentParser( description="Utility for attaching a trojan to a commercial Naomi ROM.", ) parser.add_argument( 'bin', metavar='BIN', type=str, help='The binary file we should attach the trojan to.', ) parser.add_argument( 'exe', metavar='EXE', type=str, help= 'The executable binary blob we should attach to the end of the commercial ROM.', ) parser.add_argument( '--offset', type=str, default='0x0c021000', help= 'Where to attach the springboard, in main memory. This is the hook for the trojan.', ) parser.add_argument( '--output-file', metavar='BIN', type=str, help= 'A different file to output to instead of updating the binary specified directly.', ) # Grab what we're doing args = parser.parse_args() # Grab the rom, parse it with open(args.bin, "rb") as fp: data = fp.read() naomi = NaomiRom(data) # Grab the attachment. This should be an executable binary blob. The easiest way to get # one of these is to compile a program using the toolchain in the homebrew/ directory, # and then copy out the naomi.bin file from the build/ directory and use that. Note that # in order to use this method, you will want to change the two executable start # addresses in naomi.ld to 0x0D000000 or any non-relative jumps will go to the wrong # code. Note also that if the game uses this address, you will run into trouble. You # might need to code a different springboard to a safe memory region if you run into # trouble using 0x0D000000. with open(args.exe, "rb") as fp: exe = fp.read() # Grab the springboard file. This was made by compiling the following # SH-4 code and then extracting the executable bytes. """ .section .text .globl start start: # Jump to location 0x0D000000 mov #13,r0 shll16 r0 shll8 r0 jmp @r0 nop """ springfile = bytes( [0x0d, 0xe0, 0x28, 0x40, 0x18, 0x40, 0x2b, 0x40, 0x09, 0x00]) # We need to add a EXE init section to the ROM executable = naomi.main_executable patchoffset = executable.sections[0].load_address - executable.sections[ 0].offset if len(executable.sections) >= 8: print("ROM already has the maximum number of init sections!", file=sys.stderr) return 1 for sec in executable.sections: if sec.load_address == 0x0D000000: print("ROM already is trojan'd, cowardly giving up!", file=sys.stderr) return 1 # Add a new section to the end of the rom for this binary data executable.sections.append( NaomiRomSection( offset=len(data), load_address=0x0D000000, length=len(exe), )) naomi.main_executable = executable # Now, just append it to the end of the file newdata = change( naomi.data + data[naomi.HEADER_LENGTH:] + exe, springfile, int(args.offset, 16) - patchoffset, ) if args.output_file: print(f"Added trojan to the end of {args.output_file}.", file=sys.stderr) with open(args.output_file, "wb") as fp: fp.write(newdata) else: print(f"Added trojan to the end of {args.bin}.", file=sys.stderr) with open(args.bin, "wb") as fp: fp.write(newdata) return 0
def main() -> int: # Create the argument parser parser = argparse.ArgumentParser(description=( "Utility for attaching an SRAM dump to an Atomiswave conversion Naomi ROM. " "Use this to set up preferred settings in an emulator, and then send those " "settings to your Naomi when you netboot."), ) parser.add_argument( 'bin', metavar='BIN', type=str, help='The binary file we should attach the SRAM to.', ) parser.add_argument( 'sram', metavar='SRAM', type=str, help='The SRAM file we should attach to the binary.', ) parser.add_argument( '--output-file', metavar='BIN', type=str, help= 'A different file to output to instead of updating the binary specified directly.', ) # Grab what we're doing args = parser.parse_args() # Grab the rom, parse it with open(args.bin, "rb") as fp: data = fp.read() naomi = NaomiRom(data) # Grab the SRAM with open(args.sram, "rb") as fp: sram = fp.read() if len(sram) != SRAM_SIZE: print(f"SRAM file is not the right size, should be {SRAM_SIZE} bytes!", file=sys.stderr) return 1 # First, find out if there's already an SRAM portion to the file executable = naomi.main_executable for section in executable.sections: if section.load_address == SRAM_LOCATION: # This is a SRAM load chunk if section.length != SRAM_SIZE: print("Found SRAM init section, but it is the wrong size!", file=sys.stderr) return 1 # We can just update the data to overwrite this section newdata = data[:section.offset] + sram + data[(section.offset + section.length):] break else: # We need to add a SRAM init section to the ROM if len(executable.sections) >= 8: print("ROM already has the maximum number of init sections!", file=sys.stderr) return 1 # Add a new section to the end of the rom for this SRAM section executable.sections.append( NaomiRomSection( offset=len(data), load_address=SRAM_LOCATION, length=SRAM_SIZE, )) naomi.main_executable = executable # Now, just append it to the end of the file newdata = naomi.data + data[naomi.HEADER_LENGTH:] + sram if args.output_file: print(f"Added SRAM init to the end of {args.output_file}.", file=sys.stderr) with open(args.output_file, "wb") as fp: fp.write(newdata) else: print(f"Added SRAM init to the end of {args.bin}.", file=sys.stderr) with open(args.bin, "wb") as fp: fp.write(newdata) return 0
def main() -> int: # Create the argument parser parser = argparse.ArgumentParser( description= "Command-Line Utility for patching different game defaults into a Naomi ROM.", ) parser.add_argument( 'rom', metavar='ROM', type=str, help='The ROM we should generate a patch for.', ) parser.add_argument( 'eeprom', metavar='EEPROM', type=str, help='The EEPROM settings file we should use to generate the patch.', ) parser.add_argument( '--output-file', metavar='BIN', type=str, default=None, help= 'A different file to output to instead of updating the ROM specified directly.', ) parser.add_argument( '--patch-file', metavar='PATCH', type=str, default=None, help='Write changed bytes to a patch instead of generating a new ROM.', ) parser.add_argument( '--settings-directory', metavar='DIR', type=str, default=os.path.join(root, 'naomi', 'settings', 'definitions'), help= 'The directory containing settings definition files. Defaults to %(default)s.', ) # Grab what we're doing args = parser.parse_args() if args.output_file and args.patch_file: raise Exception("Cannot write both a patch and a new ROM!") # First, try to open the EEPRom file. with open(args.eeprom, "rb") as fp: eeprom = NaomiEEPRom(fp.read()) manager = NaomiSettingsManager(args.settings_directory) defaults = manager.from_serial(eeprom.serial) defaulteeprom = NaomiEEPRom(manager.to_eeprom(defaults)) with open(args.rom, "rb" if args.output_file else "rb+") as fp: data = FileBytes(cast(BinaryIO, fp)) original = data.clone() rom = NaomiRom(data) defaultbytes = defaulteeprom.game.data updatedbytes = eeprom.game.data if len(defaultbytes) != len(updatedbytes): raise Exception("EEPROM sections aren't the same length!") for exe in [rom.main_executable, rom.test_executable]: for section in exe.sections: start = section.offset end = section.offset + section.length print(f"Searching {start} to {end}...") while True: found = data.search(defaultbytes, start=start, end=end) if found is not None: print(f"Patching offset {found}!") data[found:(found + len(updatedbytes))] = updatedbytes start = found + 1 else: # Done! break if args.patch_file: print( f"Generating EEPROM settings patch and writing to {args.patch_file}." ) changes = [ "# Description: patch default game settings", *BinaryDiff.diff(original, data) ] with open(args.patch_file, "w") as fps: fps.write(os.linesep.join(changes) + os.linesep) else: if args.output_file: print( f"Patched default game EEPROM settings to {args.output_file}." ) with open(args.output_file, "wb") as fp: data.write_changes(fp) else: print(f"Patched default game EEPROM settings to {args.rom}.") data.write_changes() return 0
def main() -> int: # Create the argument parser parser = argparse.ArgumentParser( description="Utility for printing information about a ROM file.", ) parser.add_argument( 'bin', metavar='BIN', type=str, help='The binary file we should generate info for.', ) # Grab what we're doing args = parser.parse_args() # Grab the rom, parse it with open(args.bin, "rb") as fp: data = FileBytes(fp) # Create a text LUT region_lut: Dict[NaomiRomRegionEnum, str] = { NaomiRomRegionEnum.REGION_JAPAN: "Japan", NaomiRomRegionEnum.REGION_USA: "USA", NaomiRomRegionEnum.REGION_EXPORT: "Export", NaomiRomRegionEnum.REGION_KOREA: "Korea", NaomiRomRegionEnum.REGION_AUSTRALIA: "Australia", } # First, assume its a Naomi ROM naomi = NaomiRom(data) if naomi.valid: print("NAOMI ROM") print("=========") print(f"Publisher: {naomi.publisher}") print( f"Japan Title: {naomi.names[NaomiRomRegionEnum.REGION_JAPAN]}" ) print( f"USA Title: {naomi.names[NaomiRomRegionEnum.REGION_USA]}" ) print( f"Export Title: {naomi.names[NaomiRomRegionEnum.REGION_EXPORT]}" ) print( f"Korea Title: {naomi.names[NaomiRomRegionEnum.REGION_KOREA]}" ) print( f"Australia Title: {naomi.names[NaomiRomRegionEnum.REGION_AUSTRALIA]}" ) print(f"Publish Date: {naomi.date}") print(f"Serial Number: {naomi.serial.decode('ascii')}") print(f"ROM Size: {len(data)} bytes") print("") print("Supported Configurations") print("------------------------") print( f"Regions: {', '.join(region_lut[r] for r in naomi.regions)}" ) print( f"Players: {', '.join(str(p) for p in naomi.players)}") print( f"Monitor: {', '.join(str(f) + 'khz' for f in naomi.frequencies)}" ) print( f"Orientation: {', '.join(o for o in naomi.orientations)}") print(f"Service Type: {naomi.servicetype}") print("") print("Main Executable Sections") print("------------------------") for section in naomi.main_executable.sections: print(f"ROM Offset: {hex(section.offset)}") print(f"Memory Offset: {hex(section.load_address)}") print(f"Section Length: {section.length} bytes") print("") print(f"Entrypoint: {hex(naomi.main_executable.entrypoint)}") print("") print("Test Executable Sections") print("------------------------") for section in naomi.test_executable.sections: print(f"ROM Offset: {hex(section.offset)}") print(f"Memory Offset: {hex(section.load_address)}") print(f"Section Length: {section.length} bytes") print("") print(f"Entrypoint: {hex(naomi.test_executable.entrypoint)}") print("") print("Per-Region EEPROM Defaults") print("--------------------------") for region, default in naomi.defaults.items(): print(f"{region_lut[region]}") if not default.apply_settings: print("Override: disabled") else: print("Override: enabled") print( f"Force vertical: {'yes' if default.force_vertical else 'no'}" ) print( f"Force silent: {'yes' if default.force_silent else 'no'}" ) print(f"Chute type: {default.chute}") if default.coin_setting < 27: setting = f"#{default.coin_setting}" elif default.coin_setting == 27: setting = "free play" elif default.coin_setting == 28: setting = "manual assignment" print(f"Coin setting: {setting}") if default.coin_setting == 28: print(f"Coin 1 rate: {default.coin_1_rate}") print(f"Coin 2 rate: {default.coin_2_rate}") print(f"Credit rate: {default.credit_rate}") print(f"Bonus: {default.bonus}") for i, text in enumerate(default.sequences): print(f"Sequence {i + 1}: {text}") print("") return 0 # Couldn't figure out ROM type print("Couldn't determine ROM type!", file=sys.stderr) return 1
def rom(self) -> NaomiRom: # Grab the entire rom as a parsed structure. naomi = self.__rom or NaomiRom(self.__data) self.__rom = naomi return naomi
def serial(self) -> bytes: # Parse the ROM header so we can grab the game serial code. naomi = self.__rom or NaomiRom(self.__data) self.__rom = naomi return naomi.serial
def add_or_update_trojan( data: Union[bytes, FileBytes], trojan: bytes, debug: int, options: int, datachunk: Optional[bytes] = None, header: Optional[NaomiRom] = None, verbose: bool = False, ) -> Union[bytes, FileBytes]: # Note that if an external header is supplied, it will be modified to match the updated # data. if isinstance(data, FileBytes): data = data.clone() if header is None: header = NaomiRom(data) # Grab a safe-to-mutate cop of the trojan, get its current config. executable = header.main_executable exe = trojan[:] _, location, _, _ = get_config(exe) for sec in executable.sections: if sec.load_address == location: # Grab the old entrypoint from the existing modification since the ROM header # entrypoint will be the old trojan EXE. entrypoint, _, _, _ = get_config(data, start=sec.offset, end=sec.offset + sec.length) exe = patch_bytesequence(exe, 0xAA, struct.pack("<I", entrypoint)) if datachunk: exe = patch_bytesequence(exe, 0xBB, datachunk) exe = patch_bytesequence(exe, 0xCF, struct.pack("<I", options)) exe = patch_bytesequence(exe, 0xDD, struct.pack("<I", debug)) # We can reuse this section, but first we need to get rid of the old patch. if sec.offset + sec.length == len(data): # We can just stick the new file right on top of where the old was. if verbose: print("Overwriting old section in existing ROM section.") # Cut off the old section, add our new section, make sure the length is correct. if isinstance(data, bytes): data = data[:sec.offset] + exe elif isinstance(data, FileBytes): data.truncate(sec.offset) data.append(exe) else: raise Exception("Logic error!") sec.length = len(exe) else: # It is somewhere in the middle of an executable, zero it out and # then add this section to the end of the ROM. if verbose: print( "Zeroing out old section in existing ROM section and attaching new section to the end of the file." ) # Patch the executable with the correct settings and entrypoint. data = change(data, b"\0" * sec.length, sec.offset) # Repoint the section at the new section sec.offset = len(data) sec.length = len(exe) sec.load_address = location # Add the section to the end of the ROM. data = data + exe break else: if len(executable.sections) >= 8: raise NaomiSettingsPatcherException( "ROM already has the maximum number of init sections!") # Add a new section to the end of the rom for this binary data. if verbose: print( "Attaching section to a new ROM section at the end of the file." ) executable.sections.append( NaomiRomSection( offset=len(data), load_address=location, length=len(exe), )) # Patch the executable with the correct settings and entrypoint. exe = patch_bytesequence(exe, 0xAA, struct.pack("<I", executable.entrypoint)) if datachunk: exe = patch_bytesequence(exe, 0xBB, datachunk) exe = patch_bytesequence(exe, 0xCF, struct.pack("<I", options)) exe = patch_bytesequence(exe, 0xDD, struct.pack("<I", debug)) data = data + exe # Generate new header and attach executable to end of data. executable.entrypoint = location header.main_executable = executable # Now, return the updated ROM with the new header and appended data. if isinstance(data, bytes): return header.data + data[header.HEADER_LENGTH:] elif isinstance(data, FileBytes): data[:header.HEADER_LENGTH] = header.data return data else: raise Exception("Logic error!")
def main() -> int: parser = argparse.ArgumentParser( description= "Provide an on-target menu for selecting games. Currently only works with Naomi." ) parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) parser.add_argument( "romdir", metavar="ROMDIR", type=str, default=os.path.join(root, 'roms'), help= 'The directory of ROMs to select a game from. Defaults to %(default)s.', ) parser.add_argument( '--region', metavar="REGION", type=str, default=None, help= 'The region of the Naomi which we are running the menu on. Defaults to "japan".', ) parser.add_argument( '--exe', metavar='EXE', type=str, default=os.path.join(root, 'homebrew', 'netbootmenu', 'netbootmenu.bin'), help= 'The menu executable that we should send to display games on the Naomi. Defaults to %(default)s.', ) parser.add_argument( '--menu-settings-file', metavar='SETTINGS', type=str, default=os.path.join(root, '.netdimm_menu_settings.yaml'), help= 'The settings file we will use to store persistent settings. Defaults to %(default)s.', ) parser.add_argument( "--patchdir", metavar="PATCHDIR", type=str, default=os.path.join(root, 'patches'), help= 'The directory of patches we might want to apply to games. Defaults to %(default)s.', ) parser.add_argument( '--force-analog', action="store_true", help= "Force-enable analog control inputs. Use this if you have no digital controls and cannot set up analog options in the test menu.", ) parser.add_argument( '--force-players', type=int, default=0, help= "Force set the number of players for this cabinet. Valid values are 1-4. Use this if you do not want to set the player number in the system test menu.", ) parser.add_argument( '--force-use-filenames', action="store_true", help= "Force-enable using filenames for ROM display instead of the name in the ROM.", ) parser.add_argument( '--persistent', action="store_true", help= "Don't exit after successfully booting game. Instead, wait for power cycle and then send the menu again.", ) parser.add_argument( '--debug-mode', action="store_true", help="Enable extra debugging information on the Naomi.", ) parser.add_argument( '--fallback-font', metavar="FILE", type=str, default=None, help= "Any truetype font that should be used as a fallback if the built-in font can't render a character.", ) parser.add_argument( '--verbose', action="store_true", help="Display verbose debugging information.", ) args = parser.parse_args() verbose = args.verbose # Load the settings file settings = settings_load(args.menu_settings_file, args.ip) if args.region is not None: region = { "japan": NaomiRomRegionEnum.REGION_JAPAN, "usa": NaomiRomRegionEnum.REGION_USA, "export": NaomiRomRegionEnum.REGION_EXPORT, "korea": NaomiRomRegionEnum.REGION_KOREA, }.get(args.region, NaomiRomRegionEnum.REGION_JAPAN) settings.system_region = region settings_save(args.menu_settings_file, args.ip, settings) if args.force_analog: # Force the setting on, as a safeguard against cabinets that have no digital controls. settings.enable_analog = True settings_save(args.menu_settings_file, args.ip, settings) if args.force_use_filenames: settings.use_filenames = True settings_save(args.menu_settings_file, args.ip, settings) force_players = None if args.force_players is not None: if args.force_players >= 1 and args.force_players <= 4: force_players = args.force_players # Intentionally rebuild the menu every loop if we are in persistent mode, so that # changes to the ROM directory can be reflected on subsequent menu sends. while True: # First, load the rom directory, list out the contents and figure out which ones are naomi games. games: List[Tuple[str, str, bytes]] = [] romdir = os.path.abspath(args.romdir) success: bool = True for filename in [ f for f in os.listdir(romdir) if os.path.isfile(os.path.join(romdir, f)) ]: # Grab the header so we can parse it. with open(os.path.join(romdir, filename), "rb") as fp: data = FileBytes(fp) if verbose: print(f"Discovered file {filename}.") # Validate that it is a Naomi ROM. if len(data) < NaomiRom.HEADER_LENGTH: if verbose: print("Not long enough to be a ROM!") continue rom = NaomiRom(data) if not rom.valid: if verbose: print("Not a Naomi ROM!") continue # Get the name of the game. if settings.use_filenames: name = os.path.splitext(filename)[0].replace("_", " ") else: name = rom.names[settings.system_region] serial = rom.serial if verbose: print( f"Added {name} with serial {serial.decode('ascii')} to ROM list." ) games.append((os.path.join(romdir, filename), name, serial)) # Alphabetize them. games = sorted(games, key=lambda g: g[1]) # Now, create the settings section. last_game_id: int = 0 gamesconfig = b"" for index, (filename, name, serial) in enumerate(games): namebytes = name.encode('utf-8')[:127] while len(namebytes) < 128: namebytes = namebytes + b"\0" gamesconfig += namebytes + serial + struct.pack("<I", index) if filename == settings.last_game_file: last_game_id = index fallback_data = None if args.fallback_font is not None: with open(args.fallback_font, "rb") as fp: fallback_data = fp.read() config = struct.pack( "<IIIIIIIIBBBBBBBBBBBBIII", SETTINGS_SIZE, len(games), 1 if settings.enable_analog else 0, 1 if args.debug_mode else 0, last_game_id, settings.system_region.value, 1 if settings.use_filenames else 0, 1 if settings.disable_sound else 0, settings.joy1_calibration[0], settings.joy1_calibration[1], settings.joy2_calibration[0], settings.joy2_calibration[1], settings.joy1_calibration[2], settings.joy1_calibration[3], settings.joy1_calibration[4], settings.joy1_calibration[5], settings.joy2_calibration[2], settings.joy2_calibration[3], settings.joy2_calibration[4], settings.joy2_calibration[5], SETTINGS_SIZE + len(gamesconfig) if fallback_data is not None else 0, len(fallback_data) if fallback_data is not None else 0, force_players if (force_players is not None) else 0, ) if len(config) < SETTINGS_SIZE: config = config + (b"\0" * (SETTINGS_SIZE - len(config))) config = config + gamesconfig if fallback_data is not None: config = config + fallback_data # Now, load up the menu ROM and append the settings to it. if success: with open(args.exe, "rb") as fp: menudata = add_or_update_section(FileBytes(fp), 0x0D000000, config, verbose=verbose) try: # Now, connect to the net dimm, send the menu and then start communicating with it. print("Connecting to net dimm...") netdimm = NetDimm(args.ip, log=print if verbose else None) print("Sending menu to net dimm...") netdimm.send(menudata, disable_crc_check=True) netdimm.reboot() except NetDimmException: # Mark failure so we don't try to communicate below. success = False if args.persistent: print("Sending failed, will try again...") else: print("Sending failed...") # Now, talk to the net dimm and exchange packets to handle settings and game selection. selected_file = None if success: print("Talking to net dimm to wait for ROM selection...") time.sleep(5) last_game_selection: Optional[int] = None last_game_patches: List[Tuple[str, str]] = [] last_game_parsed_settings: Optional[NaomiSettingsWrapper] = None try: # Always show game send progress. netdimm = NetDimm(args.ip, log=print) with netdimm.connection(): while True: msg = receive_message(netdimm, verbose=verbose) if msg: if msg.id == MESSAGE_SELECTION: index = struct.unpack("<I", msg.data)[0] filename = games[index][0] print( f"Requested {games[index][1]} be loaded..." ) # Save the menu position. settings.last_game_file = filename settings_save(args.menu_settings_file, args.ip, settings) # Wait a second for animation on the Naomi. This assumes that the # below section takes a relatively short amount of time (well below # about 1 second) to patch and such. If you are on a platform with # limited speed and attempting to do extra stuff such as unzipping, # this can fail. It is recommended in this case to spawn off a new # thread that sends a MESSAGE_UNPACK_PROGRESS with no data once a # second starting directly after this time.sleep() call. When you # are finished patching and ready to send, kill the thread before # the MESSAGE_LOAD_PROGRESS message is sent below and then let the # message send normally. time.sleep(1.0) # First, grab a handle to the data itself. fp = open(filename, "rb") gamedata = FileBytes(fp) gamesettings = settings.game_settings.get( filename, GameSettings.default()) # Now, patch with selected patches. patchman = PatchManager([args.patchdir]) for patchfile in gamesettings.enabled_patches: print( f"Applying patch {patchman.patch_name(patchfile)} to game..." ) with open(patchfile, "r") as pp: differences = pp.readlines() differences = [ d.strip() for d in differences if d.strip() ] try: gamedata = BinaryDiff.patch( gamedata, differences) except Exception as e: print( f"Could not patch {filename} with {patchfile}: {str(e)}", file=sys.stderr) # Now, attach any eeprom settings. if gamesettings.force_settings and gamesettings.eeprom is not None: print( f"Applying EEPROM settings to {filename}..." ) patcher = NaomiSettingsPatcher( gamedata, get_default_trojan()) try: patcher.put_eeprom( gamesettings.eeprom, enable_debugging=args.debug_mode, verbose=verbose, ) gamedata = patcher.data except Exception as e: print( f"Could not apply EEPROM settings to {filename}: {str(e)}", file=sys.stderr) # Finally, send it! send_message(netdimm, Message( MESSAGE_LOAD_PROGRESS, struct.pack( "<ii", len(gamedata), 0)), verbose=verbose) selected_file = gamedata break elif msg.id == MESSAGE_LOAD_SETTINGS: index = struct.unpack("<I", msg.data)[0] filename = games[index][0] print( f"Requested settings for {games[index][1]}..." ) send_message(netdimm, Message(MESSAGE_LOAD_SETTINGS_ACK, msg.data), verbose=verbose) # Grab the configured settings for this game. gamesettings = settings.game_settings.get( filename, GameSettings.default()) last_game_selection = index # First, gather up the patches which might be applicable. patchman = PatchManager([args.patchdir]) patchfiles = patchman.patches_for_game( filename) patches = sorted([(p, patchman.patch_name(p)) for p in patchfiles], key=lambda p: p[1]) last_game_patches = patches # Grab any EEPROM settings which might be applicable. parsedsettings = None with open(filename, "rb") as fp: data = FileBytes(fp) eepromdata = gamesettings.eeprom if eepromdata is None: # Possibly they edited the ROM directly, still let them edit the settings. patcher = NaomiSettingsPatcher( data, get_default_trojan()) if patcher.has_eeprom: eepromdata = patcher.get_eeprom() manager = NaomiSettingsManager( get_default_settings_directory()) if eepromdata is None: # We need to make them up from scratch. parsedsettings = manager.from_rom( patcher.rom, region=settings.system_region) else: # We have an eeprom to edit. parsedsettings = manager.from_eeprom( eepromdata) # Now, create the message back to the Naomi. response = struct.pack("<IB", index, len(patches)) for patch in patches: response += struct.pack( "<B", 1 if (patch[0] in gamesettings.enabled_patches) else 0) patchname = patch[1].encode('utf-8')[:255] response += struct.pack( "<B", len(patchname)) + patchname def make_setting( setting: Setting, setting_map: Dict[str, int]) -> bytes: if setting.read_only is True: # We don't encode this setting since its not visible. return struct.pack( "<BI", 0, setting.current or setting.default or 0) settingname = setting.name.encode( 'utf-8')[:255] if len(settingname) == 0: # We can't display this setting, it has no name! return struct.pack( "<BI", 0, setting.current or setting.default or 0) settingdata = struct.pack( "<B", len(settingname)) + settingname if setting.values is not None: settingdata += struct.pack( "<I", len(setting.values)) for val, label in setting.values.items( ): settingdata += struct.pack( "<I", val) valname = label.encode( 'utf-8')[:255] settingdata += struct.pack( "<B", len(valname)) + valname else: settingdata += struct.pack("<I", 0) settingdata += struct.pack( "<I", setting.current or setting.default or 0) if setting.read_only is False: settingdata += struct.pack( "<i", READ_ONLY_NEVER) elif isinstance(setting.read_only, ReadOnlyCondition): settingdata += struct.pack( "<iII", setting_map[ setting.read_only.name], 1 if setting.read_only.negate else 0, len(setting.read_only.values)) for val in setting.read_only.values: settingdata += struct.pack( "<I", val) else: raise Exception("Logic error!") return settingdata if parsedsettings is not None: # Remember the settings we parsed so we can save them later. last_game_parsed_settings = parsedsettings # Now add data for the force settings toggle. totalsettings = len( parsedsettings.system.settings) + len( parsedsettings.game.settings) response += struct.pack( "<B", 1 if (totalsettings > 0 and gamesettings.force_settings) else 0) # Construct system settings. response += struct.pack( "<B", len(parsedsettings.system.settings)) for setting in parsedsettings.system.settings: response += make_setting( setting, { s.name: i for (i, s) in enumerate(parsedsettings. system.settings) }) # Construct game settings response += struct.pack( "<B", len(parsedsettings.game.settings)) for setting in parsedsettings.game.settings: response += make_setting( setting, { s.name: i for (i, s) in enumerate(parsedsettings.game. settings) }) else: # This game has a SRAM chunk attached (atomiswave game), don't try to send settings. response += struct.pack("<BBB", 0, 0, 0) # Send settings over. send_message(netdimm, Message( MESSAGE_LOAD_SETTINGS_DATA, response), verbose=verbose) elif msg.id == MESSAGE_SAVE_SETTINGS_DATA: index, patchlen = struct.unpack( "<IB", msg.data[0:5]) msgdata = msg.data[5:] if index == last_game_selection: filename = games[index][0] gamesettings = settings.game_settings.get( filename, GameSettings.default()) last_game_selection = None print( f"Received updated settings for {games[index][1]}..." ) # Grab the updated patches. if patchlen > 0: patches_enabled = list( struct.unpack( "<" + ("B" * patchlen), msgdata[0:(1 * patchlen)])) msgdata = msgdata[(1 * patchlen):] if patchlen == len(last_game_patches): new_patches: Set[str] = set() for i in range(patchlen): if patches_enabled[i] != 0: new_patches.add( last_game_patches[i] [0]) gamesettings.enabled_patches = new_patches last_game_patches = [] # Grab system settings. force_settings, settinglen = struct.unpack( "<BB", msgdata[0:2]) msgdata = msgdata[2:] if settinglen > 0: settings_values = list( struct.unpack( "<" + ("I" * settinglen), msgdata[0:(4 * settinglen)])) msgdata = msgdata[(4 * settinglen):] if last_game_parsed_settings is not None: if len(settings_values) == len( last_game_parsed_settings. system.settings): for i, setting in enumerate( last_game_parsed_settings .system.settings): setting.current = settings_values[ i] # Grab game settings. settinglen = struct.unpack( "<B", msgdata[0:1])[0] msgdata = msgdata[1:] if settinglen > 0: settings_values = list( struct.unpack( "<" + ("I" * settinglen), msgdata[0:(4 * settinglen)])) msgdata = msgdata[(4 * settinglen):] if last_game_parsed_settings is not None: if len(settings_values) == len( last_game_parsed_settings. game.settings): for i, setting in enumerate( last_game_parsed_settings .game.settings): setting.current = settings_values[ i] if last_game_parsed_settings is not None: manager = NaomiSettingsManager( get_default_settings_directory()) gamesettings.eeprom = manager.to_eeprom( last_game_parsed_settings) gamesettings.force_settings = force_settings != 0 else: gamesettings.force_settings = False last_game_parsed_settings = None # Save the final updates. settings.game_settings[ filename] = gamesettings settings_save(args.menu_settings_file, args.ip, settings) send_message( netdimm, Message(MESSAGE_SAVE_SETTINGS_ACK), verbose=verbose) elif msg.id == MESSAGE_SAVE_CONFIG: if len(msg.data) == SETTINGS_SIZE: ( _, _, analogsetting, _, _, regionsetting, filenamesetting, soundsetting, *rest, ) = struct.unpack("<IIIIIIIIBBBBBBBBBBBB", msg.data[:44]) print("Requested configuration save...") joy1 = [ rest[0], rest[1], rest[4], rest[5], rest[6], rest[7] ] joy2 = [ rest[2], rest[3], rest[8], rest[9], rest[10], rest[11] ] settings.enable_analog = analogsetting != 0 settings.use_filenames = filenamesetting != 0 settings.system_region = NaomiRomRegionEnum( regionsetting) settings.disable_sound = soundsetting != 0 settings.joy1_calibration = joy1 settings.joy2_calibration = joy2 settings_save(args.menu_settings_file, args.ip, settings) send_message( netdimm, Message(MESSAGE_SAVE_CONFIG_ACK), verbose=verbose) elif msg.id == MESSAGE_HOST_STDOUT: print(msg.data.decode('utf-8'), end="") elif msg.id == MESSAGE_HOST_STDERR: print(msg.data.decode('utf-8'), end="", file=sys.stderr) except NetDimmException: # Mark failure so we don't try to wait for power down below. success = False if args.persistent: print("Communicating failed, will try again...") else: print("Communicating failed...") if success and selected_file is not None: try: # Always show game send progress. netdimm = NetDimm(args.ip, log=print) # Only want to send so many progress packets. old_percent = -1 old_time = time.time() def progress_callback(loc: int, size: int) -> None: nonlocal old_percent nonlocal old_time new_percent = int((loc / size) * 100) new_time = time.time() if new_percent != old_percent or (new_time - old_time) > 2.0: write_scratch1_register(netdimm, loc) old_percent = new_percent old_time = new_time # Finally, send it!. netdimm.send(selected_file, disable_crc_check=True, disable_now_loading=True, progress_callback=progress_callback) netdimm.reboot() # And clean up. selected_file.handle.close() selected_file = None except NetDimmException as e: # Mark failure so we don't try to wait for power down below. print(str(e)) success = False if args.persistent: print("Sending game failed, will try again...") else: print("Sending game failed...") if args.persistent: if success: # Wait for cabinet to disappear again before we start the process over. print( "Waiting for cabinet to be power cycled to resend menu...") failure_count: int = 0 on_windows: bool = platform.system() == "Windows" while True: # Constantly ping the net dimm to see if it is still alive. with open(os.devnull, 'w') as DEVNULL: try: if on_windows: call = ["ping", "-n", "1", "-w", "1", args.ip] else: call = ["ping", "-c1", "-W1", args.ip] subprocess.check_call(call, stdout=DEVNULL, stderr=DEVNULL) alive = True except subprocess.CalledProcessError: alive = False # We start with the understanding that the host is up, but if we # miss a ping its not that big of a deal. We just want to know that # we missed multiple pings as that tells us the host is truly gone. if alive: failure_count = 0 else: failure_count += 1 if failure_count >= 5: # We failed 5 pings in a row, so let's assume the host is # dead. break time.sleep(2.0 if failure_count == 0 else 1.0) # Now, wait for the cabinet to come back so we can send the menu again. print("Waiting for cabinet to be ready to receive the menu...") while True: try: netdimm = NetDimm(args.ip, log=print if verbose else None) info = netdimm.info() if info is not None: break except NetDimmException: # Failed to talk to the net dimm, its still down. pass else: # We sent the game, now exit! break return 0
def main() -> int: # Create the argument parser parser = argparse.ArgumentParser( description= "Utility for creating a Naomi ROM out of various parameters and sections.", ) parser.add_argument( 'bin', metavar='BIN', type=str, help='The binary file we should generate.', ) parser.add_argument( '-s', '--serial', type=str, default=None, help='Three digit ascii serial code for this game.', ) parser.add_argument( '-p', '--publisher', type=str, default=None, help='The publisher of this ROM.', ) parser.add_argument( '-t', '--title', type=str, default=None, help= ('The name of this ROM. Use this instead of individual ROM region titles ' 'to globally set the name of the ROM.'), ) parser.add_argument( '--title.japan', dest="title_japan", type=str, default=None, help='The name of this ROM in Japanese region.', ) parser.add_argument( '--title.usa', dest="title_usa", type=str, default=None, help='The name of this ROM in USA region.', ) parser.add_argument( '--title.export', dest="title_export", type=str, default=None, help='The name of this ROM in Export region.', ) parser.add_argument( '--title.korea', dest="title_korea", type=str, default=None, help='The name of this ROM in Korea region.', ) parser.add_argument( '--title.australia', dest="title_australia", type=str, default=None, help='The name of this ROM in Australia region.', ) parser.add_argument( '-d', '--date', type=str, default=None, help='The date this ROM was created, in the form YYYY-MM-DD', ) parser.add_argument( '-e', '--entrypoint', type=str, required=True, help= 'The executable entrypoint in main RAM after loading all sections.', ) parser.add_argument( '-c', '--section', type=str, action='append', help= ('An executable section that will be loaded into RAM. Must be specified ' 'in the form of /path/to/file,0x12345678 where the hexidecimal number is ' 'the load offset in main RAM.')) parser.add_argument( '-n', '--test-entrypoint', type=str, required=True, help='The test mode entrypoint in main RAM after loading all sections.', ) parser.add_argument( '-i', '--test-section', type=str, action='append', help= ('A test mode section that will be loaded into RAM. Must be specified ' 'in the form of /path/to/file,0x12345678 where the hexidecimal number is ' 'the load offset in main RAM.')) parser.add_argument( '-b', '--pad-before-data', type=str, default=None, help= 'Pad the ROM to this size in hex before appending any arbitrary data.', ) parser.add_argument( '-a', '--pad-after-data', type=str, default=None, help= 'Pad the ROM to this size in hex after appending any arbitrary data.', ) parser.add_argument( '-f', '--filedata', metavar="FILE", type=str, action='append', help='Append this arbitrary file data to the end of the ROM.', ) # Grab what we're doing args = parser.parse_args() # Create the ROM header itself header = NaomiRom.default() # Start by attaching basic info header.publisher = args.publisher or "NOBODY" title = [ args.title or "NO TITLE", args.title or "NO TITLE", args.title or "NO TITLE", args.title or "NO TITLE", args.title or "NO TITLE", ] if args.title_japan: title[header.REGION_JAPAN] = args.title_japan if args.title_usa: title[header.REGION_JAPAN] = args.title_usa if args.title_export: title[header.REGION_JAPAN] = args.title_export if args.title_korea: title[header.REGION_JAPAN] = args.title_korea if args.title_australia: title[header.REGION_JAPAN] = args.title_australia header.names = title # I am not sure whether anyone will want to overwrite these header.sequencetexts = [ "CREDIT TO START", "CREDIT TO CONTINUE", ] header.regions = [ header.REGION_JAPAN, header.REGION_USA, header.REGION_EXPORT, header.REGION_KOREA, header.REGION_AUSTRALIA, ] if args.date is not None: year, month, day = args.date.split('-') header.date = datetime.date(int(year), int(month), int(day)) else: header.date = datetime.date.today() if args.serial is not None: if len(args.serial) != 3: raise Exception("Serial must be 3 ascii digits!") header.serial = b"B" + args.serial.encode('ascii') else: header.serial = b"B999" # TODO: Command line args for setting supported number of players, # screen orientation, coin service type, supported monitor frequencies, # and defaults for various EEPROM settings such as free-play. # Construct executable and test loader sections. main_sections: List[NaomiRomSection] = [] test_sections: List[NaomiRomSection] = [] romoffset = len(header.data) romdata = b'' # Calculate locations for appended executable chunks. for file_and_offset in args.section or []: filename, offset = file_and_offset.rsplit(',', 1) with open(filename, "rb") as fpb: sectiondata = fpb.read() main_sections.append( NaomiRomSection( offset=romoffset, length=len(sectiondata), load_address=int(offset, 16), )) romoffset += len(sectiondata) romdata += sectiondata # Calculate locations for appended test mode chunks. for file_and_offset in args.test_section or []: filename, offset = file_and_offset.rsplit(',', 1) with open(filename, "rb") as fpb: sectiondata = fpb.read() test_sections.append( NaomiRomSection( offset=romoffset, length=len(sectiondata), load_address=int(offset, 16), )) romoffset += len(sectiondata) romdata += sectiondata # Create the executable itself. header.main_executable = NaomiExecutable(sections=main_sections, entrypoint=int( args.entrypoint, 16)) header.test_executable = NaomiExecutable(sections=test_sections, entrypoint=int( args.test_entrypoint, 16)) # Now, pad the ROM out to any requested padding. if args.pad_before_data and romoffset < int(args.pad_before_data, 16): amount = int(args.pad_before_data, 16) - romoffset romdata += b'\0' * amount romoffset += amount # Now, append any requested data chunks. for filename in args.filedata or []: with open(filename, "rb") as fpb: filedata = fpb.read() romoffset += len(filedata) romdata += filedata # Now, pad the ROM out to any requested padding. if args.pad_after_data and romoffset < int(args.pad_after_data, 16): amount = int(args.pad_after_data, 16) - romoffset romdata += b'\0' * amount romoffset += amount # Finally, write out the pieces with open(args.bin, "wb") as fpb: fpb.write(header.data) fpb.write(romdata) return 0
def add_or_update_section( data: Union[bytes, FileBytes], location: int, newsection: bytes, header: Optional[NaomiRom] = None, verbose: bool = False, ) -> Union[bytes, FileBytes]: # Note that if an external header is supplied, it will be modified to match the updated # data. if isinstance(data, FileBytes): data = data.clone() if header is None: header = NaomiRom(data) # First, find out if there's already a section in this header. executable = header.main_executable for section in executable.sections: if section.load_address == location: # This is a section chunk if section.length != len(newsection): raise NaomiSettingsPatcherException( "Found section in executable, but it is the wrong size!") if verbose: print("Overwriting old section in existing ROM section.") # We can just update the data to overwrite this section if isinstance(data, bytes): data = data[:section.offset] + newsection + data[ (section.offset + section.length):] elif isinstance(data, FileBytes): data[section.offset:(section.offset + section.length)] = newsection else: raise Exception("Logic error!") break else: # We need to add an init section to the ROM if len(executable.sections) >= 8: raise NaomiSettingsPatcherException( "ROM already has the maximum number of sections!") # Add a new section to the end of the rom for this binary data. if verbose: print( "Attaching section to a new ROM section at the end of the file." ) # Add a new section to the end of the rom for this section executable.sections.append( NaomiRomSection(offset=len(data), load_address=location, length=len(newsection))) header.main_executable = executable # Now, just append it to the end of the file if isinstance(data, bytes): data = header.data + data[header.HEADER_LENGTH:] + newsection elif isinstance(data, FileBytes): data[:header.HEADER_LENGTH] = header.data data.append(newsection) else: raise Exception("Logic error!") # Return the updated data. return data