- Python 99.9%
- Shell 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
psz-godot asked two capability questions and said an honest "captures only" was a useful answer to the first, because it would tell them to build the weaker capture-comparison tool instead. It is not captures only. THE ALGORITHM WAS NEVER THE GAP. |
||
| data | ||
| docs | ||
| nodes | ||
| ref | ||
| seeders | ||
| text | ||
| tools | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAIMS.md | ||
| README.md | ||
| schema.md | ||
psz-re — Phantasy Star Zero (NDS) Reverse Engineering
Reverse engineering Phantasy Star Zero (Nintendo DS, 2008) to recover the original game's data tables and behaviour, so that psz-godot can be built from measured fact instead of guesswork.
Why this repo exists
psz-godot's bottleneck is not implementation speed. It is that a human has to sit down, play the original, and hand-author a spec for every unimplemented system — damage formulas, enemy AI, spawn tables, drop rates, photon arts. That human is the slowest part of the loop and has the least free time.
This repo removes the human from that loop. The original ROM is the ground truth. If we can read a number or a state machine out of the binary, nobody needs to play the game to find out what it should be.
Output of this repo is documents and JSON, not a playable build. Its deliverables land in psz-godot as specs, issues, and data files.
Explicit non-goal: matching decompilation
We are not chasing a byte-matching decomp.
For reference, sm64ds-decomp is
the state of the art for NDS matching decomp: it required the proprietary
mwccarm 1.2/sp2p3 compiler with exact flags, and after sustained effort sits
at ~96.3% of functions matched (10,929 / 11,348). That is a multi-year
commitment whose payoff is a rebuildable ROM.
We do not want a rebuildable ROM. We want to know what the numbers are. A readable, non-matching Ghidra-driven understanding gets ~95% of the value for psz-godot at a few percent of the cost. Anyone tempted to chase matching should open an issue and argue for it first.
Current state
The ROM has been unpacked and surveyed. Nothing is decompiled yet.
ROM: Phantasy Star 0 (USA).nds — PSZERO / C24E, 128 MB
(md5 0ef2ab31b0068c0832bd6a5ef901dd0c). Kept on me-mini at
/mnt/storage/psz-roms/, not committed here.
| Component | Detail |
|---|---|
| ARM9 | 760,680 bytes, loads at 0x02000000 |
| ARM7 | 159,528 bytes, loads at 0x02380000 |
| ARM9 overlays | 32 |
| ARM7 overlays | 0 |
| NitroFS | 6,676 files across 574 directories |
Filesystem layout
apc/ camera/ chat/ common/ counter/ dwc/ ending/ enemy/ event/
item/ movie/ object/ player/ quest/ scene/ set/ sound/ sprite/
titleseq/
Largest: player/ 42M, sound/ 21M, movie/ 20M, scene/ 14M, enemy/ 14M.
Container formats
6,317 of 6,676 files are ZPR-compressed. ZPR is already solved — see
DashGL/ArchiveUnpacker, which
exposes a zpr() decompressor. Do not re-derive it.
ZPR is a 16-byte header wrapping an XOR-obscured PRS stream — the same
Sega LZ77 compression used by Phantasy Star Online, so prs.ts in
ArchiveUnpacker is shared with the PSO work.
0x00 char[4] "ZPR\0"
0x04 u32 checksum / hash
0x08 u32 decompressed size <- the one the decoder uses
0x0C u32 (slightly smaller than 0x08; purpose unconfirmed)
0x10 ... payload
Decode: skip the 16-byte header, XOR every payload byte with 0x95, then PRS
decompress to the size at 0x08.
The XOR key is why raw ZPR payloads look like high-entropy noise dominated by
0x95/0x69/0x6a — those are runs of 0x00 in the underlying PRS stream.
Anyone eyeballing a hexdump and concluding "this is entropy-coded" has been
fooled by the XOR; it is ordinary LZ77 underneath.
Remaining magics: RCSN (NSCR, 88), ZARC (68), RLCN (NCLR, 54),
MODS (18), SDAT (13). Extension histogram is dominated by .rel (3,388),
.narc (758), .ncgr (477), .ncer (379), .nsbtx (324), .nanr (314).
Most non-.rel formats are stock Nintendo NITRO formats with existing public
tooling. The .rel files are Sega-specific and are where the interesting
gameplay data lives.
High-value targets already spotted
| Path | Guess at contents |
|---|---|
item/wpnprm.rel |
Weapon parameter table |
item/uniprm.rel |
Unit (armour slot) parameter table |
item/genprm.rel |
General/global parameters |
item/mag_param.narc |
Mag growth + feeding tables |
item/weapon_id.narc, item/global_dat.narc |
Item ID ↔ data mapping |
enemy/group_*.rel |
Enemy group / spawn definitions |
enemy/<name>.narc |
Per-enemy model + animation + params (1,817 files) |
counter/shop.bin |
Shop inventory & pricing |
counter/item_conversion.bin |
Photon drop / conversion rates |
quest/**/quest_*.rel |
Per-quest script data |
set/<stage>/<var>/*.rel |
Per-stage room/spawn set definitions |
These names are inferred from filenames only. Verify before trusting.
Prior art — read before writing any extractor
This is the single most important section. A previous session started re-deriving ZPR from scratch when it was already solved in this author's own repos. Check these first, every time:
| Resource | What it covers |
|---|---|
| DashGL/psz-asset-viewer | Existing PSZ asset export: sprites, NCER cells, palettes, models, merged pa00/pa01/pa02 animation packs. Substantial and working. |
| DashGL/ArchiveUnpacker | zpr() decompressor + other archive/compression formats |
| docs.dashgl.com PSZ formats | Written format docs: NDS header, NARC archive, ZPR |
| pedro-javierf/NTRGhidra | Ghidra loader for NDS ROMs — handles ARM9/ARM7/overlay mapping so you don't have to import raw binaries at manual base addresses |
| Starcube Labs: Reverse Engineering a DS Game | General NDS RE methodology walkthrough |
| AetiasHax/ds-decomp | NDS ROM splitting toolkit (code/asset separation, overlay handling) |
| tangosdev/sm64ds-decomp | Reference for how far NDS decomp can go, and what it costs |
ref/psz_cheats_usa.xml |
DeadSkullzJr cheat DB, USA build (C24E), 86 codes — known-good RAM addresses, see below |
The cheat database is a shortcut, use it
ref/psz_cheats_usa.xml contains 86 working Action Replay codes for the exact
USA build we're analysing. Every code is a verified RAM address — money,
EXP, item slots, stats. Working backwards from a known address to the struct
that contains it, and then to the code that writes it, is dramatically faster
than searching a 760 KB ARM9 blind.
Codes for the EU (C24P) and JP (C24J) builds also exist in the source
database if a cross-region diff is ever useful.
How an agent verifies its own work
Every task in this repo must have a machine-checkable success signal. If a task's only success signal is "kion looks at it and agrees", it does not belong here — it belongs in psz-godot's playtest backlog.
Acceptable oracles, strongest first:
- Round-trip. Parse a file to a struct, re-serialise it, compare bytes to the original. Byte-identical means the layout is right. This is the gold standard for every table extractor and should be the default.
- Cross-file consistency. A parsed table must have a plausible, uniform record size that evenly divides the decompressed length, with no leftover bytes, across all files of that type — not just the one you tested.
- Known-value anchoring. A parsed value must agree with an independently known fact: a cheat-DB address, an in-game value from a wiki, or a string from the text bank.
- Cross-region diff. USA vs EU vs JP builds should differ in text and pointers but agree on gameplay constants. Disagreement means a parse bug.
State confidence explicitly in every doc you write. CONFIRMED (round-tripped
or anchored), LIKELY (consistent but unanchored), GUESS (filename-derived).
Never let a GUESS reach psz-godot unlabelled.
Work breakdown
Roughly dependency-ordered. Each is intended to be one agent's task.
Phase 0 — foundation
- Wire
ArchiveUnpacker'szpr()into a batch decompressor; decompress all 6,317 ZPR files intobuild/decompressed/preserving tree structure - Re-run the magic histogram on the decompressed output — this reveals the real inner formats, which the outer compression currently hides
- Import ARM9 + 32 overlays into Ghidra via NTRGhidra; confirm overlay base addresses resolve and code disassembles cleanly
- Audit
psz-asset-viewerand writedocs/prior-art-inventory.md: exactly which formats are already solved there, so nothing is rebuilt twice
Phase 1 — static tables (highest value, lowest risk)
item/wpnprm.rel→data/weapons.json(ATP range, ATA, required stats, class restrictions, element slots). Round-trip verified.item/uniprm.rel→data/units.jsonitem/genprm.rel→data/global_params.jsonitem/mag_param.narc→data/mags.json(growth curves, feed tables)counter/shop.bin→data/shop.json;counter/item_conversion.bin→data/photon_conversion.json- Enemy stat tables — locate first (may be in
.relor baked into ARM9/ overlay.rodata), then extract todata/enemies.json - EXP curve + level-up stat tables per class →
data/level_curves.json - Drop tables →
data/drops.json
Phase 2 — logic specs (needs Ghidra)
- Damage formula: locate the melee damage-apply path, document the exact
arithmetic including rounding and crit handling →
specs/damage.md - Hit detection: how many frames is a swing's hitbox active, and is it a window or a single frame? (directly answers psz-godot #554)
- Enemy AI state machine: enumerate states and transition conditions
→
specs/enemy-ai.md - Photon Art / Photon Blast trigger + effect logic
- Technique (Force) casting: cost, cast time, power scaling
- Quest script VM: opcode table and dispatch →
specs/quest-vm.md - Spawn/set system: how
set/<stage>/<variant>/*.reldrives room population
Phase 3 — delivery
- For each completed spec, open a psz-godot issue with the extracted numbers, linking back to the doc here
- Publish
data/*.jsonin a form psz-godot can consume directly
Layout
rom/ original ROM (gitignored — do not commit)
build/ unpacked ROM output (gitignored — regenerate with tools/)
ref/ reference data (large files gitignored)
tools/ extraction scripts
docs/ findings, format notes, prior-art inventory
specs/ behaviour specs destined for psz-godot
data/ extracted tables as JSON
Regenerating build/
python3 tools/nds_unpack.py "rom/Phantasy Star 0 (USA).nds" build/
python3 tools/blz.py build/
The first writes arm9.bin, arm7.bin, overlay_arm9/, banner.bin, the full
fs/ tree with real filenames recovered from the FNT, and rom_meta.json
describing the header, overlay table, and every file's offset/size/magic.
The second step is not optional. arm9.bin and the overlays ship BLZ-compressed;
tools/blz.py decompresses them to arm9_dec.bin and overlay_arm9_dec/, and every
static-analysis tool here reads those rather than the packed originals. Running only
the first command leaves a tree that looks restored and a gate that cannot run —
which is exactly what happened once, so tools/buildinputs.py now checks that this
section documents a producer for every path the checkers require.
Legal
No copyrighted game data is committed to this repository — no ROM, no extracted assets, no decompressed archives. Everything here is original analysis, tooling, and documentation. Contributors must supply their own legally obtained copy of the ROM.