record-daemon: add game start and end metadata
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m9s

This commit is contained in:
2026-03-24 18:16:53 +01:00
parent fc7ba40b30
commit f90e549b1e
9 changed files with 1878 additions and 98 deletions

View File

@@ -10,7 +10,7 @@ use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::protocol::Me
use tracing::{debug, error, info, trace, warn};
use super::auth::LockfileCredentials;
use super::events::{GameEvent, RawEvent};
use super::events::{GameEvent, GameflowSession, RawEvent};
use crate::error::{LqpError, Result};
/// Custom certificate verifier that accepts any certificate.
@@ -73,6 +73,7 @@ const SUBSCRIBE_ENDPOINTS: &[&str] = &[
"/lol-game-events/v1/game-events",
"/lol-champ-select/v1/session",
"/lol-lobby/v2/lobby",
"/lol-end-of-game/v1/eog-stats-block",
];
/// LQP REST API endpoints.
@@ -82,6 +83,10 @@ pub mod endpoints {
pub const CHAMPION_SELECT: &str = "/lol-champ-select/v1/session";
pub const SUMMONER: &str = "/lol-summoner/v1/current-summoner";
pub const GAME_STATS: &str = "/lol-end-of-game/v1/eog-stats-block";
pub const CHAMPION_SUMMARY: &str = "/lol-champ-select/v1/current-champion";
pub const RUNE_PAGES: &str = "/lol-perks/v1/currentpage";
pub const MATCH_HISTORY: &str = "/lol-match-history/v1/products/lol/current-summoner/matches";
pub const LIVE_CLIENT_DATA: &str = "/liveclientdata/allgamedata";
}
/// Game flow phase states.
@@ -135,6 +140,8 @@ pub struct ClientState {
pub game_id: Option<u64>,
/// Current champion name.
pub champion: Option<String>,
/// Current player's puuid.
pub local_puuid: Option<String>,
}
impl Default for ClientState {
@@ -143,6 +150,7 @@ impl Default for ClientState {
phase: GameflowPhase::None,
game_id: None,
champion: None,
local_puuid: None,
}
}
}
@@ -159,6 +167,8 @@ pub struct LqpClient {
http_client: reqwest::Client,
/// Shutdown signal.
shutdown: Arc<RwLock<bool>>,
/// Last emitted game ID for deduplication of GameStart events.
last_emitted_game_id: Arc<RwLock<Option<u64>>>,
}
impl LqpClient {
@@ -177,6 +187,7 @@ impl LqpClient {
event_sender,
http_client,
shutdown: Arc::new(RwLock::new(false)),
last_emitted_game_id: Arc::new(RwLock::new(None)),
}
}
@@ -214,6 +225,14 @@ impl LqpClient {
}
}
// Fetch local player's puuid for champion extraction
if let Ok(summoner) = self.get_summoner().await {
if let Some(puuid) = summoner.get("puuid").and_then(|p| p.as_str()) {
self.state.write().await.local_puuid = Some(puuid.to_string());
info!("Fetched local player puuid: {}", puuid);
}
}
Ok(())
}
@@ -222,6 +241,7 @@ impl LqpClient {
*self.shutdown.write().await = true;
*self.credentials.write().await = None;
*self.state.write().await = ClientState::default();
*self.last_emitted_game_id.write().await = None;
info!("Disconnected from League Client");
}
@@ -292,6 +312,7 @@ impl LqpClient {
let state = self.state.clone();
let shutdown = self.shutdown.clone();
let credentials = self.credentials.clone();
let last_emitted_game_id = self.last_emitted_game_id.clone();
// Spawn the message handler
tokio::spawn(async move {
@@ -306,10 +327,32 @@ impl LqpClient {
if text.is_empty() {
continue;
}
if let Some(event) = Self::parse_websocket_message(&text) {
// Get local_puuid from state for champion extraction
let local_puuid = state.read().await.local_puuid.clone();
if let Some(event) =
Self::parse_websocket_message(&text, local_puuid.as_deref())
{
// Update state based on event
Self::update_state_from_event(&state, &event).await;
// Check for duplicate GameStart events
if let GameEvent::GameStart(ref info) = event {
let mut last_game_id = last_emitted_game_id.write().await;
if *last_game_id == Some(info.game_id) {
info!(
"Skipping duplicate GameStart event for game_id={}",
info.game_id
);
continue;
}
*last_game_id = Some(info.game_id);
}
// Reset last_emitted_game_id on GameEnd to allow new game starts
if let GameEvent::GameEnd(_) = &event {
*last_emitted_game_id.write().await = None;
}
// Broadcast event
if event_sender.send(event.clone()).is_err() {
trace!("No event subscribers");
@@ -321,10 +364,32 @@ impl LqpClient {
// Try to parse as UTF-8
if let Ok(text) = String::from_utf8(data) {
if !text.is_empty() {
if let Some(event) = Self::parse_websocket_message(&text) {
// Get local_puuid from state for champion extraction
let local_puuid = state.read().await.local_puuid.clone();
if let Some(event) =
Self::parse_websocket_message(&text, local_puuid.as_deref())
{
// Update state based on event
Self::update_state_from_event(&state, &event).await;
// Check for duplicate GameStart events
if let GameEvent::GameStart(ref info) = event {
let mut last_game_id = last_emitted_game_id.write().await;
if *last_game_id == Some(info.game_id) {
info!(
"Skipping duplicate GameStart event for game_id={}",
info.game_id
);
continue;
}
*last_game_id = Some(info.game_id);
}
// Reset last_emitted_game_id on GameEnd to allow new game starts
if let GameEvent::GameEnd(_) = &event {
*last_emitted_game_id.write().await = None;
}
// Broadcast event
if event_sender.send(event.clone()).is_err() {
trace!("No event subscribers");
@@ -364,7 +429,7 @@ impl LqpClient {
}
/// Parse a WebSocket message into a game event.
fn parse_websocket_message(text: &str) -> Option<GameEvent> {
fn parse_websocket_message(text: &str, local_puuid: Option<&str>) -> Option<GameEvent> {
// Parse the message array format: [type, callback, data]
let value: serde_json::Value = match serde_json::from_str(text) {
Ok(v) => v,
@@ -397,6 +462,7 @@ impl LqpClient {
&raw_event.uri,
event_type,
&serde_json::to_value(raw_event.data).unwrap_or_default(),
local_puuid,
);
}
@@ -408,7 +474,7 @@ impl LqpClient {
.and_then(|t| t.as_str())
.unwrap_or("Update");
return Self::parse_event_from_uri(uri, event_type, data);
return Self::parse_event_from_uri(uri, event_type, data, local_puuid);
} else {
debug!("Unknown callback: {}", callback);
}
@@ -434,6 +500,7 @@ impl LqpClient {
uri: &str,
event_type: &str,
data: &serde_json::Value,
local_puuid: Option<&str>,
) -> Option<GameEvent> {
info!("Parsing event from URI: {} (type: {})", uri, event_type);
@@ -442,6 +509,22 @@ impl LqpClient {
let phase = data.as_str()?;
info!("Gameflow phase changed to: {}", phase);
// Only trigger GameEnd on EndOfGame phase (not WaitingForStats or PreEndOfGame)
// This ensures we wait for the stats to be available
if phase == "EndOfGame" {
info!("Game end phase detected: {}", phase);
// Generate a GameEnd event for timeline recording
return Some(
GameEvent::from_json(&serde_json::json!({
"eventType": "lcu-game-end",
"gameId": 0, // Will be filled from state if available
"victory": false, // Will be updated from end-of-game stats
"duration": 0.0
}))
.unwrap_or(GameEvent::Unknown),
);
}
// Update internal state based on phase
return Some(
GameEvent::from_json(&serde_json::json!({
@@ -461,17 +544,108 @@ impl LqpClient {
if phase == "InProgress" {
info!("Game is now in progress!");
// Extract game info
let game_id = data
// Try to parse the gameData into a GameflowSession struct
let session: Option<GameflowSession> = data
.get("gameData")
.and_then(|gd| gd.get("gameId"))
.and_then(|id| id.as_u64())
.unwrap_or(0);
.and_then(|gd| serde_json::from_value(gd.clone()).ok());
if let Some(ref session) = session {
debug!(
"Parsed GameflowSession: game_id={}, queue={:?}",
session.game_id,
session.queue_name()
);
} else {
debug!("Failed to parse gameData as GameflowSession, falling back to manual extraction");
}
// Extract game_id - prefer from parsed session, fallback to manual extraction
let game_id = session.as_ref().map(|s| s.game_id).unwrap_or_else(|| {
data.get("gameData")
.and_then(|gd| gd.get("gameId"))
.and_then(|id| id.as_u64())
.unwrap_or(0)
});
// Note: Champion, team, summoner_name will be extracted using puuid
// in handle_game_event when we have access to pregame_metadata
// Extract queue info (this is the same for all players)
let queue_type = session
.as_ref()
.and_then(|s| s.queue_name())
.map(|s| s.to_string())
.or_else(|| {
data.get("gameData")
.and_then(|gd| gd.get("queue"))
.and_then(|q| q.get("name"))
.and_then(|n| n.as_str())
.map(|s| s.to_string())
});
let queue_id = session.as_ref().and_then(|s| s.queue_id());
let game_mode = session
.as_ref()
.and_then(|s| s.game_mode())
.map(|s| s.to_string())
.or_else(|| {
data.get("gameData")
.and_then(|gd| gd.get("queue"))
.and_then(|q| q.get("gameMode"))
.and_then(|m| m.as_str())
.map(|s| s.to_string())
});
// Extract map name
let map_name = session
.as_ref()
.and_then(|s| s.map_id())
.and_then(|id| map_id_to_name(id as u64))
.or_else(|| {
data.get("gameData")
.and_then(|gd| gd.get("queue"))
.and_then(|q| q.get("mapId"))
.and_then(|id| id.as_u64())
.and_then(map_id_to_name)
});
info!("Extracted game metadata: game_id={}, queue={:?}, queue_id={:?}, mode={:?}, map={:?}",
game_id, queue_type, queue_id, game_mode, map_name);
// Extract player-specific data using puuid
let (champion, team, summoner_name) = if let Some(puuid) = local_puuid {
let champ_id = session.as_ref().and_then(|s| s.get_champion_id(puuid));
let team_id = session.as_ref().and_then(|s| s.get_team(puuid));
let summoner = session
.as_ref()
.and_then(|s| s.get_summoner_name(puuid))
.map(|s| s.to_string());
// Convert champion_id to champion name
let champ_name = champ_id.and_then(champion_id_to_name);
info!("Extracted player data via puuid: champion={:?}, team={:?}, summoner={:?}",
champ_name, team_id, summoner);
(champ_name, team_id, summoner)
} else {
info!("No local_puuid available, cannot extract player-specific data");
(None, None, None)
};
return Some(
GameEvent::from_json(&serde_json::json!({
"eventType": "lcu-game-start",
"gameId": game_id
"gameId": game_id,
"queueType": queue_type,
"queueId": queue_id,
"gameMode": game_mode,
"map": map_name,
"champion": champion,
"team": team,
"summonerName": summoner_name,
"session": session
}))
.unwrap_or(GameEvent::Unknown),
);
@@ -502,9 +676,172 @@ impl LqpClient {
// Handle champion select
if uri == "/lol-champ-select/v1/session" {
info!("Champion select event: {:?}", data);
// Check if we're in champion select phase
if let Some(timers) = data.get("timers") {
if let Some(phase) = timers.get("phase").and_then(|p| p.as_str()) {
if phase == "BAN_PICK" || phase == "FINALIZATION" {
// Extract local player's champion
if let Some(local_player_cell_id) =
data.get("localPlayerCellId").and_then(|id| id.as_i64())
{
// Check both teams for the local player
for team_key in &["myTeam", "theirTeam"] {
if let Some(team) = data.get(team_key).and_then(|t| t.as_array()) {
for member in team {
if member.get("cellId").and_then(|id| id.as_i64())
== Some(local_player_cell_id)
{
if let Some(champion_id) =
member.get("championId").and_then(|id| id.as_u64())
{
if champion_id > 0 {
let champion_name = member
.get("championName")
.and_then(|n| n.as_str())
.unwrap_or("Unknown")
.to_string();
return Some(
GameEvent::from_json(&serde_json::json!({
"eventType": "lcu-champion-pick",
"summonerName": "LocalPlayer",
"championId": champion_id,
"championName": champion_name,
"isLocalPlayer": true
}))
.unwrap_or(GameEvent::Unknown),
);
}
}
}
}
}
}
}
}
}
}
return None;
}
// Handle end-of-game stats block (contains actual game results)
if uri == "/lol-end-of-game/v1/eog-stats-block" {
info!("End-of-game stats received: {:?}", data);
// Extract game ID and duration
let game_id = data.get("gameId").and_then(|id| id.as_u64()).unwrap_or(0);
let game_duration = data
.get("gameLength")
.and_then(|d| d.as_f64())
.unwrap_or(0.0);
// Get local player data - prefer localPlayer field, fallback to teams[0].players[0]
let local_player = data.get("localPlayer");
// Extract victory status from local player's stats (WIN: 1 means victory)
let victory = local_player
.and_then(|p| p.get("stats"))
.and_then(|s| s.get("WIN"))
.and_then(|w| w.as_u64())
.map(|w| w == 1)
.or_else(|| {
// Fallback: check if player's team is winning team
data.get("teams")
.and_then(|teams| teams.as_array())
.and_then(|t| {
t.iter().find_map(|team| {
if team.get("isPlayerTeam").and_then(|p| p.as_bool()) == Some(true)
{
team.get("isWinningTeam").and_then(|w| w.as_bool())
} else {
None
}
})
})
})
.unwrap_or(false);
// Extract player stats - stats use UPPERCASE keys
let mut kills = 0u32;
let mut deaths = 0u32;
let mut assists = 0u32;
let mut creep_score = 0u32;
let mut gold_earned = 0u32;
let mut damage_dealt = 0u64;
let mut damage_taken = 0u64;
let mut vision_score = 0.0;
if let Some(stats_obj) = local_player.and_then(|p| p.get("stats")) {
kills = stats_obj
.get("CHAMPIONS_KILLED")
.and_then(|k| k.as_u64())
.unwrap_or(0) as u32;
deaths = stats_obj
.get("NUM_DEATHS")
.and_then(|d| d.as_u64())
.unwrap_or(0) as u32;
assists = stats_obj
.get("ASSISTS")
.and_then(|a| a.as_u64())
.unwrap_or(0) as u32;
creep_score = stats_obj
.get("MINIONS_KILLED")
.and_then(|cs| cs.as_u64())
.unwrap_or(0) as u32;
gold_earned = stats_obj
.get("GOLD_EARNED")
.and_then(|g| g.as_u64())
.unwrap_or(0) as u32;
damage_dealt = stats_obj
.get("TOTAL_DAMAGE_DEALT_TO_CHAMPIONS")
.and_then(|d| d.as_u64())
.unwrap_or(0);
damage_taken = stats_obj
.get("TOTAL_DAMAGE_TAKEN")
.and_then(|d| d.as_u64())
.unwrap_or(0);
vision_score = stats_obj
.get("VISION_SCORE")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
}
info!("Extracted game end stats: kills={}, deaths={}, assists={}, cs={}, gold={}, damage_dealt={}, damage_taken={}, vision={}, victory={}",
kills, deaths, assists, creep_score, gold_earned, damage_dealt, damage_taken, vision_score, victory);
// Generate a GameEnd event with actual stats
// Note: PlayerStats uses camelCase due to serde rename_all
let event_json = serde_json::json!({
"eventType": "lcu-game-end",
"gameId": game_id,
"victory": victory,
"duration": game_duration,
"stats": {
"kills": kills,
"deaths": deaths,
"assists": assists,
"minionsKilled": creep_score,
"goldEarned": gold_earned,
"damageDealt": damage_dealt,
"damageTaken": damage_taken,
"visionScore": vision_score
}
});
info!("Generating GameEnd event from eog-stats: {:?}", event_json);
match GameEvent::from_json(&event_json) {
Some(event) => {
info!("Successfully parsed GameEnd event");
return Some(event);
}
None => {
warn!("Failed to parse GameEnd event, returning Unknown");
return Some(GameEvent::Unknown);
}
}
}
// Handle lobby
if uri.starts_with("/lol-lobby") {
debug!("Lobby event: {}", uri);
@@ -607,6 +944,396 @@ impl LqpClient {
pub async fn get_game_stats(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::GAME_STATS).await
}
/// Get the currently selected champion in champ select.
pub async fn get_current_champion(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::CHAMPION_SUMMARY).await
}
/// Get current rune page.
pub async fn get_rune_page(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::RUNE_PAGES).await
}
/// Get match history.
pub async fn get_match_history(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::MATCH_HISTORY).await
}
/// Get live client data (available during game).
pub async fn get_live_client_data(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::LIVE_CLIENT_DATA).await
}
/// Fetch pre-game metadata (champion, skin, runes, queue info).
pub async fn fetch_pregame_metadata(&self) -> Result<PreGameMetadata> {
let mut metadata = PreGameMetadata::default();
// Get session info for queue type and game mode
if let Ok(session) = self.get_session().await {
if let Some(map) = session.get("map").and_then(|m| m.as_str()) {
metadata.map_name = Some(map.to_string());
}
if let Some(game_mode) = session.get("gameMode").and_then(|m| m.as_str()) {
metadata.game_mode = Some(game_mode.to_string());
}
if let Some(queue_id) = session.get("queueId").and_then(|q| q.as_u64()) {
metadata.queue_id = Some(queue_id as u32);
}
}
// Get summoner info (including puuid)
if let Ok(summoner) = self.get_summoner().await {
if let Some(name) = summoner.get("displayName").and_then(|n| n.as_str()) {
metadata.summoner_name = Some(name.to_string());
}
// Store the local player's puuid in state
if let Some(puuid) = summoner.get("puuid").and_then(|p| p.as_str()) {
metadata.local_puuid = Some(puuid.to_string());
let mut state = self.state.write().await;
state.local_puuid = Some(puuid.to_string());
}
}
// Get champion select info
if let Ok(champ_select) = self.get_champion_select().await {
// Find local player's cell
if let Some(local_player_cell_id) = champ_select
.get("localPlayerCellId")
.and_then(|id| id.as_i64())
{
if let Some(my_team) = champ_select.get("myTeam").and_then(|t| t.as_array()) {
for member in my_team {
if member.get("cellId").and_then(|id| id.as_i64())
== Some(local_player_cell_id)
{
if let Some(champion_id) =
member.get("championId").and_then(|id| id.as_u64())
{
metadata.champion_id = Some(champion_id as u32);
}
if let Some(team) = member.get("team").and_then(|t| t.as_i64()) {
metadata.team = Some(team as u32);
}
if let Some(skin_id) = member.get("skinId").and_then(|id| id.as_u64()) {
metadata.skin_id = Some(skin_id as u32);
}
}
}
}
}
}
// Get rune page
if let Ok(rune_page) = self.get_rune_page().await {
if let Some(name) = rune_page.get("name").and_then(|n| n.as_str()) {
metadata.rune_page_name = Some(name.to_string());
}
}
Ok(metadata)
}
/// Fetch end-of-game stats.
pub async fn fetch_game_end_stats(&self) -> Result<GameEndMetadata> {
let mut metadata = GameEndMetadata::default();
if let Ok(stats) = self.get_game_stats().await {
// Get game result
if let Some(victory) = stats.get("gameResult").and_then(|r| r.as_str()) {
metadata.victory = Some(victory == "WIN");
}
// Get player stats
if let Some(players) = stats.get("players").and_then(|p| p.as_array()) {
// Find the local player (first player in the array usually)
if let Some(player) = players.first() {
if let Some(stats_obj) = player.get("stats") {
metadata.kills =
stats_obj.get("kills").and_then(|k| k.as_u64()).unwrap_or(0) as u32;
metadata.deaths = stats_obj
.get("deaths")
.and_then(|d| d.as_u64())
.unwrap_or(0) as u32;
metadata.assists = stats_obj
.get("assists")
.and_then(|a| a.as_u64())
.unwrap_or(0) as u32;
metadata.creep_score = stats_obj
.get("minionsKilled")
.and_then(|cs| cs.as_u64())
.unwrap_or(0) as u32;
metadata.gold_earned = stats_obj
.get("goldEarned")
.and_then(|g| g.as_u64())
.unwrap_or(0) as u32;
metadata.damage_dealt = stats_obj
.get("totalDamageDealtToChampions")
.and_then(|d| d.as_u64())
.unwrap_or(0);
metadata.damage_taken = stats_obj
.get("totalDamageTaken")
.and_then(|d| d.as_u64())
.unwrap_or(0);
metadata.vision_score = stats_obj
.get("visionScore")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
}
}
}
// Get game duration
if let Some(duration) = stats.get("gameLength").and_then(|d| d.as_f64()) {
metadata.game_duration = duration;
}
// Get match ID
if let Some(match_id) = stats.get("matchId").and_then(|id| id.as_u64()) {
metadata.match_id = Some(match_id.to_string());
}
}
Ok(metadata)
}
}
/// Convert champion ID to champion name.
/// This is a simplified mapping for common champions.
pub fn champion_id_to_name(id: u32) -> Option<String> {
let name = match id {
1 => "Annie",
2 => "Olaf",
3 => "Galio",
4 => "TwistedFate",
5 => "XinZhao",
6 => "Urgot",
7 => "LeBlanc",
8 => "Vladimir",
9 => "Fiddlesticks",
10 => "Kayle",
11 => "MasterYi",
12 => "Alistar",
13 => "Ryze",
14 => "Sion",
15 => "Sivir",
16 => "Soraka",
17 => "Teemo",
18 => "Tristana",
19 => "Warwick",
20 => "Nunu",
21 => "MissFortune",
22 => "Ashe",
23 => "Tryndamere",
24 => "Jax",
25 => "Morgana",
26 => "Zilean",
27 => "Singed",
28 => "Evelynn",
29 => "Twitch",
30 => "Karthus",
31 => "Cho'Gath",
32 => "Amumu",
33 => "Rammus",
34 => "Anivia",
35 => "Shaco",
36 => "DrMundo",
37 => "Sona",
38 => "Kassadin",
39 => "Irelia",
40 => "Janna",
41 => "Gangplank",
42 => "Corki",
43 => "Karma",
44 => "Taric",
45 => "Veigar",
48 => "Trundle",
50 => "Swain",
51 => "Caitlyn",
52 => "Blitzcrank",
53 => "Malphite",
54 => "Katarina",
55 => "Nocturne",
56 => "Maokai",
57 => "Renekton",
58 => "JarvanIV",
59 => "Elise",
60 => "Talon",
61 => "Orianna",
62 => "Wukong",
63 => "Brand",
64 => "LeeSin",
67 => "Vayne",
68 => "Rumble",
69 => "Cassiopeia",
72 => "Skarner",
74 => "Heimerdinger",
75 => "Nasus",
76 => "Nidalee",
77 => "Udyr",
78 => "Poppy",
79 => "Gragas",
80 => "Pantheon",
81 => "Ezreal",
82 => "Mordekaiser",
83 => "Yorick",
84 => "Akali",
85 => "Kennedy",
86 => "Garen",
89 => "Leona",
90 => "Malzahar",
91 => "Talon",
92 => "Riven",
96 => "Kog'Maw",
98 => "Shen",
99 => "Lux",
101 => "Xerath",
102 => "Shyvana",
103 => "Ahri",
104 => "Graves",
105 => "Fizz",
106 => "Volibear",
107 => "Rengar",
110 => "Varus",
111 => "Nautilus",
112 => "Viktor",
113 => "Sejuani",
114 => "Fiora",
115 => "Ziggs",
117 => "Lulu",
119 => "Draven",
120 => "Hecarim",
121 => "Kha'Zix",
122 => "Darius",
126 => "Jayce",
127 => "Lissandra",
131 => "Diana",
133 => "Quinn",
134 => "Syndra",
136 => "AurelionSol",
141 => "Kayn",
142 => "Zoe",
143 => "Lillia",
145 => "Samira",
147 => "Seraphine",
150 => "Gnar",
154 => "Zac",
157 => "Yasuo",
161 => "Vel'Koz",
163 => "Taliyah",
164 => "Camille",
166 => "Akshan",
167 => "Nilah",
201 => "Braum",
202 => "Jhin",
203 => "Kindred",
222 => "Jinx",
223 => "TahmKench",
236 => "Lucian",
238 => "Zed",
240 => "Kled",
245 => "Ekko",
246 => "Qiyana",
254 => "Vi",
255 => "Janna",
256 => "Pyke",
257 => "Nami",
266 => "Aatrox",
267 => "Nami",
268 => "Azir",
350 => "Yuumi",
360 => "Samira",
412 => "Thresh",
420 => "Illaoi",
421 => "Rek'Sai",
427 => "Ivern",
429 => "Kalista",
432 => "Bard",
497 => "Rakan",
498 => "Xayah",
516 => "Ornn",
517 => "Sylas",
518 => "Neeko",
523 => "Aphelios",
526 => "Rell",
555 => "Pyke",
711 => "Vex",
777 => "Yone",
875 => "Sett",
876 => "Lillia",
887 => "Gwen",
888 => "Viego",
895 => "KSante",
901 => "Smolder",
902 => "Hwei",
950 => "Naafiri",
951 => "Briar",
_ => return None,
};
Some(name.to_string())
}
/// Convert map ID to map name.
fn map_id_to_name(id: u64) -> Option<String> {
let name = match id {
1 => "Summoner's Rift",
2 => "Summoner's Rift",
3 => "The Proving Grounds",
4 => "Twisted Treeline",
8 => "The Crystal Scar",
10 => "Twisted Treeline",
11 => "Summoner's Rift",
12 => "Howling Abyss",
14 => "Butcher's Bridge",
16 => "Cosmic Ruins",
18 => "Valoran City Park",
19 => "Substructure 43",
20 => "Crash Site",
21 => "Nexus Blitz",
22 => "Convergence",
23 => "Arena",
24 => "Arena",
25 => "Rings of Wrath",
30 => "Swarm",
31 => "Swarm",
32 => "Swarm",
33 => "Swarm",
34 => "Swarm",
35 => "Swarm",
_ => return None,
};
Some(name.to_string())
}
/// Pre-game metadata fetched before the game starts.
#[derive(Debug, Clone, Default)]
pub struct PreGameMetadata {
pub champion_id: Option<u32>,
pub skin_id: Option<u32>,
pub rune_page_name: Option<String>,
pub summoner_name: Option<String>,
pub queue_type: Option<String>,
pub queue_id: Option<u32>,
pub game_mode: Option<String>,
pub map_name: Option<String>,
pub team: Option<u32>,
pub local_puuid: Option<String>,
}
/// End-of-game metadata fetched after the game ends.
#[derive(Debug, Clone, Default)]
pub struct GameEndMetadata {
pub victory: Option<bool>,
pub match_id: Option<String>,
pub kills: u32,
pub deaths: u32,
pub assists: u32,
pub creep_score: u32,
pub gold_earned: u32,
pub damage_dealt: u64,
pub damage_taken: u64,
pub vision_score: f64,
pub game_duration: f64,
}
impl Default for LqpClient {