4.3 KiB
Historical Match Kills Per Minute Analysis
Scope
This document defines how real historical KPM works for the match detail served to historico-partida.html.
Why KPM Was Not Safe Before
Historical match detail already exposed:
- per player:
killsdeathsteamkills- weapon and matchup counters
- per match:
started_atended_atduration_seconds
What it did not expose was player-level active time. Because of that, dividing by total match duration would have produced a false player KPM.
Real KPM Rule
Real KPM means:
kills / (player_active_seconds / 60)
It does not mean:
kills / match_duration_minutes
Source of Truth
The current implementation uses connection intervals reconstructed from the materialized RCON AdminLog match model.
Reliable interval signals:
connecteddisconnectedmatch_startmatch_end
Supporting evidence still stored in the player fact:
first_seen_server_timelast_seen_server_time
The persisted field is:
player_active_seconds
The source label is:
active_time_source = "event_log"
Persistence
Forward-only persistence now stores on rcon_match_player_stats:
player_active_seconds INTEGER NULLactive_time_source TEXT
Legacy rows remain valid. If they were materialized before these columns existed, they keep player_active_seconds = NULL until new materialization or new matches populate the field.
Calculation
Observed active time is now:
sum(connected_interval_seconds clamped to [match_start, match_end])
Rules:
- if the player connects during the match:
- open interval at
connected.server_time
- open interval at
- if the player disconnects during the match:
- close interval at
disconnected.server_time
- close interval at
- if the player was already connected before
match_startand there is no later pre-match disconnect:- open interval at
match_start
- open interval at
- if the player is still connected at
match_end:- close interval at
match_end
- close interval at
- if the player reconnects multiple times:
- sum all non-overlapping intervals
This is still observed presence, not exact telemetry down to every silent second.
KPM is exposed only when:
player_active_secondsexistsplayer_active_seconds >= HLL_KPM_MIN_ACTIVE_SECONDS
Default:
HLL_KPM_MIN_ACTIVE_SECONDS = 60
Payload Contract
Historical match detail player rows may now expose:
player_active_secondsplayer_active_minuteskpmkpm_statusactive_time_source
active_time_source values currently used:
connection_intervalsconnection_intervals_carryoverevent_span_fallbackunavailable
kpm_status values:
readymissing_active_timeinsufficient_active_timemissing_connection_intervals
Rules:
- missing active time:
kpm = nullkpm_status = "missing_active_time"
- fallback event span without reliable connection intervals:
kpm = nullkpm_status = "missing_connection_intervals"
- active time below threshold:
kpm = nullkpm_status = "insufficient_active_time"
- valid active time:
kpm = round(kills / (player_active_seconds / 60), 2)kpm_status = "ready"- only when
active_time_sourceisconnection_intervalsorconnection_intervals_carryover
Historical Matches Already Stored
Old matches are not backfilled with fake KPM.
For those rows:
player_active_secondscan remainnullkpmstaysnull- frontend must not render
0.00as if the value were real - rows rematerialized without reliable connection intervals can still expose
event_span_fallback, but that fallback must not be shown as real KPM
Frontend Rule
historico-partida.js renders KPM only when:
kpm_status == "ready"
If the value is missing or below threshold, the panel stays clean and does not show a fake metric.
Limitations
- This is observed active time from AdminLog connection evidence, not exact join/leave telemetry for every silent second.
- Quiet players with kills/chat/team switches but no reliable connection chain can expose
event_span_fallback; that span is intentionally blocked from KPM. - Legacy matches remain without KPM unless rematerialized from stored AdminLog evidence.
- We do not discount time spent without squad/unit/role yet because there is no audited historical source for that dimension in this implementation.