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.5.0, 2026-10-11
- 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.
Important Note:
This project does not condone Piracy, any mentions or links will result in removal and an instant ban from this repo!
Playing the game
You need your own Fable 2 GOTY disc image (.iso) and about 7 GB of free space.
- Download the
windowszip from the latest release and unzip it anywhere. - Open
Fable2Launcher.exe. - Click Extract from ISO and pick your
.iso. The game files go into the launcher's folder. - Click Play Fable II.
Updating: unzip the new release into its own folder, copy your saves folder across and extract your .iso again.
Need help? Ask on the Discord. To compile the game yourself, see Building.
Current and planned features
Original GOTY USA/Europe and German GOTY dumps are supported by the default
goty-compatible build profile. Other XEX revisions remain rejected. See
German GOTY validation and known limits.
[x] Can be used to beat the game
[x] Guild chest fully unlocked
[x] Uncapped framerate / increased framerate
[ ] Higher Resolution support
[x] 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
[x] Hero / Dog Texture bug fix (upstream renderer, verified on German GOTY)
[ ] Vulkan support
[ ] Linux Builds
[x] Custom commands to aid in debugging
[ ] Improved Graphics rendering
[ ] Custom menu(s) / modifying menus for extra functionality (like closing the game)
[x] Supporting other languages
- Germany
- France
- Italy
- Russia
- Spain
[x] Custom Launcher
- Built in ISO extractor
Windows launcher
The optional WPF launcher detects the original game edition and configures
output resolution, internal render scale, anisotropic filtering, FXAA, VSync,
window/fullscreen mode and a 30/60/120/144/165/240/unlimited FPS limit. It keeps its preferences
in launcher-settings.toml and writes fable_2.toml, preserving other settings
and a one-time backup. Preferences survive the runtime rewriting its config.
It can also extract the game straight from a disc image: the Extract from
ISO button opens a raw .iso, pulls out just default.xex first, checks its
SHA-256 against the supported-version list, and only once it matches does it
extract the rest of the disc (default.xex and data/) into the launcher's own
folder, with a live progress screen and a Stop button. It reads Xbox 360 GDFX
discs with the shared X360Extract library, so its output
matches the x360extract tool byte for byte — making the launcher the simplest
way to get the game files in place, with no separate extractor tool.
The game-folder selection defaults to the launcher's own folder on first launch
and then remembers the user's saved location. Game files can live separately;
the launcher uses --game_data_root. Both applications are named Fable 2
Recompiled and use new project-owned icons.
There is now a discord!
I've made a custom discord for this project, as well as forks and other recomps and decomps of the fable series
Notes for running the game
- How to extract (built-in, recommended): the launcher has an Extract from ISO button. Click Extract from ISO and pick your disc image (
.iso). It first checks the iso against the supported-version list, and only once that matches does it extract disc (default.xexanddata/) into the launcher's own folder. No separate tool is required. - How to extract (external GUI tool): alternatively, rip the disc to an ISO and open it with XboxImageExtractor — a GUI tool for Xbox 360 game images. It lists the image's filesystem; select
default.xexanddataand extract them into the project root (dataextract as folders). - 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.xexanddata/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. - Although this was made for the US / Europe GOTY version I have been able to run the following regions: Germany, France, Italy, Russia, and Spain. Your milage may very if you use those regions.
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 — easiest with the launcher's Extract from ISO button, or an external GUI tool (see "How to extract")
- 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) |
--skipHash | Skip the default.xex SHA-256 integrity check. The hash is still written to the log, but a mismatch no longer blocks launch (useful for a modified or non-catalogue XEX) |
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. This includeshigh_tick_rateandhigher_hf_tick_rate(Xenia "High Tick Rate" / "Higher HF Tick Rate"; turn both on together), which also drive the guest-image data writes below. There is no separate patch file.
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, ...), or a mouse button:LMB(left),RMB(right),MMB(middle), andXMB1/XMB2(side buttons). Mouse button names are case-insensitive; a held mouse button behaves exactly like a held key. - 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 (mouse buttons plus keyboard) is:
| Key(s) | Guest input |
|---|---|
LMB | X |
RMB | Y |
MMB / B | B |
Shift / E | A |
W A S D | Left stick (up / left / down / right) |
Q / R | Left / Right trigger |
Z / Tab | Left / Right shoulder |
| Arrow keys | Dpad up / down / left / right |
Escape | Pause (Start) |
M | Select (Back) |
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
Data patches for the loaded default.xex guest image (Xenia game-patches
ops), applied before the guest module launches: Fable2App::OnPostLoadXexImage()
→ fable2::patches::ApplyAll() (code in src/core/fable2_patches.{h,cpp}).
There is no patch file: each patch is switched on by a [patches] key in
fable2_config.toml (currently high_tick_rate and higher_hf_tick_rate).
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: 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 (curren