diff --git a/README.md b/README.md index 67fe7a5..c9e6dc0 100644 --- a/README.md +++ b/README.md @@ -375,7 +375,9 @@ and how to interpret missing or rejected reports. The **Leaderboard** records persistent player totals and map records from these same reports, including highest round, cumulative kills, rounds survived, headshots, revives, downs, and games played. See [Leaderboard](docs/leaderboard.md) -for counting rules, history import, and the read API. +for counting rules, history import, and the read API. Only completed games count; +in-progress and interrupted games remain in Match History without contributing +to player rankings or profile totals. In **Match History**, select **View match** on a completed match to open its shareable results page. It shows the map, server, final round, timing, and every diff --git a/docs/leaderboard.md b/docs/leaderboard.md index 0218d92..ae0ed66 100644 --- a/docs/leaderboard.md +++ b/docs/leaderboard.md @@ -5,9 +5,11 @@ with one entry per GUID combining Black Ops I (T5) and Black Ops II (T6). Game and map filters limit which matches contribute to each player's statistics and rank. Choose a ranking, search a name or GUID, and share the page URL with its filters intact. Results refresh while the page is visible. -Matches only appear in server/global history and leaderboard map options once -they reach round 2. Player records only contribute to leaderboards, summaries, -profiles and player history once that player is observed at round 2 or later. +Matches only appear in server/global history once they reach round 2. +Leaderboards, summaries, map options, profiles and player history include only +completed matches with a received `game_end`. Active and interrupted matches do +not contribute. A player must also be observed at round 2 or later; players who +left before a completed game's end retain their recorded stats. This also excludes existing round 1 records. Early telemetry is retained so qualifying games keep their start time, roster and cumulative counters. @@ -32,7 +34,7 @@ score are the highest reported values within the match, not necessarily its fina scoreboard. Active and interrupted matches remain listed without results links. Match pages remain available after the original server is removed. -The leaderboard uses the existing three telemetry events; no reporter change is +The leaderboard uses the existing telemetry events; no reporter change is required. The leaderboard and profiles identify a player by **GUID**, displaying the latest reported name across both games, regardless of game or map filters. Different names for the same GUID do not split their leaderboard entry or @@ -47,7 +49,7 @@ leaderboard and profiles require no storage migration. GUIDs stay strings. | Highest round | Highest round reported with that player present, including players who joined mid-game. | | Total kills, headshots, revives, downs | Sum of each player's highest reported counter in each game. Round snapshots are cumulative, so they are never added together within a game. | | Total rounds | Round progression during observed participation, added across games. A player observed at game start and then round 8 receives seven rounds. A player first seen at round 8 gets a highest-round record of 8, but zero total rounds; reaching round 9 then adds one. | -| Games played | Games in which a stats report at round 2 or later contains the player. Start-only and round 1 participation do not count. | +| Games played | Completed games in which a stats report at round 2 or later contains the player, even if they left before the end. Start-only and round 1 participation do not count. | | Games completed | Games whose `game_end` roster contains the player. Disconnected players retain earlier stats without receiving completion credit. | | Best game kills | Highest kill count recorded in any one game. | | Highest reported score | Highest observed score balance. Spending points can lower the score, so this is not cumulative points earned. | @@ -59,8 +61,8 @@ across both games when unfiltered. Summary cards describe the same selection, regardless of the search text. Player counts include each qualifying GUID once, including when they have records in both games. Summary rounds are player-rounds: four players progressing from game start to round 8 contribute 28. -Highest-round records always use the round reached, including the final round -and players who joined mid-game. +Highest-round records always use the round reached in a completed match, +including the final round and players who joined mid-game. Round totals require the player to appear in consecutive observations. If a report omits them, their return starts a new participation interval and the gap diff --git a/src/lib/server/fleet/leaderboard.ts b/src/lib/server/fleet/leaderboard.ts index 99013e5..2bc1570 100644 --- a/src/lib/server/fleet/leaderboard.ts +++ b/src/lib/server/fleet/leaderboard.ts @@ -16,7 +16,7 @@ export function getLeaderboard(options: LeaderboardOptions = {}): LeaderboardRes 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')", 'mp.has_stats = 1', `m.highest_round >= ${MIN_RECORDED_ROUND}`, + 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: string[] = []; if (options.game) { conditions.push('m.game = ?'); parameters.push(options.game); } @@ -55,7 +55,7 @@ export function getLeaderboard(options: LeaderboardOptions = {}): LeaderboardRes ${scoped}`).get(...parameters) as LeaderboardResponse['summary']; // 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') + 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`) diff --git a/src/lib/server/fleet/match-history.ts b/src/lib/server/fleet/match-history.ts index 5a4e6d2..9963213 100644 --- a/src/lib/server/fleet/match-history.ts +++ b/src/lib/server/fleet/match-history.ts @@ -53,6 +53,7 @@ export function getMatchDetail(id: number): MatchDetail | null { AND EXISTS (SELECT 1 FROM leaderboard_match_players recorded JOIN leaderboard_matches history ON history.id = recorded.match_id WHERE recorded.guid = p.guid AND history.game IN ('t5', 't6') AND recorded.has_stats = 1 + AND history.status = 'completed' AND history.highest_round >= ${MIN_RECORDED_ROUND} AND recorded.highest_round >= ${MIN_RECORDED_ROUND})) AS canViewProfile FROM leaderboard_match_players p WHERE p.match_id = ? diff --git a/src/lib/server/fleet/player-profile.ts b/src/lib/server/fleet/player-profile.ts index 4f30823..c3c4524 100644 --- a/src/lib/server/fleet/player-profile.ts +++ b/src/lib/server/fleet/player-profile.ts @@ -7,7 +7,7 @@ const totals = `MAX(mp.highest_round) AS highestRound, SUM(mp.kills) AS kills, SUM(mp.revives) AS revives, SUM(mp.downs) AS downs, COUNT(*) AS gamesPlayed, SUM(mp.completed) AS gamesCompleted, MAX(mp.kills) AS bestKills, MAX(mp.best_score) AS bestScore`; const scope = `FROM leaderboard_match_players mp JOIN leaderboard_matches m ON m.id = mp.match_id - WHERE m.game IN ('t5', 't6') AND mp.guid = ? AND mp.has_stats = 1 + WHERE m.game IN ('t5', 't6') AND m.status = 'completed' AND mp.guid = ? AND mp.has_stats = 1 AND m.highest_round >= ${MIN_RECORDED_ROUND} AND mp.highest_round >= ${MIN_RECORDED_ROUND}`; export function getPlayerProfile(guid: string, requestedOffset = 0): PlayerProfile | null { diff --git a/src/routes/leaderboard/+page.svelte b/src/routes/leaderboard/+page.svelte index 21ccd26..aef97ce 100644 --- a/src/routes/leaderboard/+page.svelte +++ b/src/routes/leaderboard/+page.svelte @@ -20,7 +20,7 @@ headshots: 'Cumulative headshots across recorded games.', revives: 'Cumulative teammates revived across recorded games.', downs: 'Cumulative downs across recorded games, ranked from most to fewest.', - gamesPlayed: 'Games with recorded player stats, including games in progress.', + gamesPlayed: 'Finished games with recorded player stats, including players who left before the end.', gamesCompleted: 'Games where the player appears in the final game-end report.', bestKills: 'The most kills reported for a player in a single game.', bestScore: 'The highest reported points balance. Spending points reduces this balance; it is not total points earned.' @@ -179,7 +179,7 @@
Each GUID has one leaderboard entry across Black Ops I and Black Ops II, so name changes keep a player's records together. The latest reported name across both games appears here and on the player's profile, regardless of filters.
-Records include active and completed games. Game and map filters choose which matches contribute to totals and rankings. Player search filters the ranked entries without changing their ranks. Player totals count each GUID once.
+Only completed games contribute to records. Games in progress and interrupted games stay out of totals and rankings. Players who left a completed game keep their recorded stats. Game and map filters choose which matches contribute; player search preserves ranks. Player totals count each GUID once.
Highest round includes mid-game joins. Total rounds counts round progression during observed participation, excluding earlier rounds. Joining at round 8 records a highest round of 8; reaching round 9 adds one to total rounds. The fleet total adds all players’ rounds.
Kills, headshots, revives, and downs accumulate across recorded games. Highest reported score is the largest observed points balance, not the total points a player has earned. Equal values share a rank.
Records cover telemetry saved since tracking began; older unrecorded games cannot be included.{#if data?.summary.firstTrackedAt}{' '}Tracking began for this selection.{/if}{#if data?.summary.updatedAt}{' '}Latest report: .{/if}
diff --git a/src/routes/leaderboard/players/[guid]/+page.svelte b/src/routes/leaderboard/players/[guid]/+page.svelte index a3bb682..fb07564 100644 --- a/src/routes/leaderboard/players/[guid]/+page.svelte +++ b/src/routes/leaderboard/players/[guid]/+page.svelte @@ -54,7 +54,7 @@Averages include games in progress. A dash means there is no denominator for that metric.
+Only completed games contribute. Completion rate measures how often the player appeared in the final report. A dash means there is no denominator for that metric.
Matches from both games, most recently observed first. Stats reflect this player's participation.
Completed matches from both games, most recently observed first. Stats reflect this player's participation.