Skip to content

Server and server playtime tracking #1

@remdui

Description

@remdui

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 a gamemode table and use its auto-generated ID everywhere else. (The user text said “server table”, but intent is a dedicated gamemode table.)
  • Per-player, per-gamemode playtime

    • Track cumulative playtime keyed by (player_id, gamemode_id); optionally scoped by server_id if multiple physical servers host the same gamemode.
  • 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_ms
      • playtime_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 at last_seen_at + grace_ms.
  • 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):

  1. New gamemode names are auto-registered and referenced by ID.
  2. Global and per-gamemode totals increase correctly after each session/segment ends.
  3. Gamemode switch splits sessions so no time is double-counted.
  4. Crash/timeout recovery finalizes orphaned sessions using heartbeat + grace.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions