#!/usr/bin/env python3 from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "GC" / "game471.exe" FULL_DST = ROOT / "GC" / "game471_winefix.exe" GUARD_DST = ROOT / "GC" / "game471_guard.exe" NESYSKIP_DST = ROOT / "GC" / "game471_nesyskip.exe" OFFLINE_DST = ROOT / "GC" / "game471_offline.exe" BOOTSKIP_DST = ROOT / "GC" / "game471_bootskip.exe" LOCAL_PATCHES = ( # VA 0x004e2e74 / 0x004e2f1d: skip two calls through null [esi+0x1d8]. ( 0x0E2274, bytes.fromhex("8b 10 8b 92 88 00 00 00 ff d2"), bytes.fromhex("83 c4 0c eb 05 90 90 90 90 90"), ), ( 0x0E231D, bytes.fromhex("8b 10 8b 92 88 00 00 00 ff d2"), bytes.fromhex("83 c4 0c eb 05 90 90 90 90 90"), ), # VA 0x004e3df4 / 0x004e3e3a: force fallback path for the same missing object. (0x0E31F4, bytes.fromhex("74 19"), bytes.fromhex("eb 19")), (0x0E323A, bytes.fromhex("74 52"), bytes.fromhex("eb 52")), # VA 0x004e460e: skip renderer/state block when the object is absent under Wine. ( 0x0E3A0E, bytes.fromhex("8b 86 d8 01 00 00"), bytes.fromhex("e9 57 00 00 00 90"), ), # VA 0x004e499d: skip another matrix upload through the same absent object. ( 0x0E3D9D, bytes.fromhex("8b 86 d8 01 00 00"), bytes.fromhex("e9 18 00 00 00 90"), ), # VA 0x004e4bf0: skip final absent-object flush in the same draw/update method. ( 0x0E3FF0, bytes.fromhex("8b b6 d8 01 00 00"), bytes.fromhex("e9 0c 00 00 00 90"), ), ) FULL_ONLY_PATCHES = ( # VA 0x004e72d9: do not enable the optional path that expects [esi+0x1d8]. ( 0x0E66D9, bytes.fromhex("c6 86 d4 05 00 00 01"), bytes.fromhex("c6 86 d4 05 00 00 00"), ), ) NESYS_SKIP_PATCHES = ( # VA 0x006391c3: boot state 6 waits for CNesysBase+0x250 before it can # leave the "Starting NESYS" screen. For offline Wine runs we skip that # wait state and let the boot state machine continue to state 7. ( 0x2385C3, bytes.fromhex( "e8 58 29 00 00 8b c8 e8 71 de dc ff 85 c0 74 7f e8 28" ), bytes.fromhex( "8b 85 f0 fe ff ff c7 40 08 07 00 00 00 e9 8f 01 00 00" ), ), ) FREEPLAY_PATCHES = ( # VA 0x00634570: force the credit controller's free-play predicate. ( 0x233970, bytes.fromhex("55 8b ec 51 89 4d fc e8 e4 cc dc ff"), bytes.fromhex("b0 01 c3 90 90 90 90 90 90 90 90 90"), ), ) BOOT_IO_SKIP_PATCHES = ( # VA 0x00552f90 / 0x00553120 / 0x005532b0: adjacent serial/input # self-tests for missing cabinet devices. They use the same global serial # backend and otherwise leave the boot screen on I/O Device Error 1. ( 0x152390, bytes.fromhex("55 8b ec 81 ec 90 00 00 00"), bytes.fromhex("b0 01 c3 90 90 90 90 90 90"), ), ( 0x152520, bytes.fromhex("55 8b ec 81 ec 90 00 00 00"), bytes.fromhex("b0 01 c3 90 90 90 90 90 90"), ), ( 0x1526B0, bytes.fromhex("55 8b ec 81 ec 90 00 00 00"), bytes.fromhex("b0 01 c3 90 90 90 90 90 90"), ), # VA 0x00553410: boot state 19 calls this I/O board self-test and advances # only when AL is non-zero. The real FAST IO HUB is absent under Wine, so # bypass this gate to keep moving toward the actual game/runtime code. ( 0x152810, bytes.fromhex("55 8b ec 81 ec 50 01 00 00"), bytes.fromhex("b0 01 c3 90 90 90 90 90 90"), ), # VA 0x00633240: boot state 7 checks a low-level input-device readiness # flag and posts error 0x302 when it is false. Under Wine there is no # cabinet input backend, so let the boot sequence continue. ( 0x232640, bytes.fromhex("55 8b ec e8 08 2b e2 ff 33 c9 3b c8 1b c0 f7 d8"), bytes.fromhex("b8 01 00 00 00 c3 90 90 90 90 90 90 90 90 90 90"), ), ) RFID_NULL_OK_PATCHES = ( # VA 0x004c3980..0x004c4b20: thin RFIReader/NESiCAReader wrappers all # dispatch through global 0x7cf334. When the reader object is absent they # normally return -1, which the boot diagnostics report as I/O Device Error # 1 / RFID READ WRITE MODULE. Treat the missing reader as "no event/no card" # instead of a fatal hardware error. (0x0C2D8A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C2DAA, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C2DEA, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), ( 0x0C2E2E, bytes.fromhex("83 c8 ff 8b e5 5d c3"), bytes.fromhex("33 c0 90 8b e5 5d c3"), ), (0x0C2E6A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C2E8C, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C2F7C, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C302D, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C318A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C31AC, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C325A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C327A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C329A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C32DA, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C32FA, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C33BA, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C33FA, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C341A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C34DC, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C364C, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C380C, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C39EC, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C3C2A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C3C6C, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C3CEA, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C3D0C, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C3D9C, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), (0x0C3E2A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C3E6A, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C3EAA, bytes.fromhex("83 c8 ff c3"), bytes.fromhex("33 c0 90 c3")), (0x0C3EEC, bytes.fromhex("83 c8 ff 5d c3"), bytes.fromhex("33 c0 90 5d c3")), ) BOOT_ERROR_PUBLISH_SKIP_PATCHES = ( # VA 0x00576a00: central boot/test-mode error publisher. Keep diagnostics # from parking the screen on cabinet hardware errors while we are running # without the original reader/I/O devices. ( 0x175E00, bytes.fromhex("55 8b ec 83 3d 84 25 7f 00 00"), bytes.fromhex("c3 90 90 90 90 90 90 90 90 90"), ), ) BOOT_RFID_ERROR1_SKIP_PATCHES = ( # VA 0x006394a5: boot state 9 treats status 2 from the reader self-test as # I/O Device Error 1 / RFID READ WRITE MODULE. Ignore that failed status so # the boot flow can keep building the runtime objects. ( 0x2388A5, bytes.fromhex("0f 84 b7 01 00 00"), bytes.fromhex("90 90 90 90 90 90"), ), ) BOOT_FASTIO_ERROR_LATCH_PATCHES = ( # VA 0x00455d10: returns the current FAST I/O device error latch from the # input backend. Under Wine our iDmac shim is still skeletal, so the latch # reaches boot state 15 as I/O Device Error 3 / FAST IO UNIVERSAL PCB. # Report "no device error" while we reverse the real DMA protocol. ( 0x055110, bytes.fromhex("e8 7b 2d 00 00 8b c8 e8 94 3f 00 00 8b 80 34 11 00 00 c3"), bytes.fromhex("33 c0 c3 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90"), ), ) COMMON_TOP_LAYER_SKIP_PATCHES = ( # VA 0x006ea20c / 0x006ea59c: keep the common RVB object alive, but start # it from its hidden init frame instead of jf_com_all. The full common # frame leaves title/insert fragments at screen origin with Wine's current # renderer path, while jf_com_ini preserves the later vtable/timer setup. (0x2E8A0C, b"jf_com_all\x00", b"jf_com_ini\x00"), (0x2E8D9C, b"jf_com_all\x00", b"jf_com_ini\x00"), # VA 0x00639710 / 0x0063971e: boot state 11 fades in the common head and # foot widgets before the game task starts. With the Wine null-renderer # guards above, parts of that RVB common layer keep rendering at the top # origin instead of their intended transforms. Keep the playable title/demo # flow, but do not start those broken top widgets. (0x238B10, bytes.fromhex("e8 eb bc fb ff"), bytes.fromhex("90 90 90 90 90")), (0x238B1E, bytes.fromhex("e8 fd bb fb ff"), bytes.fromhex("90 90 90 90 90")), # VA 0x005f6d16: mode switch helper always starts the insert/credit board. # That board is the large magenta/top-left INSERT/CREDIT strip in Wine. # NOP only the insert-board update/start block; leave the common mode # selection and state latch intact. ( 0x1F6116, bytes.fromhex( "6a 01 8b 4d fc e8 e0 fd ff ff 68 a8 a5 6e 00 " "8b 4d fc 83 c1 54 e8 30 4a e1 ff 8b c8 e8 e9 41 ee ff" ), bytes.fromhex( "90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 " "90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90" ), ), ) def apply_patches(dst: Path, patches: tuple[tuple[int, bytes, bytes], ...]) -> None: data = bytearray(SRC.read_bytes()) for offset, expected, patch in patches: found = bytes(data[offset : offset + len(expected)]) if found != expected: raise SystemExit( f"unexpected bytes at 0x{offset:x}: " f"{found.hex(' ')} != {expected.hex(' ')}" ) data[offset : offset + len(patch)] = patch dst.write_bytes(data) dst.chmod(0o755) print(f"wrote {dst}") def main() -> None: apply_patches(GUARD_DST, LOCAL_PATCHES) apply_patches(FULL_DST, LOCAL_PATCHES + FULL_ONLY_PATCHES) apply_patches(NESYSKIP_DST, LOCAL_PATCHES + NESYS_SKIP_PATCHES) apply_patches(OFFLINE_DST, LOCAL_PATCHES + NESYS_SKIP_PATCHES + FREEPLAY_PATCHES) apply_patches( BOOTSKIP_DST, LOCAL_PATCHES + FULL_ONLY_PATCHES + NESYS_SKIP_PATCHES + FREEPLAY_PATCHES + BOOT_IO_SKIP_PATCHES + RFID_NULL_OK_PATCHES + BOOT_ERROR_PUBLISH_SKIP_PATCHES + BOOT_RFID_ERROR1_SKIP_PATCHES + BOOT_FASTIO_ERROR_LATCH_PATCHES + COMMON_TOP_LAYER_SKIP_PATCHES, ) if __name__ == "__main__": main()