<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged reverseengineering</title>
<link>https://coderlegion.com/tag/reverseengineering</link>
<description>Powered by Question2Answer</description>
<item>
<title>The missing break behind Digimon World’s evolution rules</title>
<link>https://coderlegion.com/31448/the-missing-break-behind-digimon-worlds-evolution-rules</link>
<description>&lt;hr&gt;
&lt;p&gt;title: The missing break behind Digimon World’s evolution rules&lt;br&gt;
published: true&lt;br&gt;
description: Digimon World’s decompiled evolution code reveals a four-point scoring system, a surprising tie-break rule, and a path toward a PC port.&lt;/p&gt;
&lt;h3 id=&quot;tags-reverseengineering-c-gamedev-cpp&quot;&gt;tags: reverseengineering, c, gamedev, cpp&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ldw0x3np6t314act44lq.gif&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;No menu. No hint. No stat screen saying &lt;em&gt;here is what went wrong&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/jype0/dw_decomp&quot;&gt;Digimon World decompilation project&lt;/a&gt; has reconstructed the evolution code. One rule comes from a loop with no &lt;code&gt;break&lt;/code&gt;: when several stats share the highest value, the game picks the last one in its array.&lt;/p&gt;
&lt;p&gt;Here is how the scoring works, what that tie means, and what I learned while testing a small PC port prototype.&lt;/p&gt;
&lt;h3 id=&quot;the-missing-break&quot;&gt;The missing break&lt;/h3&gt;
&lt;p&gt;Here is the real code that picks which of your stats the game examines. It's in &lt;code&gt;src/main/evolution.c&lt;/code&gt;, and it runs over the six stats &lt;code&gt;hp, mp, offense, defense, speed, brain&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (i = 0; i &amp;lt; 6; i++) {
  int8_t isHighestStat = 1;
  for (j = 0; j &amp;lt; 6; j++) {
    if (statsArray[i] &amp;lt; statsArray[j])
      isHighestStat = 0;
  }
  if (isHighestStat == 1)
    highestStat = i;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For six stats, the nested loop is cheap. But look at what is missing: there is no &lt;code&gt;break&lt;/code&gt; after the assignment. The loop keeps going after it finds a maximum, so a later tied stat overwrites an earlier one. &lt;strong&gt;When stats tie, the highest index wins.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I ran the same selection logic with a few inputs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;all six tied      -&amp;gt; brain
offense highest   -&amp;gt; offense
off+def tied high -&amp;gt; defense
brain highest     -&amp;gt; brain
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Reading the array order backwards gives the full tie-break priority: &lt;strong&gt;brain &amp;gt; speed &amp;gt; defense &amp;gt; offense &amp;gt; mp &amp;gt; hp&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;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 &lt;code&gt;break&lt;/code&gt; was intentional is unknown; its effect is observable.&lt;/p&gt;
&lt;p&gt;{% card %}&lt;br&gt;
Worth being careful here: I first suspected &lt;code&gt;highestStat&lt;/code&gt; was read uninitialized, which would be a genuine bug worth reporting upstream. It can't be — a maximum always exists among six values, so the flag is always set at least once. Check before you claim.&lt;br&gt;
{% endcard %}&lt;/p&gt;
&lt;h3 id=&quot;the-scoring-system-four-checks-three-needed&quot;&gt;The scoring system: four checks, three needed&lt;/h3&gt;
&lt;p&gt;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 &lt;strong&gt;a candidate needs at least 3 points to be eligible at all.&lt;/strong&gt; Below 3 it isn't considered.&lt;/p&gt;
&lt;p&gt;Here are the four checks.&lt;/p&gt;
&lt;h4 id=&quot;care-mistakes-and-the-flag-that-inverts-them&quot;&gt;Care mistakes, and the flag that inverts them&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (isMaxCM == 0) {
  if (partner-&amp;gt;careMistakes &amp;gt;= reqs-&amp;gt;care)
    reqPoints += 1;
} else if (partner-&amp;gt;careMistakes &amp;lt;= reqs-&amp;gt;care) {
  reqPoints += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read the comparisons closely. There's a flag bit that &lt;strong&gt;reverses the test&lt;/strong&gt;, so some evolutions want your care mistakes &lt;em&gt;above&lt;/em&gt; a threshold.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h4 id=&quot;weight-is-a-window-not-a-floor&quot;&gt;Weight is a window, not a floor&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (reqs-&amp;gt;weight - 5 &amp;lt;= partner-&amp;gt;weight &amp;amp;&amp;amp;
    partner-&amp;gt;weight &amp;lt;= reqs-&amp;gt;weight + 5)
  reqPoints += 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A band of &lt;strong&gt;±5&lt;/strong&gt; 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.&lt;/p&gt;
&lt;h4 id=&quot;stats-where-the-stage-changes-the-rule&quot;&gt;Stats, where the stage changes the rule&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (DIGIMON_DATA[target].level == 3U) {
  /* Champion: only your single HIGHEST stat is examined */
} else {
  /* everything else: ALL six stats must clear their thresholds */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Most evolutions need every stat above its requirement. Champion-level targets check only your highest one — which is where the missing &lt;code&gt;break&lt;/code&gt; above actually bites. &lt;strong&gt;For Champions, one selected highest stat matters; other targets require all six thresholds.&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-bonus-point-any-one-of-five&quot;&gt;The bonus point: any one of five&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (reqs-&amp;gt;digimon    != -1 &amp;amp;&amp;amp; current == reqs-&amp;gt;digimon) isBonusFulfilled = 1;
if (reqs-&amp;gt;discipline != -1 &amp;amp;&amp;amp; /* ... */)                isBonusFulfilled = 1;
if (reqs-&amp;gt;happiness  != -1 &amp;amp;&amp;amp; /* ... */)                isBonusFulfilled = 1;
if (reqs-&amp;gt;battles    != -1) { /* also invertible */ }
if (reqs-&amp;gt;techs      != -1) { /* mastered moves */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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 &lt;em&gt;fewer&lt;/em&gt; than N battles.&lt;/p&gt;
&lt;h3 id=&quot;the-rule-that-quietly-steers-your-collection&quot;&gt;The rule that quietly steers your collection&lt;/h3&gt;
&lt;p&gt;One more piece, and it's the one that made me sit back:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;if (reqPoints &amp;gt;= 3 &amp;amp;&amp;amp; currentBest != -1) {
  isTargetRaised = hasDigimonRaised(
      EVO_GAINS_DATA[target].targetDigimon);
  isCurrentBestRaised = hasDigimonRaised(
      EVO_GAINS_DATA[currentBest].targetDigimon);

  if (isTargetRaised == 1 &amp;amp;&amp;amp; isCurrentBestRaised == 0)
    reqPoints = 0;      /* you already have this one: disqualify it */

  if (isTargetRaised == 0 &amp;amp;&amp;amp; isCurrentBestRaised == 1)
    reqPoints++;        /* you don't have this one: push it ahead */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;It is a small but deliberate way to steer players toward more of the roster.&lt;/p&gt;
&lt;h3 id=&quot;who-actually-did-this&quot;&gt;Who actually did this&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;I did not decompile the game.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;p&gt;{% embed &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/jype0/dw_decomp&quot;&gt;https://github.com/jype0/dw_decomp&lt;/a&gt; %}&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/jype0/dw_decomp&quot;&gt;jype0/dw_decomp&lt;/a&gt; started in March 2026 and is MIT licensed. It passed a milestone most decompilation projects never reach: &lt;strong&gt;zero &lt;code&gt;INCLUDE_ASM&lt;/code&gt; stubs left in &lt;code&gt;src/&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you haven't worked on a decomp, that needs explaining. &lt;code&gt;INCLUDE_ASM&lt;/code&gt; is the marker for &quot;we haven't figured this function out yet, here's the raw assembly instead.&quot; 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.&lt;/p&gt;
&lt;p&gt;Credit isn't evenly spread, and shouldn't be reported as if it were. As of today the contributor counts are:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Contributor &lt;/th&gt;&lt;th&gt;Contributions&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Pekka Jylhä-Ollila (jype0) &lt;/td&gt;&lt;td&gt;409 — maintainer, and the bulk of the project&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;juandav &lt;/td&gt;&lt;td&gt;37 — matching functions and data across overlays&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ChonkMode &lt;/td&gt;&lt;td&gt;22&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;kingzmanh &lt;/td&gt;&lt;td&gt;11&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;fmil95 &lt;/td&gt;&lt;td&gt;7&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ThirstyWraith &lt;/td&gt;&lt;td&gt;6&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SydMontague &lt;/td&gt;&lt;td&gt;5 — struct and naming reference work&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;paulohpfilho &lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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 &lt;em&gt;psyq and psycross don't currently implement libgs or libsnd&lt;/em&gt;; and then &lt;strong&gt;a DW1 mod of epic proportions&lt;/strong&gt; — new maps, story, Digimon.&lt;/p&gt;
&lt;p&gt;The ambitious mod is the long-term goal. The decompilation makes that work possible.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;running-their-code-somewhere-it-has-never-run&quot;&gt;Running their code somewhere it has never run&lt;/h3&gt;
&lt;p&gt;My question was narrower: can this code run off the PlayStation at all?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;evolution.c&lt;/code&gt; is 1,087 lines. Compiling it on x86-64 produced exactly one error:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;libgte.h: No such file or directory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's Sony's geometry coprocessor header. So I grepped &lt;code&gt;evolution.c&lt;/code&gt; for &lt;code&gt;VECTOR&lt;/code&gt;, &lt;code&gt;MATRIX&lt;/code&gt;, &lt;code&gt;gte_&lt;/code&gt;, &lt;code&gt;RotTrans&lt;/code&gt; — nothing. The evolution logic doesn't touch the GTE at all. It only &lt;em&gt;inherits&lt;/em&gt; the header through &lt;code&gt;entity.h&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I wrote type-only stand-ins for the four PsyQ headers. It compiled clean, then ran:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;EvoRequirements = 28 bytes (PS1 layout: 28)
fresh(1)       -&amp;gt; 2
in-training(3) -&amp;gt; 5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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 &lt;code&gt;static_assert&lt;/code&gt; so nobody can break it quietly later.&lt;/p&gt;
&lt;h3 id=&quot;measuring-the-gte-instead-of-guessing-at-it&quot;&gt;Measuring the GTE instead of guessing at it&lt;/h3&gt;
&lt;p&gt;A major piece of a PS1 port is the GTE, the fixed-point geometry coprocessor.&lt;/p&gt;
&lt;p&gt;My first instinct was to size the problem with a grep. I got 181 and quoted that number publicly. &lt;strong&gt;It was wrong&lt;/strong&gt; — my pattern was matching a header that &lt;em&gt;defines&lt;/em&gt; every GTE macro whether the game calls it or not. Measured against &lt;code&gt;src/&lt;/code&gt; alone:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;632 call sites, 29 distinct operations, 34 of 126 files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The call sites are concentrated:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Operation &lt;/th&gt;&lt;th&gt;Sites &lt;/th&gt;&lt;th&gt;Cumulative&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ApplyMatrixSV&lt;/code&gt; &lt;/td&gt;&lt;td&gt;103 &lt;/td&gt;&lt;td&gt;16%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;RotMatrix&lt;/code&gt; &lt;/td&gt;&lt;td&gt;51 &lt;/td&gt;&lt;td&gt;24%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;RotMatrixZYX&lt;/code&gt; &lt;/td&gt;&lt;td&gt;44 &lt;/td&gt;&lt;td&gt;31%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;RotMatrixYXZ&lt;/code&gt; &lt;/td&gt;&lt;td&gt;44 &lt;/td&gt;&lt;td&gt;38%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ratan2&lt;/code&gt; &lt;/td&gt;&lt;td&gt;43 &lt;/td&gt;&lt;td&gt;45%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;gte_rtps&lt;/code&gt; &lt;/td&gt;&lt;td&gt;42 &lt;/td&gt;&lt;td&gt;58%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ScaleMatrix&lt;/code&gt; &lt;/td&gt;&lt;td&gt;39 &lt;/td&gt;&lt;td&gt;71%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;TransMatrix&lt;/code&gt; &lt;/td&gt;&lt;td&gt;37 &lt;/td&gt;&lt;td&gt;77%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ApplyMatrixLV&lt;/code&gt; &lt;/td&gt;&lt;td&gt;23 &lt;/td&gt;&lt;td&gt;&lt;strong&gt;81%&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The nine operations shown account for about 81% of these call sites.&lt;/strong&gt; That gives a useful starting point, though call-site count does not measure implementation difficulty.&lt;/p&gt;
&lt;p&gt;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 &lt;strong&gt;saturate rather than wrap&lt;/strong&gt;. 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.&lt;/p&gt;
&lt;p&gt;Then I measured the number that actually decides anything. &quot;0.17% trigonometric error&quot; 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:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;mean error:             0.825 px
worst error:            1.896 px
vertices off by &amp;gt; 2px:  0.00%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;And the honest half: &lt;strong&gt;it is not bit-exact.&lt;/strong&gt; The real GTE uses lookup tables; my polynomial approximation looks identical and is numerically different. That's fine for a &lt;em&gt;port&lt;/em&gt; and useless for &lt;em&gt;verifying a decompilation&lt;/em&gt;, 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.&lt;/p&gt;
&lt;p&gt;{% details Drawing a frame: primitives and the ordering table %}&lt;/p&gt;
&lt;p&gt;The game's rendering is all PsyQ primitives, counted across the source:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;POLY_FT4  386    (textured quad)
POLY_F4    30
POLY_GT4   17
POLY_FT3    6    (textured triangle)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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 &lt;code&gt;setXYWH&lt;/code&gt; and &lt;code&gt;setUVWH&lt;/code&gt; macros lifted from &lt;code&gt;src/main/utils.c&lt;/code&gt;, and rasterised them in software.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Then the ordering table, which is how the PS1 sorted by depth. The name misleads, so it's worth saying plainly: &lt;strong&gt;an ordering table is not a sorting algorithm.&lt;/strong&gt; It's a bucket array — one slot per depth value, each holding a linked list, with &lt;code&gt;AddPrim&lt;/code&gt; 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 &lt;code&gt;qsort&lt;/code&gt; would be slower &lt;em&gt;and&lt;/em&gt; wrong, because the order of equal-depth primitives depends on the original insertion and traversal rules.&lt;/p&gt;
&lt;p&gt;I built the occlusion test so it couldn't pass by luck: submit the &lt;strong&gt;near&lt;/strong&gt; quad first, then the far one.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;normal:          centre pixel r=16  g=104    (near/green occludes)
depths swapped:  centre pixel r=120 g=16     (far/red now occludes)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same submission order, opposite result. The table is doing the work, not the sequence of my function calls.&lt;/p&gt;
&lt;p&gt;{% enddetails %}&lt;/p&gt;
&lt;h3 id=&quot;where-i-got-things-wrong&quot;&gt;Where I got things wrong&lt;/h3&gt;
&lt;p&gt;Two corrections, because they're more useful than a clean story.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The GTE count.&lt;/strong&gt; 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.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A test that failed for the right reason.&lt;/strong&gt; 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 &quot;fix&quot; working code to match a broken expectation.&lt;/p&gt;
&lt;h3 id=&quot;what-this-does-not-prove&quot;&gt;What this does not prove&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One logic file out of 126.&lt;/strong&gt; I picked &lt;code&gt;evolution.c&lt;/code&gt; &lt;em&gt;because&lt;/em&gt; it looked dependency-free. Best case, not a representative sample.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The GTE is approximate.&lt;/strong&gt; 18 of 29 operations unimplemented, and the trig is polynomial rather than table-driven.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No model loading.&lt;/strong&gt; &lt;code&gt;GsSortObject4&lt;/code&gt; has 42 call sites and walks TMD models. The ordering table is ready for it; the format reader isn't written.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nothing validated against the retail binary.&lt;/strong&gt; That needs the MIPS toolchain and a disc image.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The assets are still Bandai's.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Where my work and jype0's roadmap touch: his stated blocker for a PC port is that psyq and psycross don't implement &lt;code&gt;libgs&lt;/code&gt; or &lt;code&gt;libsnd&lt;/code&gt;. &lt;code&gt;libgs&lt;/code&gt; 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.&lt;/p&gt;
&lt;h3 id=&quot;what-we-finally-understand&quot;&gt;What we finally understand&lt;/h3&gt;
&lt;p&gt;Twenty-five years of wiki edits, spreadsheets and arguments, and the answer is a few hundred lines of C:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;3 points of 4&lt;/strong&gt; to be eligible at all&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Care mistakes and battle counts are invertible&lt;/strong&gt; — some evolutions want neglect&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weight is a ±5 window&lt;/strong&gt; — overfeeding fails like starving&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Champions check your highest stat only&lt;/strong&gt;; everything else checks all six&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ties favour the last tied stat&lt;/strong&gt; in array order, with brain last&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Previously raised targets can lose a head-to-head comparison&lt;/strong&gt; against a new one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;That is what makes the decompilation exciting: the rules are readable and testable. It also gives modders a foundation to build on.&lt;/p&gt;
&lt;p&gt;My four experiments live on a &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/krlz-dev/dw_decomp/tree/port-spike/port&quot;&gt;branch in my fork&lt;/a&gt;. Each runs with one command and needs only &lt;code&gt;gcc&lt;/code&gt;, plus &lt;code&gt;libsdl2-dev&lt;/code&gt; for the two that draw — no PlayStation toolchain, no disc image.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31448/the-missing-break-behind-digimon-worlds-evolution-rules</guid>
<pubDate>Fri, 16 Oct 2026 19:39:38 +0000</pubDate>
</item>
</channel>
</rss>