record-daemon: add record raw events, subscribe lp change events
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m0s
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m0s
This commit is contained in:
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user