record-daemon: refactor to record raw league data

This commit is contained in:
2026-03-27 13:42:12 +01:00
parent b09f669e73
commit d67d52fa86
7 changed files with 191 additions and 951 deletions
+30 -214
View File
@@ -17,7 +17,6 @@ use super::api_types::{
use super::auth::LockfileCredentials;
use super::endpoints;
use super::events::GameEvent;
use super::mappings::{champion_id_to_name, spell_id_to_name};
use super::state::{ClientState, GameflowPhase};
use super::tls::create_insecure_tls_config;
use super::websocket::parse_websocket_message;
@@ -469,6 +468,36 @@ impl LqpClient {
// Metadata Fetching Methods
// =========================================================================
/// Fetch raw session data as JSON.
pub async fn fetch_raw_session(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::SESSION).await
}
/// Fetch raw summoner data as JSON.
pub async fn fetch_raw_summoner(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::SUMMONER).await
}
/// Fetch raw champion select data as JSON.
pub async fn fetch_raw_champion_select(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::CHAMPION_SELECT).await
}
/// Fetch raw rune page data as JSON.
pub async fn fetch_raw_rune_page(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::RUNE_PAGES).await
}
/// Fetch raw live client data as JSON.
pub async fn fetch_raw_live_client_data(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::LIVE_CLIENT_DATA).await
}
/// Fetch raw end-of-game stats as JSON.
pub async fn fetch_raw_end_game_stats(&self) -> Result<serde_json::Value> {
self.request("GET", endpoints::GAME_STATS).await
}
/// Fetch pre-game data (stores raw API responses directly).
pub async fn fetch_pregame_data(&self) -> Result<PreGameData> {
let mut data = PreGameData::default();
@@ -511,219 +540,6 @@ impl LqpClient {
pub async fn fetch_game_end_stats(&self) -> Result<EndOfGameStatsResponse> {
self.get_game_stats_typed().await
}
/// Fetch complete player game metadata including runes, summoner spells, and items.
pub async fn fetch_player_game_metadata(&self) -> Result<super::PlayerGameMetadata> {
use super::{RunePage, SummonerSpells};
let mut metadata = super::PlayerGameMetadata::default();
// Get summoner info (typed)
if let Ok(summoner) = self.get_summoner_typed().await {
metadata.puuid = summoner.puuid;
metadata.summoner_name = summoner
.display_name
.or(summoner.name)
.or(summoner.internal_name);
}
// Get rune page (typed)
if let Ok(rune_page) = self.get_rune_page_typed().await {
let primary_style_id = rune_page.primary_style_id.unwrap_or(0) as u32;
let secondary_style_id = rune_page.sub_style_id.unwrap_or(0) as u32;
let selected_perks = rune_page
.selected_perk_ids
.unwrap_or_default()
.iter()
.map(|id| *id as u32)
.collect();
if primary_style_id > 0 {
metadata.runes = Some(RunePage {
primary_style_id,
secondary_style_id,
selected_perks,
stat_modifiers: Vec::new(),
name: rune_page.name,
current: rune_page.current.unwrap_or(true),
});
}
}
// Get summoner spells from live client data (typed)
if let Ok(active_player) = self.get_live_client_active_player_typed().await {
debug!("[METADATA] Live client active player data received");
if let Some(ref spells) = active_player.summoner_spells {
let spell1_id = spells
.summoner_spell_one
.as_ref()
.and_then(|s| s.spell_id)
.or(spells.spell1_id)
.unwrap_or(0) as u32;
let spell2_id = spells
.summoner_spell_two
.as_ref()
.and_then(|s| s.spell_id)
.or(spells.spell2_id)
.unwrap_or(0) as u32;
if spell1_id > 0 || spell2_id > 0 {
metadata.summoner_spells = Some(SummonerSpells {
spell1_id,
spell2_id,
spell1_name: spell_id_to_name(spell1_id),
spell2_name: spell_id_to_name(spell2_id),
});
}
}
if metadata.summoner_name.is_none()
|| metadata.summoner_name.as_ref().is_none_or(|n| n.is_empty())
{
metadata.summoner_name = active_player
.summoner_name
.or(active_player.display_name)
.or(active_player.riot_id);
}
}
// Fallback: Get summoner spells from session gameData (typed)
if metadata.summoner_spells.is_none() {
if let Ok(session) = self.get_session_typed().await {
if let Some(local_puuid) = &metadata.puuid {
if let Some(ref game_data) = session.game_data {
// Check team one
if let Some(ref team) = game_data.team_one {
for player in team {
if player.puuid.as_deref() == Some(local_puuid.as_str()) {
let spell1_id = player.spell1_id.unwrap_or(0) as u32;
let spell2_id = player.spell2_id.unwrap_or(0) as u32;
if spell1_id > 0 || spell2_id > 0 {
metadata.summoner_spells = Some(SummonerSpells {
spell1_id,
spell2_id,
spell1_name: spell_id_to_name(spell1_id),
spell2_name: spell_id_to_name(spell2_id),
});
}
metadata.champion_id = player.champion_id.map(|id| id as u32);
metadata.team = player.team_id.map(|id| id as u32);
break;
}
}
}
// Check team two if not found
if metadata.summoner_spells.is_none() {
if let Some(ref team) = game_data.team_two {
for player in team {
if player.puuid.as_deref() == Some(local_puuid.as_str()) {
let spell1_id = player.spell1_id.unwrap_or(0) as u32;
let spell2_id = player.spell2_id.unwrap_or(0) as u32;
if spell1_id > 0 || spell2_id > 0 {
metadata.summoner_spells = Some(SummonerSpells {
spell1_id,
spell2_id,
spell1_name: spell_id_to_name(spell1_id),
spell2_name: spell_id_to_name(spell2_id),
});
}
metadata.champion_id =
player.champion_id.map(|id| id as u32);
metadata.team = player.team_id.map(|id| id as u32);
break;
}
}
}
}
}
}
}
}
if let Some(champ_id) = metadata.champion_id {
metadata.champion_name = champion_id_to_name(champ_id);
}
Ok(metadata)
}
/// Fetch all players' puuid to summoner name mapping.
pub async fn fetch_all_players_identities(&self) -> Result<Vec<super::PlayerIdentity>> {
let mut players = Vec::new();
// Try live client data first (typed)
if let Ok(player_list) = self.get_live_client_player_list_typed().await {
for player in &player_list.0 {
let summoner_name = player
.summoner_name
.as_deref()
.or(player.riot_id.as_deref())
.unwrap_or("");
if let Some(ref puuid) = player.puuid {
players.push(super::PlayerIdentity {
puuid: puuid.clone(),
summoner_name: summoner_name.to_string(),
summoner_id: player.summoner_id,
champion_name: player.champion_name.clone(),
team: player.team.map(|id| id as u32),
});
}
}
}
// Fallback: try from gameflow session (typed)
if players.is_empty() {
if let Ok(session) = self.get_session_typed().await {
if let Some(ref game_data) = session.game_data {
// Team one (team ID 100)
if let Some(ref team) = game_data.team_one {
for player in team {
if let (Some(ref puuid), Some(ref summoner_name)) =
(&player.puuid, &player.summoner_name)
{
let champion_id = player.champion_id.map(|id| id as u32);
let champion_name = champion_id.and_then(champion_id_to_name);
players.push(super::PlayerIdentity {
puuid: puuid.clone(),
summoner_name: summoner_name.clone(),
summoner_id: player.summoner_id,
champion_name,
team: Some(100),
});
}
}
}
// Team two (team ID 200)
if let Some(ref team) = game_data.team_two {
for player in team {
if let (Some(ref puuid), Some(ref summoner_name)) =
(&player.puuid, &player.summoner_name)
{
let champion_id = player.champion_id.map(|id| id as u32);
let champion_name = champion_id.and_then(champion_id_to_name);
players.push(super::PlayerIdentity {
puuid: puuid.clone(),
summoner_name: summoner_name.clone(),
summoner_id: player.summoner_id,
champion_name,
team: Some(200),
});
}
}
}
}
}
}
Ok(players)
}
}
impl Default for LqpClient {
-261
View File
@@ -1,261 +0,0 @@
//! ID to name mappings for League of Legends data.
//!
//! Provides conversion functions for champion IDs, summoner spell IDs,
//! and map IDs to their human-readable names.
/// Convert summoner spell ID to name.
pub fn spell_id_to_name(id: u32) -> Option<String> {
let name = match id {
1 => "Cleanse",
3 => "Exhaust",
4 => "Flash",
6 => "Ghost",
7 => "Heal",
11 => "Smite",
12 => "Teleport",
13 => "Clarity",
14 => "Ignite",
21 => "Barrier",
32 => "Mark",
39 => "Mark",
54 => "Placeholder",
55 => "Placeholder",
_ => return None,
};
Some(name.to_string())
}
/// 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.
pub 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())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spell_id_to_name() {
assert_eq!(spell_id_to_name(4), Some("Flash".to_string()));
assert_eq!(spell_id_to_name(7), Some("Heal".to_string()));
assert_eq!(spell_id_to_name(11), Some("Smite".to_string()));
assert_eq!(spell_id_to_name(999), None);
}
#[test]
fn test_champion_id_to_name() {
assert_eq!(champion_id_to_name(1), Some("Annie".to_string()));
assert_eq!(champion_id_to_name(22), Some("Ashe".to_string()));
assert_eq!(champion_id_to_name(157), Some("Yasuo".to_string()));
assert_eq!(champion_id_to_name(9999), None);
}
#[test]
fn test_map_id_to_name() {
assert_eq!(map_id_to_name(11), Some("Summoner's Rift".to_string()));
assert_eq!(map_id_to_name(12), Some("Howling Abyss".to_string()));
assert_eq!(map_id_to_name(999), None);
}
}
-2
View File
@@ -8,7 +8,6 @@ mod auth;
mod client;
mod endpoints;
mod events;
mod mappings;
mod state;
mod tls;
mod websocket;
@@ -33,6 +32,5 @@ pub use events::{
ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity,
QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
};
pub use mappings::{champion_id_to_name, map_id_to_name, spell_id_to_name};
pub use state::{ClientState, GameflowPhase};
pub use websocket::{parse_event_from_uri, parse_websocket_message};
+2 -17
View File
@@ -6,7 +6,6 @@
use tracing::{debug, info, warn};
use super::events::{GameEvent, GameflowSession};
use super::mappings::map_id_to_name;
/// Parse a WebSocket message into a game event.
pub fn parse_websocket_message(text: &str) -> Option<GameEvent> {
@@ -233,22 +232,9 @@ fn parse_game_start_event(data: &serde_json::Value) -> Option<GameEvent> {
.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
"Extracted game metadata: game_id={}, queue={:?}, queue_id={:?}, mode={:?}",
game_id, queue_type, queue_id, game_mode
);
// Note: Player-specific data (champion, team, summoner_name) is NOT extracted here.
@@ -263,7 +249,6 @@ fn parse_game_start_event(data: &serde_json::Value) -> Option<GameEvent> {
"queueType": queue_type,
"queueId": queue_id,
"gameMode": game_mode,
"map": map_name,
"session": session
}))
.unwrap_or(GameEvent::Unknown),