Local web IDE / Pokemon GBA decomp

gba-rom-hack-ide

Edit a Pokemon GBA decomp project visually or in plain English, then build and play the ROM in your browser.

Point it at a decompilation project on disk. It classifies the project, scans it into one typed manifest, and renders that manifest as a navigable game: maps, warps, triggers, object events, dialogue, flags and variables, encounter tables, trainers and their parties, species, moves, learnsets, abilities, items, and the type chart. Everything runs on 127.0.0.1. Nothing is uploaded anywhere.

View on GitHub ->

Tooling only

This repository contains no ROMs, no ROM images, no decompilation dumps, no extracted graphics, audio, text or save data, and no binary patches derived from a commercial game. The tool operates on a decompilation project and ROM files that you supply yourself, and you are responsible for supplying a legally obtained copy.

corpus/ is the drop-zone for your own files. It is git-ignored, and the .gitignore blocks *.gba, *.gbc, *.gb, *.nds, *.sav, *.srm, *.bps, *.ips, *.ups and *.xdelta repo-wide.

Pokemon and related names are trademarks of Nintendo, Creatures Inc., and GAME FREAK Inc. This is an unaffiliated, non-commercial fan tool.

The problem

There are two ways to change a Pokemon GBA game, and neither of them gives you an editor.

Binary patching means finding a table in a .gba image and writing bytes. It works until you want to add something the original ROM has no room for. Free space hunting, repointing and one-off patches accumulate until nobody can say what the ROM now contains.

A decompilation project removes those limits: real C source that compiles back to a byte-matching ROM. What it costs you is the editor. The game becomes thousands of files of C, headers, .inc scripts, .json map data and PNG tilesets. Editing a trainer's party means finding the right block in the right file and not breaking the build.

This project is the missing middle. It treats the decomp tree as the source of truth, reads it into one typed manifest, renders that manifest as something you can click through, and writes changes back into the real C source. Compiling and playing are part of the inner loop rather than a separate step you leave the tool to perform.

Parts worth opening

01

A generic C struct-block editor

scan/data/struct-block.ts is 159 lines. It parses decomp files shaped as [ID] = { .field = value } arrays and rewrites only the value span of named fields, so macros, #if guards, comments and unmodelled fields survive verbatim. Moves, abilities and items sit directly on that parser; learnsets and encounters reuse its bracket matcher over a macro list and over wild_encounters.json; species, the type chart and trainers have their own format-specific writers built to the same discipline. Every write is tmp-file-plus-rename.

02

An MCP server as the agent layer

agent/mcp-server.ts registers 71 tools against the open project, one per module under agent/tools/ (59 of them propose-* mutations, plus 21 colocated test files). The claude CLI runs as a child process and streams turn events over a WebSocket. No tool writes to disk: proposals become a diff card you approve, then a patch applier writes them.

03

The devkitARM build recipe, encoded once

build/decomp-build.ts shells into devkitPro's MSYS2 login bash, prepends $DEVKITARM/bin to PATH, runs make modern, and overrides CPP with a CR-stripping wrapper because devkitARM's CRLF output chokes pret's preproc. Async polled jobs, a 200,000-character log cap, a 30 minute ceiling.

04

Story order, not alphabetical order

parseMapSectionOrder in scan/decomp.ts reads region_map_sections.h into the game's own MAPSEC_* ordering, so the navigator lists Pallet, Viridian, Pewter and onward rather than Route1, Route10, Route2.

How it works

open project
  -> detect/          classify decomp | patch | hybrid
  -> scan/            parse into <projectRoot>/.editor/manifest.json
  -> frontend         render manifest as a navigable world

edit visually:   PATCH route -> domain writer -> tmp+rename into decomp source -> op-log
edit in English: WebSocket -> spawner -> claude CLI -> MCP propose_* -> patch store
                 -> diff card -> you approve -> patch-applier -> op-log

Build & Play:    make modern job -> .gba -> mGBA-wasm in the browser

Both edit paths land in the same op-log, so undo, redo and the project timeline behave identically whether a change came from a visual editor or from an approved AI patch.

TierSizeRole
app/shared/src19 files, 2,924 linesEvery type that crosses the wire, defined once
app/backend/src254 files, 70,721 linesFastify server, detection, scanners, atomic writers, op-log, MCP host, build runner
app/frontend/src254 files, 77,414 linesMap editor, navigator, data editors, inspector registry, PixiJS compositor, Zustand stores
engine/src355 files, 62,412 linesStandalone binary-ROM introspection engine (legacy path, about 29 percent of the TypeScript here)
tile-intel-svc/src45 Python files, 9,382 linesOptional FastAPI sidecar for tileset understanding

Quickstart

git clone https://github.com/csnyder256/gba-rom-hack-ide
cd gba-rom-hack-ide

.\start.ps1                 # or: node scripts\start.mjs

Status

A working single-operator tool, not a released product. It pivoted: it began as a binary ROM patcher, that path failed its own audit, and the project re-founded itself on editing decomp source and compiling with make. The binary engine is still in the tree, still imported in places, and clearly labelled legacy.

Recorded against a real cloned project, not fixtures: 1,104 learnsets, 469 wild encounter tables, 664 trainers with editable parties, 33,585 decoded script steps across 8,821 labels, 882 flags and 167 variables. The full write-up, including the audit that triggered the pivot, is in the planning docs.