Fable II Recompiled

Fable II Recompiled brings Fable II to PC.

README

Fable 2 — ReXGlue Recompiled Project

First and for most this project was made to test the capabilities of local and open source AI. This project was made using Qwen3.8 27b. After about an hour it got the game running. Then after a large amount of human trial and error I was able to get enough of the functions mapped to be able to complete the game. I currently do not consider this to be fully complete as I have yet to 100% the game. The sha256 of the iso that started this project is: "685a0d3bea9718812f17bcd155907a5359a548b6d3d8342dd2a6c944f45e35ff" and its the Fable 2 GOTY for USA and Europe.

Recompilation of Fable 2 (Xbox 360, title ID 4D5307F1) using the ReXGlue SDK v0.10.0. Guest PPC code is statically recompiled to C++ at build time by rexglue codegen, driven by fable_2_manifest.toml.

Current and planned features

[x] Can be used to beat the game
[x] Guild chest fully unlocked
[x] Uncapped framerate / increased framerate
[ ] Higher Resolution support
[ ] Built in Debug Menu

  • Enabling custom lua to run in game

[x] Keyboard / Mouse Support
[ ] In game Text changed to respect keyboard and mouse

  • Automatic swapping between text

[ ] Increased performance / framerate
[ ] Hero / Dog Texture bug fix
[ ] Vulkan support
[ ] Linux Builds
[ ] Custom commands to aid in debugging
[ ] Improved Graphics rendering [ ] Custom menu(s) / modifying menus for extra functionality (like closing the game)

Notes for running the game

  • How to extract: rip the disc to an ISO, then open it with XboxImageExtractor — a GUI tool for Xbox 360 game images. It lists the image's filesystem; select default.xex, data, and $SystemUpdate and extract them into the project root (data and $SystemUpdate extract as folders). You can also grab nxeart and anything else the tool lists.
  • You must supply the game content yourself (it is not in the repo): rip the Fable 2 GOTY (USA/EU) disc (the one with SHA-256 above) and put default.xex, data/, nxeart/, and $SystemUpdate/ in the project root. The build does not copy this into the build directories
  • Saves live in <build dir>\saves\ — back that folder up to keep your progress, and copy it between build trees (Debug/Release) or machines to carry a save over.
  • The --game_data_root <path> override still points the content root at a different tree (e.g. to run from a shared content copy without staging); saves/cache still land next to the exe.

Running

To run the game you must:

  1. Extract a downloaded copy release of the game
  2. Extract the "data" and "default.xex" from your copy of the game, See "How to extract" for more info on that
  3. Run the fable.exe program and if no errors pop up then the game should launch and you are good to go
  • If an error pops up about the hash being wrong try to extract a different version of the game and then try again.

Optional command-line overrides (all normal --cvar value args):

ArgumentEffect
--game_data_root <path>Use a different content directory (beats the exe-dir-only search)
--update_data_root <path>Optional update content root
--window_width / --window_height / --fullscreenPresentation options
--keyboard_gamepad_map <map>Host keyboard -> guest gamepad button map (see below)

User config (fable2_config.toml)

The recomp has its own human-readable user config, fable2_config.toml, next to the exe — separate from fable_2.toml, which is the ReXGlue SDK's cvar config. It is staged by the build (once; user edits survive rebuilds) and the game recreates it with defaults on launch if it is ever missing. Loaded in Fable2App::OnPostInitLogging(); code in src/core/fable2_config.{h,cpp}.

  • Missing keys -> built-in defaults; wrong types / unknown sections -> logged warnings, defaults used; a syntax error -> dialog + defaults (never blocks launch).
  • To add a setting: add a member to fable2::config::Values (with default), read it in Load(), and document the key in config/fable2_config.toml and the embedded template in src/core/fable2_config.cpp (keep both in sync).
  • Settings that back an SDK cvar (currently the [input] section: keyboard_gamepad_map, mouse_look, mouse_look_scale) are seeded into the cvar at startup — but only when no higher-priority source set it: fable_2.toml (cvar config), REX_* env vars, and the command line (e.g. --keyboard_gamepad_map) still win, and the F3 console can still change the value live.
  • The [patches] section holds runtime toggles for the recomp-level (mid-asm hook) patches, consulted by the hook bodies on every call (src/core/fable2_hooks.cpp) — flip one and relaunch to A/B a patch with no rebuild. Currently: fps_60 (60 FPS hook; true = main loop ~60/s, false = original 30/s). Guest-image data patches are a different file: fable2_patches.toml (see above).

Keyboard controls

The game normally reads a gamepad via the Xbox 360 XamInputGetState API. A synthetic "keyboard gamepad" input driver (src/input/keyboard_gamepad.h) is added on top of the default SDL driver, so host keyboard keys can drive the guest on top of (OR-merged with) whatever a real gamepad reports. The physical pad keeps working; the keyboard just adds buttons. It is wired up in Fable2App::OnPreSetup via config.input_factory.

The mapping is the keyboard_gamepad_map cvar, format Key:Button,Key:Button,.... Its default is no longer hardcoded in the binary: it comes from [input] keyboard_gamepad_map in fable2_config.toml (see User config above), which you can edit to remap permanently. The command line and the F3 console still override it per-launch / live.

  • Key — a host key name understood by rex::ui::ParseVirtualKey (E, Space, LeftShift, F1, ...).
  • Button — a guest gamepad input: A, B, X, Y, LB/RB (shoulders), LT/RT (triggers), Up/Down/Left/Right (dpad), Pause (Start), Select (Back), L3/R3 (thumb clicks), and StickUp/StickDown/ StickLeft/StickRight (left thumbstick, full deflection while held).

The default layout is:

Key(s)Guest input
E / 2 / 1 / 3A / B / X / Y
W A S DLeft stick (up / left / down / right)
EscapePause (Start)
MSelect (Back)
Q / TabLeft / Right trigger
F1 F2 F3 F4Dpad up / down / left / right

Remap at launch without recompiling, e.g.

fable_2.exe --keyboard_gamepad_map "E:A,B:B,Space:L3,Enter:Start"
Mouse look (right stick)

Mouse movement is mapped to the guest right stick for camera control. The movement since the previous poll is converted into stick deflection, so you sweep the mouse to look and stop to stop. Two cvars control it:

ArgumentEffect
--mouse_look <bool>Enable/disable mouse look (default true)
--mouse_look_scale <n>Sensitivity: right-stick units per pixel of mouse movement (default 256; larger = more sensitive)

The defaults come from [input] mouse_look / [input] mouse_look_scale in fable2_config.toml (edit there to change them permanently), the command line overrides per launch, and both cvars are hot-reloadable from the in-game console, so you can dial in the sensitivity live. Example: fable_2.exe --mouse_look_scale 512 for a more sensitive camera.

All cvars above are hot-reloadable, so they can also be changed from the in-game console.

F5 — run an external Lua script

Pressing F5 (host keyboard) runs an external Lua file in the in-game Lua state, exactly the way the game's own RunScript(path) global does — but triggered from the host. This lets you drop a plain .lua file on disk and run it against the live game (no recompile of the scripts needed).

  • Default file: data/scripts/recomp/F5.lua (the build stages src/lua/*.lua into data/scripts/recomp/ next to the exe). The shipped F5.lua snapshots the hero's position (QuestManager.HeroEntity:GetPosition()) and shows X / Y / Z in a message box.
  • Path: set by the f5_lua_path cvar (default scripts/recomp/F5.lua, resolved relative to the VFS root data/). Override per-launch with fable_2.exe --f5_lua_path "scripts/other/MyScript.lua".
  • How it works: src/core/fable2_f5_lua.h captures the CScriptManager::RunScript callable the first time the game loads a .lua script (via a probe on the LuaPlus bound-method dispatcher), then replays that call with your path when F5 is pressed. The file is loaded fresh on each press, so you can edit it live (the VFS re-reads it).
  • The script runs in the game's global Lua environment, so it has the full game API (QuestManager, Debug, GUI, Creature, Player, ...). Plain text is fine — RunScript/loadfile compile it for you.

Implementation: F5 edge-detection in src/input/keyboard_gamepad.h, a per-frame replay from the MainRenderLoop hook in src/diagnostics/fps_meter.h, and the string-build + RunScript call in src/core/fable2_f5_lua.h.





Notes for dev who want to work on the build:

Guest-image patches (fable2_patches.toml)

Data patches for the loaded default.xex guest image (Xenia game-patches format), applied before the guest module launches: Fable2App::OnPostLoadXexImage() → fable2::patches::Load() + ApplyAll() (code in src/core/fable2_patches.{h,cpp}). Same lifecycle as the user config: staged by the build, recreated with the built-in defaults if missing, and a broken file falls back to the built-ins (dialog + log) so it never blocks launch. Each [[patch]] has an enabled toggle (default true) — flip it in the file and relaunch to A/B a patch with no rebuild. Scope: data patches only — code-region ops are inert in this recomp (guest .text is never executed); code patches are mid-asm hooks instead. Full details, the current patch list, and how code patches work: docs/patches.md.

Guest function-call tracing (fable2_func_trace.log)

To figure out what each recompiled function does, every guest function entry can be logged by name. Codegen emits REX_FUNC_PROLOGUE() at the top of every function in generated/default/fable_2_recomp.*.cpp; the build hooks that one macro (via src/core/fable2_func_trace.h, appended to the recompiled PCH after the generated pch — no generated files are modified) so each entry logs its name to fable2_func_trace.log next to the exe. Consecutive calls of the same function are run-length encoded (per thread) to keep the file small:

GetNewGameLoadingGlobal
sub_82189708 x 4821
LoadingScreen_Virtual43
sub_82CC1BC0 x 3
...

(x N means that function was called N times in a row; a bare name is a run of one.)

Session summary: call counts are accumulated separately and written to fable2_func_summary.log (same folder), one line per function, sorted by total count:

18422331 x MainRenderLoop_82B9CD68
9711204 x __restgprlr_28
54 x Story_FirstChildCombat

It's refreshed every 5 s while tracing (and once more on clean exit), so you can watch it in another window to see at a glance what's being called; the last write is at most ~5 s stale even if the game exits via ExitProcess. Counts respect the same on/off + filter as the text log.

Off by default (one atomic load per call when off). Enable with:

set FABLE2_FUNC_TRACE=1            rem every guest function entry
set FABLE2_FUNC_TRACE_FILTER=LoadingScreen   rem optional: only names containing this

Either output can be turned off independently (default: both on): FABLE2_FUNC_TRACE_LOG=0 skips fable2_func_trace.log (summary only), FABLE2_FUNC_TRACE_SUMMARY=0 skips fable2_func_summary.log (trace only).

or at runtime from a named-function override (see src/diagnostics/fps_probe.h for the override pattern): Fable2FuncTraceSetEnabled(true) / Fable2FuncTraceSetFilter("LoadingScreen") / Fable2FuncTraceSetSubsOnly(true) / Fable2FuncTraceSetLogEnabled(false) / Fable2FuncTraceSetSummaryEnabled(false) / Fable2FuncTraceFlush() (declared extern "C" in src/core/fable2_func_trace.h).

Naming mode (FABLE2_FUNC_TRACE_SUBS_ONLY=1): log only the unnamed guest functions - names matching sub_ + hex digits - dropping named functions, the __savegprlr_*/__restgprlr_*/__savevmx_* register helpers, and xstart. Both the text log and the summary honor it, so the summary becomes a ranked list of the unnamed hot functions to name next. Composes with the substring filter (FABLE2_FUNC_TRACE_FILTER=82B9 narrows to an address range).

Launcher: fable2-functrace.cmd (staged next to the exe) does the env var dance for you and renames the previous session's log to fable2_func_trace_prev.log first:

Arguments are keywords in any order (subs enables naming mode, trace/summary pick which output file(s) are written, the backend word picks the GPU path, anything else is the substring filter):

fable2-functrace.cmd                  D3D12, trace every call
fable2-functrace.cmd vulkan           Vulkan, trace every call
fable2-functrace.cmd subs             naming mode: only sub_<hex> functions
fable2-functrace.cmd subs LoadingScreen   naming mode + substring filter
fable2-functrace.cmd d3d12 82B9 subs  D3D12, address-range naming mode
fable2-functrace.cmd summary          summary file only, no trace log
fable2-functrace.cmd trace            trace log only, no summary file

With no trace/summary keyword both files are written (the default). summary is the cheap option for long sessions - it avoids the multi-GB sequential log while still accumulating the call counts.

It writes its own lightweight log (same pattern as fps_probe.log) rather than the SDK spdlog logger, which would be far too slow at Fable 2's call rate. To remove the feature: delete the target_precompile_headers block in CMakeLists.txt + src/core/fable2_func_trace.h and rebuild.

Remote control (automated input channel)

fable_2.exe runs a localhost TCP remote control server so an external automation harness can drive the guest gamepad over JSON-lines messages — no human at the keyboard. Design doc: plans/ai-remote-input-control.md.

Debug builds only. This is a debugging/automation channel — it opens a localhost TCP port and injects guest input — so it must not ship to players. It is gated on the FABLE2_REMOTE_CONTROL macro, defined only for the Debug build; Release / RelWithDebInfo builds compile out the pad driver + server entirely (no port is opened and fable2_control.py is not staged).

  • Where: 127.0.0.1:8791 by default (configurable, see below). One JSON object per line; every request gets exactly one response line; keep-alive connections are supported.
  • How: a second synthetic pad driver (src/input/remote_gamepad_driver.h) is registered next to the keyboard driver, fed by the server (src/input/remote_control_server.h). It OR-merges with the human pads and is not gated on window focus, so the AI can drive the game while it's in the background.

Quick start (from the build dir, game running):

python tools\fable2_control.py ping
python tools\fable2_control.py press A --hold 120
python tools\fable2_control.py get-state
python tools\fable2_control.py script --file repro.json

Or speak the protocol directly (any language):

> {"cmd":"press","input":"RT","hold_ms":900}
< {"ok":true,"input":"RT","release_in_ms":900}
> {"cmd":"script","steps":[
      {"delay_ms":500,"op":"press","input":"LB","hold_ms":2000},
      {"delay_ms":1000,"op":"press","input":"A","hold_ms":80}]}
< {"ok":true,"duration_ms":2500}

hold_ms ≤ 0 (or omitted) means hold until released/cleared; a positive value auto-releases after that many ms. In a script, each step's delay_ms is a gap since the previous step on a single clock — the example presses LB at t=500 (held to t=2500) and A at t=1500 (held to t=1580), so duration_ms (when the last input stops being active) is 2500.

Commands: ping, info, auth, press (input, hold_ms, value), release, stick (input = StkLx/StkLy/StkRx/StkRy, value, hold_ms), state (sticky baseline: buttons[], triggers{LT,RT}, stk{lx,ly,rx,ry}), clear, script (atomic timed sequence), get_state, game_state (current boot/menu state, see below), cvar (get/set any cvar by name), enable/disable. Input names match the keyboard-gamepad vocabulary (A/B/X/Y, LB/RB, LT/RT, Up/Down/Left/Right, Start/Back, L3/R3, stick direction shorthands). StkLy positive = forward (Fable 2 convention). script is a single atomic message, so a repro sequence runs with no network round-trips between steps.

Game state

game_state reports which boot/menu screen the game is on so the AI can navigate and verify its actions:

python tools\fable2_control.py game-state
> {"cmd":"game_state"}
< {"ok":true,"state":{"code":2,"name":"PressAScreen"}}

The classifier samples once per second on a background thread (independent of the render loop, so it keeps working during the movie, which renders video with no UI text) and reports one of:

StateMeaning
? (Unknown)Arena not mapped yet / undetermined (first ~1 s).
PreMainMenuSplash / intro, before the "Press A" prompt.
PressAScreenThe "Press A to start" prompt is on screen.
MainMenuMovieThe idle movie is playing (no prompt / menu).
MainMenuThe main menu (reached by pressing A on the prompt).

This follows the game's own state machine: PreMainMenu →(time)→ PressAScreen →(time)→ MainMenuMovie →(time)→ PressAScreen, with PressAScreen →(A)→ MainMenu and MainMenuMovie →(A)→ PressAScreen. The prompt vs. the menu are visually indistinguishable (same element-list draw rate), so the transition into the menu is driven by the A-press, observed on the final merged pad state (remote + keyboard + physical — it works no matter which input drives A) and latched with its exact timestamp. State changes are logged to the in-game logger (REXSYS_INFO, same channel as the remote control) on transition only, and every sample is written to fable2_state_probe.log when FABLE2_STATE_PROBE=1 is set.

Config ([remote] in fable2_config.toml): enabled (default true), host (127.0.0.1; 0.0.0.0 exposes it on all interfaces), port (8791; if busy, +1..+9 are tried, the bound port is logged at startup), token (empty = no auth; when set, each connection's first line must be {"cmd":"auth","token":"..."}). Every received command is logged (audit trail) to logs/.

Building

Everything the build needs is either in this repo or auto-fetched — no hardcoded paths. Prerequisites (all standard tools):

  • CMake ≥ 3.25 (on PATH)
  • LLVM (clang/clang++/lld) — on PATH or the default C:\Program Files\LLVM install
  • Ninja (on PATH, %USERPROFILE%\bin, or the WinGet package dir)
  • Internet on first build — build.cmd auto-downloads the prebuilt ReXGlue SDK v0.10.0 (~100 MB) from the official release into thirdparty\rexglue-sdk\. A pre-existing SDK is used instead if found: thirdparty\rexglue-sdk\win-amd64 first, then a sibling ..\rexglue-sdk-0.10.0-win-amd64\win-amd64.
build.cmd               # configure + fable_2_codegen (runs rexglue codegen on the manifest)
build.cmd fable_2       # configure + build the full executable
build.cmd <target>      # any other CMake target
build.cmd -release [t]  # build as Release (-O3) instead of Debug (-r is short form)

The first build.cmd run fetches the SDK if needed, then configures and builds — that is all a fresh checkout requires (plus the game content above).

Manual/advanced s