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 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 main() -> int: parser = argparse.ArgumentParser( description= "Tools for sending images to NetDimm for Naomi/Chihiro/Triforce.") parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm is configured on.", ) parser.add_argument( "image", metavar="IMAGE", type=str, help="The image file we should send to the NetDimm.", ) parser.add_argument( "--key", metavar="HEX", type=str, help= "Key (as a 16 character hex string) to encrypt image file. Defaults to null key", ) parser.add_argument( "--target", metavar="TARGET", type=NetDimmTargetEnum, action=EnumAction, default=NetDimmTargetEnum.TARGET_NAOMI, help= "Target platform this image is going to. Defaults to 'naomi'. Choose from 'naomi', 'chihiro' or 'triforce'.", ) parser.add_argument( "--version", metavar="VERSION", type=NetDimmVersionEnum, action=EnumAction, default=NetDimmVersionEnum.VERSION_4_01, help= "NetDimm firmware version this image is going to. Defaults to '4.01'. Choose from '1.02', '2.06', '2.17', '3.03', '3.17', '4.01' or '4.02'.", ) parser.add_argument( '--patch-file', metavar='FILE', type=str, action='append', help=( 'Patch to apply to image on-the-fly while sending to the NetDimm. ' 'Can be specified multiple times to apply multiple patches. ' 'Patches will be applied in specified order. If not specified, the ' 'image is sent without patching.'), ) parser.add_argument( '--settings-file', metavar='FILE', type=str, action='append', help= ('Settings to apply to image on-the-fly while sending to the NetDimm. ' 'Currently only supported for the Naomi platform. For Naomi, the ' 'settings file should be a valid 128-byte EEPROM file as obtained ' 'from an emulator or as created using the "edit_settings" utility, ' 'or a 32-kbyte SRAM file as obtained from an emulator. You can apply ' 'both an EEPROM and a SRAM settings file by specifying this argument ' 'twice.'), ) parser.add_argument( '--disable-crc', action="store_true", help= "Disable checking memory screen after upload and boot straight into the game", ) parser.add_argument( '--disable-now-loading', action="store_true", help= "Disable displaying the \"NOW LOADING...\" screen when sending the game", ) # Prevent ERROR 33 GATEWAY IS NOT FOUND due to bad, or missing PIC chip. parser.add_argument( '--keyless-boot', action="store_true", help="Enable boot without Key Chip", ) # Give more time to slower netboot on some platforms. parser.add_argument( '--send-timeout', type=int, default=None, help="Specify a different send timeout in seconds", ) args = parser.parse_args() # If the user specifies a key (not normally done), convert it key: Optional[bytes] = None if args.key: if len(args.key) != 16: raise Exception("Invalid key length for image!") key = bytes( [int(args.key[x:(x + 2)], 16) for x in range(0, len(args.key), 2)]) print("sending...", file=sys.stderr) netdimm = NetDimm(args.ip, version=args.version, target=args.target, timeout=args.send_timeout, log=print) # Grab the binary, patch it with requested patches. with open(args.image, "rb") as fp: data = FileBytes(fp) for patch in args.patch_file or []: 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) except Exception as e: print(f"Could not patch {args.image}: {str(e)}", file=sys.stderr) return 1 # Grab any settings file that should be included. if args.target == NetDimmTargetEnum.TARGET_NAOMI: for sfile in args.settings_file or []: with open(sfile, "rb") as fp: settings = fp.read() patcher = NaomiSettingsPatcher(data, get_default_naomi_trojan()) if len(settings) == NaomiSettingsPatcher.SRAM_SIZE: patcher.put_sram(settings) elif len(settings) == NaomiSettingsPatcher.EEPROM_SIZE: patcher.put_eeprom(settings) else: print( f"Could not attach {sfile}: not the right size to be an SRAM or EEPROM settings", file=sys.stderr) return 1 data = patcher.data else: for sfile in args.settings_file or []: print(f"Could not attach {sfile}: not a Naomi ROM!") return 1 # Send the binary, reboot into the game. netdimm.send(data, key, disable_crc_check=args.disable_crc, disable_now_loading=args.disable_now_loading) print("rebooting into game...", file=sys.stderr) netdimm.reboot() print("ok!", file=sys.stderr) if args.keyless_boot: print("keyless boot: infinite set_time_limit() loop") while True: netdimm.set_time_limit(10) time.sleep(5) 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 main() -> int: parser = argparse.ArgumentParser( description= "Utility for dumping/restoring an SRAM file on a SEGA Naomi.") subparsers = parser.add_subparsers(help='Action to take', dest='action') dump_parser = subparsers.add_parser( 'dump', help='Dump the SRAM from a SEGA Naomi and write it to a file.', description='Dump the SRAM from a SEGA Naomi and write it to a file.', ) dump_parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm we should use is configured on.", ) dump_parser.add_argument( 'sram', metavar='SRAM', type=str, help='The SRAM file we should write the contents of the SRAM to.', ) dump_parser.add_argument( '--exe', metavar='EXE', type=str, default=os.path.join(root, 'homebrew', 'sramdump', 'sramdump.bin'), help= 'The helper executable that we should send to dump/restore SRAM files on the Naomi. Defaults to %(default)s.', ) dump_parser.add_argument( '--verbose', action="store_true", help="Display verbose debugging information.", ) restore_parser = subparsers.add_parser( 'restore', help='Restore the SRAM on a SEGA Naomi from a file.', description='Restore the SRAM on a SEGA Naomi from a file.', ) restore_parser.add_argument( "ip", metavar="IP", type=str, help="The IP address that the NetDimm we should use is configured on.", ) restore_parser.add_argument( 'sram', metavar='SRAM', type=str, help='The SRAM file we should read the contents of the SRAM from.', ) restore_parser.add_argument( '--exe', metavar='EXE', type=str, default=os.path.join(root, 'homebrew', 'sramdump', 'sramdump.bin'), help= 'The helper executable that we should send to dump/restore SRAM files on the Naomi. Defaults to %(default)s.', ) restore_parser.add_argument( '--verbose', action="store_true", help="Display verbose debugging information.", ) args = parser.parse_args() verbose = args.verbose if args.action == "dump": netdimm = NetDimm(args.ip, log=print if verbose else None) with open(args.exe, "rb") as fp: helperdata = FileBytes(fp) try: # Now, connect to the net dimm, send the menu and then start communicating with it. print("Connecting to net dimm...") print("Sending helper to net dimm...") netdimm.send(helperdata, disable_crc_check=True) netdimm.reboot() print("Waiting for Naomi to boot...") except NetDimmException: # Mark failure so we don't try to communicate below. print("Sending helper executable failed!") return 1 try: with netdimm.connection(): while True: msg = receive_message(netdimm, verbose=verbose) if not msg: continue if msg.id == MESSAGE_READY: print("Dumping SRAM...") send_message(netdimm, Message(MESSAGE_SRAM_READ_REQUEST), verbose=verbose) elif msg.id == MESSAGE_SRAM_READ: if len(msg.data) != 0x8000: print("Got wrong size for SRAM!") return 1 else: with open(args.sram, "wb") as fp: fp.write(msg.data) print(f"Wrote SRAM from Naomi to '{args.sram}'.") send_message(netdimm, Message(MESSAGE_DONE), verbose=verbose) break 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) else: print("Got unexpected packet!") return 1 except NetDimmException: # Mark failure so we don't try to wait for power down below. print("Communicating with the helper failed!") return 1 if args.action == "restore": netdimm = NetDimm(args.ip, log=print if verbose else None) with open(args.exe, "rb") as fp: helperdata = FileBytes(fp) try: # Now, connect to the net dimm, send the menu and then start communicating with it. print("Connecting to net dimm...") print("Sending helper to net dimm...") netdimm.send(helperdata, disable_crc_check=True) netdimm.reboot() print("Waiting for Naomi to boot...") except NetDimmException: # Mark failure so we don't try to communicate below. print("Sending helper executable failed!") return 1 try: with netdimm.connection(): while True: msg = receive_message(netdimm, verbose=verbose) if not msg: continue if msg.id == MESSAGE_READY: send_message(netdimm, Message(MESSAGE_SRAM_WRITE_REQUEST), verbose=verbose) print("Restoring SRAM...") with open(args.sram, "rb") as fp: data = fp.read() send_message(netdimm, Message(MESSAGE_SRAM_WRITE, data), verbose=verbose) send_message(netdimm, Message(MESSAGE_DONE), verbose=verbose) break 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) else: print("Got unexpected packet!") return 1 except NetDimmException: # Mark failure so we don't try to wait for power down below. print("Communicating with the helper failed!") return 1 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."), ) subparsers = parser.add_subparsers(help='Action to take', dest='action') attach_parser = subparsers.add_parser( 'attach', help='Attach a 32K SRAM file to a commercial Naomi ROM.', description='Attach a 32K SRAM file to a commercial Naomi ROM.', ) attach_parser.add_argument( 'bin', metavar='BIN', type=str, help='The binary file we should attach the SRAM settings to.', ) attach_parser.add_argument( 'sram', metavar='SRAM', type=str, help='The SRAM settings file we should attach to the binary.', ) attach_parser.add_argument( '--output-file', metavar='BIN', type=str, help= 'A different file to output to instead of updating the binary specified directly.', ) extract_parser = subparsers.add_parser( 'extract', help='Extract a 32K SRAM file from a commercial Naomi ROM.', description='Extract a 32K SRAM file from a commercial Naomi ROM.', ) extract_parser.add_argument( 'bin', metavar='BIN', type=str, help='The binary file we should extract the SRAM settings from.', ) extract_parser.add_argument( 'sram', metavar='SRAM', type=str, help='The SRAM settings file we should extract from the binary.', ) # Grab what we're doing args = parser.parse_args() if args.action == "attach": # Grab the rom, parse it with open(args.bin, "rb" if args.output_file else "rb+") as fp: data = FileBytes(fp) # type: ignore with open(args.sram, "rb") as fp: sram = fp.read() if len(sram) != NaomiSettingsPatcher.SRAM_SIZE: print( f"SRAM file is not the right size, should be {NaomiSettingsPatcher.SRAM_SIZE} bytes!", file=sys.stderr) return 1 patcher = NaomiSettingsPatcher(data, None) patcher.put_sram(sram, verbose=True) 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: patcher.data.write_changes(fp) else: print(f"Added SRAM init to the end of {args.bin}.", file=sys.stderr) patcher.data.write_changes() elif args.action == "extract": # Grab the rom, parse it. with open(args.bin, "rb") as rfp: data = FileBytes(rfp) # Now, search for the settings. patcher = NaomiSettingsPatcher(data, None) settings = patcher.get_sram() if settings is None: print("ROM does not have any SRAM settings attached!", file=sys.stderr) return 1 if len(settings) != NaomiSettingsPatcher.SRAM_SIZE: print( "SRAM is the wrong size! Perhaps you meant to use \"attach_settings\"?", file=sys.stderr) return 1 print(f"Wrote SRAM settings to {args.sram}.") with open(args.sram, "wb") as wfp: wfp.write(settings) return 0
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= "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 attaching, extracting and editing pre-selected EEPROM settings to a commercial Naomi ROM.", ) subparsers = parser.add_subparsers(help='Action to take', dest='action') attach_parser = subparsers.add_parser( 'attach', help='Attach a 128-byte EEPRom file to a commercial Naomi ROM.', description='Attach a 128-byte EEPRom file to a commercial Naomi ROM.', ) attach_parser.add_argument( 'rom', metavar='ROM', type=str, help='The Naomi ROM file we should attach the EEPROM settings to.', ) attach_parser.add_argument( 'eeprom', metavar='EEPROM', type=str, help='The actual EEPROM settings file we should attach to the ROM.', ) attach_parser.add_argument( '--exe', metavar='EXE', type=str, default=os.path.join(root, 'homebrew', 'settingstrojan', 'settingstrojan.bin'), help= 'The settings executable that we should attach to the ROM. Defaults to %(default)s.', ) attach_parser.add_argument( '--output-file', metavar='BIN', type=str, help= 'A different file to output to instead of updating the binary specified directly.', ) attach_parser.add_argument( '--enable-debugging', action='store_true', help= 'Display debugging information to the screen instead of silently saving settings.', ) extract_parser = subparsers.add_parser( 'extract', help= 'Extract a 128-byte EEPRom file from a commercial Naomi ROM we have previously attached settings to.', description= 'Extract a 128-byte EEPRom file from a commercial Naomi ROM we have previously attached settings to.', ) extract_parser.add_argument( 'rom', metavar='ROM', type=str, help='The Naomi ROM file we should extract the EEPROM settings from.', ) extract_parser.add_argument( 'eeprom', metavar='EEPROM', type=str, help= 'The actual EEPROM settings file we should write after extracting from the ROM.', ) info_parser = subparsers.add_parser( 'info', help='Display settings info about a commercial ROM file.', description='Display settings info about a commercial ROM file.', ) info_parser.add_argument( 'rom', metavar='ROM', type=str, help= 'The Naomi ROM file we should print EEPROM settings information from.', ) info_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.', ) edit_parser = subparsers.add_parser( 'edit', help= 'Created or edit a 128-byte EEPRom settings file and attach it to a commercial Naomi ROM.', description= 'Created or edit a 128-byte EEPRom settings file and attach it to a commercial Naomi ROM.', ) edit_parser.add_argument( 'rom', metavar='ROM', type=str, help='The Naomi ROM file we should edit the EEPROM settings for.', ) edit_parser.add_argument( '--exe', metavar='EXE', type=str, default=os.path.join(root, 'homebrew', 'settingstrojan', 'settingstrojan.bin'), help= 'The settings executable that we should attach to the ROM. Defaults to %(default)s.', ) edit_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.', ) edit_parser.add_argument( '--output-file', metavar='BIN', type=str, help= 'A different file to output to instead of updating the binary specified directly.', ) edit_parser.add_argument( '--region', metavar="REGION", type=str, help= 'The region the Naomi which will boot this ROM is set to. Defaults to "japan".', ) edit_parser.add_argument( '--enable-debugging', action='store_true', help= 'Display debugging information to the screen instead of silently saving settings.', ) # Grab what we're doing args = parser.parse_args() if args.action == "attach": # Grab the rom, parse it. with open(args.rom, "rb" if args.output_file else "rb+") as fp: data = FileBytes(fp) # type: ignore # Grab the attachment. This should be the specific settingstrojan binary blob as compiled # out of the homebrew/settingstrojan directory. with open(args.exe, "rb") as fp: exe = fp.read() # First, we need to modiffy the settings trojan with this ROM's load address and # the EEPROM we want to add. with open(args.eeprom, "rb") as fp: eeprom = fp.read() # Check some bounds. if len(eeprom) != NaomiSettingsPatcher.EEPROM_SIZE: print( "EEPROM is the wrong size! Perhaps you meant to use \"attach_sram\"?", file=sys.stderr) return 1 # Now, patch it onto the data. patcher = NaomiSettingsPatcher(data, exe) patcher.put_eeprom(eeprom, enable_debugging=args.enable_debugging, verbose=True) if args.output_file: print(f"Added EEPROM settings to {args.output_file}.") with open(args.output_file, "wb") as fp: patcher.data.write_changes(fp) else: print(f"Added EEPROM settings to {args.rom}.") patcher.data.write_changes() elif args.action == "extract": # Grab the rom, parse it. with open(args.rom, "rb") as rfp: data = FileBytes(rfp) # Now, search for the settings. patcher = NaomiSettingsPatcher(data, None) settings = patcher.get_eeprom() if settings is None: print("ROM does not have any EEPROM settings attached!", file=sys.stderr) return 1 if len(settings) != NaomiSettingsPatcher.EEPROM_SIZE: print( "EEPROM is the wrong size! Perhaps you meant to use \"attach_sram\"?", file=sys.stderr) return 1 print(f"Wrote EEPROM settings to {args.eeprom}.") with open(args.eeprom, "wb") as wfp: wfp.write(settings) elif args.action == "info": # Grab the rom, parse it. with open(args.rom, "rb") as fp: data = FileBytes(fp) # Now, search for the settings. patcher = NaomiSettingsPatcher(data, None) info = patcher.eeprom_info if info is None: print("ROM does not have any EEPROM settings attached!") else: print( f"ROM has EEPROM settings attached, with trojan version {info.date.year:04}-{info.date.month:02}-{info.date.day:02}!" ) print( f"Debug printing is {'enabled' if info.enable_debugging else 'disabled'}." ) # Grab the actual EEPRom so we can print the settings within. manager = NaomiSettingsManager(args.settings_directory) eepromdata = patcher.get_eeprom() config = None try: if eepromdata is not None: try: config = manager.from_eeprom(eepromdata) except FileNotFoundError: # We don't have the directory configured, so skip this. pass if config is not None: print("System Settings:") for setting in config.system.settings: # Don't show read-only settints. if setting.read_only is True: continue if isinstance(setting.read_only, ReadOnlyCondition): if setting.read_only.evaluate( config.system.settings): continue # This shouldn't happen, but make mypy happy. if setting.current is None: continue print( f" {setting.name}: {setting.values[setting.current]}" ) print("Game Settings:") if config.game.settings: for setting in config.game.settings: # Don't show read-only settints. if setting.read_only is True: continue if isinstance(setting.read_only, ReadOnlyCondition): if setting.read_only.evaluate( config.game.settings): continue # This shouldn't happen, but make mypy happy. if setting.current is None: continue print( f" {setting.name}: {setting.values[setting.current]}" ) else: print( " No game settings, game will use its own defaults." ) except (SettingsParseException, SettingsSaveException) as e: print(f"Error in \"{e.filename}\":", str(e), file=sys.stderr) return 1 elif args.action == "edit": # Grab the rom, parse it. with open(args.rom, "rb" if args.output_file else "rb+") as fp: data = FileBytes(fp) # type: ignore # Grab the attachment. This should be the specific settingstrojan binary blob as compiled # out of the homebrew/settingstrojan directory. with open(args.exe, "rb") as fp: exe = fp.read() # First, try to extract existing eeprom for editing. patcher = NaomiSettingsPatcher(data, exe) eepromdata = patcher.get_eeprom() manager = NaomiSettingsManager(args.settings_directory) if eepromdata is None: # We need to make them up from scratch. region = { "japan": NaomiRomRegionEnum.REGION_JAPAN, "usa": NaomiRomRegionEnum.REGION_USA, "export": NaomiRomRegionEnum.REGION_EXPORT, "korea": NaomiRomRegionEnum.REGION_KOREA, "australia": NaomiRomRegionEnum.REGION_AUSTRALIA, }.get(args.region, NaomiRomRegionEnum.REGION_JAPAN) parsedsettings = manager.from_rom(patcher.rom, region=region) else: # We have an eeprom to edit. parsedsettings = manager.from_eeprom(eepromdata) # Now, edit those created or extracted settings. editor = NaomiSettingsEditor(parsedsettings) if editor.run(): # If the editor signals to us that the user wanted to save the settings # then we should patch them into the binary. eepromdata = manager.to_eeprom(parsedsettings) patcher.put_eeprom( eepromdata, enable_debugging=args.enable_debugging, verbose=True, ) if args.output_file: print(f"Added EEPROM settings to {args.output_file}.") with open(args.output_file, "wb") as fp: patcher.data.write_changes(fp) else: print(f"Added EEPROM settings to {args.rom}.") patcher.data.write_changes() else: print(f"Invalid action {args.action}!", file=sys.stderr) return 1 return 0