import { db } from '../db/index.js'; import { MIN_RECORDED_ROUND } from '../../match-history.js'; import { getPersonalBestRecords } from './leaderboard-records.js'; import { countsAsGamePlayed } from './leaderboard-participation.js'; import { EFFICIENCY_MIN_GAMES, EFFICIENCY_MIN_KILLS, EFFICIENCY_MIN_DOWNS, isEfficiencyMetric } from '../../leaderboard-efficiency.js'; import { leaderboardMetrics, type LeaderboardEntry, type LeaderboardMap, type LeaderboardMetric, type LeaderboardOptions, type LeaderboardResponse } from '../../leaderboard.js'; const metrics = new Set(leaderboardMetrics.map(metric => metric.key)); export const MAX_LEADERBOARD_OFFSET = 1_000_000; type RankedEntry = Omit; interface RankedScope { revision: number; entries: RankedEntry[]; maps: LeaderboardMap[]; summary: LeaderboardResponse['summary'] } const scopeCaches = new WeakMap>(); // Match SQLite's built-in lower(), which folds ASCII rather than Unicode. const searchText = (value: string) => value.replace(/[A-Z]/g, ch => ch.toLowerCase()); interface LeaderboardRow extends Omit { // A left join keeps metadata available when the requested page has no players. guid: string | null; matchingTotal: number; summaryPlayers: number; summaryMatches: number; summaryKills: number; summaryRounds: number; summaryHighestRound: number; summaryFirstTrackedAt: string | null; summaryUpdatedAt: string | null; } export function isLeaderboardMetric(value: string): value is LeaderboardMetric { return metrics.has(value); } /** Rank each GUID once within the selected matches; player search keeps their actual rank. */ export function getLeaderboard(options: LeaderboardOptions = {}): LeaderboardResponse { return readLeaderboard(options); } /** Select the exact player after ranking everyone eligible in the requested scope. */ export function getPlayerLeaderboardEntry(guid: string, options: Pick = {}): LeaderboardEntry | null { return readLeaderboard({ game: options.game, map: options.map, players: options.players, metric: options.metric, limit: 1 }, guid); } function addRecordSources(handle: ReturnType, entries: RankedEntry[], options: LeaderboardOptions): LeaderboardEntry[] { const records = getPersonalBestRecords(handle, entries.map(entry => ({ ...entry, game: options.game || undefined, map: options.map || undefined, players: options.players }))); return entries.map((entry, index) => ({ ...entry, records: records[index] })); } function readLeaderboard(options: LeaderboardOptions): LeaderboardResponse; function readLeaderboard(options: LeaderboardOptions, exactGuid: string): LeaderboardEntry | null; function readLeaderboard(options: LeaderboardOptions, exactGuid?: string): LeaderboardResponse | LeaderboardEntry | null { const handle = db(); const metric = options.metric && isLeaderboardMetric(options.metric) ? options.metric : 'highestRound'; const limit = Number.isSafeInteger(options.limit) ? Math.min(100, Math.max(1, options.limit!)) : 50; const offset = Number.isSafeInteger(options.offset) ? Math.min(MAX_LEADERBOARD_OFFSET, Math.max(0, options.offset!)) : 0; const conditions = ["m.game IN ('t5', 't6')", "m.status = 'completed'", 'mp.has_stats = 1', `m.highest_round >= ${MIN_RECORDED_ROUND}`, `mp.highest_round >= ${MIN_RECORDED_ROUND}`]; const parameters: Array = []; if (options.game) { conditions.push('m.game = ?'); parameters.push(options.game); } if (options.map) { conditions.push('m.map_key = ?'); parameters.push(options.map); } if (options.players) { conditions.push('m.start_players = ?'); parameters.push(options.players); } const qualification = isEfficiencyMetric(metric) ? `WHERE measures.gamesPlayed >= ${EFFICIENCY_MIN_GAMES} AND measures.${metric} IS NOT NULL` + (metric === 'headshotRate' ? ` AND measures.kills >= ${EFFICIENCY_MIN_KILLS}` : '') + (metric === 'killsPerDown' ? ` AND measures.downs >= ${EFFICIENCY_MIN_DOWNS}` : '') : ''; // A board reuses observations for player totals and distinct match metadata. // Exact-player requests only consume the totals, so they can stream the join once. const observationStorage = exactGuid === undefined ? 'MATERIALIZED' : 'NOT MATERIALIZED'; const aggregated = `WITH observations AS ${observationStorage} ( SELECT mp.match_id, mp.guid, mp.highest_round, mp.kills, mp.rounds_survived, mp.headshots, mp.revives, mp.downs, mp.completed, mp.best_score, mp.last_seen_at, ${countsAsGamePlayed} AS counts_as_game, m.started_at AS matchStartedAt, m.last_received_at AS matchUpdatedAt FROM leaderboard_match_players mp JOIN leaderboard_matches m ON m.id = mp.match_id WHERE ${conditions.join(' AND ')} ), totals AS MATERIALIZED ( SELECT guid, MAX(highest_round) AS highestRound, SUM(kills) AS kills, SUM(rounds_survived) AS roundsSurvived, SUM(headshots) AS headshots, SUM(revives) AS revives, SUM(downs) AS downs, SUM(counts_as_game) AS gamesPlayed, SUM(completed) AS gamesCompleted, MAX(kills) AS bestKills, MAX(best_score) AS bestScore, MAX(last_seen_at) AS lastSeen FROM observations GROUP BY guid ), measures AS ( SELECT totals.*, 1.0 * headshots / NULLIF(kills, 0) AS headshotRate, 1.0 * kills / NULLIF(downs, 0) AS killsPerDown, 1.0 * kills / NULLIF(gamesPlayed, 0) AS averageKills, 1.0 * roundsSurvived / NULLIF(gamesPlayed, 0) AS averageRounds, 1.0 * gamesCompleted / NULLIF(gamesPlayed, 0) AS completionRate FROM totals ), identities AS ( SELECT guid, name, ROW_NUMBER() OVER (PARTITION BY guid ORDER BY last_seen_at DESC, game) AS recency FROM leaderboard_players WHERE game IN ('t5', 't6') ), ranked AS ( SELECT measures.*, p.name, RANK() OVER (ORDER BY measures.${metric} DESC) AS rank FROM measures JOIN identities p ON p.guid = measures.guid AND p.recency = 1 ${qualification} )`; let revision = -1; try { revision = (handle.prepare("SELECT revision FROM report_revisions WHERE topic='leaderboard'").get() as { revision: number }).revision; } catch { /* Standalone history tools can operate without cache metadata. */ } let cache = scopeCaches.get(handle); if (!cache) { cache = new Map(); scopeCaches.set(handle, cache); } const cacheKey = JSON.stringify([options.game ?? '', options.map ?? '', options.players ?? '', metric]); let saved = cache.get(cacheKey); if (!saved || revision < 0 || saved.revision !== revision) { const rows = handle.prepare(`${aggregated}, playerSummary AS ( SELECT COUNT(*) AS players, COALESCE(SUM(kills), 0) AS kills, COALESCE(SUM(roundsSurvived), 0) AS roundsSurvived, COALESCE(MAX(highestRound), 0) AS highestRound FROM totals ), matchSummary AS ( SELECT COUNT(DISTINCT match_id) AS matches, MIN(matchStartedAt) AS firstTrackedAt, MAX(matchUpdatedAt) AS updatedAt FROM observations ), metadata AS ( SELECT playerSummary.*, matchSummary.* FROM playerSummary CROSS JOIN matchSummary ) SELECT ranked.*, metadata.players AS summaryPlayers, metadata.matches AS summaryMatches, metadata.kills AS summaryKills, metadata.roundsSurvived AS summaryRounds, metadata.highestRound AS summaryHighestRound, metadata.firstTrackedAt AS summaryFirstTrackedAt, metadata.updatedAt AS summaryUpdatedAt FROM metadata LEFT JOIN ranked ON 1 ORDER BY ranked.${metric} DESC, ranked.name COLLATE NOCASE, ranked.name, ranked.guid`) .all(...parameters) as LeaderboardRow[]; const rankedEntries = rows.filter((row): row is LeaderboardRow & { guid: string } => row.guid !== null) .map(({ matchingTotal, summaryPlayers, summaryMatches, summaryKills, summaryRounds, summaryHighestRound, summaryFirstTrackedAt, summaryUpdatedAt, ...entry }) => entry); const metadata = rows[0]; const summary: LeaderboardResponse['summary'] = { players: metadata.summaryPlayers, matches: metadata.summaryMatches, kills: metadata.summaryKills, roundsSurvived: metadata.summaryRounds, highestRound: metadata.summaryHighestRound, firstTrackedAt: metadata.summaryFirstTrackedAt, updatedAt: metadata.summaryUpdatedAt }; // Keep qualifying map variants available, even after their server is removed. const maps = handle.prepare(`SELECT map_key AS key, game, map, map_name AS name FROM leaderboard_matches WHERE game IN ('t5', 't6') AND status = 'completed' AND highest_round >= ${MIN_RECORDED_ROUND}${options.game ? ' AND game = ?' : ''} GROUP BY game, map_key HAVING id = MAX(id) ORDER BY game, name COLLATE NOCASE, map_key`) .all(...(options.game ? [options.game] : [])) as LeaderboardMap[]; saved = { revision, entries: rankedEntries, maps, summary }; cache.delete(cacheKey); if (rankedEntries.length <= 100_000) cache.set(cacheKey, saved); // Bound total retained players as well as the number of scopes. while (cache.size > 8 || [...cache.values()].reduce((n, scope) => n + scope.entries.length, 0) > 100_000) cache.delete(cache.keys().next().value!); } if (exactGuid !== undefined) { const entry = saved.entries.find(entry => entry.guid === exactGuid); return entry ? addRecordSources(handle, [entry], options)[0] : null; } const query = searchText(options.q?.trim() ?? ''); const matching = query ? saved.entries.filter(entry => searchText(entry.name).includes(query) || searchText(entry.guid).includes(query)) : saved.entries; const entries = addRecordSources(handle, matching.slice(offset, offset + limit), options); return { entries, maps: saved.maps, total: matching.length, limit, offset, metric, summary: saved.summary }; }