Oracles Decomp
Oracles Decomp is a native port of The Legend of Zelda: Oracle of Ages and Oracle of Seasons to Windows, Linux, macOS, and Android.
- Type: PC port
- Game: The Legend of Zelda: Oracle of Ages, The Legend of Zelda: Oracle of Seasons
- Runs on Windows, Linux, macOS and Android
- By kirby-letsgo
- Latest release nightly, 2026-10-07
- Source: https://github.com/kirby-letsgo/oracles-decomp
README
Native reimplementation of The Legend of Zelda: Oracle of Ages and Oracle of Seasons in C.
Every routine the games run is readable C: all of Ages, and 99% of Seasons (shared with Ages where the two games agree, hand-written where Seasons differs; the rest is generated C from the disassembly). The native build runs both games with no CPU emulator and no ROM code. Behaviour is checked against the original ROM by replaying full playthroughs and comparing every C routine with the original code it replaces.
You need your own US ROMs; none are included.
AI DISCLOSURE
This native implementation has used help from generative AI, spefically claude opus.
If you don't want to use it for that I completly understand.
Prerequisites
- macOS (Linux and Windows build through CMake)
- Xcode Command Line Tools (Clang)
- CMake 3.15+ and Ninja
- SDL3 (Homebrew, or fetched automatically on first configure)
- Python 3, and wla-dx plus pyyaml to build the disassembly's symbol files
brew install cmake ninja sdl3 wla-dx
pip3 install pyyaml
Setup
git clone git@github.com:kirby-letsgo/oracles-decomp.git
cd oracles-decomp
git submodule update --init --recursive
make -C ref/oracles-disasm ages CPUS=4
make -C ref/oracles-disasm seasons CPUS=4
Put the ROMs and the CGB boot ROM in roms/ (git-ignored):
| File | SHA1 |
|---|---|
roms/Legend of Zelda, The - Oracle of Ages (USA, Australia).gbc | 880374fb978b18af4aa529e2e32f7ffb4d7dd2f4 |
roms/Legend of Zelda, The - Oracle of Seasons (USA, Australia).gbc | ba1268290fb2b1b70505d2d7b5825fc8a4816a4b |
roms/cgb_boot.bin | the Game Boy Color boot ROM |
Build
cmake -S . -B build -G Ninja
cmake --build build
ctest --test-dir build
-DORACLES_SDL=OFF builds only the headless tools. -DORACLES_SDL_VENDORED=ON builds SDL 3.4.16
from source and links it statically (release builds do this); otherwise an installed SDL3 is used
when there is one.
Linux (Debian/Ubuntu; other distros need the same libraries): tools/linux_deps.sh installs the
compiler, CMake, Ninja and SDL's build dependencies, then build as above with
-DORACLES_SDL_VENDORED=ON (distributions rarely ship SDL3 yet).
Windows (64-bit) is cross-compiled with MinGW-w64 (brew install mingw-w64 or apt install mingw-w64); the result is a single Oracles.exe that needs only system DLLs. Wine runs the tests.
cmake -S . -B build-win -G Ninja -DCMAKE_TOOLCHAIN_FILE=cmake/mingw-w64-x86_64.cmake -DORACLES_SDL_VENDORED=ON
cmake --build build-win
wine build-win/test_native_tas.exe
Downloads
Every push to main rebuilds the apps and replaces the rolling
nightly release: macOS
(universal DMG), Windows (x64 zip), Linux (x86_64 AppImage) and Android (arm64 APK). You need your
own Oracle of Ages / Seasons (USA) ROMs. The workflow is .github/workflows/release.yml; the APK is
signed with the release key from the repository secrets ANDROID_KEYSTORE_B64,
ANDROID_KEYSTORE_PASSWORD and ANDROID_KEY_ALIAS (debug-signed, with a warning, when they are
missing).
Playing
Two apps, same game:
oracles-native, the real port: no CPU emulator. On first launch it reads your ROM once, keeps the graphics, sound and data, zeroes the code bytes, and caches the result; later launches never touch the ROM again../build/oracles-native "roms/Legend of Zelda, The - Oracle of Seasons (USA, Australia).gbc"oracles, the development app: runs the ROM on our emulator core with the C routines hooked in, so it can boot the real boot ROM, record playthroughs and fall back to the original code../build/oracles "roms/Legend of Zelda, The - Oracle of Ages (USA, Australia).gbc" roms/cgb_boot.bin
Without arguments oracles-native opens a launcher: Ages and Seasons side by side (each in its own
title-screen colours), each game's three save files with name, hearts and essences, Resume (the state
from the last quit), Load state, and the title screen. Choosing a file boots straight into it.
Controls: arrow keys to move, X / Z for A / B, Return for Start, Backspace or Right Shift for
Select, M to mute, F11 or Cmd+F for fullscreen, hold Tab (gamepad: right trigger) to fast-forward.
The window resizes in whole-pixel steps. Settings (from the launcher or the pause menu): volume,
screen filter (sharp, scanlines, LCD grid, CRT), scale (pixel: whole-pixel steps; fill: the
largest size that fits, drawn sharp at the next whole scale and shrunk smoothly), Game Boy Color
colours, fullscreen, controls
(every button, Pause, Fast-forward, Swap and the item buttons remap for keyboard and gamepad), and
optional quality-of-life toggles, all off by default: fast text, faster menus, quick swap (C
swaps the A and B items) and 4 slots (A/S are two more item buttons: highlight an item in the
inventory and press one to assign it, then hold it in play to use the item).
oracles-native: messages (state saved, item set, sound off) show at the bottom of the screen. Esc (or the gamepad's Guide button) pauses: Resume, Save state, Load state (4 slots with thumbnails) and Quit, which saves a Resume state and returns to the launcher. Cmd+S / Cmd+R save and load slot 1. Everything lives in the per-user app folder (~/Library/Application Support/oracles-decomp/oracles/on macOS), including the game's own save.oracles: Esc is Start as well; Cmd+S / Cmd+R save and load one state next to the ROM, F12 takes a screenshot, and the game's save (battery RAM) is kept next to the ROM as.sav.
ORACLES_TOUCH=1 ./build/oracles-native shows the phone's touch controls on the desktop, with the
mouse as a finger.
Widescreen
Settings > WIDESCREEN shows 256x144 (16:9 at the game's 144 lines): the game's own 160 pixels in the middle, unchanged, and 48 pixels each side of the rooms around it. Large rooms (dungeons) show more of themselves; past a room's edge come the neighbouring rooms, decoded from your ROM: the overworld grid (with the right season in Seasons) and, in dungeons, the rooms through a doorway once visited. The strips scroll along with every room change. Nothing moves in them (the game only runs the room you are in), they show a room as the ROM describes it (not opened chests or cut grass until you go there), and houses, caves, menus and cutscenes get the game's border colour. DIM SIDES (on by default) draws the strips a little darker than the live room.
test_wide checks the renderer against the game through both TAS movies: the middle 160 columns
drawn its way equal the PPU's picture exactly, and what a strip showed of the next room matches
that room's own picture once the movie is in it (Seasons 96.8%, Ages 98.9% of the pixels; the rest
are things the player changed). test_room checks the ROM room decoder against every room the
movies load.
Save sync
Settings > SYNC keeps saves in step across devices through the sync server (sync-server/): CREATE
ACCOUNT gives a 16-digit code, and ENTER CODE on another device joins it (no passwords). Synced:
each game's save, save-state slots with their pictures and item buttons, and the shared settings;
never the ROM-derived files. It syncs when the launcher opens, before a game starts, and when you
leave a game or the app; SYNC NOW does it by hand. A sync that fails (offline, server down) is
tried again after 5 s, then twice as long each time up to every 5 minutes, and at once when the app
comes back to the foreground; the running game's own files wait for the game to end. When a save
changed on two devices since they last synced, a KEEP WHICH? screen shows both (files and hearts, or
the state's picture) and you pick. Save states move between every platform (all 64-bit builds share
one layout); only a build with another state format refuses one. The server address is the CMake
setting ORACLES_SYNC_URL; a device can use another one with url= in sync.ini in its app folder.
HTTP uses the system's own TLS: libcurl (macOS, Linux; sync is off without it), WinHTTP, and
Android's HttpURLConnection. ORACLES_SYNC_URL=http://host test_sync_http checks a server. The
sync_flow test plays two devices against the real server on an in-memory database (pnpm local in
sync-server/; needs its pnpm install and the Seasons ROM): a conflict kept each way, and a save
made offline that the retry uploads.
Android
oracles-native also builds as an Android app (arm64, Android 8+). Needs the Android SDK with
platform 35, build-tools 35, NDK 27.2.12479018 and CMake 3.31.6 (sdkmanager installs them), and a
JDK; android/local.properties names the SDK (sdk.dir=...).
cd android
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
The build runs this repo's CMake with SDL 3.4.16 from source (its Java glue is vendored in
android/app/src/main/java/org/libsdl/app, same release) and optimises the engine in debug APKs too.
On first launch the app opens the system file picker: choose the ROM (copy it to the phone's
Downloads first, e.g. adb push ROM /sdcard/Download/); Add ROM in the launcher adds the other game.
Touch controls sit under the game in portrait and at its sides in landscape: D-pad (diagonals by
touching between arms), A, B, Start, Select, Pause and fast-forward (>>, hold), plus X and Y when 4
slots is on. They hide when a controller or keyboard is used and come back on the next touch. The
back button pauses (and goes back in menus). Leaving the app saves the game and a Resume state, and
coming back opens the pause menu. adb logcat -s oracles shows the engine's messages.
Recording a playthrough
The recordings in tas/ are the test suite. To extend the Seasons one:
./build/oracles "roms/Legend of Zelda, The - Oracle of Seasons (USA, Australia).gbc" roms/cgb_boot.bin --record tas/seasons-play.inputs
It resumes where the file ends (from a snapshot, without replaying) and writes the file every
minute and on quit. Loading a save state while recording rewinds the recording to that point.
See tas/README.md for re-recording the reference hashes afterwards.
Verification
ctest --test-dir build # unit suites, TAS prefixes, whole native runs
TAS_FRAMES=321712 ctest --test-dir build -R tas # both whole movies with hooks too
Two full-game movies are the main gate: the console-verified Ages TAS and the console-verified
Seasons TAS (tas/README.md), plus a recorded Seasons playthrough.
The headless runner replays a movie and checks it:
./build/oracles-run --rom ROM --boot roms/cgb_boot.bin --init-ram tas/gbhawk-wram0.txt \
--tas tas/seasons-play.inputs --frames 265064 --verify-shadow --ref-check tas/seasons-play.ref
--verify-shadow runs every hooked C routine, replays the original code from the same state and
compares registers, memory and cycles. --ref-check compares the machine state every 60 frames
with the hashes the pure interpreter (--no-hooks) recorded. oracles-native-run replays
movies on the native build.
Layout
src/core/,src/hw/: SM83 CPU, memory bus, PPU, APU, timers.src/game/: the game in C, one file per disassembly file;src/game/seasons/holds the Seasons-only code.src/hooks/: the address-to-function tables and the lists the generators read.src/rt/: the native runtime (no interpreter).src/platform/: the SDL apps and the headless runner.tools/: generators and the audits run on every change.tas/: input movies and reference hashes.ref/oracles-disasm/: the community disassembly (submodule), used for symbols and routines.