Files
openroller/tools/patch_game471_winefix.py
T
tsuki 831d96e562 Initial public source release
Split reusable rendering and format support into vectorail-core and vectorail-gc.
2026-08-02 17:05:27 +02:00

264 lines
11 KiB
Python

#!/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()