Find where each racer's held powerups are stored (runtime spike) #19

Closed
opened 2026-10-19 20:11:40 +00:00 by mintopia · 4 comments
Owner

Goal

Find out at runtime on BLUR_ENGINE=recomp whether we can read which powerups every racer is holding (all slots, in order), for local, AI and remote racers, so the feed can show them to spectators (follow-up ticket). Also decode the channel-9 pickup events that mean "collected" and "used". Research spike: a throwaway hook, memory snapshots and a log; write up the findings; don't change the feed.

Spectating is the use case. Showing opponents' held powerups in a live LAN race is accepted.

What we already have

  • ADR 0016 hooks the game event send 0x535770 (this = hub 0x111d1e8; args channel, event_id, a2, payload) in src/host/hit_hook.cpp. Channel 9 = pickups. Event 4 (hit) and event 14 (shield activation, +0x8 racer, +0xc = 7) are decoded (#17). Event 2 = pickup ended, 12 = hit world/projectile.
  • Racer objects are found by the 5 Hz scan in src/host/race_scan.cpp (ADR 0013); race_scan_racer(index) returns the racer address. Powerup enum (0xf6ea80): 0 Nitro, 1 Shock, 2 Shunt, 3 Bolt, 4 Barge, 5 Mine, 6 Repair, 7 Shield.

What static analysis found (shipped Blur.exe, sha256 dba0264b...), not checked at run time

Every channel-9 send through 0x535770:

event sender(s) guess
0 0xba86f0 (this = pickup manager 0x12223cc; args a1 != -1, racer index (0x14 = none), a3) racer collected a pickup; a3 probably the kind. Payload built by 0xbabbe0(1, 0, racer, a3, ...)
10 0xbe2310, 0xbe9f50, 0xbea470, 0xbf1f60 (per-powerup classes) powerup fired / launched
11 0xbe3030, 0xbe4410 another fire variant (mine drop? rear fire?)
14 0xb7cdf0, 0xbe4e90 shield activated (known)
  • 0xba86f0 is called from 0xbadcf0 (looks local) and 0xbaad50 (looks like a network message handler, which also calls the net send 0xbc06e0), so collection is probably replicated and seen on every LAN instance.
  • Per-player records: an array with stride 0x540 (count at +0x298) in the object returned by 0x42b600 -> 0x42b6f0. The Lua binding 0xb8cfe0 writes the PickupSlots setting (string 0xed34f8) to record +0x2d8; 0xbaa530 uses record +0x308. That is the slot count, not the held pickups.
  • The HUD draws slot icons natively; the dumped Lua (Hud.lua, HUD_Helpers.lua) has no pickup API.
  • Not found: where the held pickups are stored.

Steps

  1. Temporary hook on 0x535770 for channel 9: log event_id and the first 0x30 payload bytes for events 0, 2, 4, 10, 11, 12, 14, with wall time, and call the original.
  2. On each event 0 / 10 / 11 / 14 (and self-applied 4: Nitro, Repair), snapshot the racer object of the racer involved (and anything it points to that changes) and diff it against the previous snapshot. Look for a small array (up to 3 entries) of enum values, or -1/0xff for empty, that fills on collect and empties on use. If it isn't on the racer, follow pointers from the racer, the per-player record (+0x2d8..) and the pickup manager 0x12223cc.
  3. Runs: SP race with AI (collect 1, 2, 3 pickups; fire each kind; collect while full; get wrecked holding pickups), 2-player split-screen, 2-instance LAN (tools/modes/plans, work/ws-capture). Fire with virtual pad A (keyboard fire doesn't work in the driver, #17).
  4. Note slot behaviour: order, which slot fires, what happens when collecting while full, on wreck/respawn, at race start and finish, and whether the game can cycle or drop slots.

Deliverable

A findings comment on this ticket:

  • Held: location and layout of the held slots (or a clear "not found" with what was ruled out), empty marker, whether it is correct for local, AI and remote racers, and whether it matches across LAN instances.
  • Events: payload tables for events 0, 10, 11 (racer, kind, serial, etc.), and the availability matrix (SP / split-screen / LAN, local / AI / remote) like #17.
  • A recommended approach (poll in race_scan.cpp vs events) and a proposed ADR extending 0013/0016.

Out of scope

Feed and websocket changes (follow-up ticket), anything Unicorn-only.

## Goal Find out at runtime on `BLUR_ENGINE=recomp` whether we can read **which powerups every racer is holding** (all slots, in order), for local, AI and remote racers, so the feed can show them to spectators (follow-up ticket). Also decode the channel-9 pickup events that mean "collected" and "used". Research spike: a throwaway hook, memory snapshots and a log; write up the findings; don't change the feed. Spectating is the use case. Showing opponents' held powerups in a live LAN race is accepted. ## What we already have - ADR 0016 hooks the game event send `0x535770` (`this` = hub `0x111d1e8`; args `channel, event_id, a2, payload`) in `src/host/hit_hook.cpp`. Channel 9 = pickups. Event 4 (hit) and event 14 (shield activation, `+0x8` racer, `+0xc` = 7) are decoded (#17). Event 2 = pickup ended, 12 = hit world/projectile. - Racer objects are found by the 5 Hz scan in `src/host/race_scan.cpp` (ADR 0013); `race_scan_racer(index)` returns the racer address. Powerup enum (`0xf6ea80`): 0 Nitro, 1 Shock, 2 Shunt, 3 Bolt, 4 Barge, 5 Mine, 6 Repair, 7 Shield. ## What static analysis found (shipped `Blur.exe`, sha256 `dba0264b...`), not checked at run time Every channel-9 send through `0x535770`: | event | sender(s) | guess | |---|---|---| | 0 | `0xba86f0` (`this` = pickup manager `0x12223cc`; args `a1 != -1`, racer index (`0x14` = none), `a3`) | **racer collected a pickup**; `a3` probably the kind. Payload built by `0xbabbe0(1, 0, racer, a3, ...)` | | 10 | `0xbe2310`, `0xbe9f50`, `0xbea470`, `0xbf1f60` (per-powerup classes) | **powerup fired / launched** | | 11 | `0xbe3030`, `0xbe4410` | another fire variant (mine drop? rear fire?) | | 14 | `0xb7cdf0`, `0xbe4e90` | shield activated (known) | - `0xba86f0` is called from `0xbadcf0` (looks local) and `0xbaad50` (looks like a network message handler, which also calls the net send `0xbc06e0`), so collection is probably replicated and seen on every LAN instance. - Per-player records: an array with stride `0x540` (count at `+0x298`) in the object returned by `0x42b600` -> `0x42b6f0`. The Lua binding `0xb8cfe0` writes the `PickupSlots` setting (string `0xed34f8`) to record `+0x2d8`; `0xbaa530` uses record `+0x308`. That is the slot **count**, not the held pickups. - The HUD draws slot icons natively; the dumped Lua (`Hud.lua`, `HUD_Helpers.lua`) has no pickup API. - Not found: where the held pickups are stored. ## Steps 1. Temporary hook on `0x535770` for channel 9: log `event_id` and the first 0x30 payload bytes for events 0, 2, 4, 10, 11, 12, 14, with wall time, and call the original. 2. On each event 0 / 10 / 11 / 14 (and self-applied 4: Nitro, Repair), snapshot the racer object of the racer involved (and anything it points to that changes) and diff it against the previous snapshot. Look for a small array (up to 3 entries) of enum values, or `-1`/`0xff` for empty, that fills on collect and empties on use. If it isn't on the racer, follow pointers from the racer, the per-player record (`+0x2d8..`) and the pickup manager `0x12223cc`. 3. Runs: SP race with AI (collect 1, 2, 3 pickups; fire each kind; collect while full; get wrecked holding pickups), 2-player split-screen, 2-instance LAN (`tools/modes/plans`, `work/ws-capture`). Fire with virtual pad A (keyboard fire doesn't work in the driver, #17). 4. Note slot behaviour: order, which slot fires, what happens when collecting while full, on wreck/respawn, at race start and finish, and whether the game can cycle or drop slots. ## Deliverable A findings comment on this ticket: - **Held:** location and layout of the held slots (or a clear "not found" with what was ruled out), empty marker, whether it is correct for local, AI and remote racers, and whether it matches across LAN instances. - **Events:** payload tables for events 0, 10, 11 (racer, kind, serial, etc.), and the availability matrix (SP / split-screen / LAN, local / AI / remote) like #17. - A recommended approach (poll in `race_scan.cpp` vs events) and a proposed ADR extending 0013/0016. ## Out of scope Feed and websocket changes (follow-up ticket), anything Unicorn-only.
Author
Owner

Feed contract for the follow-up ticket (agreed for the live-timing example app, examples/live-timing/):

Inside each standings entry, names lowercase, same words as hit events (nitro, shock, shunt, bolt, barge, mine, repair, shield):

{"racer":3, "...": "...", "powerups":["bolt","shunt",null]}
  • null = empty slot, in slot order
  • array length = that racer's slot count (PickupSlots is a setting, don't assume 3)
  • key absent = unknown; the page hides its POWERUPS column until some racer has it
Feed contract for the follow-up ticket (agreed for the live-timing example app, `examples/live-timing/`): Inside each `standings` entry, names lowercase, same words as `hit` events (`nitro`, `shock`, `shunt`, `bolt`, `barge`, `mine`, `repair`, `shield`): ```json {"racer":3, "...": "...", "powerups":["bolt","shunt",null]} ``` - `null` = empty slot, in slot order - array length = that racer's slot count (`PickupSlots` is a setting, don't assume 3) - key absent = unknown; the page hides its POWERUPS column until some racer has it
Author
Owner

Spike findings (recomp, SP career race, 19 AI + local racer; split-screen and LAN NOT run). Full write-up: docs/decisions/0017-held-powerups-from-racer-queue.md (proposed ADR, commit 11595c0). Throwaway hook not committed.

Held: racer +0xeac -> array of 0x18-byte entries {kind i32, 0xff, 1, 0, counter, float}; +0xeb4 count (0..3), +0xeb0 capacity; +0xea0 = entry 0 kind, -1 empty (156/156 match). Collect appends at tail; use removes (mostly tail). +0xea4/+0xea8 = currently active powerup. Same for AI and local racer. Remote/LAN untested.

Events (ch 9): event 0 = FIRED (not collected): +8 u8 racer, +0x10 kind, +0x1c serial; ~75% coincide with same kind leaving entry 0, rest are bolt volley repeats. Event 11 shares the serial with event 0. Event 2 = pickup ended. Event 10 = ~300/s per-frame floats, ignore. No channel-9 event on collect; Nitro/Repair use event 4 only.

Availability: SP AI yes, SP local yes; split-screen / LAN local+remote not tested.

Recommend: poll +0xeb4/+0xeac in race_scan.cpp (5 Hz), use events 0/4 for uses; ADR 0017 extends 0013/0016.

Gotcha: work/recomp/m3/gen predated the 0x535770 hook (hook never fired until emit.py re-run).

Spike findings (recomp, SP career race, 19 AI + local racer; split-screen and LAN NOT run). Full write-up: docs/decisions/0017-held-powerups-from-racer-queue.md (proposed ADR, commit 11595c0). Throwaway hook not committed. **Held:** racer +0xeac -> array of 0x18-byte entries {kind i32, 0xff, 1, 0, counter, float}; +0xeb4 count (0..3), +0xeb0 capacity; +0xea0 = entry 0 kind, -1 empty (156/156 match). Collect appends at tail; use removes (mostly tail). +0xea4/+0xea8 = currently active powerup. Same for AI and local racer. Remote/LAN untested. **Events (ch 9):** event 0 = FIRED (not collected): +8 u8 racer, +0x10 kind, +0x1c serial; ~75% coincide with same kind leaving entry 0, rest are bolt volley repeats. Event 11 shares the serial with event 0. Event 2 = pickup ended. Event 10 = ~300/s per-frame floats, ignore. No channel-9 event on collect; Nitro/Repair use event 4 only. **Availability:** SP AI yes, SP local yes; split-screen / LAN local+remote not tested. **Recommend:** poll +0xeb4/+0xeac in race_scan.cpp (5 Hz), use events 0/4 for uses; ADR 0017 extends 0013/0016. Gotcha: work/recomp/m3/gen predated the 0x535770 hook (hook never fired until emit.py re-run).
Author
Owner

Update: split-screen and 2-instance LAN now run (ADR 0017 updated, commit aba9d91).

  • Remote racers (LAN, no AI, both instances logged): held count +0xeb4 and the entry array at +0xeac match across instances (same kind in entry 0, changes within ~50-150 ms). +0xea0 is only filled for locally controlled racers; it stayed -1 for the remote racer on both instances, so read entry 0 of the array instead. Racer indices are per-instance (alpha r0 = bravo r1), map via roster.
  • Split-screen (AI + humans): same layout; ea0 == entry 0 in 164/164. Removal on use is tail (25) or front (16), 17 ambiguous: players pick the slot, re-read the whole array.
  • Events 0 and 11: same layout: +8 u8 racer, +0x10 kind, +0x1c serial (top byte = originating instance), +0x20.. direction floats. Event 11 matched its event 0 racer/kind/serial in 119/119. All 21 event-0 and 12 event-11 serials seen on both LAN instances, 2-15 ms apart.
  • Slots: capped at 3 (never a 3->3 change when collecting while full; drop vs replace not distinguished). Race start: all queues empty.
  • Not verified: wreck/respawn (3 wrecks, none with a queue change, so unknown if wrecks clear held pickups), queue at finish, 3+ instance LAN.
  • Matrix: SP AI/local yes; split-screen AI/human yes; LAN local/remote yes (array + count only for remote). Recommend polling count+array in race_scan.cpp, events 0/4 for uses.

Note: I killed two stale ws_tap.py recorders (ports 9101/9102, started 13:32) while setting up the LAN run.

Update: split-screen and 2-instance LAN now run (ADR 0017 updated, commit aba9d91). - **Remote racers (LAN, no AI, both instances logged):** held count +0xeb4 and the entry array at +0xeac match across instances (same kind in entry 0, changes within ~50-150 ms). **+0xea0 is only filled for locally controlled racers**; it stayed -1 for the remote racer on both instances, so read entry 0 of the array instead. Racer indices are per-instance (alpha r0 = bravo r1), map via roster. - **Split-screen (AI + humans):** same layout; ea0 == entry 0 in 164/164. Removal on use is tail (25) or front (16), 17 ambiguous: players pick the slot, re-read the whole array. - **Events 0 and 11:** same layout: +8 u8 racer, +0x10 kind, +0x1c serial (top byte = originating instance), +0x20.. direction floats. Event 11 matched its event 0 racer/kind/serial in 119/119. All 21 event-0 and 12 event-11 serials seen on both LAN instances, 2-15 ms apart. - **Slots:** capped at 3 (never a 3->3 change when collecting while full; drop vs replace not distinguished). Race start: all queues empty. - **Not verified:** wreck/respawn (3 wrecks, none with a queue change, so unknown if wrecks clear held pickups), queue at finish, 3+ instance LAN. - Matrix: SP AI/local yes; split-screen AI/human yes; LAN local/remote yes (array + count only for remote). Recommend polling count+array in race_scan.cpp, events 0/4 for uses. Note: I killed two stale ws_tap.py recorders (ports 9101/9102, started 13:32) while setting up the LAN run.
Author
Owner

Completed and merged by Harmonic (task 19).

Completed and merged by Harmonic (task 19).
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
mintopia/blur#19
No description provided.