Game award stats and awards in the feed #38

Closed
opened 2026-10-20 18:41:28 +00:00 by mintopia · 6 comments
Owner

The game builds awards from MultiplayerAwardStat (~42 stats: PlayersHitWithBolt, TimeInFirstPlace, LongestDrift, PowerupsEvaded, ...) and award types (Hunter, Marksman, WoodenSpoon, ...); Lua GetRaceAwards, Multiplayer.GetRaceResults(); possibly synced via MpDataGathering.

Spike

Find the per-racer stat values and awarded titles at race end; which are available for remote racers on each instance.

Acceptance

  • Per racer award_stats (game values, game stat names in snake_case; absent where the game has none), live in standings and final in race_result.
  • race_result.awards: [{award, racer, stat, value}], all awards the game gives.
  • Only where the game computes them (multiplayer); totals stays separate.

Rules

  • Wire format: ADR 0019 (docs/decisions/0019-feed-message-contract.md). Repo data/tools: ADR 0020.
  • Fix and verify on BLUR_ENGINE=recomp only.
  • Update docs/state-websocket.md and examples/live-timing for any feed change.
  • Test races: use the scripts/ scenarios (windowed, low settings) once #33 exists.
The game builds awards from `MultiplayerAwardStat` (~42 stats: PlayersHitWithBolt, TimeInFirstPlace, LongestDrift, PowerupsEvaded, ...) and award types (Hunter, Marksman, WoodenSpoon, ...); Lua `GetRaceAwards`, `Multiplayer.GetRaceResults()`; possibly synced via `MpDataGathering`. ## Spike Find the per-racer stat values and awarded titles at race end; which are available for remote racers on each instance. ## Acceptance - Per racer `award_stats` (game values, game stat names in snake_case; absent where the game has none), live in `standings` and final in `race_result`. - `race_result.awards`: `[{award, racer, stat, value}]`, all awards the game gives. - Only where the game computes them (multiplayer); `totals` stays separate. ## Rules - Wire format: ADR 0019 (`docs/decisions/0019-feed-message-contract.md`). Repo data/tools: ADR 0020. - Fix and verify on `BLUR_ENGINE=recomp` only. - Update `docs/state-websocket.md` and `examples/live-timing` for any feed change. - Test races: use the `scripts/` scenarios (windowed, low settings) once #33 exists.
Author
Owner

Research findings (static: Blur.exe strings + current src), 2026-10-20.

System (data-driven): MultiplayerAward, AwardType, AwardStat, AwardThreshold, MultiplayerAwardStatSortingStrategy (HighestWins/LowestWins), MultiplayerAwardDistributionSpec.
Lua: GetRaceAwards, GetRaceAwardsInLobby, GetRaceHistoryAwardIconNames. Multiplayer.GetRaceResults() is already used in Hud.post.lua. Likely cheapest route: call these from a results-screen .post.lua and publish via Host.state.

Stats (MultiplayerAwardStat, strings near exe file offset 0xad9754): PlayersHitWithBolt/Shunt/Mine/Barge/IronFist, PowerupsShielded, BlockedPowerups, NitroOvertakes, LongestJump, TimeInFirstPlace, LongestTimeNotDamaged, PowerupsDropped, TimesProtectedByStableFrame, CarsBatteringRammed, PlayersHitwithDecoys, PlayersScrambled, PowerupsAbsorbedWithSafetyNet, TimeInvisibleWithSilentRunning, MinesFragmentedWithMastermine, TimesHit, TotalCarsWrecked, LongestTimeWithoutBeingWrecked, DamageDealt, TimesOffEdge, TimesInWater, LongestDrift, PickupsCollected, EnvironmentalCrashes, Car2CarVictimHits, Car2CarAggressiveHits, TopSpeed, PowerupsEvaded, LongestChain, Accuracy, DeathStreak, KillStreak, HitsByPowerups, PowerupHits, EliminationPoints, OnTrackPoints, LapsCompleted, DistanceTravelled, HealthRemaining, SlowestLap, FastestLap, RaceTime.

Awards: Hunter, CrimsonCarapace, LitterBug, Defender, SafeHands, Booster, CloseEncounters, LongJumper, Dexterous, ButterFingers, Batterer, Environmentalist, Cliffhanger, DeepSeaDiver, PunchingBag, WriteOff, Relentless, Survivor, WreckingBall, CleanRacer, Brawler, Agile, BreakNeck, CrashTestDummy, Gladiator, Squirrel, TriggerHappy, Marksman, OldNews, FanTastic, Tortoise, WoodenSpoon.

Also ~90 fan scoring events SCR_NEW_AMAX_SCORING_* (drift, air, near miss, revenge, killing spree...).

Unknowns for the spike

  • Cross-instance sync is unverified (MpDataGathering/DataGatheringRecords suggest yes). The ADR 0016 victim counters +0x598/+0x5d8 update only on the victim's own instance.
  • No existing Lua dump covers the award/results screens. Do a BLUR_LUA_DUMP run through a race-results screen first.
Research findings (static: Blur.exe strings + current src), 2026-10-20. **System** (data-driven): `MultiplayerAward`, `AwardType`, `AwardStat`, `AwardThreshold`, `MultiplayerAwardStatSortingStrategy` (`HighestWins`/`LowestWins`), `MultiplayerAwardDistributionSpec`. **Lua**: `GetRaceAwards`, `GetRaceAwardsInLobby`, `GetRaceHistoryAwardIconNames`. `Multiplayer.GetRaceResults()` is already used in `Hud.post.lua`. Likely cheapest route: call these from a results-screen `.post.lua` and publish via `Host.state`. **Stats** (`MultiplayerAwardStat`, strings near exe file offset 0xad9754): PlayersHitWithBolt/Shunt/Mine/Barge/IronFist, PowerupsShielded, BlockedPowerups, NitroOvertakes, LongestJump, TimeInFirstPlace, LongestTimeNotDamaged, PowerupsDropped, TimesProtectedByStableFrame, CarsBatteringRammed, PlayersHitwithDecoys, PlayersScrambled, PowerupsAbsorbedWithSafetyNet, TimeInvisibleWithSilentRunning, MinesFragmentedWithMastermine, TimesHit, TotalCarsWrecked, LongestTimeWithoutBeingWrecked, DamageDealt, TimesOffEdge, TimesInWater, LongestDrift, PickupsCollected, EnvironmentalCrashes, Car2CarVictimHits, Car2CarAggressiveHits, TopSpeed, PowerupsEvaded, LongestChain, Accuracy, DeathStreak, KillStreak, HitsByPowerups, PowerupHits, EliminationPoints, OnTrackPoints, LapsCompleted, DistanceTravelled, HealthRemaining, SlowestLap, FastestLap, RaceTime. **Awards**: Hunter, CrimsonCarapace, LitterBug, Defender, SafeHands, Booster, CloseEncounters, LongJumper, Dexterous, ButterFingers, Batterer, Environmentalist, Cliffhanger, DeepSeaDiver, PunchingBag, WriteOff, Relentless, Survivor, WreckingBall, CleanRacer, Brawler, Agile, BreakNeck, CrashTestDummy, Gladiator, Squirrel, TriggerHappy, Marksman, OldNews, FanTastic, Tortoise, WoodenSpoon. Also ~90 fan scoring events `SCR_NEW_AMAX_SCORING_*` (drift, air, near miss, revenge, killing spree...). **Unknowns for the spike** - Cross-instance sync is unverified (`MpDataGathering`/`DataGatheringRecords` suggest yes). The ADR 0016 victim counters `+0x598`/`+0x5d8` update only on the victim's own instance. - No existing Lua dump covers the award/results screens. Do a `BLUR_LUA_DUMP` run through a race-results screen first.
Author
Owner

Done (commits 99ea103, 24f4aab; ADR 0024).

Spike findings (verified on BLUR_ENGINE=recomp, 2-instance LAN):

  • Multiplayer.GetRaceAwards() is empty in-race; on the results/awards screens it lists awards for ALL racers (local + remote) on each instance. Awards sync across instances.
  • No Lua API returns stat values. The awards screen renders them as text (MPL_AWARDS_AMOUNT, e.g. "184mph"); read back via UIButtons.GetItemString.
  • The ~42 per-racer MultiplayerAwardStat values are NOT exposed to Lua; nothing is available live.

Implemented: Lua hook (Commands.post.lua) publishes _awards/_award_stats on awards screen; Feed::set_awards republishes race_result with awards [{award,racer,stat?,value?,unit?,display}] and per-racer standings[].award_stats (stats of won awards). MP only. Docs (state-websocket.md), live-timing example, changelog updated. Verified in recomp LAN race: both instances got awards.

Not done (needs native reads of MpDataGathering, separate RE task): live award_stats in standings, values for stats of awards not won, full 42-stat set. Suggest a follow-up issue.

Done (commits 99ea103, 24f4aab; ADR 0024). Spike findings (verified on BLUR_ENGINE=recomp, 2-instance LAN): - Multiplayer.GetRaceAwards() is empty in-race; on the results/awards screens it lists awards for ALL racers (local + remote) on each instance. Awards sync across instances. - No Lua API returns stat values. The awards screen renders them as text (MPL_AWARDS_AMOUNT<n>, e.g. "184mph"); read back via UIButtons.GetItemString. - The ~42 per-racer MultiplayerAwardStat values are NOT exposed to Lua; nothing is available live. Implemented: Lua hook (Commands.post.lua) publishes _awards/_award_stats on awards screen; Feed::set_awards republishes race_result with awards [{award,racer,stat?,value?,unit?,display}] and per-racer standings[].award_stats (stats of won awards). MP only. Docs (state-websocket.md), live-timing example, changelog updated. Verified in recomp LAN race: both instances got awards. Not done (needs native reads of MpDataGathering, separate RE task): live award_stats in standings, values for stats of awards not won, full 42-stat set. Suggest a follow-up issue.
Author
Owner

Done (ADR 0025, extends 0024). Found the game's per-racer stat records natively: float[48] (all MultiplayerAwardStat, enum order from the exe table at 0x10faed8) at +0x30 of a tracked heap block, signature 0xd0036 at +0x14, byte +0x1b = join ref. Verified on recomp LAN races (2 and 3 instances): values match awards-screen amounts (env crashes, accuracy, nitro overtakes, longest time not damaged) and agree across instances. Feed: per-racer award_stats (all 48 snake_case stats, raw game values) in standings once tallied and in race_result (republished when late records arrive); scraped per-award stats stay as fallback. Limit: the game only fills records at race end (zero mid-race), so nothing is live mid-race; documented in docs/state-websocket.md and ADR 0025. Live-timing example updated; unit tests added.

Done (ADR 0025, extends 0024). Found the game's per-racer stat records natively: float[48] (all MultiplayerAwardStat, enum order from the exe table at 0x10faed8) at +0x30 of a tracked heap block, signature 0xd0036 at +0x14, byte +0x1b = join ref. Verified on recomp LAN races (2 and 3 instances): values match awards-screen amounts (env crashes, accuracy, nitro overtakes, longest time not damaged) and agree across instances. Feed: per-racer `award_stats` (all 48 snake_case stats, raw game values) in `standings` once tallied and in `race_result` (republished when late records arrive); scraped per-award stats stay as fallback. Limit: the game only fills records at race end (zero mid-race), so nothing is live mid-race; documented in docs/state-websocket.md and ADR 0025. Live-timing example updated; unit tests added.
mintopia removed their assignment 2026-10-21 09:42:11 +00:00
Author
Owner

Rebased onto develop (#35 totals) + review fixes. Commits 8172428, 8d6be48.

  • Merge fix: #35 makes totals (nested maps) the last field per racer; award_stats splicing anchored on the first } and landed inside totals.uses. Now inserts before ,"totals": (npos-guarded). Tests tightened.
  • No carry-over between races: scan rebuilds tallied stat records each tick; Feed::set_stats merges (freed records don't strip a published result), cleared at race start.
  • Scraped fallback award_stats time stats now seconds, matching native records (awards[].value stays ms with unit:"ms").
  • ADR 0025: notes exception to ADR 0019 _ms rule for raw game stats; amends 0024's "not done" line.

Verified: build + ctest 23/23; recomp 2-instance LAN race (scripts/race.py lan): valid JSON, award_stats beside totals, none on unfinished racers, final values agree across instances.

Known limit (unchanged): the bot run quits before the awards screen / remote record arrival, so that run's race_result had native stats for 1 of 2 racers; late records republish race_result when they arrive.

Rebased onto develop (#35 totals) + review fixes. Commits 8172428, 8d6be48. - Merge fix: #35 makes `totals` (nested maps) the last field per racer; award_stats splicing anchored on the first `}` and landed inside `totals.uses`. Now inserts before `,"totals":` (npos-guarded). Tests tightened. - No carry-over between races: scan rebuilds tallied stat records each tick; `Feed::set_stats` merges (freed records don't strip a published result), cleared at race start. - Scraped fallback `award_stats` time stats now seconds, matching native records (`awards[].value` stays ms with `unit:"ms"`). - ADR 0025: notes exception to ADR 0019 `_ms` rule for raw game stats; amends 0024's "not done" line. Verified: build + ctest 23/23; recomp 2-instance LAN race (`scripts/race.py lan`): valid JSON, award_stats beside totals, none on unfinished racers, final values agree across instances. Known limit (unchanged): the bot run quits before the awards screen / remote record arrival, so that run's race_result had native stats for 1 of 2 racers; late records republish race_result when they arrive.
Author
Owner

Critic fix (ccb65d6): removed the 4 flagged comments (race_feed.cpp splice invariant, race_feed.h Racer::stats gloss, set_awards format note, set_stats note) plus two similar ones in race_scan.cpp and Commands.post.lua; renamed Feed::set_stats to merge_stats, Racer::stats to award_stats, racer_end to before_totals. Comment check PASS; ctest 23/23.

Critic fix (ccb65d6): removed the 4 flagged comments (race_feed.cpp splice invariant, race_feed.h Racer::stats gloss, set_awards format note, set_stats note) plus two similar ones in race_scan.cpp and Commands.post.lua; renamed `Feed::set_stats` to `merge_stats`, `Racer::stats` to `award_stats`, `racer_end` to `before_totals`. Comment check PASS; ctest 23/23.
Author
Owner

Completed and merged by Harmonic (task 30).

Completed and merged by Harmonic (task 30).
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.

Reference
mintopia/blur#38
No description provided.