record-daemon: add live client events, fix video_file
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m12s

This commit is contained in:
2026-03-28 11:08:19 +01:00
parent 16d9ddaafa
commit 3223ba74fc
6 changed files with 570 additions and 5 deletions

View File

@@ -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::*;