20 KiB
Real KPM Player Active Seconds Design
Resumen ejecutivo
KPM real debe significar kills / minutos_jugados_reales. HLL Vietnam no puede calcularlo de forma publica y fiable hoy porque ningun read model materializa tiempo activo real por jugador y partida. Lo que existe actualmente permite dos lecturas parciales:
- Presencia observada por eventos AdminLog dentro de una partida:
rcon_match_player_stats.first_seen_server_timeylast_seen_server_time. - Duracion de partida:
rcon_materialized_matches.started_server_time,ended_server_time,started_at,ended_at.
La primera puede servir como base observed si se etiqueta claramente. La segunda no debe usarse como KPM real por jugador salvo como calidad estimated, y no deberia publicarse como KPM competitivo sin decision explicita futura.
Recomendacion: crear rcon_match_player_presence como tabla de detalle por jugador y partida, y derivar player_active_seconds hacia rcon_match_player_stats y read models publicos. Este enfoque conserva trazabilidad, permite backfill por calidad y evita mezclar datos exactos, observados y estimados.
Definicion de KPM real
Formula:
KPM = kills / (player_active_seconds / 60)
Reglas:
- El numerador debe ser kills agregadas del mismo scope que el tiempo.
- El denominador debe ser minutos reales jugados por el jugador, no minutos de partida.
- KPM debe calcularse en generacion de read models/snapshots, nunca en frontend salvo formateo.
kills_per_matchdebe seguir llamandoseKills/partidaoKPP; nunca KPM.
No es KPM:
kills / partidaskills_per_matchkills / duracion completa de partidasi el jugador no estuvo toda la partidakills / ventana observadasin marcar calidad
Estado actual
TASK-216 confirmo que no existe hoy un campo fiable de tiempo jugado real por jugador materializado para ranking publico.
Campos existentes:
rcon_player_profile_snapshots.play_time: texto de perfil, no normalizado, no por jugador/partida y no apto para ranking publico sin parser y reconciliacion.rcon_match_player_stats.first_seen_server_time: primerserver_timeobservado para el jugador dentro de una partida materializada.rcon_match_player_stats.last_seen_server_time: ultimoserver_timeobservado para el jugador dentro de una partida materializada.rcon_materialized_matches.started_server_timeyended_server_time: limites de partida, no presencia real por jugador.rcon_materialized_matches.started_atyended_at: timestamps de partida, no presencia real por jugador.
TASK-217 dejo kills_per_match como Kills/partida y no implemento KPM real.
Por que no sirve kills_per_match
kills_per_match responde a otra pregunta: rendimiento medio por partida contabilizada.
Ejemplo:
1222 kills / 32 partidas = 38.19 kills/partida
Eso no dice si el jugador estuvo 20 minutos, 45 minutos o 90 minutos por partida. Mostrar ese valor como KPM exagera o distorsiona la metrica porque el denominador no es tiempo real jugado.
Fuentes de datos disponibles
AdminLog raw
Tabla:
rcon_admin_log_events
Campos relevantes:
target_keyexternal_server_idevent_timestampserver_timeevent_typeparsed_payload_jsonraw_message
Eventos parseados:
connecteddisconnectedkillteam_switchchatmessagematch_startmatch_end
Calidad:
connected+disconnecteddentro de limites de match pueden dar calidadexactsi ambos eventos existen y se pueden emparejar de forma consistente.kill,team_switch,chatymessagedan presenciaobserved, no presencia continua exacta.server_timees la unidad operativa mas util para duraciones intra-partida.
rcon_match_player_stats
Tabla:
rcon_match_player_stats
Campos relevantes:
target_keymatch_keyplayer_idplayer_namekillsdeathsteamkillsfirst_seen_server_timelast_seen_server_time
Calidad:
- Ya agrupa por jugador y partida.
- Sus
first_seen_server_timeylast_seen_server_timeson presencia observada por eventos, no tiempo activo real garantizado. - Es buen destino de agregado, pero no conserva suficiente detalle para auditar intervalos de conexion/desconexion.
rcon_materialized_matches
Tabla:
rcon_materialized_matches
Campos relevantes:
target_keyexternal_server_idmatch_keystarted_server_timeended_server_timestarted_atended_atsource_basis
Calidad:
- Define limites de partida.
- No debe usarse como tiempo por jugador sin presencia individual.
- Puede acotar intervalos de presencia para evitar duraciones negativas o fuera de partida.
rcon_player_profile_snapshots
Tabla:
rcon_player_profile_snapshots
Campos relevantes:
player_idsource_server_timefirst_seensessionsmatches_playedplay_time
Calidad:
play_timees texto de perfil y puede ser acumulado global del servidor.- No esta normalizado por partida.
- No puede distribuirse con precision por weekly/monthly/annual.
- Puede servir como comparacion diagnostica, no como fuente primaria para KPM publico.
player_period_stats y snapshots
Tablas:
player_period_statsranking_snapshot_itemsrcon_annual_ranking_snapshot_items
Estado:
- Tienen kills, deaths, teamkills, matches, K/D y Kills/partida.
- No tienen
player_active_seconds,playtime_qualitynikills_per_minute.
Evaluacion de calidad de datos
| Fuente | Calidad | Uso recomendado |
|---|---|---|
connected + disconnected emparejados dentro de match |
exact |
Calcular intervalos reales acotados por partida |
connected sin disconnected pero con match end |
observed o estimated segun politica |
Acotar hasta ended_server_time, no publicar como exacto |
disconnected sin connected pero con match start |
observed |
Acotar desde primer evento observado o match start solo si se etiqueta |
kill, team_switch, chat, message |
observed |
Inferir ventana first_seen/last_seen |
| Duracion completa de partida | estimated |
Solo diagnostico o fallback no publico |
| Sin eventos suficientes | unknown |
No calcular KPM |
rcon_player_profile_snapshots.play_time |
unknown para KPM por periodo |
No usar como fuente primaria |
Valores recomendados para playtime_quality:
exact: intervalos conectados/desconectados suficientemente cerrados y acotados por partida.observed: ventana inferida por eventos del jugador dentro de la partida.estimated: duracion completa o parcial inferida sin evidencia individual suficiente.unknown: no hay base util.
Regla publica recomendada:
- Publicar KPM solo con
exactuobserved. - No publicar KPM para
estimatedounknownsalvo decision explicita futura y etiqueta visible.
Modelo de datos propuesto
Opcion A: anadir player_active_seconds a rcon_match_player_stats
Columnas nuevas:
player_active_seconds INTEGERplaytime_quality TEXT NOT NULL DEFAULT 'unknown'playtime_source TEXTplaytime_first_server_time BIGINTplaytime_last_server_time BIGINTplaytime_interval_count INTEGER NOT NULL DEFAULT 0
Clave existente:
UNIQUE(target_key, match_key, player_id)
Indices recomendados:
idx_rcon_match_player_stats_playtime_qualitysobre(playtime_quality)idx_rcon_match_player_stats_player_playtimesobre(player_id, target_key, match_key, player_active_seconds)
Como se rellena:
- Durante
materialize_rcon_admin_log, despues de derivar stats por jugador. - Se calcula a partir de eventos en la ventana del match.
- Si solo hay
first_seen_server_timeylast_seen_server_time, usarobserved.
Ventajas:
- Menor cambio para read models existentes.
- Agregaciones weekly/monthly/annual simples.
Riesgos:
- Pierde detalle de intervalos exactos.
- Mezcla metrica agregada con evidencia.
- Es mas dificil auditar reconexiones multiples.
Opcion B: crear rcon_match_player_presence
Tabla nueva propuesta:
CREATE TABLE rcon_match_player_presence (
id BIGSERIAL PRIMARY KEY,
target_key TEXT NOT NULL,
external_server_id TEXT,
match_key TEXT NOT NULL,
player_id TEXT NOT NULL,
player_name TEXT NOT NULL,
first_seen_server_time BIGINT,
last_seen_server_time BIGINT,
active_seconds INTEGER NOT NULL DEFAULT 0,
playtime_quality TEXT NOT NULL DEFAULT 'unknown',
interval_count INTEGER NOT NULL DEFAULT 0,
evidence_event_count INTEGER NOT NULL DEFAULT 0,
evidence_event_types TEXT NOT NULL DEFAULT '[]',
source_basis TEXT NOT NULL DEFAULT 'rcon-admin-log',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(target_key, match_key, player_id)
);
Indices recomendados:
(target_key, match_key)(player_id, target_key, match_key)(playtime_quality, active_seconds)(external_server_id, match_key)
Como se rellena:
- Leer
rcon_admin_log_eventsacotados porrcon_materialized_matches.started_server_timeyended_server_time. - Construir intervalos por jugador:
- abrir intervalo en
connected - cerrar intervalo en
disconnected - acotar cada intervalo por match start/end
- sumar intervalos no solapados
- abrir intervalo en
- Si no hay intervalos completos, usar ventana de eventos observados:
first_seen_server_time = MIN(server_time)de eventos del jugadorlast_seen_server_time = MAX(server_time)de eventos del jugadoractive_seconds = max(0, last_seen - first_seen)playtime_quality = observed
- Si solo existe una observacion puntual,
active_seconds = 0o minimo configurable, calidadobserved-low-confidence. Para mantener enum simple, guardarobservedy exponerevidence_event_count. - No usar duracion completa de partida salvo como
estimated.
Ventajas:
- Auditable.
- Permite recalculo/backfill independiente.
- No contamina stats base hasta tener calidad suficiente.
Riesgos:
- Requiere nueva tabla, backfill y runner.
- La exactitud depende de cobertura real de eventos
connected/disconnected.
Opcion C: ambas
Recomendacion final:
- Crear
rcon_match_player_presencecomo fuente de verdad de presencia por jugador/partida. - Denormalizar hacia
rcon_match_player_stats.player_active_secondsyplaytime_qualitypara agregaciones rapidas.
Por que:
- La tabla de detalle permite auditoria y recalculo.
- El campo denormalizado evita joins costosos en generacion de snapshots.
- La ruta publica sigue leyendo snapshots/read models, no calculando KPM en request.
Estrategia de calculo
Intervalos exactos
Entrada:
- Eventos
connectedydisconnectedconplayer_id,server_time. - Limites de match
started_server_time,ended_server_time.
Algoritmo:
- Ordenar eventos por
server_time,id. - Por jugador, abrir intervalo en
connected. - Cerrar intervalo en
disconnected. - Acotar inicio/fin al rango de la partida.
- Fusionar intervalos solapados.
- Sumar segundos.
- Marcar
playtime_quality = exactsolo si la evidencia permite explicar inicio y fin.
Ventana observada
Entrada:
- Eventos
kill,team_switch,chat,message,connected,disconnected.
Algoritmo:
- Tomar min/max
server_timepor jugador dentro del match. - Calcular
active_seconds = max(0, last_seen - first_seen). - Guardar
evidence_event_county tipos. - Marcar
playtime_quality = observed.
Estimacion por duracion de partida
Entrada:
started_server_time,ended_server_time.
Politica:
- No usar para KPM publico por defecto.
- Si se guarda, marcar
playtime_quality = estimated. - Requiere flag futuro para mostrarse.
Estrategia de backfill
Fase 1:
- Crear tabla
rcon_match_player_presence. - Backfill desde
rcon_admin_log_eventsyrcon_materialized_matches. - No cambiar API publica.
Fase 2:
- Denormalizar
player_active_secondsyplaytime_qualityenrcon_match_player_stats. - Backfill stats desde presence.
Fase 3:
- Extender
player_period_statscon:player_active_secondsplaytime_qualitykills_per_minute
- Regenerar weekly/monthly/yearly period stats.
Fase 4:
- Extender
ranking_snapshot_itemsyrcon_annual_ranking_snapshot_itemscon:player_active_secondsplaytime_qualitykills_per_minute
- Generar snapshots KPM solo para filas con calidad permitida.
Reglas de backfill:
- Si hay intervalos exactos, usar
exact. - Si solo hay
first_seen/last_seenpor eventos, usarobserved. - Si solo hay duracion completa de partida, usar
estimatedy excluir de KPM publico. - Si no hay evidencia, usar
unknowny excluir de KPM. - No mezclar
exactyobservedsin conservar calidad agregada.
Calidad agregada por periodo:
exactsi todos los segundos agregados vienen de exact.observedsi hay mezcla exact + observed o solo observed.estimatedsi incluye estimated.unknownsi no hay segundos validos.
Cambios necesarios en read models
rcon_match_player_stats
Campos recomendados:
player_active_seconds INTEGERplaytime_quality TEXTkills_per_minute REAL
Nota: kills_per_minute puede no guardarse en stats base si se prefiere derivarlo en read models. Si se guarda, debe recalcularse cada vez que cambien kills o segundos.
player_period_stats
Campos recomendados:
player_active_seconds INTEGER NOT NULL DEFAULT 0playtime_quality TEXT NOT NULL DEFAULT 'unknown'kills_per_minute REAL
Agregacion:
SUM(kills) / (SUM(player_active_seconds) / 60)
Filtro publico:
- Incluir KPM solo si
SUM(player_active_seconds) > 0y calidad agregada no esestimatedniunknown.
ranking_snapshot_items
Campos recomendados:
player_active_secondsplaytime_qualitykills_per_minute
Nueva metrica snapshot:
kills_per_minute
Orden:
metric_value DESCplayer_active_seconds DESCkills DESCmatches_considered DESCplayer_name ASC
rcon_annual_ranking_snapshot_items
Campos recomendados:
player_active_secondsplaytime_qualitykills_per_minute
Regla:
- Annual KPM debe tener snapshot propio
metric = kills_per_minute. - No representar KPM anual con
kills_per_match.
Perfil de jugador
Exponer:
player_active_secondsplayer_active_minuteskills_per_minuteplaytime_quality
Evitar:
- Calcular KPM runtime en request publico.
- Mezclar periodo semanal/mensual/anual sin metadata.
Cambios necesarios en snapshots weekly/monthly/annual
Weekly/monthly:
- Extender generacion en
rcon_historical_leaderboards. - Agregar metrica
kills_per_minutesolo si existeplayer_active_seconds. - Persistir
kills_per_minuteenranking_snapshot_items.
Annual:
- Extender
rcon_annual_rankingscuando exista presence/read model. - Agregar
kills_per_minutea metricas soportadas solo despues del backfill. - Persistir snapshot independiente por anio, servidor y metrica.
General:
- Public read path debe seguir leyendo solo snapshots.
- Missing snapshot debe devolver
snapshot_status=missing, sin fallback runtime.
Cambios necesarios en API y frontend
API:
- Incluir
kills_per_minutesolo cuando venga de read model/snapshot. - Incluir
playtime_quality. - Mantener
kills_per_matchcomoKills/partida. - Rechazar o marcar missing si se pide KPM sin snapshot real.
Frontend:
- Mostrar columna
KPMsolo si backend exponekills_per_minute. - Mostrar tooltip o metadata de calidad si se decide publicar
observed. - No calcular KPM en JS.
- No mostrar KPM para
estimatedounknown.
Impacto en refresh runner
El runner actual refresca:
- RCON capture
- materializaciones
- player search index
- player period stats
- ranking snapshots
Orden futuro recomendado:
- RCON capture/AdminLog ingestion.
- Materializar matches y player stats.
- Materializar
rcon_match_player_presence. - Denormalizar
player_active_secondsarcon_match_player_stats. - Refrescar
player_search_index. - Refrescar
player_period_stats. - Refrescar ranking snapshots weekly/monthly.
- Refrescar annual snapshots cuando aplique.
Comandos futuros de produccion
Nombres propuestos; no existen todavia:
docker compose exec backend python -m app.rcon_player_presence migrate
docker compose exec backend python -m app.rcon_player_presence backfill --year 2026 --server-key all-servers
docker compose exec backend python -m app.rcon_player_presence backfill --year 2026 --server-key comunidad-hispana-01
docker compose exec backend python -m app.rcon_player_presence backfill --year 2026 --server-key comunidad-hispana-02
docker compose exec backend python -m app.rcon_historical_player_stats refresh-player-period-stats
docker compose exec backend python -m app.rcon_historical_leaderboards refresh-ranking-snapshots --limit 30
docker compose exec backend python -m app.rcon_annual_rankings generate --year 2026 --server-key all-servers --metric kills_per_minute --limit 30 --replace-existing
docker compose exec backend python -m app.rcon_annual_rankings generate --year 2026 --server-key comunidad-hispana-01 --metric kills_per_minute --limit 30 --replace-existing
docker compose exec backend python -m app.rcon_annual_rankings generate --year 2026 --server-key comunidad-hispana-02 --metric kills_per_minute --limit 30 --replace-existing
Plan de implementacion por tasks
-
TASK-219-create-rcon-match-player-presence-schemaCrear schema SQLite/Postgres pararcon_match_player_presence, sin cambiar API publica. -
TASK-220-materialize-player-presence-from-adminlogImplementar materializacion exact/observed desdercon_admin_log_eventsyrcon_materialized_matches. -
TASK-221-backfill-player-active-secondsBackfill historico por servidor/anio y reporte de cobertura por calidad. -
TASK-222-denormalize-player-active-seconds-into-match-statsAgregarplayer_active_secondsyplaytime_qualityarcon_match_player_stats. -
TASK-223-extend-player-period-stats-with-kpmExtenderplayer_period_statscon segundos activos y KPM real. -
TASK-224-add-kpm-ranking-snapshotsAgregarkills_per_minutea weekly/monthly snapshots y annual snapshots independientes. -
TASK-225-expose-real-kpm-in-api-payloadsExponerkills_per_minuteyplaytime_qualitydesde snapshots/read models, sin runtime publico. -
TASK-226-show-real-kpm-in-ranking-uiMostrar KPM solo cuando backend entreguekills_per_minute; mantener Kills/partida separado. -
TASK-227-validate-kpm-quality-and-performanceMedir cobertura, latencia,fallback_used=falsey comparar contra muestras manuales.
Riesgos principales
- Cobertura incompleta de
connected/disconnected. server_timepuede reiniciarse o tener discontinuidades entre partidas; siempre debe estar acotado pormatch_key.- Jugadores con eventos escasos pueden tener
observedsubestimado. - Usar duracion completa de partida como tiempo de jugador inflaria o distorsionaria KPM.
- Mezclar calidades sin metadata haria la metrica poco confiable.
- Backfill masivo puede competir con lecturas publicas si se ejecuta fuera de ventana controlada.
- El frontend podria volver a confundir
Kills/partidacon KPM si no se separan nombres y payloads.
Criterios de aceptacion futuros
- Existe
player_active_secondspor jugador/partida conplaytime_quality. - Cada fila puede explicar su fuente: exact, observed, estimated o unknown.
- KPM se calcula como
kills / (player_active_seconds / 60). - No se calcula KPM en frontend.
kills_per_matchsigue separado comoKills/partida.- Weekly/monthly/annual KPM tienen snapshots propios.
- Request publico de ranking no consulta
rcon_match_player_statspara calcular KPM. fallback_used=falseen rutas publicas normales.- Si falta snapshot KPM, el endpoint devuelve missing sin fallback runtime.
- Tests cubren exact, observed, estimated/unknown excluded, division por cero y orden de ranking.