You fed it, trained it, cleaned up after it. You had a plan. Then it evolved into Numemon, a walking pile of sludge, and the game never told you why.
No menu. No hint. No stat screen saying here is what went wrong.
Players filled that silence with wikis, calculators and arguments about whether care mistakes helped or hurt. They inferred the rules from what happened on screen. Now we can read the code.
The Digimon World decompilation project has reconstructed the evolution code. One rule comes from a loop with no break: when several stats share the highest value, the game picks the last one in its array.
Here is how the scoring works, what that tie means, and what I learned while testing a small PC port prototype.
The missing break
Here is the real code that picks which of your stats the game examines. It's in src/main/evolution.c, and it runs over the six stats hp, mp, offense, defense, speed, brain:
for (i = 0; i < 6; i++) {
int8_t isHighestStat = 1;
for (j = 0; j < 6; j++) {
if (statsArray[i] < statsArray[j])
isHighestStat = 0;
}
if (isHighestStat == 1)
highestStat = i;
}
For six stats, the nested loop is cheap. But look at what is missing: there is no break after the assignment. The loop keeps going after it finds a maximum, so a later tied stat overwrites an earlier one. When stats tie, the highest index wins.
I ran the same selection logic with a few inputs:
all six tied -> brain
offense highest -> offense
off+def tied high -> defense
brain highest -> brain
Reading the array order backwards gives the full tie-break priority: brain > speed > defense > offense > mp > hp.
With all six stats tied, the game selects brain as the highest stat. You could infer this from enough experiments, but the code makes the reason clear. Whether the missing break was intentional is unknown; its effect is observable.
The scoring system: four checks, three needed
That tie-break feeds a larger system. Every Digimon has up to six possible evolution targets. For each candidate the game computes a score out of 4, and a candidate needs at least 3 points to be eligible at all. Below 3 it isn't considered.
Here are the four checks.
Care mistakes, and the flag that inverts them
if (isMaxCM == 0) {
if (partner->careMistakes >= reqs->care)
reqPoints += 1;
} else if (partner->careMistakes <= reqs->care) {
reqPoints += 1;
}
Read the comparisons closely. There's a flag bit that reverses the test, so some evolutions want your care mistakes above a threshold.
Neglect isn't a punishment in this game. For certain paths it's a requirement. Care mistakes can therefore help qualify a candidate. Whether that explains any particular Numemon depends on the other checks and available targets.
Weight is a window, not a floor
if (reqs->weight - 5 <= partner->weight &&
partner->weight <= reqs->weight + 5)
reqPoints += 1;
A band of ±5 around a target. Overfeeding fails this check exactly as hard as underfeeding. A precise target weight matters because both sides of the window have a limit.
Stats, where the stage changes the rule
if (DIGIMON_DATA[target].level == 3U) {
/* Champion: only your single HIGHEST stat is examined */
} else {
/* everything else: ALL six stats must clear their thresholds */
}
Most evolutions need every stat above its requirement. Champion-level targets check only your highest one — which is where the missing break above actually bites. For Champions, one selected highest stat matters; other targets require all six thresholds.
The bonus point: any one of five
if (reqs->digimon != -1 && current == reqs->digimon) isBonusFulfilled = 1;
if (reqs->discipline != -1 && /* ... */) isBonusFulfilled = 1;
if (reqs->happiness != -1 && /* ... */) isBonusFulfilled = 1;
if (reqs->battles != -1) { /* also invertible */ }
if (reqs->techs != -1) { /* mastered moves */ }
Any single one of these grants the fourth point. Battle count carries the same inversion trick as care mistakes: some evolutions want you to have fought fewer than N battles.
The rule that quietly steers your collection
One more piece, and it's the one that made me sit back:
if (reqPoints >= 3 && currentBest != -1) {
isTargetRaised = hasDigimonRaised(
EVO_GAINS_DATA[target].targetDigimon);
isCurrentBestRaised = hasDigimonRaised(
EVO_GAINS_DATA[currentBest].targetDigimon);
if (isTargetRaised == 1 && isCurrentBestRaised == 0)
reqPoints = 0; /* you already have this one: disqualify it */
if (isTargetRaised == 0 && isCurrentBestRaised == 1)
reqPoints++; /* you don't have this one: push it ahead */
}
The game remembers which Digimon you have raised. When it compares two eligible candidates, this code favours the one you have not raised: it can zero the already-raised candidate’s score or add a point to the new one. The outcome still depends on candidate order and the surrounding selection logic.
It is a small but deliberate way to steer players toward more of the roster.
Who actually did this
I did not decompile the game. I forked the repository, ran parts of its code on a PC, and measured a few pieces a port would need. The decompilation work belongs to its contributors.
Digimon World decomp
A work in progress decompilation of Digimon World for PS1.
Dependencies
Install the following packages:
binutils-mipsel-linux-gnu gcc-mipsel-linux-gnu git make python3 python3-venv unzip wget
Install Python dependencies:
python3 -m venv .venv
. .venv/bin/activate
pip3 install -r requirements.txt
Download tools:
tools/dl_deps.sh
Download CodeWarrior for PlayStation Release 4 and copy cc_mips.dll to bin/cc_mips/cc_mips_40.dll.
Build
# Update submodules
git submodule update --init --recursive
# Dump original PSX Digimon World (USA) ISO
bin/mkpsxiso-2.20-Linux/bin/dumpsxiso -x disks/us -s disks/us/us.xml "/path/to/Digimon World (USA).bin"
# Disassemble original binaries
make -j$(nproc) regenerate
# (Optional) Create file local.mk to override defaults
MWCCWRAP := /path/to/mwccwrap.exe
MWCCWRAP_FLAGS := -dll "/path/to/cc_mips.dll"
METROWRAP := /path/to/mw
METROWRAP_FLAGS := --use-wibo --wibo-path /path/to/wibo
CROSS := /path/to/mipsel-linux-gnu-
# Build new binaries
make -j$(nproc)
# Compare original vs new binaries
make compare
# Generate objdiff config
make objdiff
Links
Symbols and reverse engineering is based on work by SydMontague:
https://github.com/SydMontague/DW1-SydPatches
https://github.com/SydMontague/DW1-Code
jype0/dw_decomp started in March 2026 and is MIT licensed. It passed a milestone most decompilation projects never reach: zero INCLUDE_ASM stubs left in src/.
If you haven't worked on a decomp, that needs explaining. INCLUDE_ASM is the marker for "we haven't figured this function out yet, here's the raw assembly instead." Most decomps carry hundreds of them for years. The source tree no longer relies on those assembly placeholders. That milestone alone does not establish that every function matches the retail binary.
Credit isn't evenly spread, and shouldn't be reported as if it were. As of today the contributor counts are:
| Contributor | Contributions |
|---|---|
| Pekka Jylhä-Ollila (jype0) | 409 — maintainer, and the bulk of the project |
| juandav | 37 — matching functions and data across overlays |
| ChonkMode | 22 |
| kingzmanh | 11 |
| fmil95 | 7 |
| ThirstyWraith | 6 |
| SydMontague | 5 — struct and naming reference work |
| paulohpfilho | 4 |
Those are the repository’s contribution counts at the time I checked, not a measure of each person’s work or proof that every regional build is complete.
I asked about the roadmap in an issue. juandav relayed jype0's four points: clean up the hacks and name things properly; match all 10 regional versions; maybe a PC port, blocked because psyq and psycross don't currently implement libgs or libsnd; and then a DW1 mod of epic proportions — new maps, story, Digimon.
The ambitious mod is the long-term goal. The decompilation makes that work possible.
Running their code somewhere it has never run
My question was narrower: can this code run off the PlayStation at all?
evolution.c is 1,087 lines. Compiling it on x86-64 produced exactly one error:
libgte.h: No such file or directory
That's Sony's geometry coprocessor header. So I grepped evolution.c for VECTOR, MATRIX, gte_, RotTrans — nothing. The evolution logic doesn't touch the GTE at all. It only inherits the header through entity.h.
I wrote type-only stand-ins for the four PsyQ headers. It compiled clean, then ran:
EvoRequirements = 28 bytes (PS1 layout: 28)
fresh(1) -> 2
in-training(3) -> 5
That 28 matters more than it looks. The game reads its data tables as raw structs, so if the layout shifted by even one byte on a 64-bit host, every evolution requirement in the game would decode as noise. It doesn't, and I pinned it with static_assert so nobody can break it quietly later.
Measuring the GTE instead of guessing at it
A major piece of a PS1 port is the GTE, the fixed-point geometry coprocessor.
My first instinct was to size the problem with a grep. I got 181 and quoted that number publicly. It was wrong — my pattern was matching a header that defines every GTE macro whether the game calls it or not. Measured against src/ alone:
632 call sites, 29 distinct operations, 34 of 126 files
The call sites are concentrated:
| Operation | Sites | Cumulative |
|---|---|---|
ApplyMatrixSV |
103 | 16% |
RotMatrix |
51 | 24% |
RotMatrixZYX |
44 | 31% |
RotMatrixYXZ |
44 | 38% |
ratan2 |
43 | 45% |
gte_rtps |
42 | 58% |
ScaleMatrix |
39 | 71% |
TransMatrix |
37 | 77% |
ApplyMatrixLV |
23 | 81% |
The nine operations shown account for about 81% of these call sites. That gives a useful starting point, though call-site count does not measure implementation difficulty.
I implemented a software subset covering those common operations. The contract is strict: matrix entries are 1.3.12 fixed point (4096 = 1.0), angles run 4096 to a full turn, and results saturate rather than wrap. That last one isn't optional — a wrapped coordinate throws a vertex to the far side of the screen, which is exactly the class of bug that ships unnoticed.
Then I measured the number that actually decides anything. "0.17% trigonometric error" is a useless statistic; nobody can tell you whether that's visible. So I measured pixels — how far a projected vertex lands from where double-precision maths puts it, across 2,528 vertices and a full rotation sweep:
mean error: 0.825 px
worst error: 1.896 px
vertices off by > 2px: 0.00%
In that test, the error stayed under two pixels. This measures one projection sweep; it does not guarantee every game scene will look the same.
And the honest half: it is not bit-exact. The real GTE uses lookup tables; my polynomial approximation looks identical and is numerically different. That's fine for a port and useless for verifying a decompilation, whose entire premise is reproducing the original binary byte for byte. Confusing those two would be the expensive mistake, so I put the warning in the test output rather than in a comment nobody reads.
The game's rendering is all PsyQ primitives, counted across the source: The two textured primitive types in this excerpt account for 392 call sites, making them a useful first rendering test. I built them with the game's own One thing I deliberately did not fix: the texture mapping is affine, not perspective-correct. The PS1 had no perspective correction, and that wobble is part of how the console looks. Correcting it would make a port feel wrong. Then the ordering table, which is how the PS1 sorted by depth. The name misleads, so it's worth saying plainly: an ordering table is not a sorting algorithm. It's a bucket array — one slot per depth value, each holding a linked list, with I built the occlusion test so it couldn't pass by luck: submit the near quad first, then the far one. Same submission order, opposite result. The table is doing the work, not the sequence of my function calls.Drawing a frame: primitives and the ordering table
POLY_FT4 386 (textured quad)
POLY_F4 30
POLY_GT4 17
POLY_FT3 6 (textured triangle)
setXYWH and setUVWH macros lifted from src/main/utils.c, and rasterised them in software.AddPrim pushing to the front. O(1) insertion, zero comparisons. That's how a 33MHz console depth-sorted a scene every frame, and reimplementing it with qsort would be slower and wrong, because the order of equal-depth primitives depends on the original insertion and traversal rules.
normal: centre pixel r=16 g=104 (near/green occludes)
depths swapped: centre pixel r=120 g=16 (far/red now occludes)
Where I got things wrong
Two corrections, because they're more useful than a clean story.
The GTE count. I said 181 in public. It's 632. I estimated with a grep and then quoted the estimate as a measurement, which is a bad habit and I did it anyway.
A test that failed for the right reason. I asserted the far square would peek out around the near one. It doesn't: the near quad spans x=75..245 and the far one x=123..197, so full occlusion is correct physics and my sampling point was badly chosen. I checked the rendered image before touching any code, which is the only reason I didn't "fix" working code to match a broken expectation.
What this does not prove
-
One logic file out of 126. I picked
evolution.cbecause it looked dependency-free. Best case, not a representative sample. - The GTE is approximate. 18 of 29 operations unimplemented, and the trig is polynomial rather than table-driven.
-
No model loading.
GsSortObject4has 42 call sites and walks TMD models. The ordering table is ready for it; the format reader isn't written. - Nothing validated against the retail binary. That needs the MIPS toolchain and a disc image.
- The assets are still Bandai's. Models, textures, music, text. A possible distribution model is an engine that requires players to supply their own game data; the legal details would need separate review.
Where my work and jype0's roadmap touch: his stated blocker for a PC port is that psyq and psycross don't implement libgs or libsnd. libgs is one of the four headers I stubbed. I'm not claiming that solves his problem — only that it's the same wall, and it's climbable from at least one side.
What we finally understand
Twenty-five years of wiki edits, spreadsheets and arguments, and the answer is a few hundred lines of C:
- 3 points of 4 to be eligible at all
- Care mistakes and battle counts are invertible — some evolutions want neglect
- Weight is a ±5 window — overfeeding fails like starving
- Champions check your highest stat only; everything else checks all six
- Ties favour the last tied stat in array order, with brain last
- Previously raised targets can lose a head-to-head comparison against a new one
Your Numemon had an explanation in the game’s rules. The exact reason in any given run depends on the candidate scores and selection path.
That is what makes the decompilation exciting: the rules are readable and testable. It also gives modders a foundation to build on.
My four experiments live on a branch in my fork. Each runs with one command and needs only gcc, plus libsdl2-dev for the two that draw — no PlayStation toolchain, no disc image.
If you've worked on a decomp: how do you decide when an approximation is good enough to ship in a port, given it can never be good enough to verify a match? The bit-exact line is the part I'm least sure I drew correctly.









