Link personal bests to their recorded matches
This commit is contained in:
@@ -91,8 +91,8 @@ export function recordLeaderboardTelemetry(handle: Database.Database, report: Ga
|
||||
last_seen_at = MAX(last_seen_at, excluded.last_seen_at)`);
|
||||
const savePlayer = handle.prepare(`INSERT INTO leaderboard_match_players
|
||||
(match_id, guid, highest_round, rounds_survived, kills, headshots, downs, revives, best_score,
|
||||
last_round, present, has_stats, completed, first_seen_at, last_seen_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?)
|
||||
last_round, present, has_stats, completed, first_seen_at, last_seen_at, first_observed_round, present_at_start)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(match_id, guid) DO UPDATE SET
|
||||
highest_round = MAX(highest_round, excluded.highest_round),
|
||||
rounds_survived = rounds_survived + excluded.rounds_survived,
|
||||
@@ -113,7 +113,7 @@ export function recordLeaderboardTelemetry(handle: Database.Database, report: Ga
|
||||
const stats = 'kills' in player ? player : null;
|
||||
savePlayer.run(match.id, player.guid, round, survived,
|
||||
stats?.kills ?? 0, stats?.headshots ?? 0, stats?.downs ?? 0, stats?.revives ?? 0, stats?.score ?? 0,
|
||||
completedRounds, Number(!isStart), Number(report.event === 'game_end'), report.receivedAt, report.receivedAt);
|
||||
completedRounds, Number(!isStart), Number(report.event === 'game_end'), report.receivedAt, report.receivedAt, round, Number(isStart));
|
||||
}
|
||||
handle.prepare(`UPDATE leaderboard_matches SET highest_round = MAX(highest_round, ?),
|
||||
last_received_at = ?, last_event = ?, last_fingerprint = ?,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import type { PersonalBestMetric, PersonalBestRecords, RecordEvidence } from '../../leaderboard.js';
|
||||
import { MIN_RECORDED_ROUND } from '../../match-history.js';
|
||||
|
||||
interface RecordRequest extends Record<PersonalBestMetric, number> {
|
||||
guid: string;
|
||||
game?: string;
|
||||
map?: string;
|
||||
}
|
||||
|
||||
/** Fetch evidence only for requested players and scopes, never rank the whole fleet's history. */
|
||||
export function getPersonalBestRecords(handle: Database.Database, requests: RecordRequest[]): PersonalBestRecords[] {
|
||||
const result: PersonalBestRecords[] = [];
|
||||
// Page requests are at most 100 players; chunk unusually large profile map collections too.
|
||||
for (let offset = 0; offset < requests.length; offset += 100) {
|
||||
const batch = requests.slice(offset, offset + 100);
|
||||
const parameters = batch.flatMap((request, index) => [index, request.guid, request.game ?? null,
|
||||
request.map ?? null, request.highestRound, request.bestKills, request.bestScore]);
|
||||
const rows = handle.prepare(`WITH requested (requestId, guid, game, map, highestRound, bestKills, bestScore) AS (
|
||||
VALUES ${batch.map(() => '(?, ?, ?, ?, ?, ?, ?)').join(', ')}
|
||||
), candidates AS (
|
||||
SELECT r.requestId, m.id AS matchId, m.game, m.map_key AS mapKey, m.map_name AS mapName,
|
||||
m.server, m.started_at AS startedAt, m.ended_at AS endedAt, m.start_observed AS startObserved,
|
||||
mp.first_observed_round AS firstObservedRound, mp.present_at_start AS presentAtStart,
|
||||
COALESCE(m.ended_at, m.last_received_at) AS completionOrder,
|
||||
mp.highest_round AS highestRound, mp.kills AS bestKills, mp.best_score AS bestScore,
|
||||
r.highestRound AS targetRound, r.bestKills AS targetKills, r.bestScore AS targetScore
|
||||
FROM requested r JOIN leaderboard_match_players mp ON mp.guid = r.guid
|
||||
JOIN leaderboard_matches m ON m.id = mp.match_id
|
||||
WHERE m.game IN ('t5', 't6') AND m.status = 'completed' AND mp.has_stats = 1
|
||||
AND m.highest_round >= ${MIN_RECORDED_ROUND} AND mp.highest_round >= ${MIN_RECORDED_ROUND}
|
||||
AND (r.game IS NULL OR m.game = r.game) AND (r.map IS NULL OR m.map_key = r.map)
|
||||
AND (mp.highest_round = r.highestRound OR mp.kills = r.bestKills OR mp.best_score = r.bestScore)
|
||||
), valuesByMetric AS (
|
||||
SELECT *, 'highestRound' AS metric, highestRound AS value FROM candidates WHERE highestRound = targetRound
|
||||
UNION ALL SELECT *, 'bestKills', bestKills FROM candidates WHERE bestKills = targetKills
|
||||
UNION ALL SELECT *, 'bestScore', bestScore FROM candidates WHERE bestScore = targetScore
|
||||
), ranked AS (
|
||||
SELECT *, ROW_NUMBER() OVER (PARTITION BY requestId, metric ORDER BY completionOrder, matchId) AS choice
|
||||
FROM valuesByMetric
|
||||
) SELECT requestId, metric, value, matchId, game, mapKey, mapName, server, startedAt, endedAt,
|
||||
startObserved, firstObservedRound, presentAtStart FROM ranked WHERE choice = 1`)
|
||||
.all(...parameters) as Array<Omit<RecordEvidence, 'startObserved' | 'presentAtStart'> & {
|
||||
requestId: number; metric: PersonalBestMetric; startObserved: number; presentAtStart: number | null;
|
||||
}>;
|
||||
const records = batch.map(() => ({} as PersonalBestRecords));
|
||||
for (const { requestId, metric, startObserved, presentAtStart, ...record } of rows) {
|
||||
records[requestId][metric] = { ...record, startObserved: Boolean(startObserved),
|
||||
presentAtStart: presentAtStart === null ? null : Boolean(presentAtStart) };
|
||||
}
|
||||
result.push(...records);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { db } from '../db/index.js';
|
||||
import { MIN_RECORDED_ROUND } from '../../match-history.js';
|
||||
import { getPersonalBestRecords } from './leaderboard-records.js';
|
||||
import { leaderboardMetrics, type LeaderboardEntry, type LeaderboardMap, type LeaderboardMetric,
|
||||
type LeaderboardOptions, type LeaderboardResponse } from '../../leaderboard.js';
|
||||
|
||||
@@ -41,9 +42,13 @@ export function getLeaderboard(options: LeaderboardOptions = {}): LeaderboardRes
|
||||
const query = options.q?.trim() ?? '';
|
||||
const search = query ? 'WHERE instr(lower(name), lower(?)) > 0 OR instr(lower(guid), lower(?)) > 0' : '';
|
||||
const searchedParameters = query ? [...parameters, query, query] : parameters;
|
||||
const entries = handle.prepare(`${aggregated} SELECT * FROM ranked ${search}
|
||||
const rankedEntries = handle.prepare(`${aggregated} SELECT * FROM ranked ${search}
|
||||
ORDER BY ${metric} DESC, name COLLATE NOCASE, name, guid LIMIT ? OFFSET ?`)
|
||||
.all(...searchedParameters, limit, offset) as LeaderboardEntry[];
|
||||
.all(...searchedParameters, limit, offset) as Array<Omit<LeaderboardEntry, 'records'>>;
|
||||
const records = getPersonalBestRecords(handle, rankedEntries.map(entry => ({
|
||||
...entry, game: options.game || undefined, map: options.map || undefined
|
||||
})));
|
||||
const entries = rankedEntries.map((entry, index) => ({ ...entry, records: records[index] }));
|
||||
const { total } = handle.prepare(`${aggregated} SELECT COUNT(*) AS total FROM ranked ${search}`)
|
||||
.get(...searchedParameters) as { total: number };
|
||||
const summary = handle.prepare(`SELECT
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { db } from '../db/index.js';
|
||||
import { MIN_RECORDED_ROUND } from '../../match-history.js';
|
||||
import { getPersonalBestRecords } from './leaderboard-records.js';
|
||||
import type { PlayerProfile, PlayerStats, PlayerMapRecord, PlayerMatch } from '../../leaderboard.js';
|
||||
|
||||
const totals = `MAX(mp.highest_round) AS highestRound, SUM(mp.kills) AS kills,
|
||||
@@ -23,15 +24,18 @@ export function getPlayerProfile(guid: string, requestedOffset = 0): PlayerProfi
|
||||
const limit = 25;
|
||||
const lastOffset = Math.floor((stats.gamesPlayed - 1) / limit) * limit;
|
||||
const offset = Number.isSafeInteger(requestedOffset) ? Math.min(lastOffset, Math.max(0, requestedOffset)) : 0;
|
||||
const maps = handle.prepare(`SELECT m.game, m.map_key AS key, m.map_name AS name,
|
||||
const mapStats = handle.prepare(`SELECT m.game, m.map_key AS key, m.map_name AS name,
|
||||
MAX(mp.last_seen_at) AS lastSeen, ${totals} ${scope}
|
||||
GROUP BY m.game, m.map_key ORDER BY highestRound DESC, kills DESC, name COLLATE NOCASE, m.game, key`)
|
||||
.all(guid) as PlayerMapRecord[];
|
||||
.all(guid) as Array<Omit<PlayerMapRecord, 'records'>>;
|
||||
const [records, ...mapRecords] = getPersonalBestRecords(handle, [{ guid, ...stats },
|
||||
...mapStats.map(map => ({ guid, ...map, map: map.key }))]);
|
||||
const maps = mapStats.map((map, index) => ({ ...map, records: mapRecords[index] }));
|
||||
const matches = handle.prepare(`SELECT m.id, m.game, m.server, m.server_deleted_at, m.map_name AS mapName,
|
||||
m.started_at AS startedAt, mp.last_seen_at AS lastSeen, m.status, mp.completed,
|
||||
mp.highest_round AS highestRound, mp.rounds_survived AS roundsSurvived,
|
||||
mp.kills, mp.headshots, mp.revives, mp.downs, mp.best_score AS bestScore
|
||||
${scope} ORDER BY mp.last_seen_at DESC, m.id DESC LIMIT ? OFFSET ?`)
|
||||
.all(guid, limit, offset) as PlayerMatch[];
|
||||
return { ...player, stats, maps, matches, offset, limit };
|
||||
return { ...player, stats, records, maps, matches, offset, limit };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user