84 lines
4.1 KiB
TypeScript
84 lines
4.1 KiB
TypeScript
import type Database from 'better-sqlite3';
|
|
import type {
|
|
LeaderboardPlayerCount,
|
|
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;
|
|
players?: LeaderboardPlayerCount;
|
|
}
|
|
|
|
/** 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.players ?? null,
|
|
request.highestRound,
|
|
request.bestKills,
|
|
request.bestScore
|
|
]);
|
|
const rows = handle
|
|
.prepare(
|
|
`WITH requested (requestId, guid, game, map, players, 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 (r.players IS NULL OR m.start_players = r.players)
|
|
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;
|
|
}
|