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:
@@ -116,7 +116,7 @@ pub fn parse_event_from_uri(
|
||||
// Handle game events (kills, deaths, objectives)
|
||||
if uri == "/lol-game-events/v1/game-events" {
|
||||
info!("Game event received: {:?}", data);
|
||||
return GameEvent::from_json(data);
|
||||
return parse_game_event(data);
|
||||
}
|
||||
|
||||
// Handle ready check
|
||||
@@ -664,6 +664,418 @@ fn parse_ranked_stats_event(data: &serde_json::Value) -> Option<GameEvent> {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user