record raw events everywhere
record-daemon / Build, check and test (push) Waiting to run

This commit is contained in:
2026-05-06 23:53:01 +02:00
parent ff713da1e8
commit fcfa55d0aa
13 changed files with 848 additions and 2043 deletions
+67 -23
View File
@@ -11,7 +11,7 @@ use tracing::{debug, error, info, trace, warn};
use super::auth::LockfileCredentials;
use super::endpoints;
use super::events::GameEvent;
use super::events::EVENT_TYPE_GAME_END;
use super::state::{ClientState, GameflowPhase};
use super::tls::create_insecure_tls_config;
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) {
// 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
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;
if *last_game_id == Some(info.game_id) {
if *last_game_id == Some(game_id) && game_id != 0 {
info!(
"Skipping duplicate GameStart event for game_id={}",
info.game_id
game_id
);
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
if let GameEvent::GameEnd(_) = &parsed.event {
if parsed.event_type == EVENT_TYPE_GAME_END {
*last_emitted_game_id.write().await = None;
}
@@ -219,23 +231,35 @@ impl LqpClient {
if !text.is_empty() {
if let Some(parsed) = parse_websocket_message(&text) {
// 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
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;
if *last_game_id == Some(info.game_id) {
if *last_game_id == Some(game_id) && game_id != 0 {
info!(
"Skipping duplicate GameStart event for game_id={}",
info.game_id
game_id
);
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
if let GameEvent::GameEnd(_) = &parsed.event {
if parsed.event_type == EVENT_TYPE_GAME_END {
*last_emitted_game_id.write().await = None;
}
@@ -277,19 +301,36 @@ impl LqpClient {
Ok(())
}
/// Update internal state from a game event.
async fn update_state_from_event(state: &Arc<RwLock<ClientState>>, event: &GameEvent) {
/// Update internal state from a parsed event.
async fn update_state_from_event(state: &Arc<RwLock<ClientState>>, parsed: &ParsedEvent) {
let mut state = state.write().await;
match event {
GameEvent::GameStart(info) => {
match parsed.event_type.as_str() {
super::events::EVENT_TYPE_GAME_START => {
state.phase = GameflowPhase::InProgress;
state.game_id = Some(info.game_id);
state.champion = info.champion.clone();
state.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());
// Champion is not extracted here — raw session data has it
}
GameEvent::GameEnd(_) => {
super::events::EVENT_TYPE_GAME_END => {
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) => {
// 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 {
@@ -522,10 +564,12 @@ impl LqpClient {
// Parse and broadcast the 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
Self::update_state_from_event(&state, &parsed.event)
.await;
Self::update_state_from_event(&state, &parsed).await;
// Broadcast event
if event_sender.send(parsed).is_err() {
+170 -582
View File
@@ -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};
/// A game event received from the League Client.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "eventType", rename_all = "camelCase")]
pub enum GameEvent {
/// Match found in queue.
#[serde(rename = "lcu-match-found")]
MatchFound(MatchInfo),
/// Champion select phase started.
#[serde(rename = "lcu-champ-select-start")]
ChampSelectStart(ChampSelectStartInfo),
/// Player picked a champion.
#[serde(rename = "lcu-champion-pick")]
ChampionPick(ChampionPickInfo),
/// Game has started.
#[serde(rename = "lcu-game-start")]
GameStart(Box<GameStartInfo>),
/// Player killed an enemy.
#[serde(rename = "lcu-kill")]
Kill(KillEvent),
/// Player died.
#[serde(rename = "lcu-death")]
Death(DeathEvent),
/// Objective was taken.
#[serde(rename = "lcu-objective")]
Objective(ObjectiveEvent),
/// In-game stats update.
#[serde(rename = "lcu-stats-update")]
StatsUpdate(InGameStats),
/// Game has ended.
#[serde(rename = "lcu-game-end")]
GameEnd(GameEndInfo),
/// Gameflow phase changed.
#[serde(rename = "lcu-phase-change")]
PhaseChange(PhaseChangeInfo),
/// LP changed after a ranked game.
#[serde(rename = "lcu-lp-change")]
LpChange(LpChangeInfo),
/// Unknown event type.
#[serde(other)]
Unknown,
}
/// Phase change event data.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PhaseChangeInfo {
/// The new phase.
pub phase: String,
/// Timestamp when phase changed.
#[serde(default = "Utc::now")]
pub timestamp: DateTime<Utc>,
}
/// LP change event data after a ranked game.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LpChangeInfo {
/// Queue type (RANKED_SOLO_5x5, RANKED_FLEX_SR, etc.).
pub queue_type: String,
/// 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>,
}
/// Match found event data.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MatchInfo {
/// Match ID.
#[serde(default)]
pub match_id: Option<String>,
/// Queue type (ranked, normal, aram, etc.).
pub queue_type: String,
/// 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>,
}
/// Champion select start event data.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChampSelectStartInfo {
/// Session ID.
#[serde(default)]
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>,
}
/// Champion pick event data.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChampionPickInfo {
/// Player's summoner name.
pub summoner_name: String,
/// Champion ID.
pub champion_id: u32,
/// Champion name.
pub champion_name: String,
/// 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 Constants
// ============================================================================
/// Match found in queue.
pub const EVENT_TYPE_MATCH_FOUND: &str = "match_found";
/// Champion select phase started.
pub const EVENT_TYPE_CHAMP_SELECT_START: &str = "champ_select_start";
/// Player picked a champion.
pub const EVENT_TYPE_CHAMPION_PICK: &str = "champion_pick";
/// Game has started.
pub const EVENT_TYPE_GAME_START: &str = "game_start";
/// Player killed an enemy.
pub const EVENT_TYPE_KILL: &str = "kill";
/// Player died.
pub const EVENT_TYPE_DEATH: &str = "death";
/// Objective was taken.
pub const EVENT_TYPE_OBJECTIVE: &str = "objective";
/// In-game stats update.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InGameStats {
/// Current kills.
pub kills: u32,
pub const EVENT_TYPE_STATS_UPDATE: &str = "stats_update";
/// Game has ended.
pub const EVENT_TYPE_GAME_END: &str = "game_end";
/// Gameflow phase changed.
pub const EVENT_TYPE_PHASE_CHANGE: &str = "phase_change";
/// LP changed after a ranked game.
pub const EVENT_TYPE_LP_CHANGE: &str = "lp_change";
/// Unknown / unclassified event.
pub const EVENT_TYPE_UNKNOWN: &str = "unknown";
/// 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>,
}
/// Game start event data.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GameStartInfo {
/// Game ID.
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>,
}
/// Kill event data.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KillEvent {
/// Killer summoner name.
pub killer: String,
/// Killer champion name.
#[serde(default)]
pub killer_champion: Option<String>,
/// 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>,
}
/// 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
}
/// Generate a brief human-readable description from raw event data.
///
/// 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)
}
}
/// 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(),
EVENT_TYPE_CHAMP_SELECT_START => "Champion select started".to_string(),
EVENT_TYPE_CHAMPION_PICK => {
let champ = raw_data
.get("championName")
.or_else(|| raw_data.get("championName"))
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
format!("Champion picked: {}", champ)
}
}
/// 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",
EVENT_TYPE_GAME_START => {
let game_id = raw_data
.get("gameId")
.or_else(|| raw_data.get("gameData").and_then(|gd| gd.get("gameId")))
.and_then(|v| v.as_u64())
.unwrap_or(0);
format!("Game started: ID {}", game_id)
}
}
}
#[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");
EVENT_TYPE_KILL => {
let killer = raw_data
.get("KillerName")
.or_else(|| raw_data.get("killer"))
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
let victim = raw_data
.get("VictimName")
.or_else(|| raw_data.get("victim"))
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
format!("{} killed {}", killer, victim)
}
}
#[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);
EVENT_TYPE_DEATH => {
let cause = raw_data
.get("DeathCause")
.or_else(|| raw_data.get("cause"))
.and_then(|v| v.as_str())
.unwrap_or("Unknown");
format!("Player died ({})", cause)
}
EVENT_TYPE_OBJECTIVE => {
let obj_type = raw_data
.get("EventName")
.or_else(|| raw_data.get("objectiveType"))
.and_then(|v| v.as_str())
.unwrap_or("objective");
let team = raw_data
.get("Team")
.or_else(|| raw_data.get("team"))
.and_then(|v| v.as_u64())
.map(|t| if t == 100 { "Blue" } else { "Red" })
.unwrap_or("Unknown");
format!("{} took {}", team, obj_type)
}
EVENT_TYPE_STATS_UPDATE => "Stats update".to_string(),
EVENT_TYPE_GAME_END => {
let victory = raw_data
.get("victory")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let result = if victory { "Victory" } else { "Defeat" };
format!("Game ended: {}", result)
}
EVENT_TYPE_PHASE_CHANGE => {
let phase = raw_data
.as_str()
.or_else(|| raw_data.get("phase").and_then(|v| v.as_str()))
.unwrap_or("Unknown");
format!("Phase changed to: {}", phase)
}
EVENT_TYPE_LP_CHANGE => {
let lp_change = raw_data
.get("lpChange")
.and_then(|v| v.as_i64())
.unwrap_or(0);
let tier = raw_data
.get("tier")
.and_then(|v| v.as_str())
.unwrap_or("UNRANKED");
let sign = if lp_change >= 0 { "+" } else { "" };
format!("LP Change: {}{} LP ({})", sign, lp_change, tier)
}
_ => "Unknown event".to_string(),
}
}
@@ -868,3 +413,46 @@ impl GameflowSession {
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");
}
}
+6 -5
View File
@@ -20,12 +20,13 @@ pub use endpoints::{
MATCH_HISTORY, RUNE_PAGES, SESSION, SUBSCRIBE_ENDPOINTS, SUMMONER,
};
pub use events::{
ChampSelectStartInfo, ChampionPickInfo, DeathEvent, EventData, GameEndInfo, GameEvent,
GameStartInfo, GameflowSession, InGameStats, ItemBuild, ItemInfo, KillEvent, MatchInfo,
ObjectiveEvent, ObjectiveType, PlayerChampionSelection, PlayerGameMetadata, PlayerIdentity,
QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
describe_event, GameflowSession, ItemBuild, ItemInfo, PlayerChampionSelection,
PlayerGameMetadata, PlayerIdentity, QueueInfo, RunePage, RuneSlot, SummonerSpells, TeamMember,
EVENT_TYPE_CHAMPION_PICK, EVENT_TYPE_CHAMP_SELECT_START, EVENT_TYPE_DEATH, EVENT_TYPE_GAME_END,
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 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