record-daemon: add live client events, fix video_file
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m12s
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m12s
This commit is contained in:
@@ -14,7 +14,7 @@ use super::endpoints;
|
|||||||
use super::events::GameEvent;
|
use super::events::GameEvent;
|
||||||
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, ParsedEvent};
|
use super::websocket::{parse_live_client_event, parse_websocket_message, ParsedEvent};
|
||||||
use crate::error::{LqpError, Result};
|
use crate::error::{LqpError, Result};
|
||||||
|
|
||||||
/// LQP Client for League Client communication.
|
/// LQP Client for League Client communication.
|
||||||
@@ -409,12 +409,151 @@ impl LqpClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get live client event data (kills, deaths, objectives) from port 2999.
|
||||||
|
/// This endpoint is available during games and provides real-time events.
|
||||||
|
pub async fn get_live_client_events(&self) -> Result<serde_json::Value> {
|
||||||
|
// Live client data runs on port 2999, separate from the LQP client port
|
||||||
|
let url = format!(
|
||||||
|
"{}{}",
|
||||||
|
endpoints::LIVE_CLIENT_DATA_BASE_URL,
|
||||||
|
endpoints::LIVE_CLIENT_DATA_EVENTS
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.http_client
|
||||||
|
.get(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| LqpError::ConnectionFailed(e.to_string()))?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
return Err(LqpError::ConnectionFailed(format!(
|
||||||
|
"Live client events request failed: {}",
|
||||||
|
response.status()
|
||||||
|
))
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| LqpError::EventParseError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(json)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get local player selection from champion select.
|
/// Get local player selection from champion select.
|
||||||
pub async fn get_local_player_selection(&self) -> Result<serde_json::Value> {
|
pub async fn get_local_player_selection(&self) -> Result<serde_json::Value> {
|
||||||
self.request("GET", endpoints::CHAMPION_SELECT_LOCAL_PLAYER)
|
self.request("GET", endpoints::CHAMPION_SELECT_LOCAL_PLAYER)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start polling for live client events during a game.
|
||||||
|
/// This polls the /liveclientdata/eventdata endpoint and broadcasts events.
|
||||||
|
pub async fn start_live_client_event_poller(&self) {
|
||||||
|
let event_sender = self.event_sender.clone();
|
||||||
|
let state = self.state.clone();
|
||||||
|
let shutdown = self.shutdown.clone();
|
||||||
|
let http_client = self.http_client.clone();
|
||||||
|
|
||||||
|
info!("Starting live client event poller");
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut last_event_id: Option<u64> = None;
|
||||||
|
let mut poll_count = 0u32;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if *shutdown.read().await {
|
||||||
|
info!("Live client event poller shutting down");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only poll when in game
|
||||||
|
let current_phase = state.read().await.phase;
|
||||||
|
if current_phase != GameflowPhase::InProgress {
|
||||||
|
// Reset event tracking when not in game
|
||||||
|
last_event_id = None;
|
||||||
|
poll_count = 0;
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
poll_count += 1;
|
||||||
|
if poll_count % 20 == 1 {
|
||||||
|
// Log every 10 seconds (20 * 500ms)
|
||||||
|
info!("Live client event poller active, polling for events...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll for events
|
||||||
|
let url = format!(
|
||||||
|
"{}{}",
|
||||||
|
endpoints::LIVE_CLIENT_DATA_BASE_URL,
|
||||||
|
endpoints::LIVE_CLIENT_DATA_EVENTS
|
||||||
|
);
|
||||||
|
|
||||||
|
match http_client.get(&url).send().await {
|
||||||
|
Ok(response) if response.status().is_success() => {
|
||||||
|
match response.json::<serde_json::Value>().await {
|
||||||
|
Ok(events) => {
|
||||||
|
// The response has an "Events" key containing the array
|
||||||
|
let events_array = events.get("Events").and_then(|e| e.as_array());
|
||||||
|
if let Some(events_array) = events_array {
|
||||||
|
let event_count = events_array.len();
|
||||||
|
if event_count > 0 {
|
||||||
|
info!(
|
||||||
|
"Received {} events from live client API",
|
||||||
|
event_count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for event in events_array {
|
||||||
|
// Check if this is a new event
|
||||||
|
let event_id =
|
||||||
|
event.get("EventID").and_then(|id| id.as_u64());
|
||||||
|
|
||||||
|
// Skip events we've already processed
|
||||||
|
if let Some(id) = event_id {
|
||||||
|
if let Some(last_id) = last_event_id {
|
||||||
|
if id <= last_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_event_id = Some(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and broadcast the event
|
||||||
|
if let Some(parsed) = parse_live_client_event(event) {
|
||||||
|
info!("Parsed live client event: {:?}", parsed.event);
|
||||||
|
// Update state based on event
|
||||||
|
Self::update_state_from_event(&state, &parsed.event)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Broadcast event
|
||||||
|
if event_sender.send(parsed).is_err() {
|
||||||
|
trace!("No event subscribers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to parse live client events: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(response) => {
|
||||||
|
info!("Live client events request failed: {}", response.status());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
info!("Failed to fetch live client events: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll every 500ms during games
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Metadata Fetching Methods
|
// Metadata Fetching Methods
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -32,9 +32,14 @@ pub const LP_CHANGE_NOTIFICATION: &str = "/lol-ranked/v1/current-lp-change-notif
|
|||||||
pub const LIVE_CLIENT_DATA_ACTIVE_PLAYER: &str = "/liveclientdata/activeplayer";
|
pub const LIVE_CLIENT_DATA_ACTIVE_PLAYER: &str = "/liveclientdata/activeplayer";
|
||||||
/// Live client player list endpoint.
|
/// Live client player list endpoint.
|
||||||
pub const LIVE_CLIENT_DATA_PLAYER_LIST: &str = "/liveclientdata/playerlist";
|
pub const LIVE_CLIENT_DATA_PLAYER_LIST: &str = "/liveclientdata/playerlist";
|
||||||
|
/// Live client event data endpoint (kills, deaths, objectives).
|
||||||
|
pub const LIVE_CLIENT_DATA_EVENTS: &str = "/liveclientdata/eventdata";
|
||||||
/// Local player selection in champion select.
|
/// Local player selection in champion select.
|
||||||
pub const CHAMPION_SELECT_LOCAL_PLAYER: &str = "/lol-champ-select/v1/session/my-selection";
|
pub const CHAMPION_SELECT_LOCAL_PLAYER: &str = "/lol-champ-select/v1/session/my-selection";
|
||||||
|
|
||||||
|
/// Live Client Data server base URL (runs on port 2999).
|
||||||
|
pub const LIVE_CLIENT_DATA_BASE_URL: &str = "https://127.0.0.1:2999";
|
||||||
|
|
||||||
/// LQP WebSocket endpoints to subscribe to.
|
/// LQP WebSocket endpoints to subscribe to.
|
||||||
pub const SUBSCRIBE_ENDPOINTS: &[&str] = &[
|
pub const SUBSCRIBE_ENDPOINTS: &[&str] = &[
|
||||||
GAMEFLOW_PHASE,
|
GAMEFLOW_PHASE,
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ pub use client::LqpClient;
|
|||||||
pub use endpoints::{
|
pub use endpoints::{
|
||||||
ALL_RUNE_PAGES, CHAMPION_SELECT, CHAMPION_SELECT_LOCAL_PLAYER, CHAMPION_SUMMARY,
|
ALL_RUNE_PAGES, CHAMPION_SELECT, CHAMPION_SELECT_LOCAL_PLAYER, CHAMPION_SUMMARY,
|
||||||
GAMEFLOW_PHASE, GAME_STATS, LIVE_CLIENT_DATA, LIVE_CLIENT_DATA_ACTIVE_PLAYER,
|
GAMEFLOW_PHASE, GAME_STATS, LIVE_CLIENT_DATA, LIVE_CLIENT_DATA_ACTIVE_PLAYER,
|
||||||
LIVE_CLIENT_DATA_PLAYER_LIST, MATCH_HISTORY, RUNE_PAGES, SESSION, SUBSCRIBE_ENDPOINTS,
|
LIVE_CLIENT_DATA_BASE_URL, LIVE_CLIENT_DATA_EVENTS, LIVE_CLIENT_DATA_PLAYER_LIST,
|
||||||
SUMMONER,
|
MATCH_HISTORY, RUNE_PAGES, SESSION, SUBSCRIBE_ENDPOINTS, SUMMONER,
|
||||||
};
|
};
|
||||||
pub use events::{
|
pub use events::{
|
||||||
ChampSelectStartInfo, ChampionPickInfo, DeathEvent, EventData, GameEndInfo, GameEvent,
|
ChampSelectStartInfo, ChampionPickInfo, DeathEvent, EventData, GameEndInfo, GameEvent,
|
||||||
@@ -26,4 +26,6 @@ pub use events::{
|
|||||||
QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
|
QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
|
||||||
};
|
};
|
||||||
pub use state::{ClientState, GameflowPhase};
|
pub use state::{ClientState, GameflowPhase};
|
||||||
pub use websocket::{parse_event_from_uri, parse_websocket_message, ParsedEvent};
|
pub use websocket::{
|
||||||
|
parse_event_from_uri, parse_live_client_event, parse_websocket_message, ParsedEvent,
|
||||||
|
};
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ pub fn parse_event_from_uri(
|
|||||||
// Handle game events (kills, deaths, objectives)
|
// Handle game events (kills, deaths, objectives)
|
||||||
if uri == "/lol-game-events/v1/game-events" {
|
if uri == "/lol-game-events/v1/game-events" {
|
||||||
info!("Game event received: {:?}", data);
|
info!("Game event received: {:?}", data);
|
||||||
return GameEvent::from_json(data);
|
return parse_game_event(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle ready check
|
// Handle ready check
|
||||||
@@ -664,6 +664,418 @@ fn parse_ranked_stats_event(data: &serde_json::Value) -> Option<GameEvent> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse game events from the /lol-game-events/v1/game-events endpoint.
|
||||||
|
///
|
||||||
|
/// This endpoint receives events like kills, deaths, and objectives.
|
||||||
|
/// The format varies but typically includes an EventName field.
|
||||||
|
fn parse_game_event(data: &serde_json::Value) -> Option<GameEvent> {
|
||||||
|
// The game events API can send various event types
|
||||||
|
// Common event names: ChampionKill, ChampionDeath, DragonKill, BaronKill, etc.
|
||||||
|
let event_name = data.get("EventName").and_then(|n| n.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
info!("Parsing game event: {} -> {:?}", event_name, data);
|
||||||
|
|
||||||
|
match event_name {
|
||||||
|
"ChampionKill" => {
|
||||||
|
// Extract kill information
|
||||||
|
let killer = data
|
||||||
|
.get("KillerName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or("Unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let victim = data
|
||||||
|
.get("VictimName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or("Unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let killer_champion = data
|
||||||
|
.get("KillerChampionName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let victim_champion = data
|
||||||
|
.get("VictimChampionName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
// Check if it was a solo kill (no assisters)
|
||||||
|
let assisters = data
|
||||||
|
.get("Assisters")
|
||||||
|
.and_then(|a| a.as_array())
|
||||||
|
.map(|arr| arr.len() as u32)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let solo_kill = assisters == 0;
|
||||||
|
|
||||||
|
// Extract position if available
|
||||||
|
let position = data.get("Position").map(|p| super::events::Position {
|
||||||
|
x: p.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0) as f32,
|
||||||
|
y: p.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0) as f32,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get game time
|
||||||
|
let game_time = data.get("GameTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
let event_json = serde_json::json!({
|
||||||
|
"eventType": "lcu-kill",
|
||||||
|
"killer": killer,
|
||||||
|
"killerChampion": killer_champion,
|
||||||
|
"victim": victim,
|
||||||
|
"victimChampion": victim_champion,
|
||||||
|
"soloKill": solo_kill,
|
||||||
|
"assists": assisters,
|
||||||
|
"position": position,
|
||||||
|
"gameTime": game_time
|
||||||
|
});
|
||||||
|
|
||||||
|
GameEvent::from_json(&event_json)
|
||||||
|
}
|
||||||
|
"ChampionDeath" => {
|
||||||
|
// Extract death information
|
||||||
|
let killer = data
|
||||||
|
.get("KillerName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let killer_champion = data
|
||||||
|
.get("KillerChampionName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
// Death cause - could be champion, minion, tower, etc.
|
||||||
|
let cause = killer
|
||||||
|
.clone()
|
||||||
|
.or_else(|| {
|
||||||
|
data.get("DeathCause")
|
||||||
|
.and_then(|c| c.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
|
|
||||||
|
// Extract position if available
|
||||||
|
let position = data.get("Position").map(|p| super::events::Position {
|
||||||
|
x: p.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0) as f32,
|
||||||
|
y: p.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0) as f32,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get game time
|
||||||
|
let game_time = data.get("GameTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
let event_json = serde_json::json!({
|
||||||
|
"eventType": "lcu-death",
|
||||||
|
"killer": killer,
|
||||||
|
"killerChampion": killer_champion,
|
||||||
|
"cause": cause,
|
||||||
|
"position": position,
|
||||||
|
"gameTime": game_time
|
||||||
|
});
|
||||||
|
|
||||||
|
GameEvent::from_json(&event_json)
|
||||||
|
}
|
||||||
|
"DragonKill" | "BaronKill" | "HeraldKill" | "RiftHeraldKill" | "ElderDragonKill" => {
|
||||||
|
let objective_type = match event_name {
|
||||||
|
"DragonKill" => "dragon",
|
||||||
|
"BaronKill" => "baron",
|
||||||
|
"HeraldKill" | "RiftHeraldKill" => "herald",
|
||||||
|
"ElderDragonKill" => "elderdragon",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
let team = data.get("Team").and_then(|t| t.as_u64()).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let game_time = data.get("GameTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
let event_json = serde_json::json!({
|
||||||
|
"eventType": "lcu-objective",
|
||||||
|
"objectiveType": objective_type,
|
||||||
|
"team": team,
|
||||||
|
"participated": false, // Will be determined by the caller
|
||||||
|
"gameTime": game_time
|
||||||
|
});
|
||||||
|
|
||||||
|
GameEvent::from_json(&event_json)
|
||||||
|
}
|
||||||
|
"TurretKill" | "InhibitorKill" | "NexusKill" => {
|
||||||
|
let objective_type = match event_name {
|
||||||
|
"TurretKill" => "tower",
|
||||||
|
"InhibitorKill" => "inhibitor",
|
||||||
|
"NexusKill" => "nexus",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
let team = data.get("Team").and_then(|t| t.as_u64()).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let game_time = data.get("GameTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
let event_json = serde_json::json!({
|
||||||
|
"eventType": "lcu-objective",
|
||||||
|
"objectiveType": objective_type,
|
||||||
|
"team": team,
|
||||||
|
"participated": false,
|
||||||
|
"gameTime": game_time
|
||||||
|
});
|
||||||
|
|
||||||
|
GameEvent::from_json(&event_json)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// Try to parse as a generic event with eventType field
|
||||||
|
debug!(
|
||||||
|
"Unknown game event type: {}, attempting generic parse",
|
||||||
|
event_name
|
||||||
|
);
|
||||||
|
GameEvent::from_json(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse events from the Live Client Data API (port 2999).
|
||||||
|
///
|
||||||
|
/// The Live Client Data API provides real-time game events including:
|
||||||
|
/// - Champion kills and deaths
|
||||||
|
/// - Objective kills (Dragon, Baron, Herald, etc.)
|
||||||
|
/// - Building destruction (Turrets, Inhibitors)
|
||||||
|
///
|
||||||
|
/// Event format from /liveclientdata/eventdata:
|
||||||
|
/// ```json
|
||||||
|
/// {
|
||||||
|
/// "EventID": 1,
|
||||||
|
/// "EventName": "ChampionKill",
|
||||||
|
/// "EventTime": 123.456,
|
||||||
|
/// "KillerName": "Player1",
|
||||||
|
/// "VictimName": "Player2",
|
||||||
|
/// "Assisters": ["Player3"],
|
||||||
|
/// ...
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn parse_live_client_event(data: &serde_json::Value) -> Option<ParsedEvent> {
|
||||||
|
let event_name = data.get("EventName").and_then(|n| n.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
info!("Parsing live client event: {} -> {:?}", event_name, data);
|
||||||
|
|
||||||
|
let event = match event_name {
|
||||||
|
"ChampionKill" => {
|
||||||
|
// Extract kill information
|
||||||
|
let killer = data
|
||||||
|
.get("KillerName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or("Unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let victim = data
|
||||||
|
.get("VictimName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or("Unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// Get champion names if available
|
||||||
|
let killer_champion = data
|
||||||
|
.get("KillerChampionName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let victim_champion = data
|
||||||
|
.get("VictimChampionName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
// Check if it was a solo kill (no assisters)
|
||||||
|
let assisters = data
|
||||||
|
.get("Assisters")
|
||||||
|
.and_then(|a| a.as_array())
|
||||||
|
.map(|arr| arr.len() as u32)
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let solo_kill = assisters == 0;
|
||||||
|
|
||||||
|
// Get game time
|
||||||
|
let game_time = data.get("EventTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
// Extract position if available
|
||||||
|
let position = data.get("Position").map(|p| super::events::Position {
|
||||||
|
x: p.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0) as f32,
|
||||||
|
y: p.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0) as f32,
|
||||||
|
});
|
||||||
|
|
||||||
|
let event_json = serde_json::json!({
|
||||||
|
"eventType": "lcu-kill",
|
||||||
|
"killer": killer,
|
||||||
|
"killerChampion": killer_champion,
|
||||||
|
"victim": victim,
|
||||||
|
"victimChampion": victim_champion,
|
||||||
|
"soloKill": solo_kill,
|
||||||
|
"assists": assisters,
|
||||||
|
"position": position,
|
||||||
|
"gameTime": game_time
|
||||||
|
});
|
||||||
|
|
||||||
|
GameEvent::from_json(&event_json)
|
||||||
|
}
|
||||||
|
"ChampionDeath" => {
|
||||||
|
// Extract death information
|
||||||
|
let killer = data
|
||||||
|
.get("KillerName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
let killer_champion = data
|
||||||
|
.get("KillerChampionName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
// Death cause
|
||||||
|
let cause = killer
|
||||||
|
.clone()
|
||||||
|
.or_else(|| {
|
||||||
|
data.get("DeathCause")
|
||||||
|
.and_then(|c| c.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
|
|
||||||
|
// Get game time
|
||||||
|
let game_time = data.get("EventTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
// Extract position if available
|
||||||
|
let position = data.get("Position").map(|p| super::events::Position {
|
||||||
|
x: p.get("x").and_then(|x| x.as_f64()).unwrap_or(0.0) as f32,
|
||||||
|
y: p.get("y").and_then(|y| y.as_f64()).unwrap_or(0.0) as f32,
|
||||||
|
});
|
||||||
|
|
||||||
|
let event_json = serde_json::json!({
|
||||||
|
"eventType": "lcu-death",
|
||||||
|
"killer": killer,
|
||||||
|
"killerChampion": killer_champion,
|
||||||
|
"cause": cause,
|
||||||
|
"position": position,
|
||||||
|
"gameTime": game_time
|
||||||
|
});
|
||||||
|
|
||||||
|
GameEvent::from_json(&event_json)
|
||||||
|
}
|
||||||
|
"DragonKill" | "BaronKill" | "HeraldKill" | "RiftHeraldKill" | "ElderDragonKill" => {
|
||||||
|
let objective_type = match event_name {
|
||||||
|
"DragonKill" => "dragon",
|
||||||
|
"BaronKill" => "baron",
|
||||||
|
"HeraldKill" | "RiftHeraldKill" => "herald",
|
||||||
|
"ElderDragonKill" => "elderdragon",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
let _killer = data
|
||||||
|
.get("KillerName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or("Unknown");
|
||||||
|
|
||||||
|
// Determine team based on killer (would need player list to determine team)
|
||||||
|
let team = data.get("KillerTeam").and_then(|t| t.as_u64()).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let game_time = data.get("EventTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
let event_json = serde_json::json!({
|
||||||
|
"eventType": "lcu-objective",
|
||||||
|
"objectiveType": objective_type,
|
||||||
|
"team": team,
|
||||||
|
"participated": false,
|
||||||
|
"gameTime": game_time
|
||||||
|
});
|
||||||
|
|
||||||
|
GameEvent::from_json(&event_json)
|
||||||
|
}
|
||||||
|
"TurretKill" | "InhibitorKill" | "NexusKill" => {
|
||||||
|
let objective_type = match event_name {
|
||||||
|
"TurretKill" => "tower",
|
||||||
|
"InhibitorKill" => "inhibitor",
|
||||||
|
"NexusKill" => "nexus",
|
||||||
|
_ => "unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
let team = data.get("KillerTeam").and_then(|t| t.as_u64()).unwrap_or(0) as u32;
|
||||||
|
|
||||||
|
let game_time = data.get("EventTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
let event_json = serde_json::json!({
|
||||||
|
"eventType": "lcu-objective",
|
||||||
|
"objectiveType": objective_type,
|
||||||
|
"team": team,
|
||||||
|
"participated": false,
|
||||||
|
"gameTime": game_time
|
||||||
|
});
|
||||||
|
|
||||||
|
GameEvent::from_json(&event_json)
|
||||||
|
}
|
||||||
|
"Multikill" => {
|
||||||
|
// Multikill events (double, triple, quadra, penta kills)
|
||||||
|
let killer = data
|
||||||
|
.get("KillerName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or("Unknown");
|
||||||
|
|
||||||
|
let kill_count = data.get("KillCount").and_then(|k| k.as_u64()).unwrap_or(2) as u32;
|
||||||
|
|
||||||
|
let game_time = data.get("EventTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Multikill event: {} got a {}-kill at {:?}",
|
||||||
|
killer, kill_count, game_time
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't emit a separate event for multikills, they're derived from kills
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"FirstBlood" => {
|
||||||
|
let killer = data
|
||||||
|
.get("KillerName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or("Unknown");
|
||||||
|
|
||||||
|
let victim = data
|
||||||
|
.get("VictimName")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.unwrap_or("Unknown");
|
||||||
|
|
||||||
|
let game_time = data.get("EventTime").and_then(|t| t.as_f64());
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"First Blood: {} killed {} at {:?}",
|
||||||
|
killer, victim, game_time
|
||||||
|
);
|
||||||
|
|
||||||
|
// First blood is just a special kill, the kill event will be emitted separately
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"GameStart" => {
|
||||||
|
let game_time = data
|
||||||
|
.get("EventTime")
|
||||||
|
.and_then(|t| t.as_f64())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
info!("Game started at {:?}", game_time);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
"GameEnd" => {
|
||||||
|
let game_time = data
|
||||||
|
.get("EventTime")
|
||||||
|
.and_then(|t| t.as_f64())
|
||||||
|
.unwrap_or(0.0);
|
||||||
|
|
||||||
|
info!("Game ended at {:?}", game_time);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug!("Unknown live client event type: {}", event_name);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
event.map(|e| ParsedEvent {
|
||||||
|
event: e,
|
||||||
|
raw_data: data.clone(),
|
||||||
|
uri: "/liveclientdata/eventdata".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -206,6 +206,8 @@ impl Daemon {
|
|||||||
if let Some(creds) = watcher.credentials() {
|
if let Some(creds) = watcher.credentials() {
|
||||||
self.lqp_client.connect(creds.clone()).await?;
|
self.lqp_client.connect(creds.clone()).await?;
|
||||||
self.lqp_client.start_event_listener().await?;
|
self.lqp_client.start_event_listener().await?;
|
||||||
|
// Start polling for live client events (kills, deaths, objectives)
|
||||||
|
self.lqp_client.start_live_client_event_poller().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(false) => {
|
Some(false) => {
|
||||||
|
|||||||
@@ -197,6 +197,11 @@ impl TimelineStore {
|
|||||||
metadata.end_time = Some(result.end_time);
|
metadata.end_time = Some(result.end_time);
|
||||||
metadata.duration = result.duration;
|
metadata.duration = result.duration;
|
||||||
metadata.file_path = Some(result.path.clone());
|
metadata.file_path = Some(result.path.clone());
|
||||||
|
metadata.video_file = result
|
||||||
|
.path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.map(String::from);
|
||||||
metadata.file_size = result.file_size();
|
metadata.file_size = result.file_size();
|
||||||
metadata.finalized = true;
|
metadata.finalized = true;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user