#!/usr/bin/env python3 """Trace FIFA17 Screen event 0x30 through command 0x128 and ScenarioModeStart. The generated GDB program uses hardware breakpoints, only reads registers and client memory, logs, and continues. Seven breakpoints are rotated so no more than four are enabled. It never calls client functions, writes client memory, emits events, or drives input. command_128_trace.py [pid] [--output PATH] command_128_trace.py --selftest """ from __future__ import annotations import argparse import os from pathlib import Path import shutil import sys sys.path.insert(0, str(Path(__file__).resolve().parent)) import match_advance_trace as advance import match_transition_trace as transition MANAGER_SELECT_ACTION_SOURCE_RVA = 0x0705A620 MANAGER_SELECT_ACTION_RESULT_RVA = 0x07CDC4A6 SCREEN_EVENT_CHANNEL_ROUTER_RVA = 0x080CE230 SCREEN_EVENT_DISPATCH_RVA = 0x080CF790 SKILL_INSTRUCTIONS_SCREEN_RVA = 0x07DCA400 GAMEPLAY_COMMAND_DISPATCH_RVA = 0x07A8F6C0 FREE_ROAM_COMMAND_128_RVA = 0x07A92B0F SCENARIO_SCHEDULER_RVA = 0x07AC3A40 SCENARIO_MANAGER_START_RVA = 0x07B1C2B0 MODE_ZERO_SCENARIO_START_RVA = 0x07B1C190 GAMEPLAY_GLOBAL_RVA = 0x04BFB910 SCREEN_VTABLE_RVA = 0x03B3ECC0 FREE_ROAM_VTABLE_RVA = 0x03AEDF58 MODE_ZERO_CHILD_VTABLE_RVA = 0x03AE9C00 def addresses(base: int) -> dict[str, int]: return { "manager_select_source": base + MANAGER_SELECT_ACTION_SOURCE_RVA, "manager_select_action": base + MANAGER_SELECT_ACTION_RESULT_RVA, "screen_event_router": base + SCREEN_EVENT_CHANNEL_ROUTER_RVA, "screen_event_dispatch": base + SCREEN_EVENT_DISPATCH_RVA, "instructions_screen": base + SKILL_INSTRUCTIONS_SCREEN_RVA, "command_dispatch": base + GAMEPLAY_COMMAND_DISPATCH_RVA, "free_roam_case": base + FREE_ROAM_COMMAND_128_RVA, "scheduler": base + SCENARIO_SCHEDULER_RVA, "manager_start": base + SCENARIO_MANAGER_START_RVA, "scenario_start": base + MODE_ZERO_SCENARIO_START_RVA, "gameplay_global": base + GAMEPLAY_GLOBAL_RVA, "screen_vtable": base + SCREEN_VTABLE_RVA, "free_roam_vtable": base + FREE_ROAM_VTABLE_RVA, "mode_zero_child_vtable": base + MODE_ZERO_CHILD_VTABLE_RVA, } def gdb_prelude(pid: int, output: str) -> str: if any(character in output for character in "\n\r"): raise ValueError("output path cannot contain a newline") return f"""set pagination off set confirm off set print thread-events off set breakpoint always-inserted off set logging file {output} set logging overwrite on set logging redirect off set logging enabled on handle SIGSEGV nostop noprint pass handle SIGILL nostop noprint pass handle SIGFPE nostop noprint pass handle SIGPIPE nostop noprint pass handle SIGALRM nostop noprint pass handle SIGUSR1 nostop noprint pass handle SIGUSR2 nostop noprint pass attach {pid} """ def build_script(pid: int, fifa_base: int, output: str) -> str: address = addresses(fifa_base) return ( gdb_prelude(pid, output) + f"""define snapshot_gameplay set $snap_gameplay_global = *(void**)0x{address['gameplay_global']:x} set $snap_listener_manager = 0 set $snap_listener_table = 0 set $snap_listener_index = -1 set $snap_free_roam = 0 set $snap_free_state = -1 set $snap_free_111 = -1 set $snap_free_112 = -1 set $snap_free_124 = -1 set $snap_selected = 0 set $snap_selected_vtable = 0 set $snap_selected_mode = -1 if $snap_gameplay_global != 0 set $snap_listener_manager = *(void**)($snap_gameplay_global+0x58) end if $snap_listener_manager != 0 set $snap_listener_table = *(void**)$snap_listener_manager end if $snap_listener_table != 0 set $snap_free_roam = *(void**)$snap_listener_table set $snap_listener_index = *(int*)($snap_listener_table+0x20) if $snap_listener_index >= 0 && $snap_listener_index < 3 set $snap_selected = *(void**)($snap_listener_table+$snap_listener_index*8) end end if $snap_free_roam != 0 set $snap_free_state = *(int*)($snap_free_roam+0x30) set $snap_free_111 = *(unsigned char*)($snap_free_roam+0x111) set $snap_free_112 = *(unsigned char*)($snap_free_roam+0x112) set $snap_free_124 = *(int*)($snap_free_roam+0x124) end if $snap_selected != 0 set $snap_selected_vtable = *(void**)$snap_selected set $snap_selected_mode = *(int*)($snap_selected+0x18) end end set $action_count = 0 hbreak *0x{address['manager_select_action']:x} commands silent set $action_count = $action_count+1 set $provider = $rbx snapshot_gameplay if $action_count <= 128 python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d MANAGER_SELECT_ACTION" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d ordinal=%d instruction=%p caller_return=%p provider=%p provider_vtable=%p action_id=%#x free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $action_count, $pc, *(void**)($rsp+0x58), $provider, *(void**)$provider, $eax, $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode end if $eax == 0x30 bt 16 end continue end hbreak *0x{address['instructions_screen']:x} condition 2 $edx == 0x30 && *(void**)$rcx == 0x{address['screen_vtable']:x} commands silent set $screen = $rcx set $screen_owner = *(void**)($screen+0x140) set $screen_owner_vtable = 0 if $screen_owner != 0 set $screen_owner_vtable = *(void**)$screen_owner end snapshot_gameplay python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d INSTRUCTIONS_SCREEN_EVENT_30" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d handler=%p caller_return=%p screen=%p screen_vtable=%p event=%#x payload=%p allow_advance138=%d owner140=%p owner_vtable=%p free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $screen, *(void**)$screen, $edx, $r8, *(int*)($screen+0x138), $screen_owner, $screen_owner_vtable, $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode bt 16 continue end hbreak *0x{address['command_dispatch']:x} condition 3 $edx == 0x128 commands silent set $command_dispatcher = $rcx set $command_table = *(void**)$command_dispatcher snapshot_gameplay python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d GAMEPLAY_COMMAND_128" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d function=%p caller_return=%p dispatcher=%p command=%#x payload=%p arg_r9=%p table=%p free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $command_dispatcher, $edx, $r8, $r9, $command_table, $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode disable 1 disable 2 disable 3 enable 5 continue end hbreak *0x{address['free_roam_case']:x} commands silent set $owner = $rbx snapshot_gameplay python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d FREE_ROAM_COMMAND_128" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d callsite=%p caller_return=%p owner=%p owner_vtable=%p command=%#x payload=%p state=%d previous=%d free111=%d free112=%d free124=%d manager=%p selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $owner, *(void**)$owner, $esi, $rdi, *(int*)($owner+0x30), *(int*)($owner+0x34), *(unsigned char*)($owner+0x111), *(unsigned char*)($owner+0x112), *(int*)($owner+0x124), *(void**)($owner+0x168), $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode continue end hbreak *0x{address['scheduler']:x} disable 5 commands silent set $owner = $rcx set $manager = *(void**)($owner+0x168) set $manager_vtable = 0 set $manager_mode = -1 set $child = 0 set $child_vtable = 0 if $manager != 0 set $manager_vtable = *(void**)$manager set $manager_mode = *(int*)($manager+0x50) set $child = *(void**)($manager+0x8) end if $child != 0 set $child_vtable = *(void**)$child end python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d SCENARIO_SCHEDULER" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d function=%p caller_return=%p owner=%p owner_vtable=%p free124=%d command=%#x payload=%p manager=%p manager_vtable=%p manager_mode=%d child=%p child_vtable=%p\\n", $_thread, $pc, *(void**)$rsp, $owner, *(void**)$owner, *(int*)($owner+0x124), $edx, $r8, $manager, $manager_vtable, $manager_mode, $child, $child_vtable disable 4 disable 5 enable 6 continue end hbreak *0x{address['manager_start']:x} disable 6 commands silent set $manager = $rcx set $child = *(void**)($manager+0x8) python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d SCENARIO_MANAGER_START" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d function=%p caller_return=%p manager=%p manager_vtable=%p requested_countdown=%d mode=%d child=%p child_vtable=%p\\n", $_thread, $pc, *(void**)$rsp, $manager, *(void**)$manager, $rdx & 0xff, *(int*)($manager+0x50), $child, $child ? *(void**)$child : 0 disable 6 enable 7 continue end hbreak *0x{address['scenario_start']:x} disable 7 commands silent set $ctx = $rcx snapshot_gameplay python import time; print("COMMAND128 epoch_ns=%d mono_ns=%d MODE_ZERO_SCENARIO_START" % (time.time_ns(), time.monotonic_ns()), end=" ") printf "thread=%d function=%p caller_return=%p ctx=%p ctx_vtable=%p descriptor=%p scenario_index=%d requested_countdown=%d flag40_before=%d callback_owner78=%p callback_vtable48=%p dispatcher_vtable80=%p free_roam=%p free_state=%d free111=%d free112=%d free124=%d selected_index=%d selected=%p selected_vtable=%p selected_mode=%d\\n", $_thread, $pc, *(void**)$rsp, $ctx, *(void**)$ctx, $rdx, $r8d, $r9 & 0xff, *(unsigned char*)($ctx+0x40), *(void**)($ctx+0x78), *(void**)($ctx+0x48), *(void**)($ctx+0x80), $snap_free_roam, $snap_free_state, $snap_free_111, $snap_free_112, $snap_free_124, $snap_listener_index, $snap_selected, $snap_selected_vtable, $snap_selected_mode disable 7 continue end printf "COMMAND128 ARMED pid={pid} action_id=0x{address['manager_select_action']:x} screen_handler=0x{address['instructions_screen']:x} command_dispatch=0x{address['command_dispatch']:x} free_roam=0x{address['free_roam_case']:x} scheduler=0x{address['scheduler']:x} manager=0x{address['manager_start']:x} scenario=0x{address['scenario_start']:x}\\n" continue """ ) def effective_environment(pid: int) -> dict[str, str]: values: dict[str, str] = {} for item in Path(f"/proc/{pid}/environ").read_bytes().split(b"\0"): if not item.startswith(b"OPENFUT_FIFA17_"): continue key, _, value = item.decode("utf-8", errors="replace").partition("=") values[key] = value return values def selftest() -> None: address = addresses(0x140000000) script = build_script(1234, 0x140000000, "/tmp/command-128.log") assert address["manager_select_source"] == 0x14705A620 assert address["manager_select_action"] == 0x147CDC4A6 assert address["instructions_screen"] == 0x147DCA400 assert address["command_dispatch"] == 0x147A8F6C0 assert address["free_roam_case"] == 0x147A92B0F assert address["scheduler"] == 0x147AC3A40 assert address["manager_start"] == 0x147B1C2B0 assert address["scenario_start"] == 0x147B1C190 assert script.count("hbreak *") == 7 assert "set $action_count = 0" in script assert "MANAGER_SELECT_ACTION" in script assert "condition 2 $edx == 0x30" in script assert "condition 3 $edx == 0x128" in script assert "disable 4" in script assert "disable 5" in script and "enable 5" in script assert "disable 6" in script and "enable 6" in script assert "disable 7" in script and "enable 7" in script assert "set *(" not in script print("command_128_trace selftest: PASS") def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("pid", nargs="?", type=int) parser.add_argument("--output") parser.add_argument("--print-script", action="store_true") parser.add_argument("--selftest", action="store_true") args = parser.parse_args() if args.selftest: selftest() return 0 pid = args.pid or transition.find_pid() if not pid: print("FIFA17.exe not found", file=sys.stderr) return 2 try: fifa_base, fifa_path = advance.module_mapping(pid, advance.FIFA_MODULE) advance.validate_file( fifa_path, advance.PINNED_FIFA_SHA256, advance.FIFA_MODULE, ) cards_base = 0 cards_path = "" try: cards_base, cards_path = transition.cards_mapping(pid) except RuntimeError: pass else: transition.validate_cards(cards_path) output = args.output or f"/tmp/fifa17-command-128-{pid}.log" script = build_script(pid, fifa_base, output) environment = effective_environment(pid) print( "COMMAND128 PREPARED " f"pid={pid} fifa_base={fifa_base:#x} cards_base={cards_base:#x} " f"cards_path={cards_path} " f"team_compat={environment.get('OPENFUT_FIFA17_SEASON_TEAM_COMPAT', '')} " f"pma_fix={environment.get('OPENFUT_FIFA17_OFFLINE_SEASONS_PMA_FIX', '')}" ) except (OSError, RuntimeError, ValueError) as error: print(error, file=sys.stderr) return 2 if args.print_script: print(script, end="") return 0 if not shutil.which("gdb"): print("gdb not found", file=sys.stderr) return 2 script_path = f"/tmp/fifa17-command-128-{pid}.gdb" Path(script_path).write_text(script, encoding="utf-8") os.execvp("gdb", ["gdb", "-q", "-nx", "-batch", "-x", script_path]) return 127 if __name__ == "__main__": raise SystemExit(main())