Skip to content

feature: Player Management counts detailed community stats and per weapon totals - #286

Merged
lukepolo merged 24 commits into
mainfrom
feature/community-detailed-stats
Nov 2, 2026
Merged

lukepolo merged 24 commits into
mainfrom
feature/community-detailed-stats

Conversation

@Flegma

@Flegma Flegma commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Player Management counts the detailed community stats that 5stackgg/api#968 stores. Stacked on #282, which is stacked on #281: merge #281 and #282 first, then this PR shows only its own two commits.

The first commit adds the counters and the per weapon totals. The second makes them hold on servers that mix humans and bots, and names a gun built on another gun's class by the gun in hand. Everything is in Player Management, which only community servers load, and in the shared roster code compiled into it. The match plugin and the match stats are untouched.

What the plugin sends

Each roster entry and each departed entry gains two optional objects next to the counters from #282:

  • stats: running totals for the connection by key, the 33 counters below plus best_streak, the best killstreak on the current map (a maximum, not a sum).
  • weapons: running totals per weapon by weapon name, each with kills, headshots, shots, hits and damage.

Like kills and deaths, both start empty with a new connection and stay with a departed entry. Zeros are left out, a weapon whose totals are all zero is left out, and an empty object is not sent at all, so a player who has done nothing sends exactly what #282 sends. An entry excerpt:

"mvps": 1,
"stats": { "shots": 212, "hits": 54, "hits_head": 11, "trade_kills": 1, "best_streak": 4 },
"weapons": { "ak47": { "kills": 6, "headshots": 3, "shots": 180, "hits": 47, "damage": 1490 } }

Weapon names are the ones the match stats keep in player_kills."with": the event's weapon without its weapon_ prefix, in lower case, with every knife and the bayonet as knife, and a silenced gun with its silencer off (m4a1_silencer_off, usp_silencer_off) under the same name as with it on. Shots and hits are counted for the 34 CS2 guns only (CommunityWeapons, by item definition index); kills and damage for any weapon, so knife, taser, hegrenade and inferno get kills and damage but no shots. A connection keeps at most 48 weapons; a weapon beyond that still counts in stats, only not per weapon.

No plugin version bump and no new convars: the api reads the new data by its keys.

How each counter is decided

The counters take the match stats' names and rules. Only events between humans count (see Bots), and nothing in warmup counts, as in #282 (the game rules' WarmupPeriod at the event). An enemy is a player on the other side, or anyone where mp_teammates_are_enemies is on. Team stats count only where teammates are not enemies.

  • shots: a weapon_fire of a firearm by a human. A knife swing or a grenade throw is no shot. A shot whose bullet hits a bot is taken back (see Bots).
  • hits, and by hitgroup hits_head, hits_neck, hits_chest, hits_stomach, hits_left_arm, hits_right_arm, hits_left_leg, hits_right_leg: a firearm hit on an enemy, once per attacker, victim and tick, so a shotgun's pellets in one victim are one hit, as the match stats count hits (shots that hit). Hitgroups are CS2's numbers (1 head, 2 chest, 3 stomach, 4 and 5 the arms, 6 and 7 the legs, 8 the neck); a generic (0) or gear (10) hit is a hit with no group.
  • damage_taken: the damage an enemy dealt the player. team_damage: the damage a human deals a human who is not an enemy, themselves included (their own grenade), as on the match side. he_damage and molotov_damage: enemy damage by hegrenade, and by inferno or molotov. All damage is capped at the health the victim had, as damage in feature: Player Management counts assists, headshots, damage, rounds and MVPs for the community stats #282 is.
  • flash_assists: assistedflash on player_death with the assister an enemy of the victim. Such an assist does not also count in assists, as in feature: Player Management counts assists, headshots, damage, rounds and MVPs for the community stats #282.
  • enemies_flashed and flash_duration_ms: a player_blind longer than 1.0 s on a live enemy, and that blind time in milliseconds. team_flashed: the same on a teammate or on oneself. A spectator or a dead player who is blinded counts for nothing.
  • knife_kills (any knife or the bayonet), zeus_kills (taser), he_kills (hegrenade) and molotov_kills (inferno or molotov): by the counted kill's weapon.
  • no_scope_kills, thru_smoke_kills, thru_wall_kills, blinded_kills and in_air_kills: the noscope, thrusmoke, penetrated > 0, attackerblind and attackerinair flags of a counted kill's player_death.
  • team_kills: killing a teammate (never oneself). The victim still gets a death and the killer no kill.
  • suicides: a death by one's own hand or by the world, but not by the bomb (planted_c4). It is still a death.
  • trade_kills: killing the killer of a teammate within 5 s of that teammate's death, in the same round, as the match stats' trade pairs count it. Each pair counts once, so killing a player who has just killed two teammates trades both, and a killer who dies in any way has paid for the kills they made, which also keeps a respawned killer from being traded twice. The kills are cleared at round_start and at a map start. Players are told apart by slot, and there are no trades where teammates are enemies.
  • bomb_plants, bomb_defuses and hostage_rescues: bomb_planted, bomb_defused and hostage_rescued by a human.
  • best_streak: the most counted kills in a row without dying on the current map, like the match stats' best streak. Any death ends a streak, a death at a bot's hands included; a round end does not. A map start resets both the streak and the best.

Per weapon, kills and headshots count with the counted kill, shots with shots, hits with hits, and damage with the enemy damage.

Bots

On a server with humans and bots, nothing a bot does counts and nothing done to a bot counts. A bot's kill ends the victim's streak but is not a death, helping a bot to a kill is no assist, and hits, damage, flashes and kills on a bot count for nobody. Everything between humans still counts, since every event names both players. A miss names no target, so a shot counts when it is fired and is taken back when its bullet hits a bot, unless the same shot also hits a human enemy in that tick. A take-back never takes a weapon's shots below zero.

This changes two counters from before on such servers: a death at a bot's hands is no longer a death, and helping a bot to a kill is no longer an assist. Kills on bots and damage to or by bots did not count before either.

Guns built on another gun's class

CS2 builds five guns on another gun's class: the M4A1-S on the M4A4's, the USP-S on the P2000's, the CZ75-Auto on the P250's, the R8 on the Desert Eagle's and the MP5-SD on the MP7's. On a live server player_hurt named a USP-S hkp2000, which put its hits and damage under the P2000. When an event names one of those five base guns, the plugin reads the item definition index of the gun in hand and takes it only when it is the event's gun or the gun built on its class, so a player who has switched guns since keeps the event's name. Shots, hits, damage and kills all go through this, so the five guns keep their own names (m4a1_silencer, usp_silencer, cz75a, revolver, mp5sd), the names the match stats use for kills.

The api side

5stackgg/api#968 stores these. It uses the same key names (its COMMUNITY_STAT_KEYS are these 33 counters, and it reads best_streak from stats), its roster validator keeps known keys only, and it keeps the 44 weapon names the match stats use. All 34 guns here are among them; any other name the plugin may send, such as world, is dropped there. The plugin sends running totals per connection and the api works out the growth between reports, as it does for kills and deaths.

Compatibility

  • An api without #968 ignores both objects: it copies only the fields it knows from each entry, so reports work as before.
  • #968 without this PR: the plugin sends no stats or weapons, and the new columns and boards stay empty.
  • Size: an entry grows by about 1 KB in normal play. With every counter and 48 weapons filled with large numbers an entry is about 5 KB, so a full server of 64 plus the 64 departed entries the plugin keeps comes to about 650 KB, inside the api's 1 MB JSON limit.

Known limits

Tests

Flegma added 15 commits October 30, 2026 00:06
…e map, so the panel can keep a map's last kills on that map

The roster sends the counts it had when a map ended in a sync of its
own, before it holds through the map change, and every sync names the
map. A map-end report the panel misses is sent again, and given up
after the hold cap so it cannot hold up the reports after it. Panels
that predate per-map sessions ignore the map.
…nd the map is read from the map load event

A roster that changed over the map change differed from the pending
report on every poll, so each poll became a sync until the report was
taken. The map name now comes with SwiftlyS2's map load event, and a
plugin only asks the engine for it on a hot reload, where it has missed
that event and the engine has a map to answer with.
…and MVPs for the community stats

Each connection now keeps running totals of assists, headshots, damage,
rounds played, rounds won and MVPs next to its kills and deaths, and
sends them on the roster and with every departure. Flash assists, team
damage and warmup rounds do not count, a headshot only counts with a
counted kill, and a lethal hit is credited only with the health the
victim had left. Panels that predate the counters ignore them.
Kills, deaths, assists, headshots, damage and MVPs are now dropped while
the game rules say warmup is on, the same way warmup rounds already
were. The flag is read at each event, so it holds however long warmup
runs and however it ends; a server without warmup counts everything.
…rades, objectives, the best killstreak and per weapon totals for the community stats

Each connection now keeps 33 detailed counters, its best killstreak and
per weapon totals (kills, headshots, shots, hits and damage) next to the
counters it already had, reset with a new connection like those, and
sends them as `stats` and `weapons` on every roster and departed entry.
Zeros are left out and an empty map is not sent at all, so panels that
predate the detailed stats see the report as before.

The counters take the match stats' names and rules: shots are firearms
only, a shotgun's pellets are one hit per victim and tick, a flash
counts when it blinds an enemy for more than a second, a trade is the
kill of a teammate's killer within 5 s in the same round, and utility
damage is capped at the health the victim had. Every knife is one knife
and a silenced gun is one gun with its silencer on or off. Nothing done
in warmup counts, as before.

Both plugins pass the kill facts on player_death and the weapon,
hitgroup and tick on player_hurt, and hook weapon_fire, player_blind,
bomb_planted, bomb_defused, hostage_rescued and round_start.
…, a shot that hits a bot is taken back, and a shared class gun is named by the gun in hand

On a server with humans and bots, nothing a bot does counts and nothing
done to a bot does: a bot's kill ends the victim's killstreak but is no
death, helping a bot to a kill is no assist, and hits, damage and kills
on a bot count for nobody. Everything between humans still counts, since
each event names both players. A miss names no target, so a shot is
still counted when fired and taken back when its bullet hits a bot,
unless the same shot also hits a human enemy in that tick.

player_hurt names a USP-S hkp2000, so a gun built on another's class had
its hits and damage under the other gun's name. Hits and kills now read
the gun in hand for those five names (M4A1-S, USP-S, CZ75, R8 and
MP5-SD), as shots already did, and all three take it only when it is the
event's gun or the one on its class, so a player who switched guns since
keeps the event's name.
…end report, and a kill during the hold stays out of it
… next round's start, so a second round end before then adds nothing
…cks the new map's roster, not the snapshot taken at the map's end
…oo, so it closes the round's MVP like any other
… into up/community-detailed-stats

# Conflicts:
#	apps/player-management-css/src/PlayerManagement.csproj
#	apps/player-management-css/src/PlayerManagementPlugin.cs
#	apps/player-management-sw/src/PlayerManagement.csproj
#	apps/player-management-sw/src/PlayerManagementPlugin.cs
#	apps/player-management-sw/test/FiveStack.Tests.csproj
#	shared/dotnet/FiveStack.Utilities/PlayerRoster.cs
@lukepolo

lukepolo commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

thanks @Flegma, the api stack is merged (#883 #885 #886 #891 #968). #281 and the web PRs go in once the api is live.

left on the game-server side before #282 and #286 can go in:

#282

  • only credit rounds played / won to players who actually spawned that round. right now anyone on T or CT at round end gets it, so you can sit on a team without spawning and farm the +5 per round won. track who spawned since the round started and only credit them (css and sw)
  • still want the warmup check on a real pod (the PR description still has it as a todo). warmup ending, plus a server with mp_freezetime 0, so we know round_freeze_end still fires there and rounds still count

#286

  • accuracy reads low on servers with bots. a miss aimed at a bot still counts as a shot and only a bot hit is taken back, so someone who fights bots half the time shows ~14% instead of 25%. simplest is to not count shots and hits while a bot is on the server
  • IsWarmup() walks every entity looking for cs_gamerules, and it now runs on every weapon_fire and bot hit. read it once in the tick (or keep the gamerules proxy for the map) and reuse that

…e-counters

# Conflicts:
#	apps/player-management-css/src/PlayerManagementPlugin.cs
#	apps/player-management-sw/src/PlayerManagementPlugin.cs
#	apps/player-management-sw/test/PlayerRosterTests.cs
#	shared/dotnet/FiveStack.Utilities/PlayerRoster.cs
… side they spawned on, so sitting on a side or joining the winning one farms no rounds
… rules are looked up once a map rather than on every shot
…they claim, and leaving a side or the game restarting forgets it
…iled-stats

# Conflicts:
#	apps/player-management-css/src/PlayerManagementPlugin.cs
#	apps/player-management-sw/src/PlayerManagementPlugin.cs
@Flegma

Flegma commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

thanks, all three code items are pushed:

#282

  • rounds played / won now only go to players who spawned alive that round and end it on the side they spawned on (css and sw). spawns are kept from one round end to the next, so it doesn't matter whether round_start or the spawns come first, and a late spawn in buy time still counts. joining a side mid-round fires a spawn for a player who stays dead, so alive is read off the player's own pawn one tick later. leaving a side (spectator or the other team) forgets the spawn, and so does a game restart. sitting on a side, or switching to the winning one, gets nothing.
  • the warmup / mp_freezetime 0 check on a real pod is still open; I'll run it once this is on our test panel and tick it in the description.

#286

  • no shot or hit counts while a bot is on the server (checked once a second in the tick). damage and kills still count. the bot-hit take-back is gone.
  • the gamerules are looked up once a map and cached; WarmupPeriod is read off that each time.

Base automatically changed from feature/community-more-counters to main November 2, 2026 15:48
@lukepolo
lukepolo marked this pull request as ready for review November 2, 2026 15:48
@lukepolo
lukepolo merged commit 92067d4 into main Nov 2, 2026
6 of 7 checks passed
@lukepolo
lukepolo deleted the feature/community-detailed-stats branch November 2, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants