Tombi!
OriginPlayStation
TechnologyNative C/C++ port (engine reimplementation)
Available forWindows, Linux, macOS
View official projectAbout the project
Tombi! 2: Contra los cerdiablos, también conocido en inglés como Tomba! 2: The Evil Swine Return en région NTSC, simplemente Tombi! 2 en la región PAL (europea) o トンバ! ザ・ワイルドアドベンチャー en Japón), es un videojuego de acción y aventuras de plataformas, desarrollado por Whoopee Camp y publicado por Sony Computer Entertainment para la videoconsola PlayStation. Salió a la venta en el año 1999 para Japón y Norteamérica, y en el año 2000 para la región PAL. El juego es una secuela de Tombi!
Official project information
Tombi! (Tomba!) decomp + native PC port
| Version | Code | Data | Functions | Notes |
|---|---|---|---|---|
Europe, Spanish (SCES_013.31) | reference version, everything below | |||
Japan, Ore! Tomba (SLPS_011.44) | matching in progress, see docs/JP.md | |||
Japan demo, taikenban (SLPM_801.65) | shared C reuse + automatic sweep (tools/demo_sweep.py), see docs/JPDEMO.md | |||
Beta 1998-02-03 (BETA_980203) | shared C reuse, see docs/BETA_980203.md | |||
PrePre 8 demo (PCPX_960.84) | symbol pairing, see docs/PREPRE8.md |
A matching decompilation and native recompilation of Tombi! (known as Tomba! in North America and Ore! Tomba in Japan) for the original PlayStation:
- Decompilation: finished. The game's code is C source that compiles, with the game's original toolchain, into byte-identical machine code (99.34 %: every function but three memory-card ones, which have functionally equivalent C, see "Status" below). Work on the decompilation is considered complete; the remaining work is naming and documentation.
- Recompilation: the native PC port. The same C is built as a native Windows/Linux program (
port2/): OpenGL or software rendering, SDL2 controllers, the original music, voices and movies, saves, every language of the disc, and a launcher with mods (tombi-port-<ver>-windows-x64.exe/tombi-port-<ver>-linux-x64.tar.gzof the latest release, see docs/PORT.md). The game is fully playable; you supply your own disc image (Tombi! PALSCES-01331Rev 1), which the launcher extracts once.
This repository targets the European release (SCES-01331 Rev 1) and works function by function: every
function in src/ is verified to produce exactly the same instructions as the retail executable.
This repository contains no game data, executables, disc images or Sony SDK files. You need your own legally obtained copy of the game and of the toolchain.
Status
<!-- progress:start -->Whole game: 99.34 % of the game's code matches byte for byte (1520132 of 1530244 bytes).
| Part | Code | Matching bytes | Matching |
|---|---|---|---|
| MAIN0.EXE: engine, shared by every area | 286644 B | 286644 B | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 100.0 % |
| X000.BIN: AREA00 + object code reused by all areas | 313676 B | 312708 B | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟨 99.7 % |
| X001..X019.BIN: code specific to the other areas | 736440 B | 736440 B | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 100.0 % |
| SYSTEM/*.BIN: options, sub-screens, game over, info screens | 193484 B | 184340 B | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟨 95.3 % |
| Whole game | 1530244 B | 1520132 B | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟨 99.34 % |
- Bytes of machine code in game functions, counted exactly like the objdiff report on decomp.dev (
tools/units.pyis the common list); Sony's Psy-Q library (100904 B) is not counted: 90016 B of it (89.2 %) also matches, from Psy-Q sources ported from psx_tomba and other decompilations (THIRD_PARTY_NOTICES.md), and is tracked apart. - Each function counts once. The 16 area overlays share about 4800 functions with MAIN0/X000 or with each other; those copies match automatically and are not added again (counting every copy separately gives 99.7 %).
- Also tracked for MAIN0 + X000: 95.5 % of the code named by us, 67.3 % typed with the TObj structure.
Per-program and per-area breakdown, pending work and what is not counted: docs/PROGRESS.md.
The table above is regenerated with python3 tools/progress.py.
Where things stand (October 2026). The code is 99.40 % byte-matched: 2 952 of 2 955 functions. The three left are
in the memory card overlays (D0SPSUB func_800E89F8, func_800EFD6C, D0SPSUB2 func_800E999C, plus the D0SPSUB3 twin
of the first); each has functionally equivalent C in src/wip/ whose header documents the remaining difference (register
allocation and scheduler order, the structure matches), see notes/remaining.md. The D0SPSUB menu
dispatcher func_800ED0C4 (now subScreenItemListStep) was matched by michaelm-afk (PR #1) and its D0SPSUB3 twin
func_800EC89C followed from the same idioms. Function naming is complete for MAIN0, X000, the SYSTEM overlays and the area overlays X001..X019: 0
functions keep a generic name (notes/unnamed.json; empty stubs are named stubNop<addr> / stubFreeObj<addr>). Data naming is done by usage for
MAIN0 (342 globals, notes/data_names_main0.csv), X000 (115) and the eight SYSTEM overlays (266 in their
notes/data_names_<overlay>.csv), applied with tools/rename_data.py; what is still spelled by address is animation
scripts, elements of named pointer tables, field aliases and Psy-Q internals. The Japanese release is active again (see "Japanese release progress"); the demo is swept automatically (see "Japanese demo progress").
Target
| File | Description | SHA-1 |
|---|---|---|
SCES_013.31 | Boot executable (PAL, Spanish) | f6c383514e5d8367d2eed06400315cf6dd57ef6b |
EXE/MAIN0.EXE | Game core, loaded at 0x80010000 | bec8dc5f4116f5e497f2337eef8c523ff6dee8fc |
AREA00/X000.BIN | Area overlay, AREA00 (Village of All Beginnings), loaded at 0x800E8028 | ce168475f667f2ca8842eabad2aec829e301c5c5 |
AREA01/X001.BIN | Area overlay, AREA01 (also AREA07), loaded at 0x800E8028 | 923f812dcfd7da18ee0f7017e5b45a5e38ee3015 |
AREA02/X002.BIN | Area overlay, AREA02 (AREA19 ships a copy), loaded at 0x800E8028 | 646aa8b5ad97f70b299b982b3260f9f731a142bb |
AREA03/X003.BIN | Area overlay, AREA03 (Phoenix Mountain), loaded at 0x800E8028 | 440ae76f5012e3778b908743d7d030c350caf7d3 |
AREA04/X004.BIN | Area overlay, AREA04 (also AREA12), loaded at 0x800E8028 | f01c882217efee45bf76aba54ee4fadfa6c25665 |
AREA05/X005.BIN | Area overlay, AREA05 (Baccus Village), loaded at 0x800E8028 | 0d2778c0d4b3e5b5dfc40fc46053e46d9d785b17 |
AREA06/X006.BIN | Area overlay, AREA06 (Dirt Motocross), loaded at 0x800E8028 | 1bd5b648feb75a580c14db6715e057cb035c1db1 |
AREA08/X008.BIN | Area overlay, AREA08 (Baccus Lake), loaded at 0x800E8028 | 5e098dccfd123af09967ca778c83223e8a21716d |
AREA09/X009.BIN | Area overlay, AREA09 (Mushroom Village), loaded at 0x800E8028 | 1b44ea24890128c3d3789b0badb252135ba4dec1 |
AREA10/X010.BIN | Area overlay, AREA10 (Deep Jungle), loaded at 0x800E8028 | 5093e40399319f5987668bfb1d042713216d356e |
AREA11/X011.BIN | Area overlay, AREA11 (Village of Civilization), loaded at 0x800E8028 | 016559470c5a253deb5a38f685ff312c39c2cb46 |
AREA13/X013.BIN | Area overlay, AREA13 (Pig Island), loaded at 0x800E8028 | a4e8668eca85a32d6d0cdfa05ddffd6ad40bfbb0 |
AREA14/X014.BIN | Area overlay, AREA14 (Evil Pigs), loaded at 0x800E8028 | 4ee916eb3215ccae64773e1a9a547ab8dbcd1ff5 |
AREA16/X016.BIN | Area overlay, AREA16 (Village of Civilization: Clock Tower), loaded at 0x800E8028 | 7c93915bc943e8d9d8fa5d5efbd02f7e563650e4 |
AREA17/X017.BIN | Area overlay, AREA17 (Village of Civilization: Iron Tower), loaded at 0x800E8028 | 59c5964cbb4536bc62b1a65262d963fab1ad3d33 |
AREA18/X018.BIN | Area overlay, AREA18 (Village of Civilization: Y Crossing), loaded at 0x800E8028 | 83eca42eb30944bb0c61056632b2ef7bb9891b34 |
AREA19/X019.BIN | Area overlay built for AREA19 (the dwarves: Dwarf Elder's Hut, Hidden Village), never loaded by the retail game: no file table lists it and AREA19 runs AREA19/X002.BIN (= AREA02's X002); see docs/CUT_CONTENT.md | 94060c10f7b6e3af1adc55a0c9cff4b81890988c |
The 17 area overlays (X0nn.BIN, Spanish; X1nn..X8nn are the other languages) are raw code blobs, all
loaded at 0x800E8028. Their first 188 KB (0x800E8028..0x80115EA4: header, ~300 shared object handlers and
a data block) are byte-identical in every area; the rest is per-area code. X001..X019 are tracked like X000:
boundaries in notes/functions_x0nn.csv, sources in src/x0nn/ (// FUNC addr size X0nn), and functions
identical to an already matched MAIN0/X000 function are generated by tools/areas.py twins.
MAIN0.EXE corresponds to SCUS_942.36 of the NTSC-U release (same functions, shifted addresses).
See notes/disc_layout.md, notes/memory_map_main0.md
and notes/overlays.md.
How matching works
Each function lives in its own file, src/<Name>.c, with a small header:
// FUNC 8001fddc 48 MAIN0 address, size, program
// MATCHING 8001fddc 48 added once the bytes match
Optional header lines: // FLAGS -O2 -G0 ... (compiler flags) and // CC gcc-2.8.1 (compiler, by old-gcc release
name; the default is gcc-2.7.2). A few MAIN0 library objects (0x8006xxxx) were built with the newer GCC 2.8.1
(Psy-Q 4.4): ncheck.py, build_full.py and build_report.py compile those with /opt/oldgcc/gcc-2.8.1-psx, and
matchcheck.py with Psy-Q 4.4 CC1PSX.EXE (/opt/psyq/cc44/).
Two checkers compile the file and compare the result with the retail binary, masking relocations:
| Tool | Toolchain | Use |
|---|---|---|
tools/ncheck.py | native gcc-2.7.2-psx cc1 + maspsx + GNU as | fast iteration (seconds) |
tools/matchcheck.py | original PSY-Q 4.3 CC1PSX.EXE (Wine) + CPPPSX (DOSBox) + ASPSX 2.86 | reference check |
The game was built with the PSY-Q 4.3 C compiler (GCC 2.7.2.SN.1); see notes/compiler.md. Practical recipes for reproducing GCC 2.7.2 output are collected in notes/matching_guide.md.
Getting started
Requirements: Linux, Python 3, binutils-mipsel-linux-gnu. Optional: Wine + DOSBox and the PSY-Q SDK
binaries for the reference checker; Ghidra 12.1.4 + JDK 21 for analysis.
# 1. Put your own game files in game/ (ignored by git)
# game/SCES_013.31, game/MAIN0.EXE, game/AREA00/X000.BIN
# 2. Fast native toolchain (old-gcc + maspsx + binutils)
tools/setup_native.sh
# 3. Check one function, or all of them
python3 tools/ncheck.py src/MulCos.c
python3 tools/ncheck.py src/*.c
python3 tools/ncheck.py src/x0*/*.c # area overlays X001..X019
# 4. Inspect a function: disassembly + Ghidra pseudo-C
python3 tools/fn.py 8001fddc
Reference checker: copy CC1PSX.EXE (PSY-Q 4.3) to /opt/psyq/cc43/ (and PSY-Q 4.4's to /opt/psyq/cc44/ for the
// CC gcc-2.8.1 files), the DOS CPPPSX.EXE to
/opt/psyq/new/ and ASPSX.EXE 2.86 to /opt/psyq/46/BIN/, install wine and dosbox, then run
python3 tools/matchcheck.py src/<Name>.c.
Full build
tools/build_full.py links both programs from the matched C plus splat assembly for everything else and
checks them against the retail files:
python3 tools/build_full.py # splat split + compile src/*.c + link; prints SHA-1 and OK/FAIL
python3 tools/build_full.py --no-split # reuse the previous split (build/full/asm)
Run tools/setup_native.sh first (toolchain and the include/tomba/psyq headers; without them the files that
include those headers are kept as asm, the output still matches). A full run takes about 1.5 minutes.
Outputs: build/MAIN0.EXE (must equal bec8dc5f…), build/X000.BIN (must equal ce168475…) and the eight SYSTEM
overlays (O0PTSUB0.BIN, D0SPSUB.BIN, D0SPSUB2.BIN, D0SPSUB3.BIN, G0OVER.BIN, I0NFO.BIN, I0NFO2.BIN, I0NFO3.BIN,
SHA-1 checked the same way); work files
in build/full/ (linker scripts *.full.ld, maps, objects). On a mismatch it lists the first differing
offsets and the function or splat range that produced them.
- Each
src/*.creplaces the splat lines in[addr, addr+size)of its// FUNCheader (the header is the truth, even when splat had cut that range into several functions). - String literals, jump tables and static data of a C file are linked at their original addresses (found from
the retail bytes of the instructions that reference them). Data a file merely re-defines (globals ported
from psx_tomba headers) is dropped in favour of the real data from splat, and every external symbol of a C
file gets its retail address (
PROVIDEin the linker script, renamed when the name belongs to another address, e.g. NTSC-named symbols). - X000 is linked at
0x800E8028; MAIN0 symbols it uses are absolute (build/full/undefined_*_auto.x000.txt). - A file that does not compile or cannot be placed is reported as
WARN ... kept as asmand the build still links the retail assembly for it. Today 28 MAIN0 files stay as asm (1124 of 1152 src files linked): Psy-Q library objects whose.rodatacannot be placed (several bases, or bytes that differ from the game) andFUN_80068ae4(a BIOS stub whose inlineasmends in.data; it needs ASPSX). The output still matches. - Rules for new C files: a data table shared by several functions must be referenced as an
externsymbol in each file (it can only be linked once), and do not emit another function's data with__asm__(".word").
Japanese release progress
<!-- jp-progress:start -->Ore! Tomba (SLPS-01144): 83.34 % of the code matches byte for byte (1182252 of 1418632 bytes, 2721 of 3095 functions).
| Part | Code | Matching bytes | Functions | Matching |
|---|---|---|---|---|
| SLPS_011.44: engine | 378336 B | 323360 B | 1101 / 1269 | 🟩🟩🟩🟩🟩🟩🟩🟩🟨⬜ 85.5 % |
| X00.BIN: shared object code | 300480 B | 285408 B | 488 / 507 | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟨 95.0 % |
| X01.BIN: area code | 151304 B | 128436 B | 214 / 239 | 🟩🟩🟩🟩🟩🟩🟩🟩🟨⬜ 84.9 % |
| X02.BIN: area code | 25320 B | 24352 B | 32 / 34 | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟨 96.2 % |
| X03.BIN: area code | 122684 B | 100468 B | 191 / 222 | 🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜ 81.9 % |
| X04.BIN: area code | 102832 B | 70144 B | 163 / 197 | 🟩🟩🟩🟩🟩🟩🟨⬜⬜⬜ 68.2 % |
| X05.BIN: area code | 6112 B | 6112 B | 16 / 16 | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 100.0 % |
| X06.BIN: area code | 35844 B | 28200 B | 41 / 47 | 🟩🟩🟩🟩🟩🟩🟩🟨⬜⬜ 78.7 % |
| X08.BIN: area code | 228 B | 228 B | 1 / 1 | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 100.0 % |
| X09.BIN: area code | 82896 B | 58664 B | 117 / 144 | 🟩🟩🟩🟩🟩🟩🟩⬜⬜⬜ 70.8 % |
| X10.BIN: area code | 97636 B | 69804 B | 146 / 175 | 🟩🟩🟩🟩🟩🟩🟩⬜⬜⬜ 71.5 % |
| X11.BIN: area code | 13856 B | 10048 B | 26 / 31 | 🟩🟩🟩🟩🟩🟩🟩🟨⬜⬜ 72.5 % |
| X13.BIN: area code | 6336 B | 4140 B | 7 / 8 | 🟩🟩🟩🟩🟩🟩🟨⬜⬜⬜ 65.3 % |
| X14.BIN: area code | 54472 B | 36588 B | 85 / 109 | 🟩🟩🟩🟩🟩🟩🟨⬜⬜⬜ 67.2 % |
| X16.BIN: area code | 11688 B | 10852 B | 36 / 37 | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟨 92.8 % |
| X17.BIN: area code | 12124 B | 10988 B | 27 / 28 | 🟩🟩🟩🟩🟩🟩🟩🟩🟩⬜ 90.6 % |
| X18.BIN: area code | 16136 B | 14112 B | 29 / 30 | 🟩🟩🟩🟩🟩🟩🟩🟩🟨⬜ 87.5 % |
| X19.BIN: area code | 348 B | 348 B | 1 / 1 | 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 100.0 % |
Regenerated with python3 tools/build_report.py --version slps01144 then python3 tools/progress_jp.py. Analysis of JP-only code (Ghidra, PAL names ported by matching, functions with no PAL counterpart): notes/jp_retail.md.
Japanese demo progress
<!-- slpm80165-progress:start -->Ore! Tomba demo (SLPM-80165): 40.72 % of the code matches byte for byte (261320 of 641804 bytes, 1102 of 1832 functions).
| Part | Code | Matching bytes | Functions | Matching |
|---|---|---|---|---|
| SLPM_801.65: boot exe | 348752 B | 157260 B | 833 / 1309 | 🟩🟩🟩🟩🟨⬜⬜⬜⬜⬜ 45.1 % |
| X00.BIN: shared object code | 293052 B | 104060 B | 269 / 523 | 🟩🟩🟩🟨⬜⬜⬜⬜⬜⬜ 35.5 % |
Regenerated with python3 tools/build_report.py --version slpm80165 then python3 tools/progress_jp.py --version slpm80165. python3 tools/demo_sweep.py installs the PAL / JP C that matches demo functions as is (see docs/JPDEMO.md).
Versions
Every known retail, demo and prototype disc (serials, dates, executable hashes): wiki: List of builds.
| Version | Serial | Status |
|---|---|---|
| Tombi! PAL (Spanish) | SCES-01331 | matching, everything above |
| Ore! Tomba (Japan) | SLPS-01144 | split + shared C, matching in progress (figures in the table at the top and in "Japanese release progress"), see docs/JP.md |
| Ore! Tomba demo (Japan, taikenban) | SLPM-80165 | split + shared C, 40.9 % of code (objdiff; figures in "Japanese demo progress"), tools/demo_sweep.py installs what PAL / JP C matches as is, see docs/JPDEMO.md |
| Ore! Tomba beta 1998-02-03 (Japan) | SLPS-01144 (prototype) | split + shared C, 56.5 % of code, see docs/BETA_980203.md |
| Tomba! PrePre 8 (Japan demo) | PCPX-96084 | split + symbol pairing, 17.1 % of code, see docs/PREPRE8.md |
All versions live in this repository and appear on decomp.dev as versions of one project: the CI uploads one
<serial>_report artifact per version.
How the versions share code
The repository follows the usual PS1 multi-version layout (as in sotn-decomp and psx_tomba): one src/ tree is
compiled for every version, and each version has its own splat configs, symbol tables and asm.
| Version | --version (old alias) | Macro | splat configs / symbols | asm | report |
|---|---|---|---|---|---|
| SCES-01331 (PAL, reference) | sces01331 (default) | VERSION_SCES_01331 | config/*.yaml | asm/<prog>/ | build/report.json |
| SLPS-01144 (Japan) | slps01144 (jp) | VERSION_SLPS_01144 | config/slps01144/ | asm/slps01144/<prog>/ | build/slps01144/report.json |
| SLPM-80165 (Japan demo) | slpm80165 (jpdemo) | VERSION_SLPM_80165 | config/slpm80165/ | asm/slpm80165/<prog>/ | build/slpm80165/report.json |
- The C keeps the PAL names.
config/<serial>/symbol_addrs_<prog>.txtmaps PAL names to that version's addresses (tools/jpdemo_syms.py); names that only exist in a version carry the serial (func_8011F5AC_slps01144). - The build defines the version macro (
tools/ncheck.pyEXTRA_CPP;tools/build_report.py --version <v>sets it).INCLUDE_ASM_V("<prog>/nonmatchings/<prog>", name)(include/tomba/include_asm.h) resolves against the asm dir of the version being built. - A version function the PAL C matches as is just reuses the PAL file (pairing from the symbol tables and
config/<serial>/reuse.txt). A function that differs gets a branch in the PAL file, plus a// VERSION <SERIAL> <prog> <addr> <size>header line (and optional// FLAGS.<SERIAL>/// CC.<SERIAL>lines):// FUNC 800ef490 84 X000 // MATCHING 800ef490 84 // VERSION SLPS_01144 x00 800EF5FC 84 #if defined(VERSION_SLPS_01144) ... Japanese C, or INCLUDE_ASM_V(...) while it is not matched ... #else ... PAL C ... #endif - Version-only functions (the demo's stubs and debug code, JP-only helpers) live in the shared tree too, next to
the PAL files of the same program, as
<name>_<serial>.cwhose whole body is inside#if defined(VERSION_...). tools/version_share.pywrites these blocks:fold <version> <file.c>folds a standalone version file into the shared tree,asm <version>adds the INCLUDE_ASM branches for the PAL files whose version counterpart is not matched yet. The PAL build is unaffected (it never defines the other macros).
Releases
Releases of the port are published by hand by .github/workflows/release.yml (Actions > "Publish release" > Run
workflow, or gh workflow run release.yml) once the owner approves; there is no automatic release.
- Versions are
vX.Y.Z: tagv0.0.N, title "Tombi port 0.0.N", one release for every platform:tombi-port-0.0.N-windows-x64.exe,tombi-port-0.0.N-linux-x64.tar.gzandtombi-port-0.0.N-android.apk(one APK with both arm64-v8a, the 64-bit wasm2c build, and armeabi-v7a; Android picks the one the device runs), optionallyTombi-Tools-0.0.N-...(translation editor, graphics editor, assets extractor). Each release bumps the patch of the latestv*tag and is marked Latest; the notes list the player-facing commits since the previousv*tag (tools/release_notes.py). - To bump the minor/major version run the workflow with the input
version(e.g.0.1.0; must beX.Y.Zand greater than the latest):gh workflow run release.yml -f version=0.1.0. Later automatic releases continue from it (0.1.1, ...). - The old
build-YYYYMMDD-<sha7>releases are kept as pre-releases titled "dev build YYYYMMDD-<sha7>" (gh workflow run release.yml -f migrate_dev=truere-applies that without publishing anything).
Progress report (objdiff / decomp.dev)
tools/build_report.py produces the objdiff report that
decomp.dev displays: splat splits the retail
binaries into one assembly file per function (config/main0.yaml, config/x000.yaml, config/x0nn.yaml
for the 16 area overlays), each function is assembled as the target object, each src/*.c and
src/x0nn/*.c is compiled as the base object, and objdiff-cli compares them (build/report.json).
The report and docs/PROGRESS.md count exactly the same bytes: both take their unit list from
tools/units.py (every matched source file as one unit, plus the csv functions no file covers), game code only
(the Psy-Q library is left out unless --with-sdk is given) and every shared overlay function counted once, so
the decomp.dev headline is the whole-game figure to the second decimal. The GitHub Actions workflow .github/workflows/report.yml runs it on every push; it
needs a GAME_FILES_URL secret (and GAME_FILES_URL_JP / GAME_FILES_URL_JP_DEMO for the Japanese release / demo) pointing to your own disc image: a direct, Dropbox,
Drive or MEGA link to the image or to a zip of it (tools/ci_download.sh + tools/disc_unpack.sh; the downloads stay in the Actions cache), or a zip with just MAIN0.EXE and
AREA00/X000.BIN, which limits the report to those two.
Units are marked complete (decomp.dev "fully linked", complete_code) when their object is linked into
MAIN0.EXE / X000.BIN by tools/build_full.py and matches 100 % of its code. Overlay units are not (the overlays are not
yet linked from source), and no data is claimed: .data/.rodata still come from the retail binary via splat.
Repository layout
src/ matching C, one function per file (MAIN0 and X000)
src/x0nn/ matching C of the other area overlays (X001..X019), mostly generated twins of src/
src/wip/ unfinished attempts (not counted as progress)
include/ shared headers (tobj.h: game object struct)
include/tomba/ headers from psx_tomba (see THIRD_PARTY_NOTICES.md)
notes/ memory maps, structures, function lists and names, guides
docs/ generated progress report and charts
ghidra/scripts/ Ghidra scripts (headless pipeline, struct and name import)
config/ splat configurations for MAIN0.EXE and the X0nn.BIN overlays
.github/ CI workflow that publishes the objdiff report
tools/ checkers, progress report, analysis helpers, setup scripts
game/ your own game files (ignored by git)
game_jp/ your own Japanese disc files (ignored by git; game_jpdemo/ for the demo)
config/slps01144/ splat configs and symbol tables of the Japanese release (config/slpm80165/: the demo)
Tools
| Tool | Purpose |
|---|---|
tools/ncheck.py | Fast native compile-and-compare (--score, --asm, --mark) |
tools/matchcheck.py | Reference compile-and-compare with the original PSY-Q binaries |
tools/build_full.py | Full build of MAIN0.EXE and X000.BIN, byte-for-byte check against the retail files |
tools/fn.py | Disassembly (capstone) and Ghidra pseudo-C of one function |
tools/areas.py | Area overlays: function boundaries (bounds), copies of matched twins (twins) |
tools/progress.py | Regenerates docs/PROGRESS.md and the SVG charts |
tools/units.py | The unit list shared by progress.py, gen_symbols.py and build_report.py (prints the totals) |
tools/classify.py | Separates complete functions from Ghidra fragments (notes/todo_match3.csv) |
port2/tools/audit_pointers.py | Native port: static audit of the retail code pointers of every image the game loads (MAIN0's data, X000, every area, the SYSTEM overlays, the LD0* load lists), walked as the port's relocator walks them: function starts with native code, starts without (stubs), labels inside functions (jump tables vs handler entries), pointers to code the function csv lacks (gap), data pointers that would become another overlay's thunk, pointer tables inside code ranges, the round constants of the C that fall in code; needs build/port2/gen (docs/PORT.md "Pointer audit") |
port2/tools/sweep_areas.py | Native port: runs every room of the debug select headless (--skip-movies --poke 900:1F8001B4:1, area/section poked, generic walk/jump/attack pad script) and greps each log for faults, no native code, stubs, watchdog lines; table per room, --out DIR keeps the logs |
tools/fixbounds.py | Detects functions Ghidra split incorrectly |
tools/permute.py | Small random permuter (CPU only) for near-misses |
tools/ghidra_pipeline.sh | Headless Ghidra: import, apply names and structs, export pseudo-C |
tools/setup_ghidra.sh, tools/setup_native.sh | Toolchain installers |
tools/psxexe_info.py | Prints the PS-X EXE header |
tools/wfm.py | Assets: WFM font/dialogue files <-> YAML + glyph PNGs, byte-exact round trip (369/369) through a TombaTools fork |
tools/wfm_py.py | Assets: pure-Python WFM codec (no Go): dialogues as readable text with tags ({NL} {CLEAR} {WAIT} {BOX w h} {PAUSE n} {COLOR n}...), glyph table, byte-exact round trip on 369/369 files (--check); characters come from tools/wfm_charmap.json (bitmap hashes, made by wfm_charmap_gen.py) |
tools/text_editor.py | Translation editor (Qt/PySide6; catalog in text_catalog.py, checks of check_text_patch.py, box preview of text_preview.py): every text a translation patch can change by category (dialogues, signs, system messages, item names/descriptions, event titles/list names/hints, places, memory card, title/options and pause menus), disc original / improved translation / your text side by side, the other PAL languages and the aligned Japanese line (jp_text.py), live checks, Install in the game (compiles translation.tpatch with tpatch_build.py from your disc and copies it next to the game); Windows build tombi-text.exe (Releases) |
tools/gfx_editor.py | Graphics editor (tkinter; logic in gfx_core.py): open the disc image, browse screens, the UiSpriteDef table of MAIN0 (NOW / LOADING), VRAM pages per load list, WPP sprite frames and CLUT palettes with their real colours, rectangle overlay, export/import indexed PNG (RGB(A) PNG is quantised to the palette), palette editing (15 bit, shared-palette warning), undo, in-context preview, composed room views (tree node "Vista compuesta": every room as the game draws it + the sky/far background alone, captured on demand by running the game in tools/psxemu and cached in the user cache folder; needs the .bin image and numpy), save/load mod (mods/<name>/mod.json + PNG, the mod.py format), Build ISO; UI language selector (i18n.py); Windows build tombi-gfx.exe (Releases) (bundles mkpsxiso). Tests: tools/test_gfx.py |
tools/tombi_save_editor.py | Save editor (Qt/PySide6, PKHeX-style; library + CLI tombi_save.py, tests test_tombi_save.py): memory card images (.mcd/.mcr/.gme/.vgs/.mcs, the port's tombi_mc0.mcd), AP, vitality/HP, lives, exp levels, items/inventory, the 130 events (not started/started/completed, cut ones marked), play time, area; checksum rewritten, Save as / .bak. Names read at run time from your disc (English/Spanish/...). Format: docs/SAVE_FORMAT.md, guide: docs/SAVE_EDITOR.md; Windows build TombiSaveEditor.exe (artifact of save-editor-exe.yml). Editor de partidas: AP, vida, objetos, eventos y tiempo de las partidas de la tarjeta de memoria. |
tools/gam_py.py | Assets: pure-Python GAM encoder (the decoder is area000.gam_unpack), valid streams, original bytes kept for unchanged data; used by mod.py and the graphics editor so no Go is needed |
tools/unnamed.py | Names: exports to notes/unnamed.json (committed, derived from src/) every function that still has a generic name (func_8xxxxxxx / FUN_8xxxxxxx) in MAIN0, X000, the SYSTEM overlays (D0SPSUB*, O0PTSUB0, I0NFO*, G0OVER) and the own functions of the area overlays X001..X019: address, program, file, size, the // probably: note, callers, the object table slot (pointer scan of MAIN0/X000) and, when it resolves as the "Animaciones" node does (animobj.py), the SPR table / frames of the handler whose scripts reach it. The graphics editor shows the list under Sin nombre / Unnamed (names_panel.py: "Probablemente" = the functions with a note, to review, and "Resto"; note, callers, area, sprite frames and the player for linked scripts; a Nombre + comment per entry, Exportar nombres / Importar names_user.json, autosaved in the TombiTools config folder) |
tools/apply_names.py | Names: applies a names_user.json of the editor ([{address, prog, old, new, comment}]) with the mechanics of the names: commits: moves src/<old>.c to src/<new>.c (+ the twins src/x0NN/), the symbol in sources and callers, notes/functions_*.csv rows (twins by X000:addr), notes/names_main0.csv / names_x000.csv (media, the comment), a # User names line in notes/names.md, label keys, and every other mention under src/ include/ notes/ tools/ port2/ docs/ (SYSTEM overlay names only inside their folder, the rest is listed); validates the identifier, refuses collisions, --dry previews. Then tools/build_full.py and regenerate notes/unnamed.json |
tools/i18n.py | UI languages of the Windows tools (es, en, fr, de, it, nl, pt, sv, da; the port launcher is complete in all nine, port_launcher_lang.py): catalogs, system language detection, choice saved in %APPDATA%/TombiTools/settings.json |
tools/gam.py | Assets: GAM (the game's LZ) unpack/pack; --check runs the whole round trip (816 files) |
tools/mkdisc.py | Assets: rebuilds the .bin/.cue from game/disc/fs with mkpsxiso, SHA-1 identical to the original (--check) |
tools/cdfiles.py | Assets: the LBA/size file table inside EXE/MAIN0..8.EXE (dump, check against an image, update from a new layout); used by mkdisc.py build --relayout |
tools/area000.py | Assets: AREAxx/*.000 and SYSTEM/*.000 (uncompressed VRAM images, WPP/WMD tables, APD grids): info, unpack (+PNG views), pack, --check (286/286 identical); format in notes/formats.md |
tools/room.py | Assets: room models (WMD, kind 0x50): flat PNG render (AREA00:1 out.png, --all) and 3D glTF export (--glb AREA00:1 out.glb, --glb-all DIR; texture atlas, vertex colours; opens in Blender / Windows 3D Viewer); also written to rooms/ and rooms3d/ by extract_sprites |
tools/anim.py | Assets: Tombi's animations: decodes the 90-entry animation table (overlay 0x800E8404 -> scripts in MAIN0 data) into a catalogue (id, name, frames, ticks, loop; 82 distinct scripts, 36 named), list [--json], export OUT (animated GIF + PNG strip per animation + catalog.json; also step animations of extract_sprites); preview window with play/pause/step/speed/loop/zoom/GIF export in the graphics editor (node "Animaciones", anim_player.py) |
tools/wpp.py | Assets: WPP sprite frames (flag-byte RLE, 4 bpp): info, unpack (indexed PNG with the Tombi CLUT), pack, --check (40/40 files identical); area000.py page exports room pages with their CLUT |
tools/vram.py | Assets: VRAM simulation of every load list (GRPX pages, CLUTs, GAM images at their LoadImage position) and the SPR frame definitions cut out of it (--all OUT: atlas parts, composed frames, flat pages, 1024x512 snapshots; snapshot AREA00:1 x.png; --stats) |
tools/sprclut.py | Assets: per frame texture page offset and CLUT of SPR frames as the game code sets them: item descriptors of initItemObject (DAT_8007B14C: tpage offset w1e, CLUT override w08 when b0d & 1, money bag palettes) and the page that owns a palette strip; used by vram.py (items lists them) |
tools/psxemu/clutmap.py | Assets: runs the game in tools/psxemu (title, memory card screen, first room, pause menu, item menu, map), records every textured primitive and resolves texels and palette to disc files; writes tools/ui_cluts.py (generated table, only coordinates/file names), which vram.py / screens.py use for HUD, menus and fonts drawn by code |
tools/audio.py | Assets: sound, pure Python: .WVD banks -> every VAB sample as WAV (PS1 ADPCM, loop points as smpl chunk) + tone table, .SEQ (pQES) -> MIDI type 0 (kept in sound/music, useful for editing), software sequencer (numpy) that renders each SEQ with its bank (mapping in SEQ_BANK) to stereo 44.1 kHz audio in sound/music_audio (loop played twice, fade out), XA audio of the .STR movies -> WAV (bit-exact vs ffmpeg); `--audio-format wav |
tools/str.py | Assets: .STR movies: demux + pure Python MDEC decoder (BS v2/v3, matches ffmpeg at about 49 dB): info, frames (PNG sequence, ~0.2 s/frame), mp4 (needs ffmpeg in PATH), avi, all (AVI + mp4 when ffmpeg exists) |
tools/extract_sprites.py | One-shot: every graphic of the game as PNG straight from a retail disc image (WPP frames, SPR atlas cuts, pages and VRAM snapshots; no dependencies) plus sound (sound/: WAV, MIDI) and movie AVI/audio (video/); choose what to extract: window with checkboxes per group (Sprites, Screens, VRAM pages, Rooms PNG/3D, Sound banks, Music MIDI, Rendered music, Movie audio, Videos; selector WAV / MP3) or --only sprites,rooms3d,... and `--audio-format wav |
tools/mod.py | Assets: mods (mods/<name>/mod.json + PNG patches + mods/load_order.json) applied to a copy of game/disc/fs (GRPX pages/CLUTs, WPP frames; GAM re-packed), then mkdisc.py build --relayout; layout idea from TombaPatcher (US) |
tools/overlay_data.py | Assets: data layout of the area overlays X0nn.BIN (summary, dump, header, strings), see notes/overlays.md |
Assets. wfm.py, gam.py and mkdisc.py work on your own disc (game/disc/Tombi.bin and the files extracted to
game/disc/fs, never committed) and call outside tools: a PAL fork of TombaTools (Go, not vendored; TOMBATOOLS
defaults to /opt/psyq/tombatools-pal) and mkpsxiso/dumpsxiso 2.30 (MKPSXISO_DIR). wfm.py --check gives 369/369
identical WFM; gam.py --check reports how many of the 816 GAM files the re-compressor reproduces (743 by itself,
765 with its second variant, 816 when the original bytes are kept for unchanged data); mkdisc.py --check rebuilds
the image in /tmp and compares its SHA-1. Details in notes/assets_tombatools.md.
Windows exes (the port is tombi-port-<ver>-windows-x64.exe of the latest release, the tools are in Tombi-Tools-<ver>-windows-x64.zip of the releases that include them; older versions stay on the Releases page; tombi-port.exe is the native port with its launcher; built by GitHub Actions on every change, no Python needed; your own disc image is required, nothing of the game is inside them; the native port extracts it (.bin/.cue/.iso, or a MAME .chd: tools/chd.py) once into a data/ folder and runs from that, see docs/PORT.md "Running without the disc image"):
tombi-assets.exe: sprite/sound/video extractor (window with checkboxes).tombi-text.exe: translation editor (Spanish / English interface, light / dark theme). Open yourTombi.bin(or the extracteddata/folder) and start a New translation (from the disc text, or, in Spanish, from the bundled improved translation) or open one. The left tree lists every text the patch system can change by category: dialogues (by area), signs, system messages, item names (dialogues and inventory) and descriptions, event titles, event list names and hints, places (save list, travel list), memory card screen, title / options menus and pause menus. Each entry shows the disc original, the improved translation and your text side by side; search and filters (only changed, only untranslated, different from the improved one, with errors). Other languages shows the same entry in every language of the disc; Japanese reads the Japanese disc (Ore! Tomba) and shows the JP line aligned with it, with the PAL English line as its translation (tools/jp_text.py; JP lines with no PAL counterpart are a category of their own; no online service is used). While you type, the validator checks the entry (missing glyphs, box overflow, lines/pages, control codes, room in the menus) and the box is drawn as the game draws it. Save writes your*.tombipatch.json; Install in the game… compiles it intotranslation.tpatch(from your disc, a few minutes) and copiestranslation.tombipatch.json+translation.tpatchnext to the game's executable: the port loads it by itself and it wins over the bundled improved translation (docs/PORT.md "Your own translation file"); the launcher's Import translation… does the same from any folder, Android imports the.tpatch(Options → Data → Import translation). More: compile only, build a patched ISO (needs mkpsxiso, bundled), export/import the dialogues as text. Command line of the patches:python tools/text_patch.py make|apply|merge|info(make --ppffor a PPF3),python tools/tpatch_build.py DISC.bin|data/ X.tombipatch.json -o X.tpatch,python tools/data_patch.py apply data X.tombipatch.json.tombi-gfx.exe: graphics editor. OpenTombi.bin; the tree on the left has Screens, Interface/HUD (the three plausibleUiSpriteDefentries of MAIN0: 0 unused, 1 NOW, 2 LOADING), Pages by load list, Sprites (WPP frames) and Palettes (CLUT). The middle shows the page with the palette chosen in the right-hand list (x1..x8, optional sprite rectangles; click one or drag a region).Export/Import PNGwork on the whole image or the selection: an indexed PNG with the same palette is used as is, an RGB(A) PNG is converted to the nearest palette colour (transparent = index 0 / word 0x0000) with a warning when it has more than 16/256 colours. Clicking a palette swatch opens a colour chooser (15-bit colours, STP bit kept; a warning lists the other images that share the palette).Save modwritesmods/<name>/mod.json+ PNGs thatpython tools/mod.py build --mods <name>also applies;Build ISOwrites a new.bin/.cue(about 1.5 GB free; without mkpsxiso or with a folder it writes the changed files only).- All three exes have an Idioma / Language selector (Español, English, Français, Deutsch, Italiano, Português; default = system language,
saved in
%APPDATA%/TombiTools/settings.json) (Linux:~/.config/TombiTools/settings.json).
Linux binaries (tombi-port-<ver>-linux-x64.tar.gz / Tombi-Tools-<ver>-linux-x64.tar.gz of the releases, executable bits kept, built by the same workflows on ubuntu-24.04, same data policy: nothing of the game inside, except that
tombi-port embeds the MAIN0/X000 bytes exactly like tombi-port.exe): tombi-port (launcher with the game tombi-pc and a bundled
32-bit libSDL2-2.0.so.0 found through rpath $ORIGIN; Ubuntu's build links ALSA/PulseAudio/X11 directly, so the 32-bit SDL2 package below is still needed), tombi-text, tombi-gfx, tombi-assets (PyInstaller one-file; text/gfx bundle
mkpsxiso/dumpsxiso built from source). The game tombi-pc is an i386 program (port2 builds with
-m32), so a 64-bit distro needs the 32-bit runtime: on Debian/Ubuntu sudo dpkg --add-architecture i386 && sudo apt install libc6:i386 libsdl2-2.0-0:i386 (the latter pulls the 32-bit X11/Wayland/GL libraries SDL2 loads at run time); Fedora: glibc.i686 SDL2.i686. The launcher
window (Qt, 64-bit, bundled) needs the X11 cursor library many desktops already have: libxcb-cursor0 (Fedora xcb-util-cursor).
Releases. Every push only builds (the binaries stay as Actions artifacts bin-<platform>-<tool>).
.github/workflows/release.yml publishes a release build-YYYYMMDD-<sha7> from the newest green port build
(tombi-port-<ver>-windows-x64.exe, tombi-port-<ver>-linux-x64.tar.gz): on request (GitHub > Actions >
"Publish release" > Run workflow, or gh workflow run release.yml; tick include tools, or
-f include_tools=true, to add Tombi-Tools-<ver>-windows-x64.zip / -linux-x64.tar.gz with tombi-text,
tombi-gfx and tombi-assets), or automatically after a port build once master has 5 non-CI commits (subject not
starting with ci:/dist:) since the last release.
Related projects
- psx_tomba — decompilation of the NTSC-U release
(
SCUS_942.36) with a full splat/objdiff build. Its headers, names and some functions are reused here and re-verified against the PAL executable; see THIRD_PARTY_NOTICES.md.
License
Source code and documentation are released under the MIT License. Third-party material is listed in THIRD_PARTY_NOTICES.md.
Legal
This project contains only original source code written to reproduce the behaviour of the game, plus documentation. Tombi! is © Whoopee Camp / Sony Computer Entertainment. No copyrighted game data or SDK files are distributed.
