0

Decompilation Klonoa Lvl1

Fresh

klonoa decompilation

Type
RL Env
Runtime
ORS
License
unknown
Size
100 tasks
Published
Sep 2026
Updated
Nov 2026

Cite

Notes

Only stored in your browser.

Decompilation — Klonoa, level 1

Matching decompilation as an OpenReward environment.

Level 1 is 60 functions of at most 2,048 bytes each. The level is the difficulty, and it is the only thing that distinguishes this environment from its siblings: same machinery, same game, a harder draw. Raising the size cap is what makes a higher level, since the cap is what decides whether a task's score sits in work a model can reach.

A task hands the agent a real, complete decompilation project with some function bodies removed. The agent writes C; the environment compiles it with the game's original toolchain and diffs the objects against the ones the untouched project produces. The task is solved when every object matches byte for byte — not equivalent code, identical code.

The verifier is a compiler and a differ, so there is no judge and no rubric.

Games

gameplatformcompilerupstreamfunctions
Klonoa: Empire of DreamsGBAagbcc (gcc 2.95 + Nintendo's patches)testyourmine/kleod (MIT)457

More projects are added as sibling entries in decompilation/games.py and games/<slug>/; they share this repo and this environment.

The task

Decompile 60 functions of the game — that is what level 1 means. The other ~400 stay in place as gold: they are the only worked examples of the codebase's idioms, and reading the neighbours of what you are writing is the first thing a real contributor does.

There are 100 tasks, differing in which functions they remove. Each is a seeded draw, stratified by compiled size and held to a byte budget, so tasks are comparable as work while overlapping in only ~14% of their functions:

functions per task60
function size cap2,048 bytes
task size24,301 ± 536 bytes (2.2% spread)
overlap between two tasks8.5 functions (14.2%)

The size distribution of a typical task, and where its score lives:

  size band      fns/task   share of task bytes
      0-32      7.38     0.6%
     32-64      8.68     1.6%
     64-128     9.42     3.5%
    128-256    10.96     8.7%
    256-512     7.33    10.7%
    512-1024    7.53    23.1%
   1024-2048    8.70    51.8%

Two things follow from that shape. Reward is byte-weighted, so the 36 functions under 512 bytes are 61% of a task by count and 14% of its score, while the 8.7 functions of 1-2 KB are 52% of the score. And the cap matters: without it two or three functions of 4-28 KB carry half a task, the score sits entirely in work far beyond what a model completes today, and the reward goes flat long before the time does. 64% of an uncapped task's score is below 2 KB, so the cap costs little difficulty and keeps the frontier inside the task rather than behind a wall.

Every knob is an environment variable — DECOMP_FUNCTIONS_PER_TASK, DECOMP_FUNCTION_SIZE_CAP, DECOMP_TASK_COUNT, DECOMP_REMOVED_FRACTION (used only when DECOMP_FUNCTIONS_PER_TASK is 0) — so difficulty is a deployment setting rather than a rebuild.

Measured

glm-5.3-flash, four agents under AgentOrg, against an earlier 46-function draw: about 30 minutes of orientation, then roughly 16 functions and 1,250 bytes of byte-exact code per hour. Completions average 78 bytes, so the model is closing the small end and has not yet broken into the 512-1024 band. On the task above that projects to a reward around 0.15-0.25 over four hours, still climbing at the end rather than saturating.

Reward

The fraction of the remaining gap closed:

(matched_code_percent - baseline) / (100 - baseline)

with 0.15 weight on objdiff's fuzzy_match_percent for shaping, so a function taken from 0% to 85% is not invisible.

The best is tracked per function, and the score is the byte-weighted sum of those. A project-wide reading is a sample of whatever state the tree happened to be in when make ran, and with several agents editing several files some file is almost always half-written — the project figure swings by thousands of bytes independently of real progress, so a project-wide running maximum ends up rewarding the luckiest sample. Per function, a match that was once achieved stays credited, and a half-written file cannot erase a finished function elsewhere. solved still reflects the real simultaneous state of the tree, not the per-function bests.

It is emitted as increments over a running best, not as one terminal number. A run almost always ends on the harness's wall clock rather than by calling finish, and a terminal-only reward scores such a run at nothing no matter what it recovered. Emitting the increment instead means the stream sums to the best ever reached. A step that graded nothing emits None and adds no row; a step that graded without improving emits 0.0, so a run that earns nothing still scores zero rather than coming back unscored. Every tool result carries the run's numbers in its metadata, terminal or not, for the same reason.

solved additionally requires every object to compare equal after --strip-debug, and the data sections to match too. objdiff's percentages are the shaped signal; the byte compare is the verifier - it also covers the inter-function alignment padding objdiff attributes to no symbol.

No ROM

baserom.gba is referenced only from the projects' data assembly. The C translation units build without it, so tasks, targets and grading never touch the copyrighted image. The cost is that the final ROM link is out of scope: the win condition is "every object matches" rather than the ROM SHA-1. Those are equivalent as long as a solution only edits C, which the guards below enforce.

Guards

  • Edit surface. Every file is hashed when the task is built. Anything modified, added or deleted outside the project's C sources scores 0 — the linker script, the hand-written assembly and the data objects feed the link too.
  • Inline assembly. Scanned on the lines a solution adds, diffed against the task's starting state: upstream's own asm("swi 0x2A") is legitimate, an added one is not. register x asm("rN") stays allowed — it constrains register allocation without emitting instructions. A macro or token pasting hides an asm statement from a line scan, so every build is also checked after preprocessing: a code-emitting asm statement or a naked attribute that upstream's tree does not have (the same template in the same function) rejects the build. agbcc marks no inline asm in the assembly it writes, which is why the check reads the preprocessed source rather than the compiler's output. An asm statement with an empty template, such as the barrier asm("" : "+r"(x)), emits no instruction and is allowed by both checks.
  • Executable sections. A data object placed in .text pairs its bytes with a target function of the same name, so machine code can be smuggled in as a C array. The preprocessed scan also rejects a section(".text…") attribute the upstream tree does not have; only .text is executable, so data in upstream's own sections (.bss.code, common_data, iwram_data, …) stays allowed.
  • Build limits. Every build runs in its own process group under a wall-clock timeout and memory, output-size and CPU limits, so an infinite preprocessor include or a runaway allocation cannot hang the server or fill its disk. A build that exceeds them reads as a failed build the agent can retry.
  • Reference solutions. The pristine trees live at /opt/pristine, outside the work area. No tool can read them, and neither can the build: the references are root-only, and each session's tree is built by a uid of its own, so an #include or INCBIN of /opt/pristine, /opt/target or another session's tree fails.

Tools

list_targets, read_file, edit_file, build, diff, progress, finish. diff renders the target and the current assembly side by side, which is the environment's real feedback channel.

Resources

CPU only. agbcc, GNU as and objdiff are all single-threaded C; builds run at make -j4. The image carries the pristine objects, so a session starts from a built tree and make only recompiles the translation units a task's stubs touch. Measured at 4 CPUs / 16 GB: session setup 2.9s, the build tool 1.5s, the diff tool 0.1s. A full 22-object rebuild from cold is about 7 seconds. Rollouts are token-bound, not build-bound.

Set the deployment's concurrency low — around 4. Unlike most environments every session runs real compiles, so the default of 500 concurrent requests against 4 CPUs would thrash; scale with instances instead.

Running locally

docker build -t decompilation .
docker run -p 8080:8080 decompilation

Point a harness at it with OPENREWARD_SESSION_URL=http://localhost:8080.

Licence

Upstream kleod is MIT (testyourmine) — see games/klonoa/kleod/LICENSE and games/klonoa/UPSTREAM_COMMIT.