-
Notifications
You must be signed in to change notification settings - Fork 0
Server and server playtime tracking #1
Description
Is your feature request related to a problem? Please describe.
We currently track sessions, but we don’t persist online time totals. Staff and analytics need both a global playtime per player and per-gamemode playtime (e.g., Survival, Skyblock). Without this, we can’t answer questions like “how long did player X play this week?” or “which gamemode gets the most hours?” Sessions exist transiently, but there’s no normalized schema or roll-ups to query efficiently.
Describe the solution you'd like
Extend the DataRegistry plugin to track online time alongside sessions:
-
Gamemode registry table
- On first sight of a gamemode string (e.g.,
"survival","skyblock"), insert it into agamemodetable and use its auto-generated ID everywhere else. (The user text said “server table”, but intent is a dedicatedgamemodetable.)
- On first sight of a gamemode string (e.g.,
-
Per-player, per-gamemode playtime
- Track cumulative playtime keyed by
(player_id, gamemode_id); optionally scoped byserver_idif multiple physical servers host the same gamemode.
- Track cumulative playtime keyed by
-
Global playtime per player
- Aggregate across all sessions (all gamemodes) for a quick lookup.
-
Session→playtime pipeline
-
On session end (logout, proxy transfer, crash timeout), compute duration and upsert into aggregates:
playtime_total[player_id] += session_msplaytime_per_gamemode[player_id, gamemode_id(, server_id)] += session_ms
-
On gamemode switch, close the old segment and start a new one so time is attributed correctly.
-
-
Crash/timeout safety
- Use a heartbeat (
last_seen_at) per active session; if the process dies, end session atlast_seen_at + grace_ms.
- Use a heartbeat (
-
Time semantics
- Store timestamps in UTC, durations in milliseconds (or seconds) as integers.
-
APIs/queries
-
DataRegistry exposes getters:
getGlobalPlaytime(playerId)getPlaytimeByGamemode(playerId)getTopPlayersByGamemode(gamemodeId, window?)getGamemodeByName(name)/ensureGamemode(name).
-
Describe alternatives you've considered
- Compute on the fly from raw sessions only: flexible but expensive at scale; heavy GROUP BY over big tables.
- Store per-server only (no gamemode table): simpler but breaks when multiple servers share one gamemode or when names change.
- Embed gamemode string everywhere (no FK): fast to ship, but duplicates strings, risks typos, and hurts join performance.
Additional context
Example relational schema (MySQL-ish, adjust for your DB):
-- Players and servers assumed to exist already:
-- players(id BIGINT PK, uuid BINARY(16) UNIQUE, ...)
-- servers(id INT PK, name VARCHAR(64) UNIQUE, ...)
CREATE TABLE IF NOT EXISTS gamemodes (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Raw session segments (one per continuous stay in a gamemode)
CREATE TABLE IF NOT EXISTS sessions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
player_id BIGINT NOT NULL,
server_id INT NULL, -- if you want per-physical-server attribution
gamemode_id INT NOT NULL,
started_at DATETIME(3) NOT NULL,
ended_at DATETIME(3) NULL, -- null while in-progress
duration_ms BIGINT NULL, -- set on close; derived = TIMESTAMPDIFF(MICROSECOND)/1000
last_seen_at DATETIME(3) NOT NULL,
CONSTRAINT fk_sess_player FOREIGN KEY (player_id) REFERENCES players(id),
CONSTRAINT fk_sess_server FOREIGN KEY (server_id) REFERENCES servers(id),
CONSTRAINT fk_sess_gamemode FOREIGN KEY (gamemode_id) REFERENCES gamemodes(id),
INDEX idx_sess_player_started (player_id, started_at),
INDEX idx_sess_gamemode_started (gamemode_id, started_at)
);
-- Aggregates: global per player
CREATE TABLE IF NOT EXISTS playtime_total (
player_id BIGINT PRIMARY KEY,
total_ms BIGINT NOT NULL DEFAULT 0,
CONSTRAINT fk_pt_player FOREIGN KEY (player_id) REFERENCES players(id)
);
-- Aggregates: per player × gamemode (optionally include server_id if desired)
CREATE TABLE IF NOT EXISTS playtime_per_gamemode (
player_id BIGINT NOT NULL,
gamemode_id INT NOT NULL,
-- optional:
server_id INT NULL,
total_ms BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (player_id, gamemode_id, server_id),
CONSTRAINT fk_ptg_player FOREIGN KEY (player_id) REFERENCES players(id),
CONSTRAINT fk_ptg_gamemode FOREIGN KEY (gamemode_id) REFERENCES gamemodes(id),
CONSTRAINT fk_ptg_server FOREIGN KEY (server_id) REFERENCES servers(id)
);Event flow (pseudocode):
onJoin(player, serverName, gamemodeName):
gmId = ensureGamemode(gamemodeName)
srvId = ensureServer(serverName) -- if needed
open sessions row: started_at=nowUTC, last_seen_at=nowUTC, ended_at=NULL
onHeartbeat(player):
update sessions.last_seen_at = nowUTC
onGamemodeSwitch(player, newGamemode):
close current session: ended_at=nowUTC, duration_ms = ended_at - started_at
upsert aggregates (global + old gamemode)
open new session segment for newGamemode
onQuit(player) or onProxyDisconnect:
close current session similarly and upsert aggregates
crashRecoveryJob (runs every N minutes):
find sessions with ended_at IS NULL and last_seen_at < nowUTC - grace_ms
close them at last_seen_at + grace_ms and upsert aggregates
Acceptance criteria (succinct):
- New gamemode names are auto-registered and referenced by ID.
- Global and per-gamemode totals increase correctly after each session/segment ends.
- Gamemode switch splits sessions so no time is double-counted.
- Crash/timeout recovery finalizes orphaned sessions using heartbeat + grace.
- Queries return consistent numbers (raw sessions sum ≈ aggregates).
Nice-to-have (future):
- Time windows (daily/weekly/monthly materialized views).
- AFK exclusion hook (if you decide AFK shouldn’t count).
- Admin command to recalc aggregates from sessions for audit/repair.
If this matches your intent, you can drop it in as the issue body.