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

View File

@@ -17,7 +17,6 @@ use super::api_types::{
use super::auth::LockfileCredentials; use super::auth::LockfileCredentials;
use super::endpoints; use super::endpoints;
use super::events::GameEvent; use super::events::GameEvent;
use super::mappings::{champion_id_to_name, spell_id_to_name};
use super::state::{ClientState, GameflowPhase}; use super::state::{ClientState, GameflowPhase};
use super::tls::create_insecure_tls_config; use super::tls::create_insecure_tls_config;
use super::websocket::parse_websocket_message; use super::websocket::parse_websocket_message;
@@ -469,6 +468,36 @@ impl LqpClient {
// Metadata Fetching Methods // 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). /// Fetch pre-game data (stores raw API responses directly).
pub async fn fetch_pregame_data(&self) -> Result<PreGameData> { pub async fn fetch_pregame_data(&self) -> Result<PreGameData> {
let mut data = PreGameData::default(); let mut data = PreGameData::default();
@@ -511,219 +540,6 @@ impl LqpClient {
pub async fn fetch_game_end_stats(&self) -> Result<EndOfGameStatsResponse> { pub async fn fetch_game_end_stats(&self) -> Result<EndOfGameStatsResponse> {
self.get_game_stats_typed().await 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 { impl Default for LqpClient {

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);
}
}

View File

@@ -8,7 +8,6 @@ mod auth;
mod client; mod client;
mod endpoints; mod endpoints;
mod events; mod events;
mod mappings;
mod state; mod state;
mod tls; mod tls;
mod websocket; mod websocket;
@@ -33,6 +32,5 @@ pub use events::{
ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity, ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity,
QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember, 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 state::{ClientState, GameflowPhase};
pub use websocket::{parse_event_from_uri, parse_websocket_message}; pub use websocket::{parse_event_from_uri, parse_websocket_message};

View File

@@ -6,7 +6,6 @@
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use super::events::{GameEvent, GameflowSession}; use super::events::{GameEvent, GameflowSession};
use super::mappings::map_id_to_name;
/// Parse a WebSocket message into a game event. /// Parse a WebSocket message into a game event.
pub fn parse_websocket_message(text: &str) -> Option<GameEvent> { 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()) .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!( info!(
"Extracted game metadata: game_id={}, queue={:?}, queue_id={:?}, mode={:?}, map={:?}", "Extracted game metadata: game_id={}, queue={:?}, queue_id={:?}, mode={:?}",
game_id, queue_type, queue_id, game_mode, map_name game_id, queue_type, queue_id, game_mode
); );
// Note: Player-specific data (champion, team, summoner_name) is NOT extracted here. // 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, "queueType": queue_type,
"queueId": queue_id, "queueId": queue_id,
"gameMode": game_mode, "gameMode": game_mode,
"map": map_name,
"session": session "session": session
})) }))
.unwrap_or(GameEvent::Unknown), .unwrap_or(GameEvent::Unknown),

View File

@@ -354,24 +354,16 @@ impl Daemon {
// Handle recording start/stop // Handle recording start/stop
match transition { match transition {
StateTransition::GameStarted => { StateTransition::GameStarted => {
// Extract data from the GameStart event that was stored in the timeline // Extract game_id from the GameStart event
let (game_id, queue_type, queue_id, game_mode, map_name, session) = let game_id = if let GameEvent::GameStart(ref info) = event {
if let GameEvent::GameStart(ref info) = event { info.game_id
( } else {
info.game_id, 0
info.queue_type.clone(), };
info.queue_id,
info.game_mode.clone(),
info.map_name.clone(),
info.session.clone(),
)
} else {
(0, None, None, None, None, None)
};
info!( info!(
"[EVENT_HANDLER] GameStarted transition - game_id: {}, queue_type: {:?}, game_mode: {:?}", "[EVENT_HANDLER] GameStarted transition - game_id: {}",
game_id, queue_type, game_mode game_id
); );
// If already recording, stop the current recording first // If already recording, stop the current recording first
@@ -387,110 +379,34 @@ impl Daemon {
info!("[EVENT_HANDLER] Calling start_recording..."); info!("[EVENT_HANDLER] Calling start_recording...");
// Fetch player game metadata (puuid, runes, summoner spells) // Fetch raw API data in parallel
let player_metadata = let (
self.lqp_client.fetch_player_game_metadata().await.ok(); raw_session,
let (puuid, runes, summoner_spells, champion_id, team, summoner_name) = raw_summoner,
player_metadata.map_or((None, None, None, None, None, None), |m| { raw_champion_select,
( raw_rune_page,
m.puuid, raw_live_client_data,
m.runes, ) = tokio::join!(
m.summoner_spells, self.lqp_client.fetch_raw_session(),
m.champion_id, self.lqp_client.fetch_raw_summoner(),
m.team, self.lqp_client.fetch_raw_champion_select(),
m.summoner_name, self.lqp_client.fetch_raw_rune_page(),
) self.lqp_client.fetch_raw_live_client_data()
});
// If we have puuid and session, extract player-specific data from session
let (champion, final_team, final_summoner_name, final_summoner_spells) =
if let Some(ref p) = puuid {
if let Some(ref sess) = session {
let champ_id = sess.get_champion_id(p);
let champ_name =
champ_id.and_then(record_daemon::lqp::champion_id_to_name);
let team_id = sess.get_team(p);
let summoner = sess.get_summoner_name(p).map(|s| s.to_string());
// Extract summoner spells from session's player_champion_selections
let spells_from_session =
sess.get_player_selection(p).map(|selection| {
record_daemon::lqp::SummonerSpells {
spell1_id: selection.spell1_id,
spell2_id: selection.spell2_id,
spell1_name: None,
spell2_name: None,
}
});
(
champ_name.or_else(|| {
champion_id
.and_then(record_daemon::lqp::champion_id_to_name)
}),
team_id.or(team),
summoner.or(summoner_name),
spells_from_session.or(summoner_spells),
)
} else {
(
champion_id
.and_then(record_daemon::lqp::champion_id_to_name),
team,
summoner_name,
summoner_spells,
)
}
} else {
(
champion_id.and_then(record_daemon::lqp::champion_id_to_name),
team,
summoner_name,
summoner_spells,
)
};
info!(
"[EVENT_HANDLER] Final values: puuid={:?}, runes={:?}, summoner_spells={:?}",
puuid, runes, final_summoner_spells
); );
// Fetch all players identities for puuid mapping // Build game metadata for timeline with raw JSON
let all_players_identities =
self.lqp_client.fetch_all_players_identities().await.ok();
let all_players_info: Vec<record_daemon::timeline::PlayerIdentityInfo> =
all_players_identities
.unwrap_or_default()
.into_iter()
.map(|p| record_daemon::timeline::PlayerIdentityInfo {
puuid: p.puuid,
summoner_name: p.summoner_name,
champion_name: p.champion_name,
team: p.team,
})
.collect();
// Build game metadata for timeline
let metadata_update = record_daemon::timeline::MetadataUpdate { let metadata_update = record_daemon::timeline::MetadataUpdate {
queue_type, game_id: Some(game_id),
queue_id, raw_session: raw_session.ok(),
game_mode, raw_summoner: raw_summoner.ok(),
map_name, raw_champion_select: raw_champion_select.ok(),
team: final_team, raw_rune_page: raw_rune_page.ok(),
summoner_name: final_summoner_name, raw_live_client_data: raw_live_client_data.ok(),
puuid, raw_end_game_stats: None,
runes,
summoner_spells: final_summoner_spells,
all_players: all_players_info,
..Default::default()
}; };
if let Err(e) = self if let Err(e) = self
.start_recording_with_metadata( .start_recording_with_metadata(game_id, metadata_update)
game_id,
champion.as_deref(),
metadata_update,
)
.await .await
{ {
error!("[EVENT_HANDLER] Failed to start recording: {}", e); error!("[EVENT_HANDLER] Failed to start recording: {}", e);
@@ -503,15 +419,17 @@ impl Daemon {
StateTransition::GameEnded => { StateTransition::GameEnded => {
info!("[EVENT_HANDLER] GameEnded transition"); info!("[EVENT_HANDLER] GameEnded transition");
// Fetch end-of-game stats from API // Fetch raw end-of-game stats from API
let game_end_stats = self.lqp_client.fetch_game_end_stats().await.ok(); let raw_end_game_stats =
self.lqp_client.fetch_raw_end_game_stats().await.ok();
info!( info!(
"[EVENT_HANDLER] Game end stats from API: {:?}", "[EVENT_HANDLER] Game end stats from API: {:?}",
game_end_stats raw_end_game_stats.is_some()
); );
if let Err(e) = self.stop_recording_with_metadata(game_end_stats).await { if let Err(e) = self.stop_recording_with_metadata(raw_end_game_stats).await
{
error!("[EVENT_HANDLER] Failed to stop recording: {}", e); error!("[EVENT_HANDLER] Failed to stop recording: {}", e);
// Don't propagate error - keep daemon running // Don't propagate error - keep daemon running
@@ -539,19 +457,18 @@ impl Daemon {
async fn start_recording_with_metadata( async fn start_recording_with_metadata(
&self, &self,
game_id: u64, game_id: u64,
champion: Option<&str>,
metadata_update: record_daemon::timeline::MetadataUpdate, metadata_update: record_daemon::timeline::MetadataUpdate,
) -> Result<()> { ) -> Result<()> {
info!( info!(
"Daemon::start_recording_with_metadata called - game {} ({:?})", "Daemon::start_recording_with_metadata called - game {}",
game_id, champion game_id
); );
// Create a recording entry in the timeline store first // Create a recording entry in the timeline store first
let recording_id = self let recording_id = self
.timeline_store .timeline_store
.write() .write()
.start_recording_entry(Some(game_id), champion.map(|s| s.to_string())); .start_recording_entry(Some(game_id), None);
// Update metadata immediately with game start info // Update metadata immediately with game start info
if let Err(e) = self if let Err(e) = self
@@ -569,7 +486,6 @@ impl Daemon {
// Clone Arc references for use in spawn_blocking // Clone Arc references for use in spawn_blocking
let recording_engine = self.recording_engine.clone(); let recording_engine = self.recording_engine.clone();
let event_mapper = self.event_mapper.clone(); let event_mapper = self.event_mapper.clone();
let champion_owned = champion.map(|s| s.to_string());
// Use spawn_blocking to avoid blocking the async runtime // Use spawn_blocking to avoid blocking the async runtime
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
@@ -579,7 +495,7 @@ impl Daemon {
if let Some(ref mut engine) = *engine_guard { if let Some(ref mut engine) = *engine_guard {
info!("Calling engine.start_recording..."); info!("Calling engine.start_recording...");
engine.start_recording(Some(game_id), champion_owned.as_deref())?; engine.start_recording(Some(game_id), None)?;
info!("engine.start_recording returned successfully"); info!("engine.start_recording returned successfully");
event_mapper.write().start(); event_mapper.write().start();
info!("Event mapper started"); info!("Event mapper started");
@@ -604,10 +520,10 @@ impl Daemon {
self.stop_recording_with_metadata(None).await self.stop_recording_with_metadata(None).await
} }
/// Stop recording with optional game end stats. /// Stop recording with optional raw game end stats JSON.
async fn stop_recording_with_metadata( async fn stop_recording_with_metadata(
&self, &self,
game_end_stats: Option<record_daemon::lqp::EndOfGameStatsResponse>, raw_end_game_stats: Option<serde_json::Value>,
) -> Result<()> { ) -> Result<()> {
info!("Stopping recording"); info!("Stopping recording");
@@ -618,7 +534,6 @@ impl Daemon {
let recording_engine = self.recording_engine.clone(); let recording_engine = self.recording_engine.clone();
let event_mapper = self.event_mapper.clone(); let event_mapper = self.event_mapper.clone();
let timeline_store = self.timeline_store.clone(); let timeline_store = self.timeline_store.clone();
let pregame_data = self.pregame_data.read().clone();
// Use spawn_blocking to avoid blocking the async runtime // Use spawn_blocking to avoid blocking the async runtime
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
@@ -642,50 +557,11 @@ impl Daemon {
} }
}; };
// Update metadata if we have it // Update metadata with raw end game stats
let mut update = record_daemon::timeline::MetadataUpdate::default(); let update = record_daemon::timeline::MetadataUpdate {
raw_end_game_stats,
// Add pre-game data ..Default::default()
if let Some(pregame) = pregame_data { };
// Convert champion_id to name if available
if let Some(champion_id) = pregame.champion_id() {
update.champion =
record_daemon::lqp::champion_id_to_name(champion_id as u32);
}
update.summoner_name = pregame.summoner_name().map(|s| s.to_string());
update.queue_id = pregame.queue_id().map(|id| id as u32);
update.game_mode = pregame.game_mode().map(|s| s.to_string());
update.map_name = pregame.map_name().map(|s| s.to_string());
update.team = pregame.team().map(|id| id as u32);
}
// Add game end stats from API response
if let Some(stats) = game_end_stats {
update.victory = Some(stats.is_victory());
update.match_id = stats.match_id.map(|id| id.to_string());
// Store raw end-of-game stats as JSON
update.raw_end_game_stats = serde_json::to_value(&stats).ok();
// Get local player stats
if let Some(player) = stats.get_local_player() {
if let Some(player_stats) = &player.stats {
update.final_stats = Some(record_daemon::timeline::GameFinalStats {
kills: player_stats.champions_killed.unwrap_or(0) as u32,
deaths: player_stats.num_deaths.unwrap_or(0) as u32,
assists: player_stats.assists.unwrap_or(0) as u32,
creep_score: player_stats.minions_killed.unwrap_or(0) as u32,
gold_earned: player_stats.gold_earned.unwrap_or(0) as u32,
damage_dealt: player_stats
.total_damage_dealt_to_champions
.unwrap_or(0),
damage_taken: player_stats.total_damage_taken.unwrap_or(0),
vision_score: player_stats.vision_score.unwrap_or(0.0),
game_duration: stats.game_length.unwrap_or(0.0),
});
}
}
}
// Apply the update // Apply the update
if let Err(e) = timeline_store.write().update_metadata(recording_id, update) { if let Err(e) = timeline_store.write().update_metadata(recording_id, update) {

View File

@@ -4,18 +4,16 @@ mod mapper;
mod store; mod store;
pub use mapper::EventMapper; pub use mapper::EventMapper;
pub use store::{ pub use store::{MetadataUpdate, RecordingMetadata, TimelineStore, TimestampedEvent};
GameFinalStats, MetadataUpdate, PlayerIdentityInfo, RecordingMetadata, TimelineStore,
TimestampedEvent,
};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::lqp::{GameEvent, RunePage, SummonerSpells}; use crate::lqp::GameEvent;
/// A timeline of events for a recording. /// A timeline of events for a recording.
/// Stores raw API responses for maximum flexibility and future-proofing.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Timeline { pub struct Timeline {
/// Recording ID. /// Recording ID.
@@ -28,51 +26,27 @@ pub struct Timeline {
pub duration_secs: i64, pub duration_secs: i64,
/// Events in the timeline. /// Events in the timeline.
pub events: Vec<TimestampedEvent>, pub events: Vec<TimestampedEvent>,
/// Champion played. /// Game ID if available.
#[serde(default)] #[serde(default)]
pub champion: Option<String>, pub game_id: Option<u64>,
/// Skin name. /// Raw session data from `/lol-gameflow/v1/session`.
#[serde(default)] #[serde(default)]
pub skin_name: Option<String>, pub raw_session: Option<serde_json::Value>,
/// Queue type. /// Raw summoner data from `/lol-summoner/v1/current-summoner`.
#[serde(default)] #[serde(default)]
pub queue_type: Option<String>, pub raw_summoner: Option<serde_json::Value>,
/// Queue ID. /// Raw champion select data from `/lol-champ-select/v1/session`.
#[serde(default)] #[serde(default)]
pub queue_id: Option<u32>, pub raw_champion_select: Option<serde_json::Value>,
/// Game mode. /// Raw rune page data from `/lol-perks/v1/currentpage`.
#[serde(default)] #[serde(default)]
pub game_mode: Option<String>, pub raw_rune_page: Option<serde_json::Value>,
/// Map name. /// Raw live client data from `/liveclientdata/allgamedata`.
#[serde(default)] #[serde(default)]
pub map_name: Option<String>, pub raw_live_client_data: Option<serde_json::Value>,
/// Summoner name. /// Raw end-of-game stats from `/lol-end-of-game/v1/eog-stats-block`.
#[serde(default)]
pub summoner_name: Option<String>,
/// Player's PUUID.
#[serde(default)]
pub puuid: Option<String>,
/// Team (100 = blue, 200 = red).
#[serde(default)]
pub team: Option<u32>,
/// Whether the game was won.
#[serde(default)]
pub victory: Option<bool>,
/// Final player stats.
#[serde(default)]
pub final_stats: Option<GameFinalStats>,
/// Rune page at game start.
#[serde(default)]
pub runes: Option<RunePage>,
/// Summoner spells.
#[serde(default)]
pub summoner_spells: Option<SummonerSpells>,
/// Raw end-of-game stats JSON from the API.
#[serde(default)] #[serde(default)]
pub raw_end_game_stats: Option<serde_json::Value>, pub raw_end_game_stats: Option<serde_json::Value>,
/// All players in the game (puuid to summoner name mapping).
#[serde(default)]
pub all_players: Vec<PlayerIdentityInfo>,
} }
impl Timeline { impl Timeline {
@@ -91,21 +65,13 @@ impl Timeline {
end_time: None, end_time: None,
duration_secs: 0, duration_secs: 0,
events: Vec::new(), events: Vec::new(),
champion: None, game_id: None,
skin_name: None, raw_session: None,
queue_type: None, raw_summoner: None,
queue_id: None, raw_champion_select: None,
game_mode: None, raw_rune_page: None,
map_name: None, raw_live_client_data: None,
summoner_name: None,
puuid: None,
team: None,
victory: None,
final_stats: None,
runes: None,
summoner_spells: None,
raw_end_game_stats: None, raw_end_game_stats: None,
all_players: Vec::new(),
} }
} }

View File

@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::error::{Result, TimelineError}; use crate::error::{Result, TimelineError};
use crate::lqp::{GameEvent, RunePage, SummonerSpells}; use crate::lqp::GameEvent;
use crate::recording::RecordingResult; use crate::recording::RecordingResult;
/// A timestamped event in the timeline. /// A timestamped event in the timeline.
@@ -32,55 +32,36 @@ pub struct TimestampedEvent {
} }
/// Metadata for a recording. /// Metadata for a recording.
/// Stores raw API responses for maximum flexibility and future-proofing.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordingMetadata { pub struct RecordingMetadata {
/// Unique recording ID. /// Unique recording ID.
pub id: Uuid, pub id: Uuid,
/// Game ID if available. /// Game ID if available.
pub game_id: Option<u64>, pub game_id: Option<u64>,
/// Match ID if available. /// Raw session data from `/lol-gameflow/v1/session`.
pub match_id: Option<String>,
/// Champion played.
pub champion: Option<String>,
/// Skin name.
pub skin_name: Option<String>,
/// Queue type (ranked, normal, aram, etc.).
pub queue_type: Option<String>,
/// Queue ID.
pub queue_id: Option<u32>,
/// Game mode.
pub game_mode: Option<String>,
/// Map name.
pub map_name: Option<String>,
/// Player's summoner name.
pub summoner_name: Option<String>,
/// Player's PUUID.
#[serde(default)] #[serde(default)]
pub puuid: Option<String>, pub raw_session: Option<serde_json::Value>,
/// Team (100 = blue, 200 = red). /// Raw summoner data from `/lol-summoner/v1/current-summoner`.
pub team: Option<u32>,
/// Whether the game was won.
pub victory: Option<bool>,
/// Final player stats.
pub final_stats: Option<GameFinalStats>,
/// Player's rune page at game start.
#[serde(default)] #[serde(default)]
pub runes: Option<RunePage>, pub raw_summoner: Option<serde_json::Value>,
/// Player's summoner spells. /// Raw champion select data from `/lol-champ-select/v1/session`.
#[serde(default)] #[serde(default)]
pub summoner_spells: Option<SummonerSpells>, pub raw_champion_select: Option<serde_json::Value>,
/// Raw end-of-game stats JSON from the API. /// Raw rune page data from `/lol-perks/v1/currentpage`.
#[serde(default)]
pub raw_rune_page: Option<serde_json::Value>,
/// Raw live client data from `/liveclientdata/allgamedata`.
#[serde(default)]
pub raw_live_client_data: Option<serde_json::Value>,
/// Raw end-of-game stats from `/lol-end-of-game/v1/eog-stats-block`.
#[serde(default)] #[serde(default)]
pub raw_end_game_stats: Option<serde_json::Value>, pub raw_end_game_stats: Option<serde_json::Value>,
/// All players in the game (puuid -> summoner name mapping).
#[serde(default)]
pub all_players: Vec<PlayerIdentityInfo>,
/// Recording start time. /// Recording start time.
pub start_time: DateTime<Utc>, pub start_time: DateTime<Utc>,
/// Recording end time. /// Recording end time.
pub end_time: Option<DateTime<Utc>>, pub end_time: Option<DateTime<Utc>>,
/// Recording duration. /// Recording duration.
// #[serde(with = "chrono::serde::seconds")]
pub duration: Duration, pub duration: Duration,
/// Output file path. /// Output file path.
pub file_path: Option<PathBuf>, pub file_path: Option<PathBuf>,
@@ -92,64 +73,16 @@ pub struct RecordingMetadata {
pub finalized: bool, pub finalized: bool,
} }
/// Player identity information for puuid to summoner name mapping.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayerIdentityInfo {
/// Player's PUUID.
pub puuid: String,
/// Player's summoner name.
pub summoner_name: String,
/// Player's champion name.
#[serde(default)]
pub champion_name: Option<String>,
/// Player's team (100 or 200).
#[serde(default)]
pub team: Option<u32>,
}
/// Final game statistics for the player.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameFinalStats {
/// Kills.
pub kills: u32,
/// Deaths.
pub deaths: u32,
/// Assists.
pub assists: u32,
/// Creep score.
pub creep_score: u32,
/// Gold earned.
pub gold_earned: u32,
/// Damage dealt.
pub damage_dealt: u64,
/// Damage taken.
pub damage_taken: u64,
/// Vision score.
pub vision_score: f64,
/// Game duration in seconds.
pub game_duration: f64,
}
/// Update for recording metadata. /// Update for recording metadata.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct MetadataUpdate { pub struct MetadataUpdate {
pub champion: Option<String>, pub game_id: Option<u64>,
pub match_id: Option<String>, pub raw_session: Option<serde_json::Value>,
pub skin_name: Option<String>, pub raw_summoner: Option<serde_json::Value>,
pub queue_type: Option<String>, pub raw_champion_select: Option<serde_json::Value>,
pub queue_id: Option<u32>, pub raw_rune_page: Option<serde_json::Value>,
pub game_mode: Option<String>, pub raw_live_client_data: Option<serde_json::Value>,
pub map_name: Option<String>,
pub summoner_name: Option<String>,
pub puuid: Option<String>,
pub team: Option<u32>,
pub victory: Option<bool>,
pub final_stats: Option<GameFinalStats>,
pub runes: Option<RunePage>,
pub summoner_spells: Option<SummonerSpells>,
pub raw_end_game_stats: Option<serde_json::Value>, pub raw_end_game_stats: Option<serde_json::Value>,
pub all_players: Vec<PlayerIdentityInfo>,
} }
impl RecordingMetadata { impl RecordingMetadata {
@@ -158,22 +91,12 @@ impl RecordingMetadata {
Self { Self {
id: Uuid::new_v4(), id: Uuid::new_v4(),
game_id: result.game_id, game_id: result.game_id,
match_id: None, raw_session: None,
champion: result.champion.clone(), raw_summoner: None,
skin_name: None, raw_champion_select: None,
queue_type: None, raw_rune_page: None,
queue_id: None, raw_live_client_data: None,
game_mode: None,
map_name: None,
summoner_name: None,
puuid: None,
team: None,
victory: None,
final_stats: None,
runes: None,
summoner_spells: None,
raw_end_game_stats: None, raw_end_game_stats: None,
all_players: Vec::new(),
start_time: result.start_time, start_time: result.start_time,
end_time: Some(result.end_time), end_time: Some(result.end_time),
duration: result.duration, duration: result.duration,
@@ -220,27 +143,17 @@ impl TimelineStore {
/// Start a new recording entry (called when recording begins). /// Start a new recording entry (called when recording begins).
/// Returns the recording ID for tracking events during recording. /// Returns the recording ID for tracking events during recording.
pub fn start_recording_entry(&self, game_id: Option<u64>, champion: Option<String>) -> Uuid { pub fn start_recording_entry(&self, game_id: Option<u64>, _champion: Option<String>) -> Uuid {
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let metadata = RecordingMetadata { let metadata = RecordingMetadata {
id, id,
game_id, game_id,
match_id: None, raw_session: None,
champion, raw_summoner: None,
skin_name: None, raw_champion_select: None,
queue_type: None, raw_rune_page: None,
queue_id: None, raw_live_client_data: None,
game_mode: None,
map_name: None,
summoner_name: None,
puuid: None,
team: None,
victory: None,
final_stats: None,
runes: None,
summoner_spells: None,
raw_end_game_stats: None, raw_end_game_stats: None,
all_players: Vec::new(),
start_time: Utc::now(), start_time: Utc::now(),
end_time: None, end_time: None,
duration: Duration::zero(), duration: Duration::zero(),
@@ -286,22 +199,12 @@ impl TimelineStore {
let metadata = RecordingMetadata { let metadata = RecordingMetadata {
id, id,
game_id: result.game_id, game_id: result.game_id,
match_id: None, raw_session: None,
champion: result.champion.clone(), raw_summoner: None,
skin_name: None, raw_champion_select: None,
queue_type: None, raw_rune_page: None,
queue_id: None, raw_live_client_data: None,
game_mode: None,
map_name: None,
summoner_name: None,
puuid: None,
team: None,
victory: None,
final_stats: None,
runes: None,
summoner_spells: None,
raw_end_game_stats: None, raw_end_game_stats: None,
all_players: Vec::new(),
start_time: result.start_time, start_time: result.start_time,
end_time: Some(result.end_time), end_time: Some(result.end_time),
duration: result.duration, duration: result.duration,
@@ -344,54 +247,27 @@ impl TimelineStore {
pub fn update_metadata(&self, recording_id: Uuid, update: MetadataUpdate) -> Result<()> { pub fn update_metadata(&self, recording_id: Uuid, update: MetadataUpdate) -> Result<()> {
let mut recordings = self.recordings.write(); let mut recordings = self.recordings.write();
if let Some(metadata) = recordings.get_mut(&recording_id) { if let Some(metadata) = recordings.get_mut(&recording_id) {
if let Some(champion) = update.champion { if let Some(game_id) = update.game_id {
metadata.champion = Some(champion); metadata.game_id = Some(game_id);
} }
if let Some(match_id) = update.match_id { if let Some(raw_session) = update.raw_session {
metadata.match_id = Some(match_id); metadata.raw_session = Some(raw_session);
} }
if let Some(skin_name) = update.skin_name { if let Some(raw_summoner) = update.raw_summoner {
metadata.skin_name = Some(skin_name); metadata.raw_summoner = Some(raw_summoner);
} }
if let Some(queue_type) = update.queue_type { if let Some(raw_champion_select) = update.raw_champion_select {
metadata.queue_type = Some(queue_type); metadata.raw_champion_select = Some(raw_champion_select);
} }
if let Some(queue_id) = update.queue_id { if let Some(raw_rune_page) = update.raw_rune_page {
metadata.queue_id = Some(queue_id); metadata.raw_rune_page = Some(raw_rune_page);
} }
if let Some(game_mode) = update.game_mode { if let Some(raw_live_client_data) = update.raw_live_client_data {
metadata.game_mode = Some(game_mode); metadata.raw_live_client_data = Some(raw_live_client_data);
}
if let Some(map_name) = update.map_name {
metadata.map_name = Some(map_name);
}
if let Some(summoner_name) = update.summoner_name {
metadata.summoner_name = Some(summoner_name);
}
if let Some(puuid) = update.puuid {
metadata.puuid = Some(puuid);
}
if let Some(team) = update.team {
metadata.team = Some(team);
}
if let Some(victory) = update.victory {
metadata.victory = Some(victory);
}
if let Some(final_stats) = update.final_stats {
metadata.final_stats = Some(final_stats);
}
if let Some(runes) = update.runes {
metadata.runes = Some(runes);
}
if let Some(summoner_spells) = update.summoner_spells {
metadata.summoner_spells = Some(summoner_spells);
} }
if let Some(raw_end_game_stats) = update.raw_end_game_stats { if let Some(raw_end_game_stats) = update.raw_end_game_stats {
metadata.raw_end_game_stats = Some(raw_end_game_stats); metadata.raw_end_game_stats = Some(raw_end_game_stats);
} }
if !update.all_players.is_empty() {
metadata.all_players = update.all_players;
}
} }
drop(recordings); drop(recordings);
self.persist_recording(recording_id)?; self.persist_recording(recording_id)?;
@@ -429,21 +305,13 @@ impl TimelineStore {
end_time: metadata.end_time, end_time: metadata.end_time,
duration_secs: metadata.duration.num_seconds(), duration_secs: metadata.duration.num_seconds(),
events, events,
champion: metadata.champion.clone(), game_id: metadata.game_id,
skin_name: metadata.skin_name.clone(), raw_session: metadata.raw_session.clone(),
queue_type: metadata.queue_type.clone(), raw_summoner: metadata.raw_summoner.clone(),
queue_id: metadata.queue_id, raw_champion_select: metadata.raw_champion_select.clone(),
game_mode: metadata.game_mode.clone(), raw_rune_page: metadata.raw_rune_page.clone(),
map_name: metadata.map_name.clone(), raw_live_client_data: metadata.raw_live_client_data.clone(),
summoner_name: metadata.summoner_name.clone(),
puuid: metadata.puuid.clone(),
team: metadata.team,
victory: metadata.victory,
final_stats: metadata.final_stats.clone(),
runes: metadata.runes.clone(),
summoner_spells: metadata.summoner_spells.clone(),
raw_end_game_stats: metadata.raw_end_game_stats.clone(), raw_end_game_stats: metadata.raw_end_game_stats.clone(),
all_players: metadata.all_players.clone(),
}) })
} }
@@ -480,21 +348,13 @@ impl TimelineStore {
end_time: metadata.end_time, end_time: metadata.end_time,
duration_secs: metadata.duration.num_seconds(), duration_secs: metadata.duration.num_seconds(),
events, events,
champion: metadata.champion, game_id: metadata.game_id,
skin_name: metadata.skin_name, raw_session: metadata.raw_session,
queue_type: metadata.queue_type, raw_summoner: metadata.raw_summoner,
queue_id: metadata.queue_id, raw_champion_select: metadata.raw_champion_select,
game_mode: metadata.game_mode, raw_rune_page: metadata.raw_rune_page,
map_name: metadata.map_name, raw_live_client_data: metadata.raw_live_client_data,
summoner_name: metadata.summoner_name,
puuid: metadata.puuid,
team: metadata.team,
victory: metadata.victory,
final_stats: metadata.final_stats,
runes: metadata.runes,
summoner_spells: metadata.summoner_spells,
raw_end_game_stats: metadata.raw_end_game_stats, raw_end_game_stats: metadata.raw_end_game_stats,
all_players: metadata.all_players,
}; };
let file_path = self.storage_dir.join(format!("{}.json", id)); let file_path = self.storage_dir.join(format!("{}.json", id));
@@ -518,40 +378,40 @@ impl TimelineStore {
if path.extension().map(|e| e == "json").unwrap_or(false) { if path.extension().map(|e| e == "json").unwrap_or(false) {
if let Ok(contents) = std::fs::read_to_string(&path) { if let Ok(contents) = std::fs::read_to_string(&path) {
if let Ok(timeline) = serde_json::from_str::<super::Timeline>(&contents) { if let Ok(timeline) = serde_json::from_str::<super::Timeline>(&contents) {
let recording_id = timeline.recording_id;
let start_time = timeline.start_time;
let end_time = timeline.end_time;
let duration = timeline.duration();
let event_count = timeline.events.len();
let game_id = timeline.game_id;
let raw_session = timeline.raw_session;
let raw_summoner = timeline.raw_summoner;
let raw_champion_select = timeline.raw_champion_select;
let raw_rune_page = timeline.raw_rune_page;
let raw_live_client_data = timeline.raw_live_client_data;
let raw_end_game_stats = timeline.raw_end_game_stats;
let events = timeline.events;
let metadata = RecordingMetadata { let metadata = RecordingMetadata {
id: timeline.recording_id, id: recording_id,
game_id: None, game_id,
match_id: None, raw_session,
champion: None, raw_summoner,
skin_name: None, raw_champion_select,
queue_type: None, raw_rune_page,
queue_id: None, raw_live_client_data,
game_mode: None, raw_end_game_stats,
map_name: None, start_time,
summoner_name: None, end_time,
puuid: None, duration,
team: None,
victory: None,
final_stats: None,
runes: None,
summoner_spells: None,
raw_end_game_stats: None,
all_players: Vec::new(),
start_time: timeline.start_time,
end_time: timeline.end_time,
duration: timeline.duration(),
file_path: None, file_path: None,
file_size: None, file_size: None,
event_count: timeline.events.len(), event_count,
finalized: true, finalized: true,
}; };
self.recordings self.recordings.write().insert(recording_id, metadata);
.write() self.timelines.write().insert(recording_id, events);
.insert(timeline.recording_id, metadata);
self.timelines
.write()
.insert(timeline.recording_id, timeline.events);
} }
} }
} }
@@ -597,6 +457,6 @@ mod tests {
assert_eq!(recordings.len(), 1); assert_eq!(recordings.len(), 1);
let metadata = store.get_recording(id).unwrap(); let metadata = store.get_recording(id).unwrap();
assert_eq!(metadata.champion, Some("Ahri".to_string())); assert_eq!(metadata.game_id, Some(12345));
} }
} }