record-daemon: add record raw events, subscribe lp change events
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m0s

This commit is contained in:
2026-03-27 22:25:24 +01:00
parent aa53a84a46
commit b64937601a
8 changed files with 401 additions and 30 deletions

View File

@@ -7,8 +7,19 @@ use tracing::{debug, info, warn};
use super::events::{GameEvent, GameflowSession};
/// Parse a WebSocket message into a game event.
pub fn parse_websocket_message(text: &str) -> Option<GameEvent> {
/// Parsed event with raw data preserved.
#[derive(Debug, Clone)]
pub struct ParsedEvent {
/// The parsed game event.
pub event: GameEvent,
/// The raw JSON data from the API.
pub raw_data: serde_json::Value,
/// The URI of the endpoint that triggered this event.
pub uri: String,
}
/// Parse a WebSocket message into a parsed event with raw data.
pub fn parse_websocket_message(text: &str) -> Option<ParsedEvent> {
// Parse the message array format: [type, callback, data]
let value: serde_json::Value = match serde_json::from_str(text) {
Ok(v) => v,
@@ -37,22 +48,33 @@ pub fn parse_websocket_message(text: &str) -> Option<GameEvent> {
.get("eventType")
.and_then(|t| t.as_str())
.unwrap_or("Update");
return parse_event_from_uri(
&raw_event.uri,
event_type,
&serde_json::to_value(raw_event.data).unwrap_or_default(),
);
let raw_data =
serde_json::to_value(raw_event.data.clone()).unwrap_or_default();
let uri = raw_event.uri.clone();
if let Some(event) = parse_event_from_uri(&uri, event_type, &raw_data) {
return Some(ParsedEvent {
event,
raw_data,
uri,
});
}
}
// Fallback to manual extraction
let uri = event_data.get("uri")?.as_str()?;
let data = event_data.get("data")?;
let uri = event_data.get("uri")?.as_str()?.to_string();
let data = event_data.get("data")?.clone();
let event_type = event_data
.get("eventType")
.and_then(|t| t.as_str())
.unwrap_or("Update");
return parse_event_from_uri(uri, event_type, data);
if let Some(event) = parse_event_from_uri(&uri, event_type, &data) {
return Some(ParsedEvent {
event,
raw_data: data,
uri,
});
}
} else {
debug!("Unknown callback: {}", callback);
}
@@ -113,6 +135,16 @@ pub fn parse_event_from_uri(
return parse_end_of_game_stats(data);
}
// Handle LP change notifications
if uri == "/lol-ranked/v1/current-lp-change-notification" {
return parse_lp_change_notification(data);
}
// Handle ranked stats updates (with UUID suffix)
if uri.starts_with("/lol-ranked/v1/ranked-stats/") {
return parse_ranked_stats_event(data);
}
// Handle lobby
if uri.starts_with("/lol-lobby") {
debug!("Lobby event: {}", uri);
@@ -423,6 +455,215 @@ fn parse_end_of_game_stats(data: &serde_json::Value) -> Option<GameEvent> {
}
}
/// Parse LP change notification event.
///
/// This is the primary source for LP changes after a ranked game.
/// The notification contains the LP delta and current rank info.
fn parse_lp_change_notification(data: &serde_json::Value) -> Option<GameEvent> {
info!("LP change notification received: {:?}", data);
// Extract queue type
let queue_type = data
.get("queueType")
.and_then(|q| q.as_str())
.unwrap_or("UNKNOWN")
.to_string();
// Extract LP change amount
let lp_change = data.get("lpChange").and_then(|lp| lp.as_i64()).unwrap_or(0) as i32;
// Extract LP before and after
let lp_before = data.get("lpBefore").and_then(|lp| lp.as_i64()).unwrap_or(0) as i32;
let lp_after = data.get("lpAfter").and_then(|lp| lp.as_i64()).unwrap_or(0) as i32;
// Extract tier and division
let tier = data
.get("tier")
.and_then(|t| t.as_str())
.unwrap_or("UNRANKED")
.to_string();
let division = data
.get("division")
.and_then(|d| d.as_str())
.map(|s| s.to_string());
// Check for promotional series
let in_promos = data.get("miniSeries").is_some();
let promo_progress = if in_promos {
data.get("miniSeries")
.and_then(|ms| ms.get("progress"))
.and_then(|p| p.as_str())
.map(|s| s.to_string())
} else {
None
};
let promo_wins = data
.get("miniSeries")
.and_then(|ms| ms.get("wins"))
.and_then(|w| w.as_u64())
.map(|w| w as u32);
let promo_losses = data
.get("miniSeries")
.and_then(|ms| ms.get("losses"))
.and_then(|l| l.as_u64())
.map(|l| l as u32);
// Extract total games stats if available
let total_wins = data.get("wins").and_then(|w| w.as_u64()).unwrap_or(0) as u32;
let total_losses = data.get("losses").and_then(|l| l.as_u64()).unwrap_or(0) as u32;
let total_games = total_wins + total_losses;
info!(
"LP change notification: {} {} LP change: {} ({} -> {})",
queue_type, tier, lp_change, lp_before, lp_after
);
let event_json = serde_json::json!({
"eventType": "lcu-lp-change",
"queueType": queue_type,
"lpChange": lp_change,
"lpBefore": lp_before,
"lpAfter": lp_after,
"tier": tier,
"division": division,
"leaguePoints": lp_after,
"inPromos": in_promos,
"promoProgress": promo_progress,
"promoWins": promo_wins,
"promoLosses": promo_losses,
"totalGames": total_games,
"totalWins": total_wins,
"totalLosses": total_losses
});
GameEvent::from_json(&event_json)
}
/// Parse ranked stats event for LP changes.
///
/// The ranked stats endpoint provides updates when LP changes occur.
/// We extract the queue-specific data and emit an LpChange event.
fn parse_ranked_stats_event(data: &serde_json::Value) -> Option<GameEvent> {
info!("Ranked stats event received: {:?}", data);
// The ranked stats data contains queue-specific stats
// We look for RANKED_SOLO_5x5 and RANKED_FLEX_SR queues
let queues = data.get("queues")?.as_array()?;
for queue_data in queues {
let queue_type = queue_data
.get("queueType")
.and_then(|q| q.as_str())
.unwrap_or("");
// Only process ranked queues
if queue_type != "RANKED_SOLO_5x5" && queue_type != "RANKED_FLEX_SR" {
continue;
}
// Extract tier and division
let tier = queue_data
.get("tier")
.and_then(|t| t.as_str())
.unwrap_or("UNRANKED")
.to_string();
let division = queue_data
.get("division")
.and_then(|d| d.as_str())
.map(|s| s.to_string());
// Extract LP
let league_points = queue_data
.get("leaguePoints")
.and_then(|lp| lp.as_i64())
.unwrap_or(0) as i32;
// Extract previous LP if available (for calculating change)
let previous_lp = queue_data
.get("previousLeaguePoints")
.and_then(|lp| lp.as_i64())
.unwrap_or(league_points as i64) as i32;
// Calculate LP change
let lp_change = league_points - previous_lp;
// Only emit event if there was an actual change
if lp_change == 0 {
continue;
}
// Check for promotional series
let in_promos = queue_data.get("miniSeries").is_some();
let promo_progress = if in_promos {
queue_data
.get("miniSeries")
.and_then(|ms| ms.get("progress"))
.and_then(|p| p.as_str())
.map(|s| s.to_string())
} else {
None
};
let promo_wins = queue_data
.get("miniSeries")
.and_then(|ms| ms.get("wins"))
.and_then(|w| w.as_u64())
.map(|w| w as u32);
let promo_losses = queue_data
.get("miniSeries")
.and_then(|ms| ms.get("losses"))
.and_then(|l| l.as_u64())
.map(|l| l as u32);
// Extract total games stats
let total_wins = queue_data.get("wins").and_then(|w| w.as_u64()).unwrap_or(0) as u32;
let total_losses = queue_data
.get("losses")
.and_then(|l| l.as_u64())
.unwrap_or(0) as u32;
let total_games = total_wins + total_losses;
info!(
"LP change detected: {} {} LP change: {} ({} -> {})",
queue_type, tier, lp_change, previous_lp, league_points
);
let event_json = serde_json::json!({
"eventType": "lcu-lp-change",
"queueType": queue_type,
"lpChange": lp_change,
"lpBefore": previous_lp,
"lpAfter": league_points,
"tier": tier,
"division": division,
"leaguePoints": league_points,
"inPromos": in_promos,
"promoProgress": promo_progress,
"promoWins": promo_wins,
"promoLosses": promo_losses,
"totalGames": total_games,
"totalWins": total_wins,
"totalLosses": total_losses
});
return GameEvent::from_json(&event_json);
}
None
}
#[cfg(test)]
mod tests {
use super::*;