record raw events everywhere
Some checks are pending
record-daemon / Build, check and test (push) Waiting to run
Some checks are pending
record-daemon / Build, check and test (push) Waiting to run
This commit is contained in:
693
record-daemon/Cargo.lock
generated
693
record-daemon/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::config::Settings;
|
use crate::config::Settings;
|
||||||
use crate::lqp::GameEvent;
|
|
||||||
use crate::recording::RecordingResult;
|
use crate::recording::RecordingResult;
|
||||||
use crate::state::DaemonStatus;
|
use crate::state::DaemonStatus;
|
||||||
use crate::timeline::RecordingMetadata;
|
use crate::timeline::RecordingMetadata;
|
||||||
@@ -153,7 +152,10 @@ pub enum IpcNotification {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Game event received.
|
/// Game event received.
|
||||||
GameEvent { event: Box<GameEvent> },
|
GameEvent {
|
||||||
|
event_type: String,
|
||||||
|
raw_data: serde_json::Value,
|
||||||
|
},
|
||||||
|
|
||||||
/// Daemon status changed.
|
/// Daemon status changed.
|
||||||
StatusChanged { status: DaemonStatus },
|
StatusChanged { status: DaemonStatus },
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use tracing::{debug, error, info, trace, warn};
|
|||||||
|
|
||||||
use super::auth::LockfileCredentials;
|
use super::auth::LockfileCredentials;
|
||||||
use super::endpoints;
|
use super::endpoints;
|
||||||
use super::events::GameEvent;
|
use super::events::EVENT_TYPE_GAME_END;
|
||||||
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_live_client_event, parse_websocket_message, ParsedEvent};
|
use super::websocket::{parse_live_client_event, parse_websocket_message, ParsedEvent};
|
||||||
@@ -186,23 +186,35 @@ impl LqpClient {
|
|||||||
}
|
}
|
||||||
if let Some(parsed) = parse_websocket_message(&text) {
|
if let Some(parsed) = parse_websocket_message(&text) {
|
||||||
// Update state based on event
|
// Update state based on event
|
||||||
Self::update_state_from_event(&state, &parsed.event).await;
|
Self::update_state_from_event(&state, &parsed).await;
|
||||||
|
|
||||||
// Check for duplicate GameStart events
|
// Check for duplicate GameStart events
|
||||||
if let GameEvent::GameStart(ref info) = parsed.event {
|
if parsed.event_type == super::events::EVENT_TYPE_GAME_START {
|
||||||
|
let game_id = parsed
|
||||||
|
.raw_data
|
||||||
|
.get("gameId")
|
||||||
|
.or_else(|| {
|
||||||
|
parsed
|
||||||
|
.raw_data
|
||||||
|
.get("gameData")
|
||||||
|
.and_then(|gd| gd.get("gameId"))
|
||||||
|
})
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
let mut last_game_id = last_emitted_game_id.write().await;
|
let mut last_game_id = last_emitted_game_id.write().await;
|
||||||
if *last_game_id == Some(info.game_id) {
|
if *last_game_id == Some(game_id) && game_id != 0 {
|
||||||
info!(
|
info!(
|
||||||
"Skipping duplicate GameStart event for game_id={}",
|
"Skipping duplicate GameStart event for game_id={}",
|
||||||
info.game_id
|
game_id
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
*last_game_id = Some(info.game_id);
|
*last_game_id = Some(game_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset last_emitted_game_id on GameEnd to allow new game starts
|
// Reset last_emitted_game_id on GameEnd to allow new game starts
|
||||||
if let GameEvent::GameEnd(_) = &parsed.event {
|
if parsed.event_type == EVENT_TYPE_GAME_END {
|
||||||
*last_emitted_game_id.write().await = None;
|
*last_emitted_game_id.write().await = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,23 +231,35 @@ impl LqpClient {
|
|||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
if let Some(parsed) = parse_websocket_message(&text) {
|
if let Some(parsed) = parse_websocket_message(&text) {
|
||||||
// Update state based on event
|
// Update state based on event
|
||||||
Self::update_state_from_event(&state, &parsed.event).await;
|
Self::update_state_from_event(&state, &parsed).await;
|
||||||
|
|
||||||
// Check for duplicate GameStart events
|
// Check for duplicate GameStart events
|
||||||
if let GameEvent::GameStart(ref info) = parsed.event {
|
if parsed.event_type == super::events::EVENT_TYPE_GAME_START {
|
||||||
|
let game_id = parsed
|
||||||
|
.raw_data
|
||||||
|
.get("gameId")
|
||||||
|
.or_else(|| {
|
||||||
|
parsed
|
||||||
|
.raw_data
|
||||||
|
.get("gameData")
|
||||||
|
.and_then(|gd| gd.get("gameId"))
|
||||||
|
})
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
let mut last_game_id = last_emitted_game_id.write().await;
|
let mut last_game_id = last_emitted_game_id.write().await;
|
||||||
if *last_game_id == Some(info.game_id) {
|
if *last_game_id == Some(game_id) && game_id != 0 {
|
||||||
info!(
|
info!(
|
||||||
"Skipping duplicate GameStart event for game_id={}",
|
"Skipping duplicate GameStart event for game_id={}",
|
||||||
info.game_id
|
game_id
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
*last_game_id = Some(info.game_id);
|
*last_game_id = Some(game_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset last_emitted_game_id on GameEnd to allow new game starts
|
// Reset last_emitted_game_id on GameEnd to allow new game starts
|
||||||
if let GameEvent::GameEnd(_) = &parsed.event {
|
if parsed.event_type == EVENT_TYPE_GAME_END {
|
||||||
*last_emitted_game_id.write().await = None;
|
*last_emitted_game_id.write().await = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,19 +301,36 @@ impl LqpClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update internal state from a game event.
|
/// Update internal state from a parsed event.
|
||||||
async fn update_state_from_event(state: &Arc<RwLock<ClientState>>, event: &GameEvent) {
|
async fn update_state_from_event(state: &Arc<RwLock<ClientState>>, parsed: &ParsedEvent) {
|
||||||
let mut state = state.write().await;
|
let mut state = state.write().await;
|
||||||
|
|
||||||
match event {
|
match parsed.event_type.as_str() {
|
||||||
GameEvent::GameStart(info) => {
|
super::events::EVENT_TYPE_GAME_START => {
|
||||||
state.phase = GameflowPhase::InProgress;
|
state.phase = GameflowPhase::InProgress;
|
||||||
state.game_id = Some(info.game_id);
|
state.game_id = parsed
|
||||||
state.champion = info.champion.clone();
|
.raw_data
|
||||||
|
.get("gameId")
|
||||||
|
.or_else(|| {
|
||||||
|
parsed
|
||||||
|
.raw_data
|
||||||
|
.get("gameData")
|
||||||
|
.and_then(|gd| gd.get("gameId"))
|
||||||
|
})
|
||||||
|
.and_then(|v| v.as_u64());
|
||||||
|
// Champion is not extracted here — raw session data has it
|
||||||
}
|
}
|
||||||
GameEvent::GameEnd(_) => {
|
super::events::EVENT_TYPE_GAME_END => {
|
||||||
state.phase = GameflowPhase::EndOfGame;
|
state.phase = GameflowPhase::EndOfGame;
|
||||||
}
|
}
|
||||||
|
super::events::EVENT_TYPE_PHASE_CHANGE => {
|
||||||
|
let phase_str = parsed
|
||||||
|
.raw_data
|
||||||
|
.as_str()
|
||||||
|
.or_else(|| parsed.raw_data.get("phase").and_then(|v| v.as_str()))
|
||||||
|
.unwrap_or("");
|
||||||
|
state.phase = GameflowPhase::from(phase_str);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,6 +538,7 @@ impl LqpClient {
|
|||||||
Ok(events) => {
|
Ok(events) => {
|
||||||
// The response has an "Events" key containing the array
|
// The response has an "Events" key containing the array
|
||||||
let events_array = events.get("Events").and_then(|e| e.as_array());
|
let events_array = events.get("Events").and_then(|e| e.as_array());
|
||||||
|
|
||||||
if let Some(events_array) = events_array {
|
if let Some(events_array) = events_array {
|
||||||
let event_count = events_array.len();
|
let event_count = events_array.len();
|
||||||
if event_count > 0 {
|
if event_count > 0 {
|
||||||
@@ -522,10 +564,12 @@ impl LqpClient {
|
|||||||
|
|
||||||
// Parse and broadcast the event
|
// Parse and broadcast the event
|
||||||
if let Some(parsed) = parse_live_client_event(event) {
|
if let Some(parsed) = parse_live_client_event(event) {
|
||||||
info!("Parsed live client event: {:?}", parsed.event);
|
info!(
|
||||||
|
"Parsed live client event: type={}",
|
||||||
|
parsed.event_type
|
||||||
|
);
|
||||||
// Update state based on event
|
// Update state based on event
|
||||||
Self::update_state_from_event(&state, &parsed.event)
|
Self::update_state_from_event(&state, &parsed).await;
|
||||||
.await;
|
|
||||||
|
|
||||||
// Broadcast event
|
// Broadcast event
|
||||||
if event_sender.send(parsed).is_err() {
|
if event_sender.send(parsed).is_err() {
|
||||||
|
|||||||
@@ -1,596 +1,141 @@
|
|||||||
//! Game event types from the League Client API.
|
//! Event type constants and metadata structures for the League Client API.
|
||||||
//!
|
//!
|
||||||
//! These events are received via WebSocket subscription to LQP endpoints.
|
//! The record-daemon stores raw API JSON data only — no typed event parsing.
|
||||||
|
//! Event types are simple string constants derived from the URI, used for
|
||||||
|
//! classification and state machine transitions. The tauri-app parses the
|
||||||
|
//! raw data it needs on the frontend side.
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// A game event received from the League Client.
|
// ============================================================================
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
// Event Type Constants
|
||||||
#[serde(tag = "eventType", rename_all = "camelCase")]
|
// ============================================================================
|
||||||
pub enum GameEvent {
|
|
||||||
/// Match found in queue.
|
/// Match found in queue.
|
||||||
#[serde(rename = "lcu-match-found")]
|
pub const EVENT_TYPE_MATCH_FOUND: &str = "match_found";
|
||||||
MatchFound(MatchInfo),
|
|
||||||
|
|
||||||
/// Champion select phase started.
|
/// Champion select phase started.
|
||||||
#[serde(rename = "lcu-champ-select-start")]
|
pub const EVENT_TYPE_CHAMP_SELECT_START: &str = "champ_select_start";
|
||||||
ChampSelectStart(ChampSelectStartInfo),
|
|
||||||
|
|
||||||
/// Player picked a champion.
|
/// Player picked a champion.
|
||||||
#[serde(rename = "lcu-champion-pick")]
|
pub const EVENT_TYPE_CHAMPION_PICK: &str = "champion_pick";
|
||||||
ChampionPick(ChampionPickInfo),
|
|
||||||
|
|
||||||
/// Game has started.
|
/// Game has started.
|
||||||
#[serde(rename = "lcu-game-start")]
|
pub const EVENT_TYPE_GAME_START: &str = "game_start";
|
||||||
GameStart(Box<GameStartInfo>),
|
|
||||||
|
|
||||||
/// Player killed an enemy.
|
/// Player killed an enemy.
|
||||||
#[serde(rename = "lcu-kill")]
|
pub const EVENT_TYPE_KILL: &str = "kill";
|
||||||
Kill(KillEvent),
|
|
||||||
|
|
||||||
/// Player died.
|
/// Player died.
|
||||||
#[serde(rename = "lcu-death")]
|
pub const EVENT_TYPE_DEATH: &str = "death";
|
||||||
Death(DeathEvent),
|
|
||||||
|
|
||||||
/// Objective was taken.
|
/// Objective was taken.
|
||||||
#[serde(rename = "lcu-objective")]
|
pub const EVENT_TYPE_OBJECTIVE: &str = "objective";
|
||||||
Objective(ObjectiveEvent),
|
|
||||||
|
|
||||||
/// In-game stats update.
|
/// In-game stats update.
|
||||||
#[serde(rename = "lcu-stats-update")]
|
pub const EVENT_TYPE_STATS_UPDATE: &str = "stats_update";
|
||||||
StatsUpdate(InGameStats),
|
|
||||||
|
|
||||||
/// Game has ended.
|
/// Game has ended.
|
||||||
#[serde(rename = "lcu-game-end")]
|
pub const EVENT_TYPE_GAME_END: &str = "game_end";
|
||||||
GameEnd(GameEndInfo),
|
|
||||||
|
|
||||||
/// Gameflow phase changed.
|
/// Gameflow phase changed.
|
||||||
#[serde(rename = "lcu-phase-change")]
|
pub const EVENT_TYPE_PHASE_CHANGE: &str = "phase_change";
|
||||||
PhaseChange(PhaseChangeInfo),
|
|
||||||
|
|
||||||
/// LP changed after a ranked game.
|
/// LP changed after a ranked game.
|
||||||
#[serde(rename = "lcu-lp-change")]
|
pub const EVENT_TYPE_LP_CHANGE: &str = "lp_change";
|
||||||
LpChange(LpChangeInfo),
|
/// Unknown / unclassified event.
|
||||||
|
pub const EVENT_TYPE_UNKNOWN: &str = "unknown";
|
||||||
|
|
||||||
/// Unknown event type.
|
/// Generate a brief human-readable description from raw event data.
|
||||||
#[serde(other)]
|
///
|
||||||
Unknown,
|
/// This is used for logging and timeline display. It extracts minimal
|
||||||
|
/// information from the raw JSON — no full parsing required.
|
||||||
|
pub fn describe_event(event_type: &str, raw_data: &serde_json::Value) -> String {
|
||||||
|
match event_type {
|
||||||
|
EVENT_TYPE_MATCH_FOUND => {
|
||||||
|
let queue = raw_data
|
||||||
|
.get("queueType")
|
||||||
|
.or_else(|| {
|
||||||
|
raw_data
|
||||||
|
.get("gameData")
|
||||||
|
.and_then(|gd| gd.get("queue"))
|
||||||
|
.and_then(|q| q.get("name"))
|
||||||
|
})
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("Unknown");
|
||||||
|
format!("Match found: {}", queue)
|
||||||
}
|
}
|
||||||
|
EVENT_TYPE_CHAMP_SELECT_START => "Champion select started".to_string(),
|
||||||
/// Phase change event data.
|
EVENT_TYPE_CHAMPION_PICK => {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
let champ = raw_data
|
||||||
#[serde(rename_all = "camelCase")]
|
.get("championName")
|
||||||
pub struct PhaseChangeInfo {
|
.or_else(|| raw_data.get("championName"))
|
||||||
/// The new phase.
|
.and_then(|v| v.as_str())
|
||||||
pub phase: String,
|
.unwrap_or("Unknown");
|
||||||
|
format!("Champion picked: {}", champ)
|
||||||
/// Timestamp when phase changed.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
EVENT_TYPE_GAME_START => {
|
||||||
/// LP change event data after a ranked game.
|
let game_id = raw_data
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
.get("gameId")
|
||||||
#[serde(rename_all = "camelCase")]
|
.or_else(|| raw_data.get("gameData").and_then(|gd| gd.get("gameId")))
|
||||||
pub struct LpChangeInfo {
|
.and_then(|v| v.as_u64())
|
||||||
/// Queue type (RANKED_SOLO_5x5, RANKED_FLEX_SR, etc.).
|
.unwrap_or(0);
|
||||||
pub queue_type: String,
|
format!("Game started: ID {}", game_id)
|
||||||
|
|
||||||
/// LP change amount (positive for gain, negative for loss).
|
|
||||||
pub lp_change: i32,
|
|
||||||
|
|
||||||
/// LP before the game.
|
|
||||||
pub lp_before: i32,
|
|
||||||
|
|
||||||
/// LP after the game.
|
|
||||||
pub lp_after: i32,
|
|
||||||
|
|
||||||
/// Current tier (IRON, BRONZE, SILVER, GOLD, PLATINUM, DIAMOND, MASTER, GRANDMASTER, CHALLENGER).
|
|
||||||
pub tier: String,
|
|
||||||
|
|
||||||
/// Current division (I, II, III, IV).
|
|
||||||
#[serde(default)]
|
|
||||||
pub division: Option<String>,
|
|
||||||
|
|
||||||
/// Current league points.
|
|
||||||
#[serde(default)]
|
|
||||||
pub league_points: Option<i32>,
|
|
||||||
|
|
||||||
/// Whether the player is in a promotional series.
|
|
||||||
#[serde(default)]
|
|
||||||
pub in_promos: bool,
|
|
||||||
|
|
||||||
/// Promotional series progress (e.g., "W:2 L:1").
|
|
||||||
#[serde(default)]
|
|
||||||
pub promo_progress: Option<String>,
|
|
||||||
|
|
||||||
/// Number of wins in promo series.
|
|
||||||
#[serde(default)]
|
|
||||||
pub promo_wins: Option<u32>,
|
|
||||||
|
|
||||||
/// Number of losses in promo series.
|
|
||||||
#[serde(default)]
|
|
||||||
pub promo_losses: Option<u32>,
|
|
||||||
|
|
||||||
/// Total games played in this queue.
|
|
||||||
#[serde(default)]
|
|
||||||
pub total_games: Option<u32>,
|
|
||||||
|
|
||||||
/// Total wins in this queue.
|
|
||||||
#[serde(default)]
|
|
||||||
pub total_wins: Option<u32>,
|
|
||||||
|
|
||||||
/// Total losses in this queue.
|
|
||||||
#[serde(default)]
|
|
||||||
pub total_losses: Option<u32>,
|
|
||||||
|
|
||||||
/// Timestamp when LP change occurred.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
EVENT_TYPE_KILL => {
|
||||||
/// Match found event data.
|
let killer = raw_data
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
.get("KillerName")
|
||||||
#[serde(rename_all = "camelCase")]
|
.or_else(|| raw_data.get("killer"))
|
||||||
pub struct MatchInfo {
|
.and_then(|v| v.as_str())
|
||||||
/// Match ID.
|
.unwrap_or("Unknown");
|
||||||
#[serde(default)]
|
let victim = raw_data
|
||||||
pub match_id: Option<String>,
|
.get("VictimName")
|
||||||
|
.or_else(|| raw_data.get("victim"))
|
||||||
/// Queue type (ranked, normal, aram, etc.).
|
.and_then(|v| v.as_str())
|
||||||
pub queue_type: String,
|
.unwrap_or("Unknown");
|
||||||
|
format!("{} killed {}", killer, victim)
|
||||||
/// Queue ID (numeric identifier).
|
|
||||||
#[serde(default)]
|
|
||||||
pub queue_id: Option<u32>,
|
|
||||||
|
|
||||||
/// Map name.
|
|
||||||
pub map: String,
|
|
||||||
|
|
||||||
/// Game mode.
|
|
||||||
pub game_mode: String,
|
|
||||||
|
|
||||||
/// Timestamp when match was found.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
EVENT_TYPE_DEATH => {
|
||||||
/// Champion select start event data.
|
let cause = raw_data
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
.get("DeathCause")
|
||||||
#[serde(rename_all = "camelCase")]
|
.or_else(|| raw_data.get("cause"))
|
||||||
pub struct ChampSelectStartInfo {
|
.and_then(|v| v.as_str())
|
||||||
/// Session ID.
|
.unwrap_or("Unknown");
|
||||||
#[serde(default)]
|
format!("Player died ({})", cause)
|
||||||
pub session_id: Option<String>,
|
|
||||||
|
|
||||||
/// Team (100 = blue, 200 = red).
|
|
||||||
#[serde(default)]
|
|
||||||
pub team: Option<u32>,
|
|
||||||
|
|
||||||
/// Timestamp when champ select started.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
EVENT_TYPE_OBJECTIVE => {
|
||||||
/// Champion pick event data.
|
let obj_type = raw_data
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
.get("EventName")
|
||||||
#[serde(rename_all = "camelCase")]
|
.or_else(|| raw_data.get("objectiveType"))
|
||||||
pub struct ChampionPickInfo {
|
.and_then(|v| v.as_str())
|
||||||
/// Player's summoner name.
|
.unwrap_or("objective");
|
||||||
pub summoner_name: String,
|
let team = raw_data
|
||||||
|
.get("Team")
|
||||||
/// Champion ID.
|
.or_else(|| raw_data.get("team"))
|
||||||
pub champion_id: u32,
|
.and_then(|v| v.as_u64())
|
||||||
|
.map(|t| if t == 100 { "Blue" } else { "Red" })
|
||||||
/// Champion name.
|
.unwrap_or("Unknown");
|
||||||
pub champion_name: String,
|
format!("{} took {}", team, obj_type)
|
||||||
|
|
||||||
/// Whether this is the local player's pick.
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_local_player: bool,
|
|
||||||
|
|
||||||
/// Skin ID selected.
|
|
||||||
#[serde(default)]
|
|
||||||
pub skin_id: Option<u32>,
|
|
||||||
|
|
||||||
/// Skin name.
|
|
||||||
#[serde(default)]
|
|
||||||
pub skin_name: Option<String>,
|
|
||||||
|
|
||||||
/// Timestamp when champion was picked.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
EVENT_TYPE_STATS_UPDATE => "Stats update".to_string(),
|
||||||
/// In-game stats update.
|
EVENT_TYPE_GAME_END => {
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
let victory = raw_data
|
||||||
#[serde(rename_all = "camelCase")]
|
.get("victory")
|
||||||
pub struct InGameStats {
|
.and_then(|v| v.as_bool())
|
||||||
/// Current kills.
|
.unwrap_or(false);
|
||||||
pub kills: u32,
|
let result = if victory { "Victory" } else { "Defeat" };
|
||||||
|
format!("Game ended: {}", result)
|
||||||
/// Current deaths.
|
|
||||||
pub deaths: u32,
|
|
||||||
|
|
||||||
/// Current assists.
|
|
||||||
pub assists: u32,
|
|
||||||
|
|
||||||
/// Current creep score.
|
|
||||||
#[serde(default)]
|
|
||||||
pub creep_score: u32,
|
|
||||||
|
|
||||||
/// Current gold.
|
|
||||||
#[serde(default)]
|
|
||||||
pub gold: u32,
|
|
||||||
|
|
||||||
/// Current level.
|
|
||||||
#[serde(default)]
|
|
||||||
pub level: u32,
|
|
||||||
|
|
||||||
/// Game time in seconds.
|
|
||||||
#[serde(default)]
|
|
||||||
pub game_time: f64,
|
|
||||||
|
|
||||||
/// Timestamp of the stats update.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
EVENT_TYPE_PHASE_CHANGE => {
|
||||||
/// Game start event data.
|
let phase = raw_data
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
.as_str()
|
||||||
#[serde(rename_all = "camelCase")]
|
.or_else(|| raw_data.get("phase").and_then(|v| v.as_str()))
|
||||||
pub struct GameStartInfo {
|
.unwrap_or("Unknown");
|
||||||
/// Game ID.
|
format!("Phase changed to: {}", phase)
|
||||||
pub game_id: u64,
|
|
||||||
|
|
||||||
/// Server address.
|
|
||||||
#[serde(default)]
|
|
||||||
pub server: Option<String>,
|
|
||||||
|
|
||||||
/// Player's champion name.
|
|
||||||
#[serde(default)]
|
|
||||||
pub champion: Option<String>,
|
|
||||||
|
|
||||||
/// Player's summoner name.
|
|
||||||
#[serde(default)]
|
|
||||||
pub summoner_name: Option<String>,
|
|
||||||
|
|
||||||
/// Team (100 = blue, 200 = red).
|
|
||||||
#[serde(default)]
|
|
||||||
pub team: Option<u32>,
|
|
||||||
|
|
||||||
/// Queue type (ranked, normal, aram, etc.).
|
|
||||||
#[serde(default)]
|
|
||||||
pub queue_type: Option<String>,
|
|
||||||
|
|
||||||
/// Queue ID.
|
|
||||||
#[serde(default)]
|
|
||||||
pub queue_id: Option<u32>,
|
|
||||||
|
|
||||||
/// Game mode.
|
|
||||||
#[serde(default)]
|
|
||||||
pub game_mode: Option<String>,
|
|
||||||
|
|
||||||
/// Map name.
|
|
||||||
#[serde(default, rename = "map")]
|
|
||||||
pub map_name: Option<String>,
|
|
||||||
|
|
||||||
/// Full gameflow session data (if available).
|
|
||||||
#[serde(default)]
|
|
||||||
pub session: Option<GameflowSession>,
|
|
||||||
|
|
||||||
/// Game start timestamp.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
EVENT_TYPE_LP_CHANGE => {
|
||||||
/// Kill event data.
|
let lp_change = raw_data
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
.get("lpChange")
|
||||||
#[serde(rename_all = "camelCase")]
|
.and_then(|v| v.as_i64())
|
||||||
pub struct KillEvent {
|
.unwrap_or(0);
|
||||||
/// Killer summoner name.
|
let tier = raw_data
|
||||||
pub killer: String,
|
.get("tier")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
/// Killer champion name.
|
.unwrap_or("UNRANKED");
|
||||||
#[serde(default)]
|
let sign = if lp_change >= 0 { "+" } else { "" };
|
||||||
pub killer_champion: Option<String>,
|
format!("LP Change: {}{} LP ({})", sign, lp_change, tier)
|
||||||
|
|
||||||
/// Victim summoner name.
|
|
||||||
pub victim: String,
|
|
||||||
|
|
||||||
/// Victim champion name.
|
|
||||||
#[serde(default)]
|
|
||||||
pub victim_champion: Option<String>,
|
|
||||||
|
|
||||||
/// Whether this was a solo kill.
|
|
||||||
#[serde(default)]
|
|
||||||
pub solo_kill: bool,
|
|
||||||
|
|
||||||
/// Number of assists.
|
|
||||||
#[serde(default)]
|
|
||||||
pub assists: u32,
|
|
||||||
|
|
||||||
/// Kill position on map.
|
|
||||||
#[serde(default)]
|
|
||||||
pub position: Option<Position>,
|
|
||||||
|
|
||||||
/// Game time when kill occurred.
|
|
||||||
#[serde(default)]
|
|
||||||
pub game_time: Option<f64>,
|
|
||||||
|
|
||||||
/// Real timestamp.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
}
|
||||||
|
_ => "Unknown event".to_string(),
|
||||||
/// Death event data.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct DeathEvent {
|
|
||||||
/// Killer summoner name (if killed by champion).
|
|
||||||
#[serde(default)]
|
|
||||||
pub killer: Option<String>,
|
|
||||||
|
|
||||||
/// Killer champion name.
|
|
||||||
#[serde(default)]
|
|
||||||
pub killer_champion: Option<String>,
|
|
||||||
|
|
||||||
/// Death cause (champion, minion, tower, etc.).
|
|
||||||
pub cause: String,
|
|
||||||
|
|
||||||
/// Death position on map.
|
|
||||||
#[serde(default)]
|
|
||||||
pub position: Option<Position>,
|
|
||||||
|
|
||||||
/// Game time when death occurred.
|
|
||||||
#[serde(default)]
|
|
||||||
pub game_time: Option<f64>,
|
|
||||||
|
|
||||||
/// Real timestamp.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Objective event data.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ObjectiveEvent {
|
|
||||||
/// Type of objective.
|
|
||||||
pub objective_type: ObjectiveType,
|
|
||||||
|
|
||||||
/// Team that took the objective (100 = blue, 200 = red).
|
|
||||||
pub team: u32,
|
|
||||||
|
|
||||||
/// Whether the player participated.
|
|
||||||
#[serde(default)]
|
|
||||||
pub participated: bool,
|
|
||||||
|
|
||||||
/// Game time when objective was taken.
|
|
||||||
#[serde(default)]
|
|
||||||
pub game_time: Option<f64>,
|
|
||||||
|
|
||||||
/// Real timestamp.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type of objective.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum ObjectiveType {
|
|
||||||
Dragon,
|
|
||||||
Herald,
|
|
||||||
Baron,
|
|
||||||
Tower,
|
|
||||||
Inhibitor,
|
|
||||||
Nexus,
|
|
||||||
RiftHerald,
|
|
||||||
ElderDragon,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 2D position on the map.
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
||||||
pub struct Position {
|
|
||||||
pub x: f32,
|
|
||||||
pub y: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Game end event data.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct GameEndInfo {
|
|
||||||
/// Game ID.
|
|
||||||
pub game_id: u64,
|
|
||||||
|
|
||||||
/// Whether the player's team won.
|
|
||||||
pub victory: bool,
|
|
||||||
|
|
||||||
/// Game duration in seconds.
|
|
||||||
pub duration: f64,
|
|
||||||
|
|
||||||
/// Player's stats.
|
|
||||||
#[serde(default)]
|
|
||||||
pub stats: Option<PlayerStats>,
|
|
||||||
|
|
||||||
/// End timestamp.
|
|
||||||
#[serde(default = "Utc::now")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Player statistics at game end.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct PlayerStats {
|
|
||||||
/// Kills.
|
|
||||||
pub kills: u32,
|
|
||||||
|
|
||||||
/// Deaths.
|
|
||||||
pub deaths: u32,
|
|
||||||
|
|
||||||
/// Assists.
|
|
||||||
pub assists: u32,
|
|
||||||
|
|
||||||
/// Total gold earned.
|
|
||||||
pub gold_earned: u32,
|
|
||||||
|
|
||||||
/// Total damage dealt.
|
|
||||||
pub damage_dealt: u64,
|
|
||||||
|
|
||||||
/// Total damage taken.
|
|
||||||
pub damage_taken: u64,
|
|
||||||
|
|
||||||
/// Minions killed (CS).
|
|
||||||
pub minions_killed: u32,
|
|
||||||
|
|
||||||
/// Vision score.
|
|
||||||
#[serde(default)]
|
|
||||||
pub vision_score: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Raw event data from LQP WebSocket.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct RawEvent {
|
|
||||||
/// Event type URI.
|
|
||||||
#[serde(rename = "uri")]
|
|
||||||
pub uri: String,
|
|
||||||
|
|
||||||
/// Event data.
|
|
||||||
pub data: EventData,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event data payload.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum EventData {
|
|
||||||
/// Game event.
|
|
||||||
GameEvent(Box<GameEvent>),
|
|
||||||
|
|
||||||
/// Raw JSON value.
|
|
||||||
Raw(serde_json::Value),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameEvent {
|
|
||||||
/// Parse a game event from raw WebSocket data.
|
|
||||||
pub fn from_json(value: &serde_json::Value) -> Option<Self> {
|
|
||||||
match serde_json::from_value(value.clone()) {
|
|
||||||
Ok(event) => Some(event),
|
|
||||||
Err(e) => {
|
|
||||||
// Log the parsing error for debugging
|
|
||||||
tracing::warn!("Failed to parse game event: {}. Data: {:?}", e, value);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if this event is relevant for recording.
|
|
||||||
pub fn is_relevant(&self) -> bool {
|
|
||||||
!matches!(self, GameEvent::Unknown)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a human-readable description of the event.
|
|
||||||
pub fn description(&self) -> String {
|
|
||||||
match self {
|
|
||||||
GameEvent::MatchFound(info) => {
|
|
||||||
format!("Match found: {} ({})", info.game_mode, info.queue_type)
|
|
||||||
}
|
|
||||||
GameEvent::ChampSelectStart(info) => {
|
|
||||||
format!("Champion select started (team: {:?})", info.team)
|
|
||||||
}
|
|
||||||
GameEvent::ChampionPick(pick) => {
|
|
||||||
format!("{} picked {}", pick.summoner_name, pick.champion_name)
|
|
||||||
}
|
|
||||||
GameEvent::GameStart(info) => {
|
|
||||||
format!("Game started: ID {}", info.game_id)
|
|
||||||
}
|
|
||||||
GameEvent::Kill(kill) => {
|
|
||||||
format!("{} killed {}", kill.killer, kill.victim)
|
|
||||||
}
|
|
||||||
GameEvent::Death(death) => {
|
|
||||||
format!("Player died ({})", death.cause)
|
|
||||||
}
|
|
||||||
GameEvent::Objective(obj) => {
|
|
||||||
let team = if obj.team == 100 { "Blue" } else { "Red" };
|
|
||||||
format!("{} took {:?}", team, obj.objective_type)
|
|
||||||
}
|
|
||||||
GameEvent::StatsUpdate(stats) => {
|
|
||||||
format!(
|
|
||||||
"Stats: {}/{}/{} CS: {} Gold: {}",
|
|
||||||
stats.kills, stats.deaths, stats.assists, stats.creep_score, stats.gold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
GameEvent::GameEnd(end) => {
|
|
||||||
let result = if end.victory { "Victory" } else { "Defeat" };
|
|
||||||
format!("Game ended: {} ({:.1}s)", result, end.duration)
|
|
||||||
}
|
|
||||||
GameEvent::PhaseChange(info) => {
|
|
||||||
format!("Phase changed to: {}", info.phase)
|
|
||||||
}
|
|
||||||
GameEvent::LpChange(lp) => {
|
|
||||||
let sign = if lp.lp_change >= 0 { "+" } else { "" };
|
|
||||||
format!(
|
|
||||||
"LP Change: {}{} LP ({} {} {} -> {} LP)",
|
|
||||||
sign,
|
|
||||||
lp.lp_change,
|
|
||||||
lp.tier,
|
|
||||||
lp.division.as_deref().unwrap_or(""),
|
|
||||||
lp.queue_type,
|
|
||||||
lp.lp_after
|
|
||||||
)
|
|
||||||
}
|
|
||||||
GameEvent::Unknown => "Unknown event".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the event type name for categorization.
|
|
||||||
pub fn event_type_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
GameEvent::MatchFound(_) => "match_found",
|
|
||||||
GameEvent::ChampSelectStart(_) => "champ_select_start",
|
|
||||||
GameEvent::ChampionPick(_) => "champion_pick",
|
|
||||||
GameEvent::GameStart(_) => "game_start",
|
|
||||||
GameEvent::Kill(_) => "kill",
|
|
||||||
GameEvent::Death(_) => "death",
|
|
||||||
GameEvent::Objective(_) => "objective",
|
|
||||||
GameEvent::StatsUpdate(_) => "stats_update",
|
|
||||||
GameEvent::GameEnd(_) => "game_end",
|
|
||||||
GameEvent::PhaseChange(_) => "phase_change",
|
|
||||||
GameEvent::LpChange(_) => "lp_change",
|
|
||||||
GameEvent::Unknown => "unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_kill_event() {
|
|
||||||
let json = serde_json::json!({
|
|
||||||
"eventType": "lcu-kill",
|
|
||||||
"killer": "Player1",
|
|
||||||
"killerChampion": "Ahri",
|
|
||||||
"victim": "Player2",
|
|
||||||
"victimChampion": "Lux",
|
|
||||||
"soloKill": true,
|
|
||||||
"assists": 0
|
|
||||||
});
|
|
||||||
|
|
||||||
let event: GameEvent = serde_json::from_value(json).unwrap();
|
|
||||||
if let GameEvent::Kill(kill) = event {
|
|
||||||
assert_eq!(kill.killer, "Player1");
|
|
||||||
assert!(kill.solo_kill);
|
|
||||||
} else {
|
|
||||||
panic!("Expected Kill event");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_objective_type_deserialization() {
|
|
||||||
let json = serde_json::json!("dragon");
|
|
||||||
let obj: ObjectiveType = serde_json::from_value(json).unwrap();
|
|
||||||
assert_eq!(obj, ObjectiveType::Dragon);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -868,3 +413,46 @@ impl GameflowSession {
|
|||||||
self.queue.as_ref().map(|q| q.id)
|
self.queue.as_ref().map(|q| q.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_event_type_constants() {
|
||||||
|
assert_eq!(EVENT_TYPE_GAME_START, "game_start");
|
||||||
|
assert_eq!(EVENT_TYPE_LP_CHANGE, "lp_change");
|
||||||
|
assert_eq!(EVENT_TYPE_KILL, "kill");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_describe_event_lp_change() {
|
||||||
|
let raw = serde_json::json!({
|
||||||
|
"lpChange": 22,
|
||||||
|
"lpAfter": 75,
|
||||||
|
"tier": "GOLD",
|
||||||
|
"queueType": "RANKED_SOLO_5x5"
|
||||||
|
});
|
||||||
|
let desc = describe_event(EVENT_TYPE_LP_CHANGE, &raw);
|
||||||
|
assert!(desc.contains("+22"));
|
||||||
|
assert!(desc.contains("GOLD"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_describe_event_kill() {
|
||||||
|
let raw = serde_json::json!({
|
||||||
|
"KillerName": "Player1",
|
||||||
|
"VictimName": "Player2"
|
||||||
|
});
|
||||||
|
let desc = describe_event(EVENT_TYPE_KILL, &raw);
|
||||||
|
assert_eq!(desc, "Player1 killed Player2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_describe_event_phase_change() {
|
||||||
|
// Phase change raw_data is just the phase string
|
||||||
|
let raw = serde_json::json!("InProgress");
|
||||||
|
let desc = describe_event(EVENT_TYPE_PHASE_CHANGE, &raw);
|
||||||
|
assert_eq!(desc, "Phase changed to: InProgress");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ pub use endpoints::{
|
|||||||
MATCH_HISTORY, RUNE_PAGES, SESSION, SUBSCRIBE_ENDPOINTS, SUMMONER,
|
MATCH_HISTORY, RUNE_PAGES, SESSION, SUBSCRIBE_ENDPOINTS, SUMMONER,
|
||||||
};
|
};
|
||||||
pub use events::{
|
pub use events::{
|
||||||
ChampSelectStartInfo, ChampionPickInfo, DeathEvent, EventData, GameEndInfo, GameEvent,
|
describe_event, GameflowSession, ItemBuild, ItemInfo, PlayerChampionSelection,
|
||||||
GameStartInfo, GameflowSession, InGameStats, ItemBuild, ItemInfo, KillEvent, MatchInfo,
|
PlayerGameMetadata, PlayerIdentity, QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
|
||||||
ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity,
|
EVENT_TYPE_CHAMPION_PICK, EVENT_TYPE_CHAMP_SELECT_START, EVENT_TYPE_DEATH, EVENT_TYPE_GAME_END,
|
||||||
QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
|
EVENT_TYPE_GAME_START, EVENT_TYPE_KILL, EVENT_TYPE_LP_CHANGE, EVENT_TYPE_MATCH_FOUND,
|
||||||
|
EVENT_TYPE_OBJECTIVE, EVENT_TYPE_PHASE_CHANGE, EVENT_TYPE_STATS_UPDATE, EVENT_TYPE_UNKNOWN,
|
||||||
};
|
};
|
||||||
pub use state::{ClientState, GameflowPhase};
|
pub use state::{ClientState, GameflowPhase};
|
||||||
pub use websocket::{
|
pub use websocket::{
|
||||||
parse_event_from_uri, parse_live_client_event, parse_websocket_message, ParsedEvent,
|
classify_event_from_uri, parse_live_client_event, parse_websocket_message, ParsedEvent,
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,10 @@ use record_daemon::{
|
|||||||
config::{self, Settings},
|
config::{self, Settings},
|
||||||
error::Result,
|
error::Result,
|
||||||
ipc::{self, IpcHandlers, IpcServer, IpcServerConfig},
|
ipc::{self, IpcHandlers, IpcServer, IpcServerConfig},
|
||||||
lqp::{GameEvent, LockfileWatcher, LqpClient},
|
lqp::{
|
||||||
|
describe_event, LockfileWatcher, LqpClient, EVENT_TYPE_CHAMPION_PICK,
|
||||||
|
EVENT_TYPE_GAME_START, EVENT_TYPE_PHASE_CHANGE,
|
||||||
|
},
|
||||||
recording::RecordingEngine,
|
recording::RecordingEngine,
|
||||||
state::{DaemonStateMachine, DaemonStatus, StateTransition},
|
state::{DaemonStateMachine, DaemonStatus, StateTransition},
|
||||||
timeline::{EventMapper, TimelineStore, TimestampedEvent},
|
timeline::{EventMapper, TimelineStore, TimestampedEvent},
|
||||||
@@ -226,25 +229,44 @@ impl Daemon {
|
|||||||
|
|
||||||
/// Handle a game event.
|
/// Handle a game event.
|
||||||
async fn handle_game_event(&self, parsed: record_daemon::lqp::ParsedEvent) -> Result<()> {
|
async fn handle_game_event(&self, parsed: record_daemon::lqp::ParsedEvent) -> Result<()> {
|
||||||
let event = &parsed.event;
|
let event_type = &parsed.event_type;
|
||||||
info!("[EVENT_HANDLER] Game event received: {:?}", event);
|
let raw_data = &parsed.raw_data;
|
||||||
|
let description = describe_event(event_type, raw_data);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"[EVENT_HANDLER] Game event received: type={}, desc={}",
|
||||||
|
event_type, description
|
||||||
|
);
|
||||||
|
|
||||||
// Handle pre-game data collection
|
// Handle pre-game data collection
|
||||||
match event {
|
match event_type.as_str() {
|
||||||
GameEvent::PhaseChange(info) if info.phase == "ChampSelect" => {
|
EVENT_TYPE_PHASE_CHANGE => {
|
||||||
|
let phase = raw_data
|
||||||
|
.as_str()
|
||||||
|
.or_else(|| raw_data.get("phase").and_then(|v| v.as_str()))
|
||||||
|
.unwrap_or("");
|
||||||
|
if phase == "ChampSelect" {
|
||||||
info!("[EVENT_HANDLER] Champion select started");
|
info!("[EVENT_HANDLER] Champion select started");
|
||||||
}
|
}
|
||||||
GameEvent::ChampionPick(pick) if pick.is_local_player => {
|
|
||||||
info!(
|
|
||||||
"[EVENT_HANDLER] Local player picked champion: {}",
|
|
||||||
pick.champion_name
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
GameEvent::GameStart(info) => {
|
EVENT_TYPE_CHAMPION_PICK => {
|
||||||
info!(
|
let is_local = raw_data
|
||||||
"[EVENT_HANDLER] Game started with metadata: queue={:?}, mode={:?}, map={:?}",
|
.get("isLocalPlayer")
|
||||||
info.queue_type, info.game_mode, info.map_name
|
.or_else(|| raw_data.get("is_local_player"))
|
||||||
);
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if is_local {
|
||||||
|
let champion = raw_data
|
||||||
|
.get("championName")
|
||||||
|
.or_else(|| raw_data.get("champion_name"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
info!("[EVENT_HANDLER] Local player picked champion: {}", champion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EVENT_TYPE_GAME_START => {
|
||||||
|
let game_id = raw_data.get("gameId").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
|
info!("[EVENT_HANDLER] Game started with game_id: {}", game_id);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -252,7 +274,7 @@ impl Daemon {
|
|||||||
// Record event to timeline if recording (BEFORE state transition for GameEnd)
|
// Record event to timeline if recording (BEFORE state transition for GameEnd)
|
||||||
// This ensures GameEnd events are recorded while still in recording state
|
// This ensures GameEnd events are recorded while still in recording state
|
||||||
if self.state_machine.is_recording() {
|
if self.state_machine.is_recording() {
|
||||||
if let Some((video_ts, game_ts)) = self.event_mapper.write().handle_event(event) {
|
if let Some((video_ts, game_ts)) = self.event_mapper.write().handle_event(event_type) {
|
||||||
// Get the current recording ID
|
// Get the current recording ID
|
||||||
if let Some(recording_id) = *self.current_recording_id.read() {
|
if let Some(recording_id) = *self.current_recording_id.read() {
|
||||||
// Create a timestamped event with raw data
|
// Create a timestamped event with raw data
|
||||||
@@ -260,11 +282,10 @@ impl Daemon {
|
|||||||
video_timestamp: video_ts,
|
video_timestamp: video_ts,
|
||||||
game_timestamp: game_ts,
|
game_timestamp: game_ts,
|
||||||
timestamp: chrono::Utc::now(),
|
timestamp: chrono::Utc::now(),
|
||||||
event_type: event.event_type_name().to_string(),
|
event_type: event_type.clone(),
|
||||||
description: event.description(),
|
description: description.clone(),
|
||||||
event: event.clone(),
|
raw_data: parsed.raw_data.clone(),
|
||||||
raw_data: Some(parsed.raw_data.clone()),
|
uri: parsed.uri.clone(),
|
||||||
uri: Some(parsed.uri.clone()),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the event to the timeline store
|
// Add the event to the timeline store
|
||||||
@@ -277,9 +298,7 @@ impl Daemon {
|
|||||||
} else {
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
"Event added to timeline: video_ts={:?}, game_ts={:?}, type={}",
|
"Event added to timeline: video_ts={:?}, game_ts={:?}, type={}",
|
||||||
video_ts,
|
video_ts, game_ts, event_type
|
||||||
game_ts,
|
|
||||||
event.event_type_name()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -289,7 +308,7 @@ impl Daemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process state transitions
|
// Process state transitions
|
||||||
if let Some(transition) = self.state_machine.process_event(event) {
|
if let Some(transition) = self.state_machine.process_event(event_type, raw_data) {
|
||||||
info!("[EVENT_HANDLER] State transition: {:?}", transition);
|
info!("[EVENT_HANDLER] State transition: {:?}", transition);
|
||||||
|
|
||||||
// Only process the transition if it's valid
|
// Only process the transition if it's valid
|
||||||
@@ -297,12 +316,8 @@ impl Daemon {
|
|||||||
// Handle recording start/stop
|
// Handle recording start/stop
|
||||||
match transition {
|
match transition {
|
||||||
StateTransition::GameStarted => {
|
StateTransition::GameStarted => {
|
||||||
// Extract game_id from the GameStart event
|
// Extract game_id from raw_data
|
||||||
let game_id = if let GameEvent::GameStart(ref info) = event {
|
let game_id = raw_data.get("gameId").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
info.game_id
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"[EVENT_HANDLER] GameStarted transition - game_id: {}",
|
"[EVENT_HANDLER] GameStarted transition - game_id: {}",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use parking_lot::RwLock;
|
|||||||
use tracing::{info, trace, warn};
|
use tracing::{info, trace, warn};
|
||||||
|
|
||||||
use super::DaemonStatus;
|
use super::DaemonStatus;
|
||||||
use crate::lqp::{GameEvent, GameflowPhase};
|
use crate::lqp::GameflowPhase;
|
||||||
|
|
||||||
/// Internal daemon state.
|
/// Internal daemon state.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -203,22 +203,29 @@ impl DaemonStateMachine {
|
|||||||
|
|
||||||
/// Process a game event and potentially trigger a transition.
|
/// Process a game event and potentially trigger a transition.
|
||||||
///
|
///
|
||||||
/// Only returns state transitions
|
/// Uses event_type string and raw_data instead of typed GameEvent.
|
||||||
/// All data is stored in events in the timeline and processed at game end.
|
/// Only returns state transitions — all data is stored in raw events.
|
||||||
pub fn process_event(&self, event: &GameEvent) -> Option<StateTransition> {
|
pub fn process_event(
|
||||||
|
&self,
|
||||||
|
event_type: &str,
|
||||||
|
raw_data: &serde_json::Value,
|
||||||
|
) -> Option<StateTransition> {
|
||||||
trace!(
|
trace!(
|
||||||
"Processing event in state {:?}: {:?}",
|
"Processing event in state {:?}: type={}",
|
||||||
self.current_state(),
|
self.current_state(),
|
||||||
event
|
event_type
|
||||||
);
|
);
|
||||||
|
|
||||||
match event {
|
match event_type {
|
||||||
GameEvent::GameStart(_) => Some(StateTransition::GameStarted),
|
crate::lqp::EVENT_TYPE_GAME_START => Some(StateTransition::GameStarted),
|
||||||
GameEvent::GameEnd(_) => Some(StateTransition::GameEnded),
|
crate::lqp::EVENT_TYPE_GAME_END => Some(StateTransition::GameEnded),
|
||||||
GameEvent::PhaseChange(info) => {
|
crate::lqp::EVENT_TYPE_PHASE_CHANGE => {
|
||||||
// Only trigger GameEnded on EndOfGame phase (stats are available by then)
|
// Only trigger GameEnded on EndOfGame phase (stats are available by then)
|
||||||
// The actual GameEnd event with stats comes from /lol-end-of-game/v1/eog-stats-block
|
let phase = raw_data
|
||||||
if info.phase == "EndOfGame" && self.is_recording() {
|
.as_str()
|
||||||
|
.or_else(|| raw_data.get("phase").and_then(|v| v.as_str()))
|
||||||
|
.unwrap_or("");
|
||||||
|
if phase == "EndOfGame" && self.is_recording() {
|
||||||
Some(StateTransition::GameEnded)
|
Some(StateTransition::GameEnded)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -294,4 +301,26 @@ mod tests {
|
|||||||
assert_eq!(machine.current_state(), DaemonState::Idle);
|
assert_eq!(machine.current_state(), DaemonState::Idle);
|
||||||
assert_eq!(machine.last_error(), None);
|
assert_eq!(machine.last_error(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_process_event_game_start() {
|
||||||
|
let machine = DaemonStateMachine::new();
|
||||||
|
machine.transition(StateTransition::ClientStarted);
|
||||||
|
|
||||||
|
let raw = serde_json::json!({"gameId": 12345});
|
||||||
|
let transition = machine.process_event(crate::lqp::EVENT_TYPE_GAME_START, &raw);
|
||||||
|
assert!(matches!(transition, Some(StateTransition::GameStarted)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_process_event_phase_change_end_of_game() {
|
||||||
|
let machine = DaemonStateMachine::new();
|
||||||
|
machine.transition(StateTransition::ClientStarted);
|
||||||
|
machine.transition(StateTransition::GameStarted);
|
||||||
|
|
||||||
|
// Phase change to EndOfGame while recording should trigger GameEnded
|
||||||
|
let raw = serde_json::json!("EndOfGame");
|
||||||
|
let transition = machine.process_event(crate::lqp::EVENT_TYPE_PHASE_CHANGE, &raw);
|
||||||
|
assert!(matches!(transition, Some(StateTransition::GameEnded)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::lqp::GameEvent;
|
|
||||||
|
|
||||||
/// Event mapper that tracks recording start time and maps events to video timestamps.
|
/// Event mapper that tracks recording start time and maps events to video timestamps.
|
||||||
pub struct EventMapper {
|
pub struct EventMapper {
|
||||||
/// Recording start time.
|
/// Recording start time.
|
||||||
@@ -45,8 +43,8 @@ impl EventMapper {
|
|||||||
self.start_time.is_some()
|
self.start_time.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map a game event to video and game timestamps.
|
/// Map an event to video and game timestamps.
|
||||||
pub fn map_event(&self, _event: &GameEvent) -> Option<(Duration, Option<Duration>)> {
|
pub fn map_event(&self) -> Option<(Duration, Option<Duration>)> {
|
||||||
let start_time = self.start_time?;
|
let start_time = self.start_time?;
|
||||||
let now = Utc::now();
|
let now = Utc::now();
|
||||||
|
|
||||||
@@ -60,25 +58,23 @@ impl EventMapper {
|
|||||||
self.synchronizer.adjust_game_timestamp(raw_ts)
|
self.synchronizer.adjust_game_timestamp(raw_ts)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update game start time if this is a game start event
|
|
||||||
// (handled separately in handle_event)
|
|
||||||
|
|
||||||
Some((video_timestamp, game_timestamp))
|
Some((video_timestamp, game_timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a game event and return mapped timestamps.
|
/// Handle a game event and return mapped timestamps.
|
||||||
pub fn handle_event(&mut self, event: &GameEvent) -> Option<(Duration, Option<Duration>)> {
|
/// event_type is used to detect game start for timestamp tracking.
|
||||||
|
pub fn handle_event(&mut self, event_type: &str) -> Option<(Duration, Option<Duration>)> {
|
||||||
if !self.is_active() {
|
if !self.is_active() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track game start time
|
// Track game start time
|
||||||
if let GameEvent::GameStart(_) = event {
|
if event_type == crate::lqp::EVENT_TYPE_GAME_START {
|
||||||
self.game_start_time = Some(Utc::now());
|
self.game_start_time = Some(Utc::now());
|
||||||
debug!("Game start time recorded: {:?}", self.game_start_time);
|
debug!("Game start time recorded: {:?}", self.game_start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = self.map_event(event);
|
let result = self.map_event();
|
||||||
|
|
||||||
// Add sync point if we have both timestamps
|
// Add sync point if we have both timestamps
|
||||||
if let Some((video_ts, Some(game_ts))) = result {
|
if let Some((video_ts, Some(game_ts))) = result {
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ use chrono::{DateTime, Duration, Utc};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
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.
|
/// Stores raw API responses for maximum flexibility and future-proofing.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -79,31 +77,11 @@ impl Timeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add an event to the timeline.
|
/// Add an event to the timeline with raw data.
|
||||||
pub fn add_event(
|
pub fn add_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: GameEvent,
|
event_type: &str,
|
||||||
video_timestamp: Duration,
|
description: &str,
|
||||||
game_timestamp: Option<Duration>,
|
|
||||||
) {
|
|
||||||
let timestamped = TimestampedEvent {
|
|
||||||
video_timestamp,
|
|
||||||
game_timestamp,
|
|
||||||
timestamp: Utc::now(),
|
|
||||||
event_type: event_type_name(&event),
|
|
||||||
description: event.description(),
|
|
||||||
event,
|
|
||||||
raw_data: None,
|
|
||||||
uri: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.events.push(timestamped);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an event to the timeline with raw data.
|
|
||||||
pub fn add_event_with_raw(
|
|
||||||
&mut self,
|
|
||||||
event: GameEvent,
|
|
||||||
video_timestamp: Duration,
|
video_timestamp: Duration,
|
||||||
game_timestamp: Option<Duration>,
|
game_timestamp: Option<Duration>,
|
||||||
raw_data: serde_json::Value,
|
raw_data: serde_json::Value,
|
||||||
@@ -113,11 +91,10 @@ impl Timeline {
|
|||||||
video_timestamp,
|
video_timestamp,
|
||||||
game_timestamp,
|
game_timestamp,
|
||||||
timestamp: Utc::now(),
|
timestamp: Utc::now(),
|
||||||
event_type: event_type_name(&event),
|
event_type: event_type.to_string(),
|
||||||
description: event.description(),
|
description: description.to_string(),
|
||||||
event,
|
raw_data,
|
||||||
raw_data: Some(raw_data),
|
uri,
|
||||||
uri: Some(uri),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.events.push(timestamped);
|
self.events.push(timestamped);
|
||||||
@@ -192,29 +169,9 @@ fn format_timestamp(duration: Duration) -> String {
|
|||||||
format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
|
format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the event type name.
|
|
||||||
fn event_type_name(event: &GameEvent) -> String {
|
|
||||||
match event {
|
|
||||||
GameEvent::MatchFound(_) => "match_found",
|
|
||||||
GameEvent::ChampSelectStart(_) => "champ_select_start",
|
|
||||||
GameEvent::ChampionPick(_) => "champion_pick",
|
|
||||||
GameEvent::GameStart(_) => "game_start",
|
|
||||||
GameEvent::Kill(_) => "kill",
|
|
||||||
GameEvent::Death(_) => "death",
|
|
||||||
GameEvent::Objective(_) => "objective",
|
|
||||||
GameEvent::StatsUpdate(_) => "stats_update",
|
|
||||||
GameEvent::GameEnd(_) => "game_end",
|
|
||||||
GameEvent::PhaseChange(_) => "phase_change",
|
|
||||||
GameEvent::LpChange(_) => "lp_change",
|
|
||||||
GameEvent::Unknown => "unknown",
|
|
||||||
}
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::lqp::KillEvent;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_timeline_creation() {
|
fn test_timeline_creation() {
|
||||||
@@ -230,21 +187,22 @@ mod tests {
|
|||||||
let id = Uuid::new_v4();
|
let id = Uuid::new_v4();
|
||||||
let mut timeline = Timeline::new(id);
|
let mut timeline = Timeline::new(id);
|
||||||
|
|
||||||
let event = GameEvent::Kill(KillEvent {
|
let raw_data = serde_json::json!({
|
||||||
killer: "Player1".to_string(),
|
"killer": "Player1",
|
||||||
killer_champion: Some("Ahri".to_string()),
|
"victim": "Player2"
|
||||||
victim: "Player2".to_string(),
|
|
||||||
victim_champion: Some("Lux".to_string()),
|
|
||||||
solo_kill: true,
|
|
||||||
assists: 0,
|
|
||||||
position: None,
|
|
||||||
game_time: Some(120.0),
|
|
||||||
timestamp: Utc::now(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
timeline.add_event(event, Duration::seconds(5), Some(Duration::seconds(120)));
|
timeline.add_event(
|
||||||
|
"kill",
|
||||||
|
"Player1 killed Player2",
|
||||||
|
Duration::seconds(5),
|
||||||
|
Some(Duration::seconds(120)),
|
||||||
|
raw_data,
|
||||||
|
"/lol-game-events/v1/events".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(timeline.event_count(), 1);
|
assert_eq!(timeline.event_count(), 1);
|
||||||
|
assert_eq!(timeline.events[0].event_type, "kill");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -9,32 +9,30 @@ use serde::{Deserialize, Serialize};
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::error::{Result, TimelineError};
|
use crate::error::{Result, TimelineError};
|
||||||
use crate::lqp::GameEvent;
|
|
||||||
use crate::recording::RecordingResult;
|
use crate::recording::RecordingResult;
|
||||||
|
|
||||||
/// A timestamped event in the timeline.
|
/// A timestamped event in the timeline.
|
||||||
|
/// Stores raw API data only — no typed event parsing.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct TimestampedEvent {
|
pub struct TimestampedEvent {
|
||||||
/// Video timestamp (offset from recording start).
|
/// Video timestamp (offset from recording start).
|
||||||
// #[serde(with = "chrono::serde::seconds")]
|
|
||||||
pub video_timestamp: Duration,
|
pub video_timestamp: Duration,
|
||||||
/// Game timestamp (in-game time).
|
/// Game timestamp (in-game time).
|
||||||
// #[serde(with = "chrono::serde::seconds_option")]
|
|
||||||
pub game_timestamp: Option<Duration>,
|
pub game_timestamp: Option<Duration>,
|
||||||
/// Real-world timestamp.
|
/// Real-world timestamp.
|
||||||
pub timestamp: DateTime<Utc>,
|
pub timestamp: DateTime<Utc>,
|
||||||
/// Event type name (derived from URI).
|
/// Event type string derived from URI (e.g. "game_start", "lp_change", "kill").
|
||||||
pub event_type: String,
|
pub event_type: String,
|
||||||
/// Human-readable description.
|
/// Human-readable description for logging.
|
||||||
pub description: String,
|
|
||||||
/// The actual event data.
|
|
||||||
pub event: GameEvent,
|
|
||||||
/// Raw JSON data from the API (for flexibility).
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub raw_data: Option<serde_json::Value>,
|
pub description: String,
|
||||||
|
/// The raw JSON data from the API — the single source of truth.
|
||||||
|
/// The tauri-app parses the values it needs from this data.
|
||||||
|
#[serde(default)]
|
||||||
|
pub raw_data: serde_json::Value,
|
||||||
/// URI of the endpoint that triggered this event.
|
/// URI of the endpoint that triggered this event.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub uri: Option<String>,
|
pub uri: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Metadata for a recording.
|
/// Metadata for a recording.
|
||||||
|
|||||||
@@ -94,10 +94,11 @@ function getEventColor(event: TimestampedEvent): string {
|
|||||||
case "phase_change":
|
case "phase_change":
|
||||||
return "#60a5fa"; // blue
|
return "#60a5fa"; // blue
|
||||||
case "kill":
|
case "kill":
|
||||||
case "champions_killed":
|
|
||||||
return getKillEventColor(event);
|
return getKillEventColor(event);
|
||||||
case "objective":
|
case "objective":
|
||||||
return "#fbbf24"; // yellow
|
return "#fbbf24"; // yellow
|
||||||
|
case "lp_change":
|
||||||
|
return "#a78bfa"; // purple
|
||||||
default:
|
default:
|
||||||
return "#9ca3af"; // gray
|
return "#9ca3af"; // gray
|
||||||
}
|
}
|
||||||
@@ -105,14 +106,13 @@ function getEventColor(event: TimestampedEvent): string {
|
|||||||
|
|
||||||
// Get kill event color based on player involvement
|
// Get kill event color based on player involvement
|
||||||
function getKillEventColor(event: TimestampedEvent): string {
|
function getKillEventColor(event: TimestampedEvent): string {
|
||||||
const rawEvent = event.event as { killer?: string; victim?: string } | null;
|
const rawData = event.raw_data as { KillerName?: string; VictimName?: string; Assisters?: string[] } | null;
|
||||||
const rawData = event.raw_data as { Assisters?: string[] } | null;
|
|
||||||
|
|
||||||
if (!rawEvent) return "#9ca3af"; // gray for unknown
|
if (!rawData) return "#9ca3af"; // gray for unknown
|
||||||
|
|
||||||
const killer = rawEvent.killer;
|
const killer = rawData.KillerName;
|
||||||
const victim = rawEvent.victim;
|
const victim = rawData.VictimName;
|
||||||
const assisters = rawData?.Assisters || [];
|
const assisters = rawData.Assisters || [];
|
||||||
|
|
||||||
// Get local player's summoner name
|
// Get local player's summoner name
|
||||||
const localPlayerName = localPlayer.value?.riotIdGameName;
|
const localPlayerName = localPlayer.value?.riotIdGameName;
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ export interface TimestampedEvent {
|
|||||||
game_timestamp: [number, number] | null;
|
game_timestamp: [number, number] | null;
|
||||||
/** Real-world timestamp (ISO 8601). */
|
/** Real-world timestamp (ISO 8601). */
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
/** Event type name. */
|
/** Event type name (e.g. "game_start", "lp_change", "kill"). */
|
||||||
event_type: string;
|
event_type: string;
|
||||||
/** Human-readable description. */
|
/** Human-readable description. */
|
||||||
description: string;
|
description: string;
|
||||||
|
/** Raw JSON data from the League Client API — the single source of truth. */
|
||||||
|
raw_data: Record<string, unknown>;
|
||||||
|
/** URI of the endpoint that triggered this event. */
|
||||||
|
uri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -601,12 +605,19 @@ export function getEventCategory(eventType: string): EventCategory {
|
|||||||
return "death";
|
return "death";
|
||||||
case "objective":
|
case "objective":
|
||||||
return "objective";
|
return "objective";
|
||||||
case "gameend":
|
case "game_end":
|
||||||
case "gamestart":
|
case "game_start":
|
||||||
case "matchfound":
|
case "match_found":
|
||||||
return "game";
|
return "game";
|
||||||
case "phasechange":
|
case "phase_change":
|
||||||
return "phase";
|
return "phase";
|
||||||
|
case "lp_change":
|
||||||
|
return "game";
|
||||||
|
case "champ_select_start":
|
||||||
|
case "champion_pick":
|
||||||
|
return "phase";
|
||||||
|
case "stats_update":
|
||||||
|
return "unknown";
|
||||||
default:
|
default:
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
@@ -623,14 +634,22 @@ export function getEventLabel(eventType: string): string {
|
|||||||
return "Death";
|
return "Death";
|
||||||
case "objective":
|
case "objective":
|
||||||
return "Objective";
|
return "Objective";
|
||||||
case "gameend":
|
case "game_end":
|
||||||
return "Game End";
|
return "Game End";
|
||||||
case "gamestart":
|
case "game_start":
|
||||||
return "Game Start";
|
return "Game Start";
|
||||||
case "matchfound":
|
case "match_found":
|
||||||
return "Match Found";
|
return "Match Found";
|
||||||
case "phasechange":
|
case "phase_change":
|
||||||
return "Phase Change";
|
return "Phase Change";
|
||||||
|
case "lp_change":
|
||||||
|
return "LP Change";
|
||||||
|
case "champ_select_start":
|
||||||
|
return "Champ Select";
|
||||||
|
case "champion_pick":
|
||||||
|
return "Champion Pick";
|
||||||
|
case "stats_update":
|
||||||
|
return "Stats Update";
|
||||||
default:
|
default:
|
||||||
return eventType;
|
return eventType;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user