diff --git a/record-daemon/src/lqp/client.rs b/record-daemon/src/lqp/client.rs index 3b114b0..935e042 100644 --- a/record-daemon/src/lqp/client.rs +++ b/record-daemon/src/lqp/client.rs @@ -68,8 +68,11 @@ impl rustls::client::danger::ServerCertVerifier for InsecureVerifier { /// LQP WebSocket endpoints to subscribe to. const SUBSCRIBE_ENDPOINTS: &[&str] = &[ "/lol-gameflow/v1/gameflow-phase", + "/lol-gameflow/v1/session", "/lol-matchmaking/v1/ready-check", "/lol-game-events/v1/game-events", + "/lol-champ-select/v1/session", + "/lol-lobby/v2/lobby", ]; /// LQP REST API endpoints. @@ -274,16 +277,18 @@ impl LqpClient { let (mut write, mut read) = ws_stream.split(); - // Subscribe to endpoints + // Subscribe to endpoints using OnJsonApiEvent format + // Format: [5, "OnJsonApiEvent", endpoint] for endpoint in SUBSCRIBE_ENDPOINTS { - let subscribe_msg = serde_json::json!([5, endpoint]); + let subscribe_msg = serde_json::json!([5, "OnJsonApiEvent", endpoint]); let msg = Message::Text(subscribe_msg.to_string()); + info!("Subscribing to: {} with OnJsonApiEvent", endpoint); write .send(msg) .await .map_err(|e| LqpError::WebSocketError(e.to_string()))?; - trace!("Subscribed to {}", endpoint); } + info!("All subscriptions sent"); // Clone references for the async block let event_sender = self.event_sender.clone(); @@ -301,6 +306,11 @@ impl LqpClient { match msg { Ok(Message::Text(text)) => { + debug!("Received text message: {} bytes", text.len()); + if text.is_empty() { + debug!("Empty text message received, skipping"); + continue; + } if let Some(event) = Self::parse_websocket_message(&text) { // Update state based on event Self::update_state_from_event(&state, &event).await; @@ -311,19 +321,42 @@ impl LqpClient { } } } + Ok(Message::Binary(data)) => { + debug!("Received binary message: {} bytes", data.len()); + // Try to parse as UTF-8 + if let Ok(text) = String::from_utf8(data) { + if !text.is_empty() { + if let Some(event) = Self::parse_websocket_message(&text) { + // Update state based on event + Self::update_state_from_event(&state, &event).await; + + // Broadcast event + if event_sender.send(event.clone()).is_err() { + trace!("No event subscribers"); + } + } + } + } + } Ok(Message::Close(_)) => { info!("WebSocket closed by server"); break; } Ok(Message::Ping(data)) => { // Respond with pong + debug!("Received ping, sending pong"); let _ = write.send(Message::Pong(data)).await; } + Ok(Message::Pong(_)) => { + debug!("Received pong"); + } + Ok(Message::Frame(_)) => { + debug!("Received raw frame"); + } Err(e) => { error!("WebSocket error: {}", e); break; } - _ => {} } } @@ -337,48 +370,135 @@ impl LqpClient { /// Parse a WebSocket message into a game event. fn parse_websocket_message(text: &str) -> Option { - trace!("WebSocket message: {}", text); + debug!("WebSocket message: {}", text); - // Parse the message array format: [type, endpoint, data] - let value: serde_json::Value = serde_json::from_str(text).ok()?; + // Parse the message array format: [type, callback, data] + let value: serde_json::Value = match serde_json::from_str(text) { + Ok(v) => v, + Err(e) => { + warn!("Failed to parse WebSocket message as JSON: {}", e); + return None; + } + }; // Check if it's an event message (type 8) if let Some(arr) = value.as_array() { + debug!("Message is array with {} elements", arr.len()); if arr.len() >= 3 { let msg_type = arr.first()?.as_u64()?; + debug!("Message type: {}", msg_type); if msg_type == 8 { - // Event message - let endpoint = arr.get(1)?.as_str()?; - let data = arr.get(2)?; - - return Self::parse_event_from_endpoint(endpoint, data); + // Event message format: [8, "OnJsonApiEvent", {"data": ..., "eventType": ..., "uri": ...}] + let callback = arr.get(1)?.as_str()?; + let event_data = arr.get(2)?; + + if callback == "OnJsonApiEvent" { + // Extract the actual URI and data from the event + let uri = event_data.get("uri")?.as_str()?; + let data = event_data.get("data")?; + let event_type = event_data.get("eventType").and_then(|t| t.as_str()).unwrap_or("Update"); + + debug!("OnJsonApiEvent: uri={}, eventType={}, data={:?}", uri, event_type, data); + + return Self::parse_event_from_uri(uri, event_type, data); + } else { + debug!("Unknown callback: {}", callback); + } + } else if msg_type == 4 { + // Response to subscription - this is normal + debug!("Subscription response received"); + } else if msg_type == 0 { + // Welcome message + info!("WebSocket welcome message received"); } } + } else { + debug!("Message is not an array: {:?}", value); } None } - /// Parse an event based on the endpoint. - fn parse_event_from_endpoint(endpoint: &str, data: &serde_json::Value) -> Option { - match endpoint { - "/lol-gameflow/v1/gameflow-phase" => { - let phase = data.as_str()?; - Some( + /// Parse an event based on the URI. + fn parse_event_from_uri(uri: &str, event_type: &str, data: &serde_json::Value) -> Option { + info!("Parsing event from URI: {} (type: {})", uri, event_type); + + // Handle gameflow phase changes + if uri == "/lol-gameflow/v1/gameflow-phase" { + let phase = data.as_str()?; + info!("Gameflow phase changed to: {}", phase); + + // Update internal state based on phase + return Some( + GameEvent::from_json(&serde_json::json!({ + "eventType": "lcu-phase-change", + "phase": phase + })) + .unwrap_or(GameEvent::Unknown), + ); + } + + // Handle gameflow session updates + if uri == "/lol-gameflow/v1/session" { + if let Some(phase) = data.get("phase").and_then(|p| p.as_str()) { + info!("Gameflow session phase: {}", phase); + + // Check for game start + if phase == "InProgress" { + info!("Game is now in progress!"); + + // Extract game info + let game_id = data.get("gameData") + .and_then(|gd| gd.get("gameId")) + .and_then(|id| id.as_u64()) + .unwrap_or(0); + + return Some( + GameEvent::from_json(&serde_json::json!({ + "eventType": "lcu-game-start", + "gameId": game_id + })) + .unwrap_or(GameEvent::Unknown), + ); + } + + return Some( GameEvent::from_json(&serde_json::json!({ "eventType": "lcu-phase-change", "phase": phase })) .unwrap_or(GameEvent::Unknown), - ) - } - "/lol-game-events/v1/game-events" => GameEvent::from_json(data), - _ => { - trace!("Unhandled endpoint: {}", endpoint); - None + ); } } + + // Handle game events (kills, deaths, objectives) + if uri == "/lol-game-events/v1/game-events" { + info!("Game event received: {:?}", data); + return GameEvent::from_json(data); + } + + // Handle ready check + if uri == "/lol-matchmaking/v1/ready-check" { + info!("Ready check event: {:?}", data); + return None; + } + + // Handle champion select + if uri == "/lol-champ-select/v1/session" { + info!("Champion select event: {:?}", data); + return None; + } + + // Handle lobby + if uri.starts_with("/lol-lobby") { + debug!("Lobby event: {}", uri); + return None; + } + + debug!("Unhandled URI: {}", uri); + None } /// Update internal state from a game event. diff --git a/record-daemon/src/lqp/events.rs b/record-daemon/src/lqp/events.rs index 92d8128..9167c37 100644 --- a/record-daemon/src/lqp/events.rs +++ b/record-daemon/src/lqp/events.rs @@ -276,7 +276,14 @@ pub enum EventData { impl GameEvent { /// Parse a game event from raw WebSocket data. pub fn from_json(value: &serde_json::Value) -> Option { - serde_json::from_value(value.clone()).ok() + 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.