fix(backend): dedup per-vehicle rows; add vehicle lineup, tournament, duration, draw

This commit is contained in:
FURRO404
2026-06-18 00:31:42 -07:00
parent 8965c3c19b
commit dc3a0b33f4
+267 -139
View File
@@ -79,6 +79,11 @@ struct LimitQuery {
limit: Option<u32>, limit: Option<u32>,
} }
#[derive(Deserialize)]
struct LangQuery {
lang: Option<String>,
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct ResolveQuery { struct ResolveQuery {
name: String, name: String,
@@ -222,6 +227,11 @@ struct GameRow {
player_count: i64, player_count: i64,
winning_team: Option<String>, winning_team: Option<String>,
losing_team: Option<String>, losing_team: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
tournament_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
duration: Option<f64>,
draw: bool,
stats: GameStats, stats: GameStats,
} }
@@ -251,9 +261,17 @@ struct GameParticipant {
struct GamePlayer { struct GamePlayer {
uid: String, uid: String,
nick: Option<String>, nick: Option<String>,
vehicles: Vec<Vehicle>,
stats: GameStats, stats: GameStats,
} }
#[derive(Serialize)]
struct Vehicle {
cdk: String,
name: String,
icon: String,
}
#[derive(Serialize)] #[derive(Serialize)]
struct PlayerSearchResponse { struct PlayerSearchResponse {
players: Vec<PlayerRef>, players: Vec<PlayerRef>,
@@ -535,12 +553,14 @@ async fn recent_games(
async fn game_detail( async fn game_detail(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
Path(session_id): Path<String>, Path(session_id): Path<String>,
Query(query): Query<LangQuery>,
) -> ApiResult<GameResponse> { ) -> ApiResult<GameResponse> {
let session_id = validate_session_id(&session_id)?; let session_id = validate_session_id(&session_id)?;
let lang = query.lang.as_deref().unwrap_or("en");
let battles_conn = open_db(&state.battles_db)?; let battles_conn = open_db(&state.battles_db)?;
let game = game_for(&battles_conn, session_id)? let game = game_for(&battles_conn, session_id)?
.ok_or_else(|| ApiError::not_found("Game not found"))?; .ok_or_else(|| ApiError::not_found("Game not found"))?;
let participants = game_participants_for(&battles_conn, session_id)?; let participants = game_participants_for(&battles_conn, session_id, &state, lang)?;
Ok(Json(GameResponse { game, participants })) Ok(Json(GameResponse { game, participants }))
} }
@@ -1164,30 +1184,48 @@ fn period_history_for(conn: &Connection, team_name: &str) -> Result<Vec<PeriodHi
} }
fn games_for(conn: &Connection, team_name: &str) -> Result<Vec<GameRow>, ApiError> { fn games_for(conn: &Connection, team_name: &str) -> Result<Vec<GameRow>, ApiError> {
// Reduce per-vehicle duplicate rows to one row per UID (MAX) before summing.
let mut stmt = conn let mut stmt = conn
.prepare( .prepare(
"SELECT "WITH per_player AS (
p.session_id, SELECT session_id, UID,
COALESCE(m.endtime_unix, MAX(p.endtime_unix), 0) AS timestamp, MAX(endtime_unix) AS endtime_unix,
MAX(CASE WHEN victor_bool = 'Win' THEN 1 ELSE 0 END) AS won,
MAX(ground_kills) AS ground_kills,
MAX(air_kills) AS air_kills,
MAX(assists) AS assists,
MAX(captures) AS captures,
MAX(deaths) AS deaths,
MAX(score) AS score,
MAX(missile_evades) AS missile_evades,
MAX(shell_interceptions) AS shell_interceptions,
MAX(team_kills_stat) AS team_kills_stat
FROM player_games_hist
WHERE team_name = ?1 COLLATE NOCASE
GROUP BY session_id, UID
)
SELECT
pp.session_id,
COALESCE(m.endtime_unix, MAX(pp.endtime_unix), 0) AS timestamp,
m.mission_name, m.mission_name,
m.mission_mode, m.mission_mode,
CASE m.tournament_name,
WHEN MAX(CASE WHEN p.victor_bool = 'Win' THEN 1 ELSE 0 END) = 1 THEN 'Win' m.duration,
ELSE 'Loss' COALESCE(m.draw, 0),
END AS result, CASE WHEN MAX(pp.won) = 1 THEN 'Win' ELSE 'Loss' END AS result,
COUNT(DISTINCT p.UID), COUNT(*),
COALESCE(SUM(p.ground_kills), 0), COALESCE(SUM(pp.ground_kills), 0),
COALESCE(SUM(p.air_kills), 0), COALESCE(SUM(pp.air_kills), 0),
COALESCE(SUM(p.assists), 0), COALESCE(SUM(pp.assists), 0),
COALESCE(SUM(p.captures), 0), COALESCE(SUM(pp.captures), 0),
COALESCE(SUM(p.deaths), 0), COALESCE(SUM(pp.deaths), 0),
COALESCE(SUM(p.score), 0), COALESCE(SUM(pp.score), 0),
COALESCE(SUM(p.missile_evades), 0), COALESCE(SUM(pp.missile_evades), 0),
COALESCE(SUM(p.shell_interceptions), 0), COALESCE(SUM(pp.shell_interceptions), 0),
COALESCE(SUM(p.team_kills_stat), 0), COALESCE(SUM(pp.team_kills_stat), 0),
(SELECT pg.team_name (SELECT pg.team_name
FROM player_games_hist pg FROM player_games_hist pg
WHERE pg.session_id = p.session_id WHERE pg.session_id = pp.session_id
AND pg.team_name IS NOT NULL AND pg.team_name IS NOT NULL
AND pg.team_name != '' AND pg.team_name != ''
AND pg.victor_bool = 'Win' AND pg.victor_bool = 'Win'
@@ -1196,17 +1234,16 @@ fn games_for(conn: &Connection, team_name: &str) -> Result<Vec<GameRow>, ApiErro
LIMIT 1), LIMIT 1),
(SELECT pg.team_name (SELECT pg.team_name
FROM player_games_hist pg FROM player_games_hist pg
WHERE pg.session_id = p.session_id WHERE pg.session_id = pp.session_id
AND pg.team_name IS NOT NULL AND pg.team_name IS NOT NULL
AND pg.team_name != '' AND pg.team_name != ''
AND pg.victor_bool = 'Loss' AND pg.victor_bool = 'Loss'
GROUP BY pg.team_name COLLATE NOCASE GROUP BY pg.team_name COLLATE NOCASE
ORDER BY COUNT(DISTINCT pg.UID) DESC, pg.team_name COLLATE NOCASE ORDER BY COUNT(DISTINCT pg.UID) DESC, pg.team_name COLLATE NOCASE
LIMIT 1) LIMIT 1)
FROM player_games_hist p FROM per_player pp
LEFT JOIN match_summary m ON m.session_id = p.session_id LEFT JOIN match_summary m ON m.session_id = pp.session_id
WHERE p.team_name = ?1 COLLATE NOCASE GROUP BY pp.session_id
GROUP BY p.session_id
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT 100", LIMIT 100",
) )
@@ -1218,6 +1255,7 @@ fn games_for(conn: &Connection, team_name: &str) -> Result<Vec<GameRow>, ApiErro
let timestamp: i64 = row.get(1)?; let timestamp: i64 = row.get(1)?;
let map_name: Option<String> = row.get(2)?; let map_name: Option<String> = row.get(2)?;
let mission_mode: Option<String> = row.get(3)?; let mission_mode: Option<String> = row.get(3)?;
let draw_int: i64 = row.get(6)?;
Ok(GameRow { Ok(GameRow {
team_name: None, team_name: None,
session_id, session_id,
@@ -1225,20 +1263,23 @@ fn games_for(conn: &Connection, team_name: &str) -> Result<Vec<GameRow>, ApiErro
endtime_unix: timestamp, endtime_unix: timestamp,
map_name, map_name,
mission_mode, mission_mode,
result: row.get(4)?, result: row.get(7)?,
player_count: row.get(5)?, player_count: row.get(8)?,
winning_team: row.get(15)?, winning_team: row.get(18)?,
losing_team: row.get(16)?, losing_team: row.get(19)?,
tournament_name: row.get(4)?,
duration: row.get(5)?,
draw: draw_int != 0,
stats: GameStats { stats: GameStats {
ground_kills: row.get(6)?, ground_kills: row.get(9)?,
air_kills: row.get(7)?, air_kills: row.get(10)?,
assists: row.get(8)?, assists: row.get(11)?,
captures: row.get(9)?, captures: row.get(12)?,
deaths: row.get(10)?, deaths: row.get(13)?,
score: row.get(11)?, score: row.get(14)?,
missile_evades: row.get(12)?, missile_evades: row.get(15)?,
shell_interceptions: row.get(13)?, shell_interceptions: row.get(16)?,
team_kills_stat: row.get(14)?, team_kills_stat: row.get(17)?,
}, },
}) })
}) })
@@ -1252,36 +1293,54 @@ fn recent_games_for(conn: &Connection, limit: i64) -> Result<Vec<GameRow>, ApiEr
let mut stmt = conn let mut stmt = conn
.prepare( .prepare(
"WITH recent AS ( "WITH recent AS (
SELECT team_name, session_id, MAX(endtime_unix) AS timestamp SELECT session_id, MAX(endtime_unix) AS timestamp
FROM player_games_hist FROM player_games_hist
WHERE team_name IS NOT NULL AND team_name != '' WHERE team_name IS NOT NULL AND team_name != ''
GROUP BY session_id GROUP BY session_id
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT ?1 LIMIT ?1
),
per_player AS (
SELECT session_id, team_name, UID,
MAX(endtime_unix) AS endtime_unix,
MAX(CASE WHEN victor_bool = 'Win' THEN 1 ELSE 0 END) AS won,
MAX(ground_kills) AS ground_kills,
MAX(air_kills) AS air_kills,
MAX(assists) AS assists,
MAX(captures) AS captures,
MAX(deaths) AS deaths,
MAX(score) AS score,
MAX(missile_evades) AS missile_evades,
MAX(shell_interceptions) AS shell_interceptions,
MAX(team_kills_stat) AS team_kills_stat
FROM player_games_hist
WHERE session_id IN (SELECT session_id FROM recent)
AND team_name IS NOT NULL AND team_name != ''
GROUP BY session_id, team_name COLLATE NOCASE, UID
) )
SELECT SELECT
r.team_name, pp.team_name,
r.session_id, pp.session_id,
COALESCE(m.endtime_unix, r.timestamp, 0) AS timestamp, COALESCE(m.endtime_unix, MAX(pp.endtime_unix), 0) AS timestamp,
m.mission_name, m.mission_name,
m.mission_mode, m.mission_mode,
CASE m.tournament_name,
WHEN MAX(CASE WHEN p.victor_bool = 'Win' THEN 1 ELSE 0 END) = 1 THEN 'Win' m.duration,
ELSE 'Loss' COALESCE(m.draw, 0),
END AS result, CASE WHEN MAX(pp.won) = 1 THEN 'Win' ELSE 'Loss' END AS result,
COUNT(DISTINCT p.UID), COUNT(*),
COALESCE(SUM(p.ground_kills), 0), COALESCE(SUM(pp.ground_kills), 0),
COALESCE(SUM(p.air_kills), 0), COALESCE(SUM(pp.air_kills), 0),
COALESCE(SUM(p.assists), 0), COALESCE(SUM(pp.assists), 0),
COALESCE(SUM(p.captures), 0), COALESCE(SUM(pp.captures), 0),
COALESCE(SUM(p.deaths), 0), COALESCE(SUM(pp.deaths), 0),
COALESCE(SUM(p.score), 0), COALESCE(SUM(pp.score), 0),
COALESCE(SUM(p.missile_evades), 0), COALESCE(SUM(pp.missile_evades), 0),
COALESCE(SUM(p.shell_interceptions), 0), COALESCE(SUM(pp.shell_interceptions), 0),
COALESCE(SUM(p.team_kills_stat), 0), COALESCE(SUM(pp.team_kills_stat), 0),
(SELECT pg.team_name (SELECT pg.team_name
FROM player_games_hist pg FROM player_games_hist pg
WHERE pg.session_id = r.session_id WHERE pg.session_id = pp.session_id
AND pg.team_name IS NOT NULL AND pg.team_name IS NOT NULL
AND pg.team_name != '' AND pg.team_name != ''
AND pg.victor_bool = 'Win' AND pg.victor_bool = 'Win'
@@ -1290,18 +1349,16 @@ fn recent_games_for(conn: &Connection, limit: i64) -> Result<Vec<GameRow>, ApiEr
LIMIT 1), LIMIT 1),
(SELECT pg.team_name (SELECT pg.team_name
FROM player_games_hist pg FROM player_games_hist pg
WHERE pg.session_id = r.session_id WHERE pg.session_id = pp.session_id
AND pg.team_name IS NOT NULL AND pg.team_name IS NOT NULL
AND pg.team_name != '' AND pg.team_name != ''
AND pg.victor_bool = 'Loss' AND pg.victor_bool = 'Loss'
GROUP BY pg.team_name COLLATE NOCASE GROUP BY pg.team_name COLLATE NOCASE
ORDER BY COUNT(DISTINCT pg.UID) DESC, pg.team_name COLLATE NOCASE ORDER BY COUNT(DISTINCT pg.UID) DESC, pg.team_name COLLATE NOCASE
LIMIT 1) LIMIT 1)
FROM recent r FROM per_player pp
JOIN player_games_hist p LEFT JOIN match_summary m ON m.session_id = pp.session_id
ON p.session_id = r.session_id AND p.team_name = r.team_name COLLATE NOCASE GROUP BY pp.team_name COLLATE NOCASE, pp.session_id
LEFT JOIN match_summary m ON m.session_id = r.session_id
GROUP BY r.team_name COLLATE NOCASE, r.session_id
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT ?1", LIMIT ?1",
) )
@@ -1310,6 +1367,7 @@ fn recent_games_for(conn: &Connection, limit: i64) -> Result<Vec<GameRow>, ApiEr
let rows = stmt let rows = stmt
.query_map(params![limit], |row| { .query_map(params![limit], |row| {
let timestamp: i64 = row.get(2)?; let timestamp: i64 = row.get(2)?;
let draw_int: i64 = row.get(7)?;
Ok(GameRow { Ok(GameRow {
team_name: row.get(0)?, team_name: row.get(0)?,
session_id: row.get(1)?, session_id: row.get(1)?,
@@ -1317,20 +1375,23 @@ fn recent_games_for(conn: &Connection, limit: i64) -> Result<Vec<GameRow>, ApiEr
endtime_unix: timestamp, endtime_unix: timestamp,
map_name: row.get(3)?, map_name: row.get(3)?,
mission_mode: row.get(4)?, mission_mode: row.get(4)?,
result: row.get(5)?, result: row.get(8)?,
player_count: row.get(6)?, player_count: row.get(9)?,
winning_team: row.get(16)?, winning_team: row.get(19)?,
losing_team: row.get(17)?, losing_team: row.get(20)?,
tournament_name: row.get(5)?,
duration: row.get(6)?,
draw: draw_int != 0,
stats: GameStats { stats: GameStats {
ground_kills: row.get(7)?, ground_kills: row.get(10)?,
air_kills: row.get(8)?, air_kills: row.get(11)?,
assists: row.get(9)?, assists: row.get(12)?,
captures: row.get(10)?, captures: row.get(13)?,
deaths: row.get(11)?, deaths: row.get(14)?,
score: row.get(12)?, score: row.get(15)?,
missile_evades: row.get(13)?, missile_evades: row.get(16)?,
shell_interceptions: row.get(14)?, shell_interceptions: row.get(17)?,
team_kills_stat: row.get(15)?, team_kills_stat: row.get(18)?,
}, },
}) })
}) })
@@ -1342,25 +1403,31 @@ fn recent_games_for(conn: &Connection, limit: i64) -> Result<Vec<GameRow>, ApiEr
} }
fn game_for(conn: &Connection, session_id: &str) -> Result<Option<GameRow>, ApiError> { fn game_for(conn: &Connection, session_id: &str) -> Result<Option<GameRow>, ApiError> {
// player_games_hist stores one row per used vehicle per player, with the
// per-player stats duplicated across those rows. Reduce to one row per UID
// (MAX, since the values are identical) BEFORE summing team/game totals.
conn.query_row( conn.query_row(
"SELECT "SELECT
p.session_id, ?1 AS session_id,
COALESCE(m.endtime_unix, MAX(p.endtime_unix), 0) AS timestamp, COALESCE(m.endtime_unix, agg.timestamp, 0) AS timestamp,
m.mission_name, m.mission_name,
m.mission_mode, m.mission_mode,
COUNT(DISTINCT p.UID), m.tournament_name,
COALESCE(SUM(p.ground_kills), 0), m.duration,
COALESCE(SUM(p.air_kills), 0), COALESCE(m.draw, 0),
COALESCE(SUM(p.assists), 0), agg.player_count,
COALESCE(SUM(p.captures), 0), agg.ground_kills,
COALESCE(SUM(p.deaths), 0), agg.air_kills,
COALESCE(SUM(p.score), 0), agg.assists,
COALESCE(SUM(p.missile_evades), 0), agg.captures,
COALESCE(SUM(p.shell_interceptions), 0), agg.deaths,
COALESCE(SUM(p.team_kills_stat), 0), agg.score,
agg.missile_evades,
agg.shell_interceptions,
agg.team_kills_stat,
(SELECT pg.team_name (SELECT pg.team_name
FROM player_games_hist pg FROM player_games_hist pg
WHERE pg.session_id = p.session_id WHERE pg.session_id = ?1
AND pg.team_name IS NOT NULL AND pg.team_name IS NOT NULL
AND pg.team_name != '' AND pg.team_name != ''
AND pg.victor_bool = 'Win' AND pg.victor_bool = 'Win'
@@ -1369,20 +1436,49 @@ fn game_for(conn: &Connection, session_id: &str) -> Result<Option<GameRow>, ApiE
LIMIT 1), LIMIT 1),
(SELECT pg.team_name (SELECT pg.team_name
FROM player_games_hist pg FROM player_games_hist pg
WHERE pg.session_id = p.session_id WHERE pg.session_id = ?1
AND pg.team_name IS NOT NULL AND pg.team_name IS NOT NULL
AND pg.team_name != '' AND pg.team_name != ''
AND pg.victor_bool = 'Loss' AND pg.victor_bool = 'Loss'
GROUP BY pg.team_name COLLATE NOCASE GROUP BY pg.team_name COLLATE NOCASE
ORDER BY COUNT(DISTINCT pg.UID) DESC, pg.team_name COLLATE NOCASE ORDER BY COUNT(DISTINCT pg.UID) DESC, pg.team_name COLLATE NOCASE
LIMIT 1) LIMIT 1)
FROM player_games_hist p FROM (
LEFT JOIN match_summary m ON m.session_id = p.session_id SELECT
WHERE p.session_id = ?1 COUNT(*) AS player_count,
GROUP BY p.session_id", MAX(endtime_unix) AS timestamp,
COALESCE(SUM(ground_kills), 0) AS ground_kills,
COALESCE(SUM(air_kills), 0) AS air_kills,
COALESCE(SUM(assists), 0) AS assists,
COALESCE(SUM(captures), 0) AS captures,
COALESCE(SUM(deaths), 0) AS deaths,
COALESCE(SUM(score), 0) AS score,
COALESCE(SUM(missile_evades), 0) AS missile_evades,
COALESCE(SUM(shell_interceptions), 0) AS shell_interceptions,
COALESCE(SUM(team_kills_stat), 0) AS team_kills_stat
FROM (
SELECT UID,
MAX(endtime_unix) AS endtime_unix,
MAX(ground_kills) AS ground_kills,
MAX(air_kills) AS air_kills,
MAX(assists) AS assists,
MAX(captures) AS captures,
MAX(deaths) AS deaths,
MAX(score) AS score,
MAX(missile_evades) AS missile_evades,
MAX(shell_interceptions) AS shell_interceptions,
MAX(team_kills_stat) AS team_kills_stat
FROM player_games_hist
WHERE session_id = ?1
GROUP BY UID
)
) agg
LEFT JOIN match_summary m ON m.session_id = ?1
WHERE agg.player_count > 0",
params![session_id], params![session_id],
|row| { |row| {
let timestamp: i64 = row.get(1)?; let timestamp: i64 = row.get(1)?;
let draw_int: i64 = row.get(6)?;
Ok(GameRow { Ok(GameRow {
team_name: None, team_name: None,
session_id: row.get(0)?, session_id: row.get(0)?,
@@ -1391,19 +1487,22 @@ fn game_for(conn: &Connection, session_id: &str) -> Result<Option<GameRow>, ApiE
map_name: row.get(2)?, map_name: row.get(2)?,
mission_mode: row.get(3)?, mission_mode: row.get(3)?,
result: String::new(), result: String::new(),
player_count: row.get(4)?, player_count: row.get(7)?,
winning_team: row.get(14)?, winning_team: row.get(17)?,
losing_team: row.get(15)?, losing_team: row.get(18)?,
tournament_name: row.get(4)?,
duration: row.get(5)?,
draw: draw_int != 0,
stats: GameStats { stats: GameStats {
ground_kills: row.get(5)?, ground_kills: row.get(8)?,
air_kills: row.get(6)?, air_kills: row.get(9)?,
assists: row.get(7)?, assists: row.get(10)?,
captures: row.get(8)?, captures: row.get(11)?,
deaths: row.get(9)?, deaths: row.get(12)?,
score: row.get(10)?, score: row.get(13)?,
missile_evades: row.get(11)?, missile_evades: row.get(14)?,
shell_interceptions: row.get(12)?, shell_interceptions: row.get(15)?,
team_kills_stat: row.get(13)?, team_kills_stat: row.get(16)?,
}, },
}) })
}, },
@@ -1415,34 +1514,44 @@ fn game_for(conn: &Connection, session_id: &str) -> Result<Option<GameRow>, ApiE
fn game_participants_for( fn game_participants_for(
conn: &Connection, conn: &Connection,
session_id: &str, session_id: &str,
state: &AppState,
lang: &str,
) -> Result<Vec<GameParticipant>, ApiError> { ) -> Result<Vec<GameParticipant>, ApiError> {
let mut stmt = conn let mut stmt = conn
.prepare( .prepare(
"SELECT "SELECT
p.team_name, pp.team_name,
CASE CASE WHEN MAX(pp.won) = 1 THEN 'Win' ELSE 'Loss' END AS result,
WHEN MAX(CASE WHEN p.victor_bool = 'Win' THEN 1 ELSE 0 END) = 1 THEN 'Win' COUNT(*),
ELSE 'Loss' COALESCE(SUM(pp.ground_kills), 0),
END AS result, COALESCE(SUM(pp.air_kills), 0),
COUNT(DISTINCT p.UID), COALESCE(SUM(pp.assists), 0),
COALESCE(SUM(p.ground_kills), 0), COALESCE(SUM(pp.captures), 0),
COALESCE(SUM(p.air_kills), 0), COALESCE(SUM(pp.deaths), 0),
COALESCE(SUM(p.assists), 0), COALESCE(SUM(pp.score), 0),
COALESCE(SUM(p.captures), 0), COALESCE(SUM(pp.missile_evades), 0),
COALESCE(SUM(p.deaths), 0), COALESCE(SUM(pp.shell_interceptions), 0),
COALESCE(SUM(p.score), 0), COALESCE(SUM(pp.team_kills_stat), 0)
COALESCE(SUM(p.missile_evades), 0), FROM (
COALESCE(SUM(p.shell_interceptions), 0), SELECT UID, team_name,
COALESCE(SUM(p.team_kills_stat), 0) MAX(CASE WHEN victor_bool = 'Win' THEN 1 ELSE 0 END) AS won,
FROM player_games_hist p MAX(ground_kills) AS ground_kills,
WHERE p.session_id = ?1 AND p.team_name IS NOT NULL AND p.team_name != '' MAX(air_kills) AS air_kills,
GROUP BY p.team_name COLLATE NOCASE MAX(assists) AS assists,
MAX(captures) AS captures,
MAX(deaths) AS deaths,
MAX(score) AS score,
MAX(missile_evades) AS missile_evades,
MAX(shell_interceptions) AS shell_interceptions,
MAX(team_kills_stat) AS team_kills_stat
FROM player_games_hist
WHERE session_id = ?1 AND team_name IS NOT NULL AND team_name != ''
GROUP BY UID, team_name COLLATE NOCASE
) pp
GROUP BY pp.team_name COLLATE NOCASE
ORDER BY ORDER BY
CASE CASE WHEN MAX(pp.won) = 1 THEN 0 ELSE 1 END,
WHEN MAX(CASE WHEN p.victor_bool = 'Win' THEN 1 ELSE 0 END) = 1 THEN 0 pp.team_name COLLATE NOCASE",
ELSE 1
END,
p.team_name COLLATE NOCASE",
) )
.map_err(db_error)?; .map_err(db_error)?;
@@ -1471,7 +1580,8 @@ fn game_participants_for(
.map_err(db_error)?; .map_err(db_error)?;
for participant in &mut participants { for participant in &mut participants {
participant.players = game_players_for(conn, session_id, &participant.team_name)?; participant.players =
game_players_for(conn, session_id, &participant.team_name, state, lang)?;
} }
Ok(participants) Ok(participants)
@@ -1481,7 +1591,11 @@ fn game_players_for(
conn: &Connection, conn: &Connection,
session_id: &str, session_id: &str,
team_name: &str, team_name: &str,
state: &AppState,
lang: &str,
) -> Result<Vec<GamePlayer>, ApiError> { ) -> Result<Vec<GamePlayer>, ApiError> {
// Stats are duplicated across a player's per-vehicle rows, so take MAX (not
// SUM) per UID. vehicle_internal collected (DISTINCT, comma-joined) for the lineup.
let mut stmt = conn let mut stmt = conn
.prepare( .prepare(
"SELECT "SELECT
@@ -1494,27 +1608,41 @@ fn game_players_for(
AND pg.nick != '' AND pg.nick != ''
ORDER BY pg.endtime_unix DESC ORDER BY pg.endtime_unix DESC
LIMIT 1), LIMIT 1),
COALESCE(SUM(p.ground_kills), 0), MAX(p.ground_kills),
COALESCE(SUM(p.air_kills), 0), MAX(p.air_kills),
COALESCE(SUM(p.assists), 0), MAX(p.assists),
COALESCE(SUM(p.captures), 0), MAX(p.captures),
COALESCE(SUM(p.deaths), 0), MAX(p.deaths),
COALESCE(SUM(p.score), 0), MAX(p.score),
COALESCE(SUM(p.missile_evades), 0), MAX(p.missile_evades),
COALESCE(SUM(p.shell_interceptions), 0), MAX(p.shell_interceptions),
COALESCE(SUM(p.team_kills_stat), 0) MAX(p.team_kills_stat),
GROUP_CONCAT(DISTINCT p.vehicle_internal)
FROM player_games_hist p FROM player_games_hist p
WHERE p.session_id = ?1 AND p.team_name = ?2 COLLATE NOCASE WHERE p.session_id = ?1 AND p.team_name = ?2 COLLATE NOCASE
GROUP BY p.UID GROUP BY p.UID
ORDER BY COALESCE(SUM(p.score), 0) DESC, p.UID", ORDER BY MAX(p.score) DESC, p.UID",
) )
.map_err(db_error)?; .map_err(db_error)?;
let players = stmt let players = stmt
.query_map(params![session_id, team_name], |row| { .query_map(params![session_id, team_name], |row| {
let cdks: Option<String> = row.get(11)?;
let vehicles = cdks
.unwrap_or_default()
.split(',')
.map(|c| c.trim())
.filter(|c| !c.is_empty())
.map(|cdk| Vehicle {
name: lookup_vehicle_name(&state.vehicle_names, cdk, lang),
icon: lookup_vehicle_icon(&state.vehicle_icons, cdk),
cdk: cdk.to_string(),
})
.collect();
Ok(GamePlayer { Ok(GamePlayer {
uid: row.get(0)?, uid: row.get(0)?,
nick: row.get(1)?, nick: row.get(1)?,
vehicles,
stats: GameStats { stats: GameStats {
ground_kills: row.get(2)?, ground_kills: row.get(2)?,
air_kills: row.get(3)?, air_kills: row.get(3)?,