Skip to content

From research to code

This page follows a finding from the original to Coney's code: what an engineer puts on a research page so that the next engineer finds the original code and trusts the claim, what the implementation does with it, and three real features traced from the page to the merged result. The rules behind it are in Research workflow and Testing; this page shows them working.

What goes on the page

The engineer writes a research page in docs/research/. Every claim on it carries an evidence level and a cited address. The page is a map, not a substitute for the original: the engineer who ports a behaviour reads the original function in Ghidra, and the page says which function that is and what the code alone does not show. A page that serves that contains:

  • Where each behaviour lives. Every behaviour the page describes names the original function that implements it: its address and its name, and the function where the logic sits when several take part. Then, in prose, what the function does that is worth knowing before reading it: its inputs and outputs, and when it is called; its branches and conditions, in order; its edge cases; its constants, with units, such as "radius 0.75 m with a slot, 2.5 m without".
  • Data layouts. Each structure's fields as offset, size, meaning and who reads or writes them, such as "human +0xd0 is the character type".
  • Short snippets only. A line or two next to an address, never a whole function listing from Ghidra or a disassembly (Writing up a finding).
  • Runtime numbers from the real game. The engineer hooks addresses in PCSX2 and logs values per update (Recording a trace). The fence work's "the cut comes 15-44 updates after the hit" is one example. These become the test values.
  • A fix list. Numbered, ordered units, each with "done" spelled out, so an engineer can take one at a time.

What the implementation does with it

Before writing anything, the engineer re-reads the addresses the page cites in Ghidra and checks them against the page; a disagreement goes on the page first, with the address and evidence level (the runtime comparison). The engineer then writes Coney's code and tags each function @orig 0x<addr> with a link to the page (Conventions). Tests are written first, from the page's numbers; that is how "correctly" gets checked (Testing). For whole behaviours we also run both games through the same scenario and diff their traces (Comparing with Coney, and Diff against the original); the trace tooling records the same columns from each game and coney-tools trace diff reports the first update where they part. For work a player sees this comparison with the original running is part of "done" (Testing), and the pull request reports it in its Runtime: line.

If a page doesn't say enough, the engineer goes back to the original and puts the answer on the page, or writes the question down when it can't be answered yet (When a research page isn't enough). Where the code must go on without the answer, it uses a marked stand-in.

Worked examples

Each example follows one feature from the page, through the code and its test, to the pull request that merged it. All the code and tests named are in the repository; open them beside this page.

The revive of a downed Warrior

The page. Reviving a downed Warrior describes the Player_TryRevive function (0x00286ba8) as an ordered list of refusals, then the way test, then the flash. It gives:

  • A constant with its unit: the way test sweeps a capsule of radius 0.2 m, as tall as the reviver's own.
  • Branches in order: a downed AI human is refused when the way is not clear, and a downed player accepts only a Warrior as the reviver.
  • A rule about who pays: at the end of clip 351 the downed human's flash is spent if he has one, else the reviver's.
  • Runtime numbers, in At runtime: clip 351 lasts 21 updates (0.70 s), 353 lasts 33, 389 lasts 25 and 354 lasts 72; the reviver ends about 0.70 m from the downed human and 11-13 degrees to the left of the line to him.

The code. The rules are a small function with no game state, so they can be tested on their own: mayRevive is tagged @orig 0x00286ba8 Player_TryRevive and its header points at the page. The clips, the flash and the heal are wired in the game mode, whose declarations in gameplay_mode.h carry the tags for Revive_Start (0x002752a0) and Revive_Finish (0x00274ee0). The 0.2 m is kWayClearRadius in way_clear.h.

The tests. Written first, in layers. The branches of the page become revive_test.cpp: a case for the refusals, one for the flash, one for "the way is not clear, so only a Warrior reviving a player goes on" and one for whose flash is spent. The runtime numbers become the bounds of a disc playthrough, disc_revive_test.cpp, which presses d-pad left beside a downed Warrior in the real mission and checks that 351 lasts 20-22 updates, 353 lasts 32-34, 354 lasts 71-73, the stand-off is 0.6-0.9 m, the steer ends 8-14 degrees off the line, the flash count goes from 1 to 0 and the health is full. Each bound allows a margin of an update (or a few degrees) round the measured value, which is the centre.

The result. Pull request #169 added the revive. It also did what the process asks when the page is silent: the crew Warriors' revivable flag was not set by anything in Coney, so the pull request said so and put "what sets that flag in the original" on the page as a new open question, instead of guessing in code.

The crew's steal command

The page. WarriorSteal lists each function of the tactic by address with what it does. Among them: WarriorStealTactic_PickStore (0x00321028) keeps its store while the chief is within 12 m, otherwise takes the nearest store flag of activity 0xe within 12 m; WarriorSteal_ClaimObject (0x00321540) claims an object for the Warrior's gang for 5 s unless another member stands within 1.5 m of it. The page also lists how the store's objects are grouped (1.0 m clusters) and in what order they are walked, step by step.

The code. steal_goals.h names the page's numbers as constants with their units (kStealStoreReach is 12.0 m, kStealClusterRadius 1.0 m) and tags each function with its address, for example @orig 0x00321028 WarriorStealTactic_PickStore on the store pick. The same file shows the gap rule: the page gives the original's reachability test only by name, so Coney checks the distance alone, marks that as a Coney stand-in in the comment, and the page lists each stand-in as an open question beside the original behaviour.

The tests. steal_goals_test.cpp builds a store with two groups of two objects half a metre apart and eight metres from each other, and checks the page: the store front within 12 m is taken, two clusters of two are found, every crew member but the chief gets the steal goal. A second case puts the store front beyond 12 m and checks that nothing is handed out. A third lets two Warriors fetch four objects and checks that both take part and that each stands within reach of what he takes. A fourth checks that a claimed object is not handed out again until its 5 s run out.

The result. Pull request #168 made mission 2 (level80) get past its store checkpoint: the ten thefts the mission script waits for now happen. It also left a short list of "Remaining" questions (message 7's receivers, clustering details, the pick-up animation), each of them an open question rather than a hole filled with a guess.

From a runtime trace to a fix list: the fence and the dealer

This one shows the research half in detail, because its numbers were measured rather than read.

The page. In Player 2, Vermin's runs and the fence, item 44, the engineer hooked the fence's hit, its removal and the scripted cut to the dealer's camera in PCSX2 and ran the scenario twice, putting the update number of each moment in a table. The result is a range, not a single figure: the removal comes 0-29 updates after the breaking hit (the fence's 60-tick phase), the cut comes a fixed 14-15 updates after the removal, so the hit-to-cut gap is 15-44 updates, and nothing (no fade, no letterbox, no camera change) happens in between. The scenario that produced it is checked in as fence_dealer.toml, so anybody can run it again.

The fix list. The page ends with numbered fixes (Coney fixes (item 44)) in the order to take them, the last of them a test with its pass condition spelled out: with the hit at update H, the fence is removed at an update between H and H + 30, and the dealer camera is current 14 or 15 updates after that, so the cut lands within H + 14 ... H + 45. An engineer takes that item as it stands: the test is the bounds, written first, and the fix is whatever makes it pass.

Why this is the model. A report that said "the cut is about a second after the hit" could not be tested; a range with a cause (the barrier's phase) and the exact moment it is measured from can. And when the first reading of the timing was corrected by the second run (3-17 became 0-29), the page changed in place, so the test value an engineer reads is always the current one.

Writing the page as a map

A checklist before calling a page ready:

  1. Does every behaviour name the original function (address and name) where its logic lives? An engineer should reach the code from the page in one step. If a branch, a constant or a unit that the code does not make obvious is missing, add it.
  2. Is every number's unit stated, and is it clear whether a time is in updates, ticks or seconds?
  3. For each structure field the code reads, is there an offset, a size and a meaning?
  4. Is there a runtime number for every behaviour that has a duration, a distance or an order? Those are the tests.
  5. Are the unknowns listed as open questions, so the code's stand-ins have a page to point at?
  6. Are the fixes numbered, in order, each with how to tell it is done?
  7. Does coney-tools docs check pass on the page? A page you touched is brought wholly up to the standard (Touch a page, fix the page).

The page kinds and the skeleton are in Writing these docs.