Report cumulative player scores alongside balances

This commit is contained in:
2026-10-12 13:57:36 -04:00
parent 158916b664
commit b508b6739b
6 changed files with 121 additions and 18 deletions
+23 -8
View File
@@ -336,11 +336,23 @@ The reporter emits exactly four event types:
All four events also include `match` and `seq` (see delivery below).
Each player in round/heartbeat/end reports has `name`, `guid`, `kills`, `headshots`,
`downs`, `revives`, and `score`. Names and GUIDs remain strings; GUIDs never
`downs`, `revives`, `score`, and `score_total`. Names and GUIDs remain strings; GUIDs never
pass through numeric conversion. Round and stats are nonnegative integers,
with missing or negative stat values reported as zero. The four counters
come from T5's `player.stats`; `score` is the player's current spendable
`player.score`, rather than cumulative points earned. `map` is `mapname`.
`player.score`. `score_total` reads the stock `player.score_total` cumulative
match score used by the final scoreboard; spending points does not reduce it.
It follows stock scoring rules, including grants excluded from the total.
Missing or negative totals report as zero, independently of the balance.
`game_start` includes only player identity. `map` is `mapname`.
Stock `end_game` yields before `player_intermission` replaces `player.score`
with `player.score_total`. The reporter captures both immediately at the first
end signal without yielding; retries and replay retain that exact snapshot.
Custom scripts that overwrite the score before emitting any supported end
signal must preserve this ordering to retain a spendable final balance.
Deploy receiver support for `score_total` before the reporters. Old payloads
remain valid; the receiver treats their missing totals as unknown, not zero.
Every report includes everyone returned by `getPlayers()`, including
spectators, downed players, and AFK players. Empty rosters are sent as `[]`.
@@ -353,9 +365,9 @@ use `jsonSerialize` for escaping.
```json
{"event":"game_start","match":"process-token-1","seq":1,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","players":[{"name":"Josh","guid":"12345678901234567"}]}
{"event":"round_change","match":"process-token-1","seq":2,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":420,"headshots":160,"downs":1,"revives":3,"score":32000}]}
{"event":"heartbeat","match":"process-token-1","seq":3,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":430,"headshots":165,"downs":1,"revives":3,"score":32500}]}
{"event":"game_end","match":"process-token-1","seq":4,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":18,"players":[{"name":"Josh","guid":"12345678901234567","kills":445,"headshots":170,"downs":2,"revives":3,"score":33500}]}
{"event":"round_change","match":"process-token-1","seq":2,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":420,"headshots":160,"downs":1,"revives":3,"score":32000,"score_total":62000}]}
{"event":"heartbeat","match":"process-token-1","seq":3,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":17,"players":[{"name":"Josh","guid":"12345678901234567","kills":430,"headshots":165,"downs":1,"revives":3,"score":32500,"score_total":62500}]}
{"event":"game_end","match":"process-token-1","seq":4,"server":"t5_zombie_prototype_1","map":"zombie_cod5_prototype","round":18,"players":[{"name":"Josh","guid":"12345678901234567","kills":445,"headshots":170,"downs":2,"revives":3,"score":33500,"score_total":65000}]}
```
Events are captured immediately into an in-memory queue. One HTTP worker
@@ -439,11 +451,14 @@ and [score tracking](https://github.com/plutoniummod/t5-scripts/blob/main/ZM/Com
1. Start a match and verify one `game_start`, then an initial `round_change`
and one per new round. Check the exact server name, map, string GUIDs,
all player identities, and all five stats against the game.
all player identities, and all six stats against the game. Earn points, then
spend some: the balance should decrease while the total stays unchanged.
2. Join again in the same round, go AFK, become downed, and spectate. Verify
the next report includes the full roster without another start event.
3. End a match and verify one `game_end` with final stats after all round
reports, including if more than one stock end notification fires. Check
3. End a match and verify one `game_end` with both scores captured before the
scoreboard overwrite; compare `score_total` to the final scoreboard. Verify
the final stats arrive after all round reports, including if more than one
stock end notification fires. Check
a round-0 intro and an empty roster, including last-player disconnect.
4. Delay HTTP completion while rounds advance and the match ends. Verify
requests remain ordered and retain the stats captured at each event.
+4 -1
View File
@@ -158,6 +158,8 @@ telemetry_report_round()
telemetry_game_end_watcher()
{
// One waiter consumes the first end signal, even if a map fires all three.
// Stock end_game yields before player_intermission replaces score with
// score_total. Serialize here without waiting so score stays spendable.
level waittill_any("end_game", "game_ended", "intermission");
telemetry_log(1, "Game-end signal received; stopping producers and draining queue");
@@ -227,8 +229,9 @@ telemetry_players_json(include_stats)
data["headshots"] = telemetry_stat(player, "headshots");
data["downs"] = telemetry_stat(player, "downs");
data["revives"] = telemetry_stat(player, "revives");
// Current spendable points, not the lifetime points-earned stat.
// Stock match total is independent of the spendable balance.
data["score"] = telemetry_nonnegative_int(player.score);
data["score_total"] = telemetry_nonnegative_int(player.score_total);
}
if (body != "[")