Contributing Build From Source - drizztdourden08/relic-of-the-past GitHub Wiki
Build from Source
Prerequisites
- Node.js ≥ 24 (see
.nvmrc). - A legally obtained A Link to the Past ROM, supplied at runtime and kept out of commits.
- Emscripten SDK (Windows; at
third_party/emsdkin the main checkout, or$EMSDK; version pinned inpackage.json). The WASM core is gitignored, not committed.npm run dev/npm run buildauto-build it on first run, and again whenever C undercore/changes, via theensure-wasmpre-step. See Building the WASM Core.
Run & build
npm install # install deps; auto-fetches the Electron binary (ensure-electron)
npm run dev # electron-vite dev server + Electron (auto-builds WASM if missing/stale)
npm run build # production build (electron-vite), auto-builds WASM too
npm run build:win # packaged build (see package.json for :mac / :linux)
npm install runs ensure-electron (re-fetches Electron's native binary if a bare node_modules
left it out), and dev/build run ensure-wasm first. That is a fast mtime check, and it rebuilds the
WASM core only when it's missing or a C source changed. Force a WASM rebuild with npm run ensure-wasm.
For testing, always launch so the app never steals focus or makes noise:
npm run dev -- -- --no-focus --muted
Local ports
Every local server the project runs sits in one fixed block of ports. The base and each role
are defined once, in shared/config/ports.constants.ts; nothing else writes a port number.
| Offset | Role |
|---|---|
| +0 | the app's renderer dev server (npm run dev) |
| +1 | reserved for a component catalogue |
| +2 | the Sanctuary site (npm run sanctuary:dev) |
| +3 | the Sanctuary API (npm run sanctuary:api), which the site proxies /api to |
| +4 | a static file server, for testing install manifests |
| +5 | the mGBA Lua socket tracer |
| +6 to +9 | free for other project tools |
A worktree can carry a .rotp-port-slot file holding a slot number N (or set
ROTP_PORT_SLOT); the whole block then moves up by 10 x N, so two checkouts never ask for
the same port. The main checkout is slot 0. The dev servers use strictPort, so a port
already in use stops them with an error instead of moving them somewhere unexpected.
Quality gate
npm run lint # tsc --noEmit && eslint .
npm run analyze # classify + lint ALL file types (analyze:diff / :ci / :tag variants)
npm run ci # tsc + eslint + repo analysis (what CI runs)
A PostToolUse lint hook flags coding-standard violations on every edit, and a Stop hook gates changed
files each turn (analyze:ci). Run the coding-standards skill's checkup after changes.
First run
The app needs a user-provided ROM and asset blob (assets/*.sfc, assets/assets.dat). These stay
gitignored and out of commits, as do test-roms/ and saves/. On first launch, import your ROM and the
app extracts what it needs locally. See Quick Start and
Importing a ROM.
Repo orientation
| Path | What |
|---|---|
core/zelda3/ |
Vendored C decompilation (treat as upstream). |
core/game-hooks/ · core/wasm-build/ |
Our C hook layer + Emscripten build. |
apps/web/src/ |
React renderer (lib/game/ is the bridge; stores/, ui/). |
apps/desktop/electron/ |
Main process + preload (Electron & IPC). |
shared/ |
Pure code shared by renderer + electron (asset-extraction, navigation, types). |
tests/ · scripts/ · docs/ |
Tests, build helpers, documentation. |
Path aliases: @shared/* → shared/, @app/* → apps/web/src/. See the
Architecture overview for zones and dependency invariants.
The private companion repository (optional)
Some material this project uses is derived from the original game and is not tracked in
this repository: the named save states the end-to-end tests load. Those live in a
separate private repository and are copied into place on demand. The record dataset
itself is not one of them: shared/game/data/records/ (screens, connections, checks,
items, actors and the rest) is an ordinary tracked directory in this repository, and
needs no access to the private one.
You do not need the private repository. Without access:
- the app builds and runs, with the full record dataset already in place,
npm run lintand the unit tests pass,- the end-to-end tests that need a save state report as skipped.
The private repository is a sibling checkout, not something this repository clones.
Put it beside this one as ../rotp-vault, or point ROTP_VAULT_DIR at it. Inside it,
the tree/ folder mirrors this repository's own paths, so a file at
tree/shared/game/data/records/areas.ts lands at shared/game/data/records/areas.ts.
The path is the whole mapping; there is nothing else to configure, and anything the
vault keeps outside tree/ is never touched.
npm run vault:sync
Sync runs both ways. It indexes both sides by content, compares them against the state recorded by the last run, and applies every change that is unambiguous: a file edited only here is written to the vault, a file edited only there is written to your checkout, and a deletion on either side is carried across. Anything edited on both sides since the last sync is a conflict, so it is reported and left alone. Writes into the vault are committed there on its current branch; pushing them onward is left to you.
It also runs automatically after npm install, and is a no-op with a single line of
output when you have no access, and it never fails a build. To see what would change
without writing anything:
npm run vault:status
To settle conflicts wholesale, declare a winner:
npm run vault:force-pull
npm run vault:force-push
Access is granted per contributor; ask the repository owner if you need it.