Epoch & Equinox
A native player for The Legend of Zelda: Oracle of Ages and
Oracle of Seasons — modern launcher, mod support, and an optional
3D voxel mode with a third-person chase camera. Bring your own ROMs.
---
> [!IMPORTANT]
> **This project contains no game code, and neither do its binaries.** The
> player reads a ROM you supply at runtime, like any emulator — nothing
> derived from the games ships in this repository or in a release.
Both games run from one binary at full native speed, with savestates, shader
presets, controller remapping, SGB borders, IPS/BPS mod loading — and a voxel
mode that rebuilds the overworld out of the Game Boy's own tilemap: tilted
dioramas at 3× internal resolution, hand-sculptable room heights, and a
third-person chase camera that follows Link through the world in perspective.
The same room, untouched · as a diorama · from the chase camera
## What's inside
| | |
|---|---|
| **Player** | Both carts native from one binary; savestates, rewind (~12 s), shader presets, SGB borders, controller remapping, 40 ms audio |
| **Voxel mode** | Tilted dioramas and a third-person chase camera built from the game's own collision data, at 3× internal resolution — dungeons included, with distance fog and live-tunable look (`voxel/tuning.ini`) |
| **Sculpting** | Press F4 and paint room heights in-game (Backspace undoes); the same plain text files hand editors use |
| **Launcher** | Cover art, ROM install, IPS/BPS mods, controller navigation, self-update that never touches your saves |
| **Achievements** | Data-driven packs watched over the game's memory, Steam-style toasts, custom icons, a browser in launcher and Esc menu |
| **Secrets** | Every code your save can produce, generated with the game's own cipher — and the panel can type them into the grid for you |
| **Continue Legend** | Finish one game, click once: the other game starts, enters your transfer secret by itself, and hands you the linked quest to the true ending |
| **Saves** | Import from any emulator (validated by content), export, and a backup trail where every replacement is one click to undo — each slot's essences, hearts, rings, deaths and playtime at a glance |
| **Streaming** | OBS overlays fed live by the player: framed layouts with a real hole for the game, achievement cards, and a Stream page in the launcher to configure it all |
| **Speedrunning** | Auto-splits that drive LiveSplit, run timer, split list, input display, and an item tracker that can wear icons ripped from your own ROM |
## Quick start
**Easiest: grab a build from [Releases](../../releases)** (or the latest
`build` artifact under Actions), unzip it, and drop your ROMs into the
`roms/` folder next to the binary:
| game | file | SHA-1 |
|---|---|---|
| Oracle of Ages (USA/Australia) | `roms/tlozooa.gbc` | `880374fb978b18af4aa529e2e32f7ffb4d7dd2f4` |
| Oracle of Seasons (USA/Australia) | `roms/tlozoos.gbc` | `ba1268290fb2b1b70505d2d7b5825fc8a4816a4b` |
Then run `epoch` (double-click works). Either game alone is fine — you'll
just get that one, and any other Game Boy / Game Boy Color ROM in `roms/`
shows up too. The Oracles hashes are verified at launch; a different
revision gets a warning.
**Staying current.** The launcher checks GitHub for a newer release on
startup and notes one in the corner if it finds it; **Updates** in the menu
checks on demand, shows the release notes, and installs the new build in
place. It replaces what the release ships and nothing else — your `roms/`,
`mods/`, `covers/` and save files are left exactly as they are. Being
offline is not an error: the startup check gives up quietly. A launcher run
from a source checkout won't overwrite itself; use `git pull`.
**Building from source** takes about a minute — no game data is involved:
```sh
git clone --recurse-submodules https://github.com/ZakyPew/epoch-equinox.git
cd epoch-equinox
./setup.sh # Linux/macOS; Windows: setup.ps1 (needs VS + vcpkg)
```
If the repository was cloned without submodules, fetch the optional native Ages
game source with `git submodule update --init --recursive`.
History: why this repo used to take 25 minutes to build
Earlier revisions statically recompiled each ROM into ~170 MB of generated C
and linked it into the binary — which is also why binaries could never be
distributed. Then [`tools/interp_probe.c`](tools/interp_probe.c) demonstrated
the generated code was never executed: the runtime's dispatch interprets
straight from the loaded ROM image, and a cart-free binary produces
byte-identical frames. The generation step is gone; what remains is a player
that happens to have unusually good knowledge of two specific games.
## Controls
Defaults. All of it is rebindable in the in-game menu, which also has per-brand
button labels for Xbox, PlayStation, Switch Pro and Joy-Con.
| Action | Keyboard | Controller |
|---|---|---|
| D-pad | Arrow keys / `WASD` | D-pad or left stick |
| A | `Z` / `J` | B *(right face button)* |
| B | `X` / `K` | A *(bottom face button)* |
| Select | `Backspace` / `Right Shift` | Back |
| Start | `Enter` | Start |
| Fast forward *(hold)* | `Tab` | Right trigger |
| Max speed *(toggle)* | `` ` `` | Left trigger |
> Face buttons follow the Nintendo layout, so on an Xbox pad the Game Boy's A
> is the physically right button and B is the bottom one — the same positions
> as the original hardware.
## Hotkeys
| Key | Action | Controller |
|---|---|---|
| `Esc` / `F10` | Open the runtime menu | L3 / R3 |
| `F5` | Save state | X |
| `F8` | Load state | Y |
| `F6` / `F7` | Previous / next savestate slot | — |
| `F1` | Toggle FPS overlay | — |
| `M` | Mute | — |
| **`F2`** | **Achievements & Secrets panel** | — |
| **`F3`** | **Cycle voxel mode** (off → 15° → 30° → 45° → chase cam) | — |
| **`F4`** | **Sculpt room heights** (voxel modes; number keys paint, `Bksp` undoes) | — |
| **`R`** *(hold)* | **Rewind** — step back through the last ~12 seconds | — |
| **`F9`** | Jump back to the moment you entered this room | — |
| **`C`** | Chase cam: swing behind Link (hold to keep following) | **R3** |
The Esc menu opens with a **Display** section — fullscreen, scaling mode
(Pixel Perfect / Aspect Fit / Aspect Fill / Stretch), scale filter, window
size, and a live readout of the exact scale on screen (e.g. `Showing 9x:
1440x1296 in a 2560x1400 window`). Pixel Perfect grows in whole steps and
letterboxes the rest — that is what keeps it razor sharp; use Aspect Fit to
fill the window instead. Below it: the voxel diorama section, then the rest
(savestates, palettes, shader presets, SGB borders, hardware mode, audio and
input remapping).
### Rewind and room checkpoints
Old games are unforgiving in ways that stopped being fun; these give the time
back without touching the game.
**Hold `R`** and play runs backwards through the last twelve seconds or so —
let go and it continues from wherever you stopped. A bad jump, a cheap hit, a
puzzle you'd rather retry costs a second instead of a walk.
**`F9`** returns you to the moment you walked into the room you're standing in.
The player watches the game's own `wActiveRoom` and drops a checkpoint on every
room change, which is LynnaLab's "quickstart" idea from the other side of the
screen — the editor boots you into a room, this puts you back at the start of
one.
A corner readout shows the room you're in (`Room 0-6A`) while you play, which is
also the number you need to name its
[height-override file](#sculpting-rooms-by-hand).
## Launch options
The launcher handles this for you, but the binary stands alone:
| option | effect |
|---|---|
| `--game ` | run a cart directly (`tlozooa`, `tlozoos`) |
| `--list-games` | print the ids in this build |
| `--no-mods` | boot the stock ROM, ignoring `mods/` |
| `--voxel ` | start with the voxel mode on (`0`–`4`; `4` = chase cam) |
| `--games-json` | machine-readable game table (what the launcher reads) |
```sh
./build/epoch --game tlozooa --voxel 2
```
### Native Ages reconstruction (developer preview)
The Godot version is included as the separate
[`ooa-godot` submodule](backends/ooa-godot), pinned to our fork's
`codex/mod-asset-overlays` branch. It is an in-progress native reconstruction,
not the original ROM running in an emulator. ROM files and generated,
ROM-derived runtime assets are not included. Follow the submodule README to
generate local assets from a clean supported US ROM and `oracles-disasm`.
The backend remains hidden from the normal player-facing launcher menu. With
Godot 4.7.1/.NET available via `GODOT4`, `GODOT`, or `PATH`, developers can
reveal its launcher action with the hidden `--dev-native-backend` flag:
```powershell
$env:GODOT4 = 'C:\path\to\Godot_v4.7.1-stable_mono_win64.exe'
python launcher/epoch_launcher.py --runner build/epoch.exe --dev-native-backend
```
Epoch launches the submodule's full game project and passes its `mods/`
directory to the native mod loader. Set `OOA_GODOT_PATH` to use another
checkout, or `--godot-mods-dir` to point at a separate local mod collection.
The small Epoch-owned room lab remains as a fallback when the submodule is not
available; it is not the implementation Epoch will grow in parallel.
See the [Godot backend contributor and build guide](docs/GODOT_BACKEND_CONTRIBUTING.md)
for setup, validation, launcher testing, and the two-repository contribution
workflow.
The [native Ages launcher and Godot starter tasks](docs/GODOT_STARTER_TASKS.md)
include a launcher-only test task and scoped reconstruction/modding proposals.
## Voxel mode
**Off by default — press `F3`.** The game plays exactly as it always did until
you turn it on.
The overworld is re-rendered as a tilted 3D diorama, rebuilt every frame from
PPU state, plus the game's own collision and camera data read live from WRAM
at the addresses named by
[oracles-disasm](https://github.com/Stewmath/oracles-disasm).
- **on the Oracles carts, terrain shape comes from the game's own collision
data** — `wRoomCollisions` and the camera, read live from WRAM at the
addresses named by [oracles-disasm](https://github.com/Stewmath/oracles-disasm).
Values `$01`-`$0F` are decoded as the game's real four-bit 8×8-quadrant
mask, so a half wall occupies only its solid half instead of becoming a
mysteriously shorter 16×16 block. Water sinks because the game says it's
water; walls rise because collision says solid; menus render flat because
`wOpenedMenuType` says a menu is open
- on anything else, tiles are classified per 8×8 by colour — water sinks,
paths lie flat, bushes and rocks rise, trees and walls rise highest
- the world is marched far-to-near, projecting cell tops and filling the
exposed front wall where height steps down. In chase view, authoritative
height edges also receive true planar faces. Their full 8px bands come from
the original raised tiles with nearest-neighbour sampling -- no invented
masonry and no stretched scanlines -- so cliffs keep both their cart artwork
and their straight room-layout corners
- walkable ground enclosed by those cliff lips becomes a raised plateau. Its
base inherits the bordering lip's actual mid/high class, so Link and props
stand on a shelf exactly level with the edge instead of above it or behind a
fence on flat ground
- motion is eased: terrain grows in after a room change instead of
popping, and Link's ground height ramps across cell boundaries
- terrain is textured from the game's *own* BG tilemap — palettes, season
tints and tile animation carry through untouched, and because sprites are
not part of the tilemap, nobody leaves a flattened ghost of themselves in
the ground
- sprites are re-decoded from VRAM and stood upright as billboards. Trees and
cuttable shrubs are different: `wRoomLayout` identifies each object while
the original 16x16 BG pixels become fixed geometry. A full tree keeps that
complete drawing on a hard canopy tile above a separate trunk tile sampled
from its lower centre. Exposed canopy sides unfold the corresponding half
of the same source art; connected forest variants still meet exactly as the
cart drew them, with no rounded shell or camera-facing tree billboard.
Tufts recover their silhouette and become shallow pixel reliefs. The
overhead copy is removed from the ground, and the depth buffer lets these
world objects hide Link correctly
- water ripples; room-to-room walks keep the sky up and slide the rooms
through flat rather than flickering guessed terrain
- **the sky follows the game**: season-tinted in Seasons (spring through
winter, ember-red in Subrosia), day blue in Ages' present, golden dusk in
the past — with slow procedural clouds. Interiors keep a neutral backdrop
- the status bar stays flat and composites back on top
- **chase cam** (`F3` to the last stop): a third-person camera that starts
directly behind Link, raycasting the same heightfield in true perspective —
distance fog and depth-scaled sprite billboards. It trails behind him while
he walks, but **the right stick owns it**: the stick (or `Q`/`E`) orbits and
briefly holds the chosen heading without fighting you. Click the right stick
(or press `C`) to recenter immediately behind him; the Esc menu can disable
automatic trailing entirely
- **a cliff has exact footprint and one shared height**: the collision nibble
gives the occupied top-left, top-right, bottom-left and bottom-right
quadrants exactly. The cartridge does not store a world-space Z coordinate;
its art only implies whether a solid is a low ledge or a tall wall. Each
connected solid mass therefore makes that visual choice once, rather than
letting every shaded 8px tile create a different level. Chase view projects
one planar face per exposed quarter-cell edge and textures it from the
original tile bands, preserving ledges, inside corners and right-angle room
geometry. Enclosed walkable regions inherit their cliff lip's height class,
while tree lines are ignored as elevation boundaries. Known objects from `wRoomLayout`
(such as trees) take their own semantic height, and unusual rooms can still
set an explicit class in a height override. "One height per cliff" in the
Esc menu turns the mass vote
off
- **the world persists**: every room you visit is remembered, and the chase
camera draws remembered neighbours past the room border — terrain, cliff
faces, tree masses — so the world runs to the horizon and fills in as
you explore instead of ending at the edge of the screen
Walking back the way he came: the room he already crossed is still
there (left) where it used to end in fog (right)
Output is a normal 160×144 frame handed back through the runtime's present
path, so shaders, scaling and screenshots all still apply.
### Tuning the look live
The override files decide *which* height class a cell is; the Esc menu's
**Voxel Diorama** section decides what a height class **looks like** — and
the sliders reshape the world under the menu as you drag them:
| | |
|---|---|
| **Shape** | height of grass, bushes and trees; water depth; foliage footprint; tilt height |
| **Chase camera** | distance, height, field of view, vertical scale |
| **Atmosphere** | fog start and strength, and ground mist that pools over water and low ground in the distance; fog wears the sky's own colour outdoors and the room's own palette indoors, so a blue-stone dungeon hazes blue and a lava cave ember |
| **Diorama finish** | pixel cubes (every pixel a tiny lit block) and tilt-shift blur (a diorama-photo focus band); either slider to zero turns it off |

Synthetic scenes from tools/vox_synth.c (no cartridge involved): ground mist off and on over a lake, and the same crypt geometry hazing blue where the forge hazes red — fog takes the room's own palette
`chase_follow` in `voxel/tuning.ini` is how fast the camera swings behind
Link, per frame: `0.05` ships, higher snaps harder, and `0` pins the camera
to a fixed heading the way earlier builds did.
*Foliage footprint* is how far a tree pulls back from its cell edge — `0`
gives hard blocks, high values give tufts standing in the grass.
**Save tuning** writes `voxel/tuning.ini` next to the binary: a plain text
file you can share, commit, or delete to go back to the defaults. Finding a
better look is a slider and a save, not a rebuild.
### Sculpting rooms by hand
Collision decides what rises — which is right until a room decorates itself
with props you can walk past (statue rows, gates, cave mouths): solid to the
eye, `$00` to the collision map, flat in the diorama. Plain text files get
the final word, per room:
```
voxel/overrides/ages-0-6a.txt # game, group, room (hex), next to the binary
```
8 rows × 10 characters, one per 16×16 room cell: `.` keep, `_` flat,
`w` water, `l` low, `m` mid, `h` high. Comments with `#`.
**You can sculpt from inside the game.** Toggle *Sculpt room heights* in
the Esc menu's voxel section (or press **F4** in a voxel mode): the cell in
front of Link glows gold, and the number keys paint it — `1` flat, `2`
water, `3` low, `4` mid, `5` high, `0` back to whatever collision says.
**Backspace** undoes your paints in the room you are standing in, newest
first — paints made in other rooms wait on the stack until you walk back.
Every press rewrites the room's override file for you, atomically, so what
you sculpt is exactly what the file format above describes — hand edits and
in-game edits are the same thing, and cells you authored by hand survive
in-game painting (undo puts back exactly what each paint replaced,
including hand-authored values).
Prefer a text editor? **Editing is live either way.** The file is polled a
few times a second while you stand in the room, so saving reshapes the
terrain in front of you — no restart, no leaving the room. `VOXEL_EDIT=1`
still writes a ready-to-edit template for any room without a file (with the
collision-derived heights as a comment), and now also starts the player
with sculpt mode armed.
Colour-only vs the game's own collision data, same frame:

Honest limits
- On non-Oracles carts, heights come from what tiles *look like* — a plausible
relief rather than a correct one. Thresholds are tunable at the top of
[`voxel_tiles.c`](src/voxel/voxel_tiles.c).
- During room-to-room scroll transitions the collision grid describes the
*next* room while the screen still shows both, so those half-seconds render
as a flat slide — calm, but flat. `VOXEL_NO_ORACLE=1` forces the colour
classifier everywhere, for A/B tests.
- The trusted-frame gate accepts exactly `wScrollMode == 1`. If some corner
of the game uses another value during normal play (candidates: large
scrolling dungeon rooms), those rooms fall back to the flat slide too —
wrong-but-calm, never wrong-and-extruded.
- Fixed pitch ladder. No free-roam or first-person camera — moving the player
off the grid would mean fighting the game's own movement code.
- The diorama renders at 3× internal resolution (`VOXEL_SCALE=1`–`4` to
change): texels stay chunky — that's the pixel art — but silhouettes,
domes and the camera tilt resolve at sub-GB precision instead of a
160×144 staircase. Flat (non-voxel) play is still native 160×144.
- On the Oracles carts, menus and boot cinematics now render flat
automatically. Interiors extrude by their real collision, which reads well.
On other carts the classifier only sees tiles, so menus extrude — `F3` off.
- **No widescreen, and that's measured not guessed.** `VOXEL_DUMP_MAP=`
writes the whole 32×32 BG map; mid-gameplay it holds about one screen of real
tiles and flat filler everywhere else. Oracles only maintains the columns
it's about to scroll into, so there's no off-screen world to reveal.
- **60 FPS is already the case.** The runtime paces at the Game Boy's native
59.7 FPS and Oracles runs its logic every frame — there's no 20/30→60 unlock
to do like on N64 recomps.
`VOXEL_DEBUG=1` prints per-frame classification stats.
## Achievements

The player watches the game's own memory and pops a toast over the window
— Steam-style, top right — when you earn something. The game is never
touched: no patched ROM, no injected sprites, just the host reading WRAM
and drawing over the presented frame.
The shipped packs cover essences, sword and heart upgrades, rings, kill
counts and a few signature treasures per cart, with every address taken
from the [oracles disassembly](https://github.com/Stewmath/oracles-disasm).
Unlocks live in `states/achievements-.txt` — plain text, one id per
line, delete a line to earn it again.
Packs are just data: `achievements/.txt` ships with the player, and
any `achievements/..txt` beside it loads too, so a mod or
a player can add achievements without code. The format (six condition
kinds over WRAM addresses) is documented in
[achievements/README.md](achievements/README.md), and
`EPOCH_TOAST_TEST=1` pops a sample toast at boot so you can see the card
without earning anything.
Browse them in two places: **Achievements** in the launcher menu, and the
in-game panel on **F2** — earned entries lit, the rest dimmed, with the
tally up top. (The panel is ours; the emulator's Esc menu stays for
display and emulator settings.) Each achievement can carry its own 48×48
icon (`achievements/icons//.pam`, with real per-pixel alpha); the
gilded card and the lists use it, and anything without art gets the
built-in medal. The wanted list and exact spec live in
[achievements/icons/README.md](achievements/icons/README.md).
Nothing can unlock outside actual play: evaluation is gated on both
`wLinkMaxHealth` and `wScrollMode`, so the title screen and the file
select — which loads a file's data into WRAM just to draw its preview
card — stay inert.
## Secrets
Oracle secrets are not universal passwords: every save carries a random
Game ID, and every code is encoded against it with a cipher and a
checksum, so a code from a website will not validate on your file.
**Secrets** in the launcher menu generates yours — from your own save,
using the game's own algorithm (ported from the
[disassembly](https://github.com/Stewmath/oracles-disasm)'s bank 3):
- the **game secret** that starts your linked game in the other cart
(or the hero's secret, if this file is already linked),
- the **ring secret** that carries your ring collection across,
- and all twenty **NPC memory secrets** with their return codes —
labelled by who to tell, so the tedious half of linking is a
read-off instead of a scavenger hunt.
Codes are spelled in the games' symbol alphabet (♠ ♥ ● ▲ → …), grouped
in fives the way the entry grid expects.
**Or let it type them.** Open the game's own secret screen, press F2 for
the panel, pick a secret on the Secrets tab, and the cursor walks the
grid and enters it — twenty symbols without touching the d-pad. It steers
by reading the game's own cursor position each frame rather than writing
into its memory, so if the game disagrees, the game wins. A file that has never used a
secret has no Game ID yet; its codes are accepted by any file, which is
the game's own behaviour, and the dialog says so when it applies.
## Continue the Legend
The Oracles are halves of one story: finish either game and Farore
speaks a twenty-symbol secret; enter it in the other cart and the
**linked quest** begins — the only road to the true ending. This player
closes that seam. **Continue Legend** in the launcher menu (on the game
you finished) starts the other game, and the player enters the secret
*itself*: it walks the file select to SECRETS on a free file, drives the
game's own cursor around the symbol grid — the same typist as the Esc
menu, reading where the cursor is from the game's memory and never
writing into it — accepts, steers the file select back to the file the
cart just made, pages through the linked prologue, and stops only when
your linked game is standing in a room. The whole drive runs at max
speed — splash screens, title, file select, prologue all fast-forward —
and normal speed returns the instant the machine finishes. A strip in
the top-right corner says what it is doing, then the verdict.
Touch nothing while it types: any real button press cancels the machine
and hands the game to you (a key still held from the launcher's dialog
does not count). Full slots or a missing ROM are told to you up front;
if the slot turns out to hold a game after all, the machine notices the
moment that game starts and stops with your own file on screen. The
whole handoff is one plain file (`states/handoff.txt`) the player
consumes the moment it reads it — one shot, whatever happens, so a
stopped handoff never lies in wait for a later launch.
## Saves
Your progress is a plain battery save — the same `.sav` any Game Boy
emulator writes — kept beside the binary under the cartridge's own name.
**Saves** in the launcher menu manages it:
- **Import** a save from anywhere: another emulator, another machine, a
downloaded 100% file. It is checked before it lands — a Seasons save
on the Ages page is refused by content, not by filename — and works
even before your first in-game save, since the file's name is derived
from the ROM the same way the player derives it.
- **Export a copy** to take your file somewhere else.
- **Back up now**, and browse the backup trail with one-click
**Restore**.
Nothing on that page can lose a file: anything that replaces the save —
an import, a restore — backs the old one up first into `save-backups/`,
which self-updates leave untouched. The dialog also shows what is inside
the save — each file slot's hero, linked/hero status, Game ID, and the
run at a glance: essences, hearts, rings, deaths, playtime — read with
the same parser the secret generator trusts, at the same addresses the
live stream feed reads.
## Mods
Mods are applied to the extracted ROM *before* the cart boots, which is why the
toggles live in the launcher. A pristine snapshot is kept and the live ROM
rebuilt from it every launch, so turning a mod off genuinely undoes it and two
runs with the same mod set are byte-identical.
One directory per mod under `mods/`:
```
mods/my-randomizer/
manifest.json required
seed.bps patch named by the manifest
overlay/ optional raw byte splices
voxel/tree.ppm optional 16x16 source-art override for trees (chase cam)
voxel/tuft.ppm ... and for bushes and grass tufts
```
The `voxel/` images are plain 16×16 P6 PPMs (`magick art.png art.ppm`). By
default, recognized tree and tuft cells become voxel volumes whose colours and
pixel detail come from the cart's live tile art, including season and tileset
changes. Supplying `voxel/tree.ppm` or `voxel/tuft.ppm` replaces that source
art but keeps the same world-space geometry—never a camera-facing billboard.
When several enabled mods supply the same file, the highest-priority mod wins
— the same order the ROM patches apply in.
```json
{
"id": "my-randomizer",
"name": "Seasons Randomizer",
"version": "1.0.0",
"games": ["tlozoos"],
"patch": "seed.bps",
"priority": 100,
"enabled": true
}
```
**IPS and BPS both work** — that's what Oracles randomizers and romhacks
already ship as, so most existing patches drop straight in. Unknown manifest
keys are ignored, so a manifest written for a newer loader still loads.
Worked example and the full field reference: [`examples/mods/`](examples/mods).
> [!NOTE]
> A patch that changes the ROM's **size** can't work here — the generated C is
> bound to the original bank layout, so the loader reports it and skips the mod
> rather than booting something broken. BPS patches also carry a source
> checksum; one built for a different revision is rejected with a message
> instead of silently producing garbage.
## Cover art
Each launcher panel draws a procedural motif unless you supply art.
| where | for | committed? |
|---|---|---|
| `build/covers/.png` | your own art, this machine only | no |
| `art/covers/.png` | art shipped with the project | yes |
```sh
python3 tools/install_cover_art.py --ages ages.png --seasons seasons.png
```
That names, centre-crops and resizes both correctly. **1600 × 1600 square** is
ideal; keep the subject in the middle 70%, since the diagonal seam cuts the
inner edge and the title sits over the outer one. Add `--local` for art that
shouldn't be redistributed. Details: [`art/covers/README.md`](art/covers/README.md).
## Building by hand
```sh
sudo apt-get install -y build-essential cmake ninja-build \
libsdl2-dev libcurl4-openssl-dev # Debian/Ubuntu
# brew install cmake ninja sdl2 curl # macOS
cmake -S . -B build -G Ninja
cmake --build build -j$(nproc) # ~1 minute
pip install -r launcher/requirements.txt
python3 launcher/epoch_launcher.py # or just ./build/epoch
```
| CMake option | default | effect |
|---|---|---|
| `EPOCH_WITH_VOXEL` | `ON` | build the voxel renderer |
| `GBRT_REF` | `main` | pin a git ref for the fetched runtime |
Windows source builds need Visual Studio's C++ workload plus
[vcpkg](https://github.com/microsoft/vcpkg) for SDL2/libcurl — `setup.ps1`
walks through it. Or skip all of that and take the release binary.
Troubleshooting
**"No playable game"** — the binary looks for `roms/` next to itself. Put
your ROMs there (see [Quick start](#quick-start)).
**pip refuses: "externally managed environment"** — use a virtualenv:
```sh
python3 -m venv .venv && . .venv/bin/activate
pip install -r launcher/requirements.txt
python launcher/epoch_launcher.py
```
**SHA-256 warning at launch** — your dump isn't the USA/Australia revision.
It will still run; the warning just means the hashes don't match the
revision this project is tuned against.
**No controller in the launcher** — `pip install pygame`. Optional; the game
itself handles pads either way.
**Windows: "cannot be loaded because running scripts is disabled"** — use
`powershell -ExecutionPolicy Bypass -File setup.ps1` verbatim; it applies to
that one run only.
## Streaming
Four transparent OBS overlays, landscape and vertical, each in two styles:
`overlay.html` / `overlay-vertical.html` float a few panels over your capture,
and `overlay-framed.html` / `overlay-framed-vertical.html` dress the whole
canvas — a navy mat, a gilded frame around the play area with a real hole cut
for the game, and a rail of live stats beside it (with an optional camera
opening). The vertical pair stay clear of the UI mobile platforms draw over
the frame.

While the player is running it writes `stream/live.js` once a second, and the
overlays show the game, the room, essences, hearts, rings, rupees, deaths,
play time and achievements earned — with an unlock card that lands the moment
one is. Nothing is shown until you are actually in a room with a file loaded.
The launcher's **Stream** page does the setup for you: pick a layout, copy
the exact numbers to give OBS, move either opening (with a Snap button that
rounds to a whole multiple of 160×144), toggle the camera hole and the
alignment guide, and type the "now building" line. Everything it writes is
a plain file you can still edit by hand — see
[stream/README.md](stream/README.md).
**Speedrunning:** the Stream page can also switch on a run timer (the
file's own clock, to hundredths), a split list driven by auto-splits the
player watches in the game's memory (`splits/`, and they push
`startorsplit` to LiveSplit's TCP server if you tick that on), an item
tracker, and an input display. See the
[v0.6.0 notes](docs/release-notes-v0.6.0.md) for the full tour.
**The tracker can wear the game's own item icons.** They rip from *your*
ROM, on your machine — the tool boots your cart headlessly, walks the
game's menus, equips each item and reads its icon out of the HUD's own
sprites, transparent background and all:
```sh
python3 tools/rip_item_icons.py
```
Icons land in `stream/icons/items/` (gitignored — cartridge art never
leaves your machine), and any cell without one keeps its text label.
Everything that can sit on a button gets ripped; passive treasures
(flippers, the mermaid suit, rings) stay text.
## Roadmap
What's next, what it costs, and what blocks what: **[ROADMAP.md](ROADMAP.md)**.
## Contributing — we'd love more hands
The surface area is now bigger than one keyboard. The build is ~1 minute
from a cold clone (`setup.sh` / `setup.ps1`), CI covers Linux + Windows +
the patch chain, and every runtime modification is a reviewable patch file
in `patches/`. `src/` and `launcher/` are the project's own code.
If you are joining specifically to improve the 3D mode, start with the
**[CONTRIBUTING.md](CONTRIBUTING.md)** and the **[voxel contributor starter tasks](docs/CONTRIBUTOR_STARTER_TASKS.md)**. They point to focused work that can be reviewed independently. The **[voxel contributor guide](docs/VOXEL_CONTRIBUTING.md)** maps the render pipeline, gives the fast screenshot/test loop, and explains how to distinguish terrain, background objects and OAM sprites before writing geometry.
If any of this sounds fun, open an issue or just send a PR:
- **Renderer** (`src/voxel/`): dynamic prop reconstruction (the opening-scene
chest is the first target), dungeon/large-room mapping, more compound
structures, and eventually first-person mode. Plain C, one pass, no GPU
code — the GLES side is already handled.
- **Room sculpting** (no code!): press F4 in a voxel mode and paint room
heights with the number keys — the player writes the override files for
you. A curated override pack for the whole overworld would ship as a
default.
- **Mod API** (`src/mod_loader.c`): IPS/BPS/overlays work today; the next
step is a scripting surface over the games' named WRAM symbols
(`oracles-disasm` names hundreds — the voxel mode already reads a dozen
live).
- **Achievements**: the memory-watch machinery exists; it needs a
condition format, a definitions file, and a toast.
- **Voice packs**: dialog open/close is already detected; a playback mixer
and a dialogue-transcript logger would make fan dubs possible.
- **Testing** (`tools/vox_shot.c`): boots a ROM headless, drives it with
an input script, dumps frame pairs — the whole voxel pipeline was built
against it. More scripted routes (dungeons!) directly increase coverage.
`VOX_SHOT_NOCACHE=1` renders the same frame as if no room had ever been
visited, which turns "does the persistent world do anything here?" into a
pixel diff, and `VOX_DUMP_WORLD=1` prints hit/miss past each border.
**Never attach a ROM or ROM-derived asset to an issue or PR.** Battery
saves (`.sav`) are your own play data and are welcome — `tests/saves/`
exists exactly for save files parked in interesting places.
## Credits
Built on [GB-Recomp/gb-recompiled](https://github.com/GB-Recomp/gb-recompiled)
(MIT) — the static recompiler and runtime that make all of this possible, and
itself a fork of [arcanite24/gb-recompiled](https://github.com/arcanite24/gb-recompiled).
Symbol names come from [Stewmath/oracles-disasm](https://github.com/Stewmath/oracles-disasm),
the community disassembly.
Full attribution, including what this project started from and what is original
to it, is in [CREDITS.md](CREDITS.md).
## Legal
No ROM data is included, distributed, or produced by CI. The Legend of Zelda:
Oracle of Ages and Oracle of Seasons are © Nintendo / Capcom. Supply your own
dumps of games you own.
This project's own code is [MIT](LICENSE). Game Boy and Game Boy Color are
trademarks of Nintendo. This project is not affiliated with, endorsed by, or
associated with Nintendo or Capcom.