Esempio n. 1
0
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
Esempio n. 2
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
Esempio n. 3
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