Fable II Recompiled
Fable II Recompiled brings Fable II to PC.
- Type: PC port
- Game: Fable II
- Runs on Windows
- By himdo
- Latest release 1.2.0, 2026-09-25
- Source: https://github.com/himdo/Fable-2-Recomp
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$SystemUpdateand extract them into the project root (dataand$SystemUpdateextract as folders). You can also grabnxeartand 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:
- Extract a downloaded copy release of the game
- Extract the "data" and "default.xex" from your copy of the game, See "How to extract" for more info on that
- 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):
| Argument | Effect |
|---|---|
--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 / --fullscreen | Presentation 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 inLoad(), and document the key inconfig/fable2_config.tomland the embedded template insrc/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), andStickUp/StickDown/StickLeft/StickRight(left thumbstick, full deflection while held).
The default layout is:
| Key(s) | Guest input |
|---|---|
E / 2 / 1 / 3 | A / B / X / Y |
W A S D | Left stick (up / left / down / right) |
Escape | Pause (Start) |
M | Select (Back) |
Q / Tab | Left / Right trigger |
F1 F2 F3 F4 | Dpad 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:
| Argument | Effect |
|---|---|
--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 stagessrc/lua/*.luaintodata/scripts/recomp/next to the exe). The shippedF5.luasnapshots the hero's position (QuestManager.HeroEntity:GetPosition()) and showsX / Y / Zin a message box. - Path: set by the
f5_lua_pathcvar (defaultscripts/recomp/F5.lua, resolved relative to the VFS rootdata/). Override per-launch withfable_2.exe --f5_lua_path "scripts/other/MyScript.lua". - How it works:
src/core/fable2_f5_lua.hcaptures theCScriptManager::RunScriptcallable the first time the game loads a.luascript (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/loadfilecompile 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:8791by 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:
| State | Meaning |
|---|---|
? (Unknown) | Arena not mapped yet / undetermined (first ~1 s). |
PreMainMenu | Splash / intro, before the "Press A" prompt. |
PressAScreen | The "Press A to start" prompt is on screen. |
MainMenuMovie | The idle movie is playing (no prompt / menu). |
MainMenu | The 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\LLVMinstall - Ninja (on PATH,
%USERPROFILE%\bin, or the WinGet package dir) - Internet on first build —
build.cmdauto-downloads the prebuilt ReXGlue SDK v0.10.0 (~100 MB) from the official release intothirdparty\rexglue-sdk\. A pre-existing SDK is used instead if found:thirdparty\rexglue-sdk\win-amd64first, 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