コード例 #1
0
    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
コード例 #2
0
ファイル: settings.py プロジェクト: DragonMinded/netboot
    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
コード例 #3
0
ファイル: settings.py プロジェクト: DragonMinded/netboot
    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
コード例 #4
0
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
コード例 #5
0
ファイル: rominfo.py プロジェクト: DragonMinded/netboot
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
コード例 #6
0
ファイル: naomi_sram.py プロジェクト: DragonMinded/netboot
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
コード例 #7
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
コード例 #8
0
ファイル: netdimm_menu.py プロジェクト: DragonMinded/netboot
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
コード例 #9
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
コード例 #10
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