record-daemon: refactor client.rs, introducing typed structs for api types
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m17s
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m17s
This commit is contained in:
576
record-daemon/src/lqp/api_types.rs
Normal file
576
record-daemon/src/lqp/api_types.rs
Normal file
@@ -0,0 +1,576 @@
|
||||
//! API response types for League Client API.
|
||||
//!
|
||||
//! These structs map directly to the JSON responses from the LQP REST API,
|
||||
//! allowing automatic deserialization via serde.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// =============================================================================
|
||||
// Summoner API Responses
|
||||
// =============================================================================
|
||||
|
||||
/// Response from `/lol-summoner/v1/current-summoner`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SummonerResponse {
|
||||
/// Summoner ID.
|
||||
#[serde(default)]
|
||||
pub summoner_id: Option<u64>,
|
||||
|
||||
/// Account ID.
|
||||
#[serde(default)]
|
||||
pub account_id: Option<u64>,
|
||||
|
||||
/// PUUID (globally unique identifier).
|
||||
#[serde(default)]
|
||||
pub puuid: Option<String>,
|
||||
|
||||
/// Display name.
|
||||
#[serde(default)]
|
||||
pub display_name: Option<String>,
|
||||
|
||||
/// Internal name.
|
||||
#[serde(default)]
|
||||
pub internal_name: Option<String>,
|
||||
|
||||
/// Name (legacy field).
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Profile icon ID.
|
||||
#[serde(default)]
|
||||
pub profile_icon_id: Option<u32>,
|
||||
|
||||
/// Summoner level.
|
||||
#[serde(default)]
|
||||
pub summoner_level: Option<u32>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Gameflow Session API Responses
|
||||
// =============================================================================
|
||||
|
||||
/// Response from `/lol-gameflow/v1/session`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameflowSessionResponse {
|
||||
/// Current gameflow phase.
|
||||
#[serde(default)]
|
||||
pub phase: Option<String>,
|
||||
|
||||
/// Game data (present when in game).
|
||||
#[serde(default)]
|
||||
pub game_data: Option<GameData>,
|
||||
|
||||
/// Map name.
|
||||
#[serde(default)]
|
||||
pub map: Option<String>,
|
||||
|
||||
/// Game mode.
|
||||
#[serde(default)]
|
||||
pub game_mode: Option<String>,
|
||||
|
||||
/// Queue ID.
|
||||
#[serde(default)]
|
||||
pub queue_id: Option<u64>,
|
||||
}
|
||||
|
||||
/// Game data within a gameflow session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GameData {
|
||||
/// Game ID.
|
||||
#[serde(default)]
|
||||
pub game_id: Option<u64>,
|
||||
|
||||
/// Queue information.
|
||||
#[serde(default)]
|
||||
pub queue: Option<QueueData>,
|
||||
|
||||
/// Team one players.
|
||||
#[serde(default)]
|
||||
pub team_one: Option<Vec<TeamPlayer>>,
|
||||
|
||||
/// Team two players.
|
||||
#[serde(default)]
|
||||
pub team_two: Option<Vec<TeamPlayer>>,
|
||||
}
|
||||
|
||||
/// Queue data within game data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueueData {
|
||||
/// Queue ID.
|
||||
#[serde(default)]
|
||||
pub id: Option<u64>,
|
||||
|
||||
/// Queue name.
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Game mode.
|
||||
#[serde(default)]
|
||||
pub game_mode: Option<String>,
|
||||
|
||||
/// Map ID.
|
||||
#[serde(default)]
|
||||
pub map_id: Option<u64>,
|
||||
}
|
||||
|
||||
/// Player in a team.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TeamPlayer {
|
||||
/// Player PUUID.
|
||||
#[serde(default)]
|
||||
pub puuid: Option<String>,
|
||||
|
||||
/// Summoner name.
|
||||
#[serde(default)]
|
||||
pub summoner_name: Option<String>,
|
||||
|
||||
/// Summoner ID.
|
||||
#[serde(default)]
|
||||
pub summoner_id: Option<u64>,
|
||||
|
||||
/// Champion ID.
|
||||
#[serde(default)]
|
||||
pub champion_id: Option<u64>,
|
||||
|
||||
/// Team ID.
|
||||
#[serde(default)]
|
||||
pub team_id: Option<u64>,
|
||||
|
||||
/// First summoner spell ID.
|
||||
#[serde(default)]
|
||||
pub spell1_id: Option<u64>,
|
||||
|
||||
/// Second summoner spell ID.
|
||||
#[serde(default)]
|
||||
pub spell2_id: Option<u64>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Champion Select API Responses
|
||||
// =============================================================================
|
||||
|
||||
/// Response from `/lol-champ-select/v1/session`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChampionSelectResponse {
|
||||
/// Local player cell ID.
|
||||
#[serde(default)]
|
||||
pub local_player_cell_id: Option<i64>,
|
||||
|
||||
/// Timer information.
|
||||
#[serde(default)]
|
||||
pub timers: Option<Timers>,
|
||||
|
||||
/// My team.
|
||||
#[serde(default)]
|
||||
pub my_team: Option<Vec<ChampionSelectPlayer>>,
|
||||
|
||||
/// Their team.
|
||||
#[serde(default)]
|
||||
pub their_team: Option<Vec<ChampionSelectPlayer>>,
|
||||
}
|
||||
|
||||
/// Timer information in champion select.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Timers {
|
||||
/// Current phase.
|
||||
#[serde(default)]
|
||||
pub phase: Option<String>,
|
||||
}
|
||||
|
||||
/// Player in champion select.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChampionSelectPlayer {
|
||||
/// Cell ID.
|
||||
#[serde(default)]
|
||||
pub cell_id: Option<i64>,
|
||||
|
||||
/// Champion ID.
|
||||
#[serde(default)]
|
||||
pub champion_id: Option<u64>,
|
||||
|
||||
/// Champion name.
|
||||
#[serde(default)]
|
||||
pub champion_name: Option<String>,
|
||||
|
||||
/// Team.
|
||||
#[serde(default)]
|
||||
pub team: Option<i64>,
|
||||
|
||||
/// Skin ID.
|
||||
#[serde(default)]
|
||||
pub skin_id: Option<u64>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Rune Pages API Responses
|
||||
// =============================================================================
|
||||
|
||||
/// Response from `/lol-perks/v1/currentpage`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RunePageResponse {
|
||||
/// Rune page ID.
|
||||
#[serde(default)]
|
||||
pub id: Option<u64>,
|
||||
|
||||
/// Rune page name.
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// Whether this is the current page.
|
||||
#[serde(default)]
|
||||
pub current: Option<bool>,
|
||||
|
||||
/// Primary style ID.
|
||||
#[serde(default)]
|
||||
pub primary_style_id: Option<u64>,
|
||||
|
||||
/// Secondary style ID.
|
||||
#[serde(default)]
|
||||
pub sub_style_id: Option<u64>,
|
||||
|
||||
/// Selected perk IDs.
|
||||
#[serde(default)]
|
||||
pub selected_perk_ids: Option<Vec<u64>>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// End of Game Stats API Responses
|
||||
// =============================================================================
|
||||
|
||||
/// Response from `/lol-end-of-game/v1/eog-stats-block`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EndOfGameStatsResponse {
|
||||
/// Game ID.
|
||||
#[serde(default)]
|
||||
pub game_id: Option<u64>,
|
||||
|
||||
/// Game length in seconds.
|
||||
#[serde(default)]
|
||||
pub game_length: Option<f64>,
|
||||
|
||||
/// Match ID.
|
||||
#[serde(default)]
|
||||
pub match_id: Option<u64>,
|
||||
|
||||
/// Game result.
|
||||
#[serde(default)]
|
||||
pub game_result: Option<String>,
|
||||
|
||||
/// Local player data.
|
||||
#[serde(default)]
|
||||
pub local_player: Option<EndOfGamePlayer>,
|
||||
|
||||
/// Teams data.
|
||||
#[serde(default)]
|
||||
pub teams: Option<Vec<EndOfGameTeam>>,
|
||||
|
||||
/// Players (legacy format).
|
||||
#[serde(default)]
|
||||
pub players: Option<Vec<EndOfGamePlayer>>,
|
||||
}
|
||||
|
||||
/// Player in end-of-game stats.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EndOfGamePlayer {
|
||||
/// Whether this is the local player.
|
||||
#[serde(default)]
|
||||
pub is_local_player: Option<bool>,
|
||||
|
||||
/// Player stats.
|
||||
#[serde(default)]
|
||||
pub stats: Option<PlayerStats>,
|
||||
|
||||
/// Items.
|
||||
#[serde(default)]
|
||||
pub items: Option<Vec<u64>>,
|
||||
}
|
||||
|
||||
/// Team in end-of-game stats.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EndOfGameTeam {
|
||||
/// Whether this is the player's team.
|
||||
#[serde(default)]
|
||||
pub is_player_team: Option<bool>,
|
||||
|
||||
/// Whether this is the winning team.
|
||||
#[serde(default)]
|
||||
pub is_winning_team: Option<bool>,
|
||||
|
||||
/// Players on the team.
|
||||
#[serde(default)]
|
||||
pub players: Option<Vec<EndOfGamePlayer>>,
|
||||
}
|
||||
|
||||
/// Player stats in end-of-game.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub struct PlayerStats {
|
||||
/// Kills.
|
||||
#[serde(default)]
|
||||
pub champions_killed: Option<u64>,
|
||||
|
||||
/// Deaths.
|
||||
#[serde(default)]
|
||||
pub num_deaths: Option<u64>,
|
||||
|
||||
/// Assists.
|
||||
#[serde(default)]
|
||||
pub assists: Option<u64>,
|
||||
|
||||
/// Minions killed (CS).
|
||||
#[serde(default)]
|
||||
pub minions_killed: Option<u64>,
|
||||
|
||||
/// Gold earned.
|
||||
#[serde(default)]
|
||||
pub gold_earned: Option<u64>,
|
||||
|
||||
/// Total damage dealt to champions.
|
||||
#[serde(default)]
|
||||
pub total_damage_dealt_to_champions: Option<u64>,
|
||||
|
||||
/// Total damage taken.
|
||||
#[serde(default)]
|
||||
pub total_damage_taken: Option<u64>,
|
||||
|
||||
/// Vision score.
|
||||
#[serde(default)]
|
||||
pub vision_score: Option<f64>,
|
||||
|
||||
/// Win status (1 = win).
|
||||
#[serde(default)]
|
||||
pub win: Option<u64>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Live Client Data API Responses
|
||||
// =============================================================================
|
||||
|
||||
/// Response from `/liveclientdata/activeplayer`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActivePlayerResponse {
|
||||
/// Active player's summoner name.
|
||||
#[serde(default)]
|
||||
pub summoner_name: Option<String>,
|
||||
|
||||
/// Display name.
|
||||
#[serde(default)]
|
||||
pub display_name: Option<String>,
|
||||
|
||||
/// Riot ID.
|
||||
#[serde(default)]
|
||||
pub riot_id: Option<String>,
|
||||
|
||||
/// Summoner spells.
|
||||
#[serde(default)]
|
||||
pub summoner_spells: Option<SummonerSpellsData>,
|
||||
}
|
||||
|
||||
/// Summoner spells data from live client.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SummonerSpellsData {
|
||||
/// First summoner spell.
|
||||
#[serde(default)]
|
||||
pub summoner_spell_one: Option<SpellData>,
|
||||
|
||||
/// Second summoner spell.
|
||||
#[serde(default)]
|
||||
pub summoner_spell_two: Option<SpellData>,
|
||||
|
||||
/// First spell ID (alternative field).
|
||||
#[serde(default)]
|
||||
pub spell1_id: Option<u64>,
|
||||
|
||||
/// Second spell ID (alternative field).
|
||||
#[serde(default)]
|
||||
pub spell2_id: Option<u64>,
|
||||
}
|
||||
|
||||
/// Individual spell data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SpellData {
|
||||
/// Spell ID.
|
||||
#[serde(default)]
|
||||
pub spell_id: Option<u64>,
|
||||
|
||||
/// Display name.
|
||||
#[serde(default)]
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Response from `/liveclientdata/playerlist`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlayerListResponse(pub Vec<LiveClientPlayer>);
|
||||
|
||||
/// Player in live client data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LiveClientPlayer {
|
||||
/// Whether this is the local player.
|
||||
#[serde(default)]
|
||||
pub is_local_player: Option<bool>,
|
||||
|
||||
/// Summoner name.
|
||||
#[serde(default)]
|
||||
pub summoner_name: Option<String>,
|
||||
|
||||
/// Riot ID.
|
||||
#[serde(default)]
|
||||
pub riot_id: Option<String>,
|
||||
|
||||
/// PUUID.
|
||||
#[serde(default)]
|
||||
pub puuid: Option<String>,
|
||||
|
||||
/// Summoner ID.
|
||||
#[serde(default)]
|
||||
pub summoner_id: Option<u64>,
|
||||
|
||||
/// Champion name.
|
||||
#[serde(default)]
|
||||
pub champion_name: Option<String>,
|
||||
|
||||
/// Team.
|
||||
#[serde(default)]
|
||||
pub team: Option<u64>,
|
||||
|
||||
/// Items.
|
||||
#[serde(default)]
|
||||
pub items: Option<Vec<LiveClientItem>>,
|
||||
}
|
||||
|
||||
/// Item in live client data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LiveClientItem {
|
||||
/// Item ID.
|
||||
#[serde(default)]
|
||||
pub item_id: Option<u64>,
|
||||
|
||||
/// Display name.
|
||||
#[serde(default)]
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Helper implementations
|
||||
// =============================================================================
|
||||
|
||||
impl EndOfGameStatsResponse {
|
||||
/// Get the local player from the response.
|
||||
pub fn get_local_player(&self) -> Option<&EndOfGamePlayer> {
|
||||
// First check local_player field
|
||||
if let Some(ref player) = self.local_player {
|
||||
return Some(player);
|
||||
}
|
||||
|
||||
// Then check teams
|
||||
if let Some(ref teams) = self.teams {
|
||||
for team in teams {
|
||||
if let Some(ref players) = team.players {
|
||||
for player in players {
|
||||
if player.is_local_player == Some(true) {
|
||||
return Some(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally check legacy players array
|
||||
if let Some(ref players) = self.players {
|
||||
return players.first();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if the local player won.
|
||||
pub fn is_victory(&self) -> bool {
|
||||
// Check local player stats
|
||||
if let Some(player) = self.get_local_player() {
|
||||
if let Some(ref stats) = player.stats {
|
||||
if stats.win == Some(1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check teams
|
||||
if let Some(ref teams) = self.teams {
|
||||
for team in teams {
|
||||
if team.is_player_team == Some(true) && team.is_winning_team == Some(true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl ChampionSelectResponse {
|
||||
/// Get the local player's champion selection.
|
||||
pub fn get_local_player_selection(&self) -> Option<&ChampionSelectPlayer> {
|
||||
let cell_id = self.local_player_cell_id?;
|
||||
|
||||
// Check my team first
|
||||
if let Some(ref team) = self.my_team {
|
||||
for player in team {
|
||||
if player.cell_id == Some(cell_id) {
|
||||
return Some(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check their team
|
||||
if let Some(ref team) = self.their_team {
|
||||
for player in team {
|
||||
if player.cell_id == Some(cell_id) {
|
||||
return Some(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_summoner_deserialization() {
|
||||
let json = r#"{"summonerId": 12345, "puuid": "abc-123", "displayName": "TestPlayer"}"#;
|
||||
let summoner: SummonerResponse = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(summoner.summoner_id, Some(12345));
|
||||
assert_eq!(summoner.puuid, Some("abc-123".to_string()));
|
||||
assert_eq!(summoner.display_name, Some("TestPlayer".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_player_stats_deserialization() {
|
||||
let json = r#"{"CHAMPIONS_KILLED": 10, "NUM_DEATHS": 3, "ASSISTS": 15}"#;
|
||||
let stats: PlayerStats = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(stats.champions_killed, Some(10));
|
||||
assert_eq!(stats.num_deaths, Some(3));
|
||||
assert_eq!(stats.assists, Some(15));
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use serde::de::DeserializeOwned;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::protocol::Message};
|
||||
use tracing::{debug, error, info, trace, warn};
|
||||
|
||||
use super::api_types::{
|
||||
ActivePlayerResponse, ChampionSelectResponse, EndOfGameStatsResponse, GameflowSessionResponse,
|
||||
PlayerListResponse, RunePageResponse, SummonerResponse,
|
||||
};
|
||||
use super::auth::LockfileCredentials;
|
||||
use super::endpoints;
|
||||
use super::events::{GameEvent, ItemBuild};
|
||||
@@ -353,6 +358,13 @@ impl LqpClient {
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
/// Make a typed REST API request to the League Client.
|
||||
async fn request_typed<T: DeserializeOwned>(&self, method: &str, endpoint: &str) -> Result<T> {
|
||||
let json = self.request(method, endpoint).await?;
|
||||
serde_json::from_value(json)
|
||||
.map_err(|e| LqpError::EventParseError(format!("Deserialization failed: {}", e)).into())
|
||||
}
|
||||
|
||||
/// Get the current gameflow phase.
|
||||
pub async fn get_gameflow_phase(&self) -> Result<GameflowPhase> {
|
||||
let json = self.request("GET", endpoints::GAMEFLOW_PHASE).await?;
|
||||
@@ -362,22 +374,42 @@ impl LqpClient {
|
||||
Ok(GameflowPhase::from(phase_str))
|
||||
}
|
||||
|
||||
/// Get the current game session info.
|
||||
/// Get the current game session info (typed).
|
||||
pub async fn get_session_typed(&self) -> Result<GameflowSessionResponse> {
|
||||
self.request_typed("GET", endpoints::SESSION).await
|
||||
}
|
||||
|
||||
/// Get the current game session info (raw JSON for backward compatibility).
|
||||
pub async fn get_session(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::SESSION).await
|
||||
}
|
||||
|
||||
/// Get current summoner info.
|
||||
/// Get current summoner info (typed).
|
||||
pub async fn get_summoner_typed(&self) -> Result<SummonerResponse> {
|
||||
self.request_typed("GET", endpoints::SUMMONER).await
|
||||
}
|
||||
|
||||
/// Get current summoner info (raw JSON for backward compatibility).
|
||||
pub async fn get_summoner(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::SUMMONER).await
|
||||
}
|
||||
|
||||
/// Get champion select session info.
|
||||
/// Get champion select session info (typed).
|
||||
pub async fn get_champion_select_typed(&self) -> Result<ChampionSelectResponse> {
|
||||
self.request_typed("GET", endpoints::CHAMPION_SELECT).await
|
||||
}
|
||||
|
||||
/// Get champion select session info (raw JSON for backward compatibility).
|
||||
pub async fn get_champion_select(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::CHAMPION_SELECT).await
|
||||
}
|
||||
|
||||
/// Get end-of-game stats.
|
||||
/// Get end-of-game stats (typed).
|
||||
pub async fn get_game_stats_typed(&self) -> Result<EndOfGameStatsResponse> {
|
||||
self.request_typed("GET", endpoints::GAME_STATS).await
|
||||
}
|
||||
|
||||
/// Get end-of-game stats (raw JSON for backward compatibility).
|
||||
pub async fn get_game_stats(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::GAME_STATS).await
|
||||
}
|
||||
@@ -387,7 +419,12 @@ impl LqpClient {
|
||||
self.request("GET", endpoints::CHAMPION_SUMMARY).await
|
||||
}
|
||||
|
||||
/// Get current rune page.
|
||||
/// Get current rune page (typed).
|
||||
pub async fn get_rune_page_typed(&self) -> Result<RunePageResponse> {
|
||||
self.request_typed("GET", endpoints::RUNE_PAGES).await
|
||||
}
|
||||
|
||||
/// Get current rune page (raw JSON for backward compatibility).
|
||||
pub async fn get_rune_page(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::RUNE_PAGES).await
|
||||
}
|
||||
@@ -407,13 +444,25 @@ impl LqpClient {
|
||||
self.request("GET", endpoints::LIVE_CLIENT_DATA).await
|
||||
}
|
||||
|
||||
/// Get active player data from live client.
|
||||
/// Get active player data from live client (typed).
|
||||
pub async fn get_live_client_active_player_typed(&self) -> Result<ActivePlayerResponse> {
|
||||
self.request_typed("GET", endpoints::LIVE_CLIENT_DATA_ACTIVE_PLAYER)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get active player data from live client (raw JSON for backward compatibility).
|
||||
pub async fn get_live_client_active_player(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::LIVE_CLIENT_DATA_ACTIVE_PLAYER)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get player list from live client.
|
||||
/// Get player list from live client (typed).
|
||||
pub async fn get_live_client_player_list_typed(&self) -> Result<PlayerListResponse> {
|
||||
self.request_typed("GET", endpoints::LIVE_CLIENT_DATA_PLAYER_LIST)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get player list from live client (raw JSON for backward compatibility).
|
||||
pub async fn get_live_client_player_list(&self) -> Result<serde_json::Value> {
|
||||
self.request("GET", endpoints::LIVE_CLIENT_DATA_PLAYER_LIST)
|
||||
.await
|
||||
@@ -434,62 +483,33 @@ impl LqpClient {
|
||||
let mut metadata = PreGameMetadata::default();
|
||||
|
||||
// Get session info for queue type and game mode
|
||||
if let Ok(session) = self.get_session().await {
|
||||
if let Some(map) = session.get("map").and_then(|m| m.as_str()) {
|
||||
metadata.map_name = Some(map.to_string());
|
||||
}
|
||||
if let Some(game_mode) = session.get("gameMode").and_then(|m| m.as_str()) {
|
||||
metadata.game_mode = Some(game_mode.to_string());
|
||||
}
|
||||
if let Some(queue_id) = session.get("queueId").and_then(|q| q.as_u64()) {
|
||||
metadata.queue_id = Some(queue_id as u32);
|
||||
}
|
||||
if let Ok(session) = self.get_session_typed().await {
|
||||
metadata.map_name = session.map;
|
||||
metadata.game_mode = session.game_mode;
|
||||
metadata.queue_id = session.queue_id.map(|id| id as u32);
|
||||
}
|
||||
|
||||
// Get summoner info (including puuid)
|
||||
if let Ok(summoner) = self.get_summoner().await {
|
||||
if let Some(name) = summoner.get("displayName").and_then(|n| n.as_str()) {
|
||||
metadata.summoner_name = Some(name.to_string());
|
||||
}
|
||||
if let Some(puuid) = summoner.get("puuid").and_then(|p| p.as_str()) {
|
||||
metadata.local_puuid = Some(puuid.to_string());
|
||||
self.state.write().await.local_puuid = Some(puuid.to_string());
|
||||
if let Ok(summoner) = self.get_summoner_typed().await {
|
||||
metadata.summoner_name = summoner.display_name;
|
||||
if let Some(puuid) = &summoner.puuid {
|
||||
self.state.write().await.local_puuid = Some(puuid.clone());
|
||||
}
|
||||
metadata.local_puuid = summoner.puuid;
|
||||
}
|
||||
|
||||
// Get champion select info
|
||||
if let Ok(champ_select) = self.get_champion_select().await {
|
||||
if let Some(local_player_cell_id) = champ_select
|
||||
.get("localPlayerCellId")
|
||||
.and_then(|id| id.as_i64())
|
||||
{
|
||||
if let Some(my_team) = champ_select.get("myTeam").and_then(|t| t.as_array()) {
|
||||
for member in my_team {
|
||||
if member.get("cellId").and_then(|id| id.as_i64())
|
||||
== Some(local_player_cell_id)
|
||||
{
|
||||
if let Some(champion_id) =
|
||||
member.get("championId").and_then(|id| id.as_u64())
|
||||
{
|
||||
metadata.champion_id = Some(champion_id as u32);
|
||||
}
|
||||
if let Some(team) = member.get("team").and_then(|t| t.as_i64()) {
|
||||
metadata.team = Some(team as u32);
|
||||
}
|
||||
if let Some(skin_id) = member.get("skinId").and_then(|id| id.as_u64()) {
|
||||
metadata.skin_id = Some(skin_id as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(champ_select) = self.get_champion_select_typed().await {
|
||||
if let Some(player) = champ_select.get_local_player_selection() {
|
||||
metadata.champion_id = player.champion_id.map(|id| id as u32);
|
||||
metadata.team = player.team.map(|id| id as u32);
|
||||
metadata.skin_id = player.skin_id.map(|id| id as u32);
|
||||
}
|
||||
}
|
||||
|
||||
// Get rune page
|
||||
if let Ok(rune_page) = self.get_rune_page().await {
|
||||
if let Some(name) = rune_page.get("name").and_then(|n| n.as_str()) {
|
||||
metadata.rune_page_name = Some(name.to_string());
|
||||
}
|
||||
if let Ok(rune_page) = self.get_rune_page_typed().await {
|
||||
metadata.rune_page_name = rune_page.name;
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
@@ -499,55 +519,25 @@ impl LqpClient {
|
||||
pub async fn fetch_game_end_stats(&self) -> Result<GameEndMetadata> {
|
||||
let mut metadata = GameEndMetadata::default();
|
||||
|
||||
if let Ok(stats) = self.get_game_stats().await {
|
||||
if let Some(victory) = stats.get("gameResult").and_then(|r| r.as_str()) {
|
||||
metadata.victory = Some(victory == "WIN");
|
||||
}
|
||||
if let Ok(stats) = self.get_game_stats_typed().await {
|
||||
metadata.victory = Some(stats.is_victory());
|
||||
metadata.game_duration = stats.game_length.unwrap_or(0.0);
|
||||
metadata.match_id = stats.match_id.map(|id| id.to_string());
|
||||
|
||||
if let Some(players) = stats.get("players").and_then(|p| p.as_array()) {
|
||||
if let Some(player) = players.first() {
|
||||
if let Some(stats_obj) = player.get("stats") {
|
||||
metadata.kills =
|
||||
stats_obj.get("kills").and_then(|k| k.as_u64()).unwrap_or(0) as u32;
|
||||
metadata.deaths = stats_obj
|
||||
.get("deaths")
|
||||
.and_then(|d| d.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
metadata.assists = stats_obj
|
||||
.get("assists")
|
||||
.and_then(|a| a.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
metadata.creep_score = stats_obj
|
||||
.get("minionsKilled")
|
||||
.and_then(|cs| cs.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
metadata.gold_earned = stats_obj
|
||||
.get("goldEarned")
|
||||
.and_then(|g| g.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
metadata.damage_dealt = stats_obj
|
||||
.get("totalDamageDealtToChampions")
|
||||
.and_then(|d| d.as_u64())
|
||||
.unwrap_or(0);
|
||||
metadata.damage_taken = stats_obj
|
||||
.get("totalDamageTaken")
|
||||
.and_then(|d| d.as_u64())
|
||||
.unwrap_or(0);
|
||||
metadata.vision_score = stats_obj
|
||||
.get("visionScore")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
}
|
||||
// Extract player stats
|
||||
if let Some(player) = stats.get_local_player() {
|
||||
if let Some(player_stats) = &player.stats {
|
||||
metadata.kills = player_stats.champions_killed.unwrap_or(0) as u32;
|
||||
metadata.deaths = player_stats.num_deaths.unwrap_or(0) as u32;
|
||||
metadata.assists = player_stats.assists.unwrap_or(0) as u32;
|
||||
metadata.creep_score = player_stats.minions_killed.unwrap_or(0) as u32;
|
||||
metadata.gold_earned = player_stats.gold_earned.unwrap_or(0) as u32;
|
||||
metadata.damage_dealt =
|
||||
player_stats.total_damage_dealt_to_champions.unwrap_or(0);
|
||||
metadata.damage_taken = player_stats.total_damage_taken.unwrap_or(0);
|
||||
metadata.vision_score = player_stats.vision_score.unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(duration) = stats.get("gameLength").and_then(|d| d.as_f64()) {
|
||||
metadata.game_duration = duration;
|
||||
}
|
||||
|
||||
if let Some(match_id) = stats.get("matchId").and_then(|id| id.as_u64()) {
|
||||
metadata.match_id = Some(match_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
@@ -559,39 +549,25 @@ impl LqpClient {
|
||||
|
||||
let mut metadata = super::PlayerGameMetadata::default();
|
||||
|
||||
// Get summoner info
|
||||
if let Ok(summoner) = self.get_summoner().await {
|
||||
metadata.puuid = summoner
|
||||
.get("puuid")
|
||||
.and_then(|p| p.as_str())
|
||||
.map(|s| s.to_string());
|
||||
// Get summoner info (typed)
|
||||
if let Ok(summoner) = self.get_summoner_typed().await {
|
||||
metadata.puuid = summoner.puuid;
|
||||
metadata.summoner_name = summoner
|
||||
.get("displayName")
|
||||
.or_else(|| summoner.get("name"))
|
||||
.or_else(|| summoner.get("internalName"))
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|s| s.to_string());
|
||||
.display_name
|
||||
.or(summoner.name)
|
||||
.or(summoner.internal_name);
|
||||
}
|
||||
|
||||
// Get rune page
|
||||
if let Ok(rune_page) = self.get_rune_page().await {
|
||||
let primary_style_id = rune_page
|
||||
.get("primaryStyleId")
|
||||
.and_then(|id| id.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
let secondary_style_id = rune_page
|
||||
.get("subStyleId")
|
||||
.and_then(|id| id.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
// Get rune page (typed)
|
||||
if let Ok(rune_page) = self.get_rune_page_typed().await {
|
||||
let primary_style_id = rune_page.primary_style_id.unwrap_or(0) as u32;
|
||||
let secondary_style_id = rune_page.sub_style_id.unwrap_or(0) as u32;
|
||||
let selected_perks = rune_page
|
||||
.get("selectedPerkIds")
|
||||
.and_then(|ids| ids.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|id| id.as_u64().map(|v| v as u32))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
.selected_perk_ids
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|id| *id as u32)
|
||||
.collect();
|
||||
|
||||
if primary_style_id > 0 {
|
||||
metadata.runes = Some(RunePage {
|
||||
@@ -599,47 +575,30 @@ impl LqpClient {
|
||||
secondary_style_id,
|
||||
selected_perks,
|
||||
stat_modifiers: Vec::new(),
|
||||
name: rune_page
|
||||
.get("name")
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|s| s.to_string()),
|
||||
current: rune_page
|
||||
.get("current")
|
||||
.and_then(|c| c.as_bool())
|
||||
.unwrap_or(true),
|
||||
name: rune_page.name,
|
||||
current: rune_page.current.unwrap_or(true),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get summoner spells from live client data
|
||||
if let Ok(active_player) = self.get_live_client_active_player().await {
|
||||
debug!(
|
||||
"[METADATA] Live client active player data: {:?}",
|
||||
active_player
|
||||
);
|
||||
// Get summoner spells from live client data (typed)
|
||||
if let Ok(active_player) = self.get_live_client_active_player_typed().await {
|
||||
debug!("[METADATA] Live client active player data received");
|
||||
|
||||
if let Some(summoner_spells) = active_player.get("summonerSpells") {
|
||||
let spell1_id = summoner_spells
|
||||
.get("summonerSpellOne")
|
||||
.and_then(|s| s.get("spellId"))
|
||||
.and_then(|id| id.as_u64())
|
||||
.unwrap_or_else(|| {
|
||||
summoner_spells
|
||||
.get("spell1Id")
|
||||
.and_then(|id| id.as_u64())
|
||||
.unwrap_or(0)
|
||||
}) as u32;
|
||||
if let Some(ref spells) = active_player.summoner_spells {
|
||||
let spell1_id = spells
|
||||
.summoner_spell_one
|
||||
.as_ref()
|
||||
.and_then(|s| s.spell_id)
|
||||
.or(spells.spell1_id)
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
let spell2_id = summoner_spells
|
||||
.get("summonerSpellTwo")
|
||||
.and_then(|s| s.get("spellId"))
|
||||
.and_then(|id| id.as_u64())
|
||||
.unwrap_or_else(|| {
|
||||
summoner_spells
|
||||
.get("spell2Id")
|
||||
.and_then(|id| id.as_u64())
|
||||
.unwrap_or(0)
|
||||
}) as u32;
|
||||
let spell2_id = spells
|
||||
.summoner_spell_two
|
||||
.as_ref()
|
||||
.and_then(|s| s.spell_id)
|
||||
.or(spells.spell2_id)
|
||||
.unwrap_or(0) as u32;
|
||||
|
||||
if spell1_id > 0 || spell2_id > 0 {
|
||||
metadata.summoner_spells = Some(SummonerSpells {
|
||||
@@ -655,35 +614,46 @@ impl LqpClient {
|
||||
|| metadata.summoner_name.as_ref().is_none_or(|n| n.is_empty())
|
||||
{
|
||||
metadata.summoner_name = active_player
|
||||
.get("summonerName")
|
||||
.or_else(|| active_player.get("displayName"))
|
||||
.or_else(|| active_player.get("riotId"))
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|s| s.to_string());
|
||||
.summoner_name
|
||||
.or(active_player.display_name)
|
||||
.or(active_player.riot_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Get summoner spells from session gameData
|
||||
// Fallback: Get summoner spells from session gameData (typed)
|
||||
if metadata.summoner_spells.is_none() {
|
||||
if let Ok(session) = self.get_session().await {
|
||||
if let Ok(session) = self.get_session_typed().await {
|
||||
if let Some(local_puuid) = &metadata.puuid {
|
||||
if let Some(game_data) = session.get("gameData") {
|
||||
for team_key in &["teamOne", "teamTwo"] {
|
||||
if let Some(team) = game_data.get(team_key).and_then(|t| t.as_array()) {
|
||||
if let Some(ref game_data) = session.game_data {
|
||||
// Check team one
|
||||
if let Some(ref team) = game_data.team_one {
|
||||
for player in team {
|
||||
if player.puuid.as_deref() == Some(local_puuid.as_str()) {
|
||||
let spell1_id = player.spell1_id.unwrap_or(0) as u32;
|
||||
let spell2_id = player.spell2_id.unwrap_or(0) as u32;
|
||||
|
||||
if spell1_id > 0 || spell2_id > 0 {
|
||||
metadata.summoner_spells = Some(SummonerSpells {
|
||||
spell1_id,
|
||||
spell2_id,
|
||||
spell1_name: spell_id_to_name(spell1_id),
|
||||
spell2_name: spell_id_to_name(spell2_id),
|
||||
});
|
||||
}
|
||||
|
||||
metadata.champion_id = player.champion_id.map(|id| id as u32);
|
||||
metadata.team = player.team_id.map(|id| id as u32);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check team two if not found
|
||||
if metadata.summoner_spells.is_none() {
|
||||
if let Some(ref team) = game_data.team_two {
|
||||
for player in team {
|
||||
if player.get("puuid").and_then(|p| p.as_str())
|
||||
== Some(local_puuid.as_str())
|
||||
{
|
||||
let spell1_id = player
|
||||
.get("spell1Id")
|
||||
.and_then(|id| id.as_u64())
|
||||
.unwrap_or(0)
|
||||
as u32;
|
||||
let spell2_id = player
|
||||
.get("spell2Id")
|
||||
.and_then(|id| id.as_u64())
|
||||
.unwrap_or(0)
|
||||
as u32;
|
||||
if player.puuid.as_deref() == Some(local_puuid.as_str()) {
|
||||
let spell1_id = player.spell1_id.unwrap_or(0) as u32;
|
||||
let spell2_id = player.spell2_id.unwrap_or(0) as u32;
|
||||
|
||||
if spell1_id > 0 || spell2_id > 0 {
|
||||
metadata.summoner_spells = Some(SummonerSpells {
|
||||
@@ -694,14 +664,9 @@ impl LqpClient {
|
||||
});
|
||||
}
|
||||
|
||||
metadata.champion_id = player
|
||||
.get("championId")
|
||||
.and_then(|id| id.as_u64())
|
||||
.map(|v| v as u32);
|
||||
metadata.team = player
|
||||
.get("teamId")
|
||||
.and_then(|id| id.as_u64())
|
||||
.map(|v| v as u32);
|
||||
metadata.champion_id =
|
||||
player.champion_id.map(|id| id as u32);
|
||||
metadata.team = player.team_id.map(|id| id as u32);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -723,70 +688,64 @@ impl LqpClient {
|
||||
pub async fn fetch_all_players_identities(&self) -> Result<Vec<super::PlayerIdentity>> {
|
||||
let mut players = Vec::new();
|
||||
|
||||
// Try live client data first
|
||||
if let Ok(player_list) = self.get_live_client_player_list().await {
|
||||
if let Some(arr) = player_list.as_array() {
|
||||
for player in arr {
|
||||
let summoner_name = player
|
||||
.get("summonerName")
|
||||
.or_else(|| player.get("riotId"))
|
||||
.and_then(|n| n.as_str())
|
||||
.unwrap_or("");
|
||||
// Try live client data first (typed)
|
||||
if let Ok(player_list) = self.get_live_client_player_list_typed().await {
|
||||
for player in &player_list.0 {
|
||||
let summoner_name = player
|
||||
.summoner_name
|
||||
.as_deref()
|
||||
.or(player.riot_id.as_deref())
|
||||
.unwrap_or("");
|
||||
|
||||
let team = player
|
||||
.get("team")
|
||||
.and_then(|t| t.as_u64())
|
||||
.map(|v| v as u32);
|
||||
|
||||
let champion_name = player
|
||||
.get("championName")
|
||||
.and_then(|n| n.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
if let Some(puuid) = player.get("puuid").and_then(|p| p.as_str()) {
|
||||
players.push(super::PlayerIdentity {
|
||||
puuid: puuid.to_string(),
|
||||
summoner_name: summoner_name.to_string(),
|
||||
summoner_id: player.get("summonerId").and_then(|id| id.as_u64()),
|
||||
champion_name,
|
||||
team,
|
||||
});
|
||||
}
|
||||
if let Some(ref puuid) = player.puuid {
|
||||
players.push(super::PlayerIdentity {
|
||||
puuid: puuid.clone(),
|
||||
summoner_name: summoner_name.to_string(),
|
||||
summoner_id: player.summoner_id,
|
||||
champion_name: player.champion_name.clone(),
|
||||
team: player.team.map(|id| id as u32),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try from gameflow session
|
||||
// Fallback: try from gameflow session (typed)
|
||||
if players.is_empty() {
|
||||
if let Ok(session) = self.get_session().await {
|
||||
if let Some(game_data) = session.get("gameData") {
|
||||
for team_key in &["teamOne", "teamTwo"] {
|
||||
let team_id = if *team_key == "teamOne" {
|
||||
100u32
|
||||
} else {
|
||||
200u32
|
||||
};
|
||||
if let Some(team) = game_data.get(team_key).and_then(|t| t.as_array()) {
|
||||
for player in team {
|
||||
if let (Some(puuid), Some(summoner_name)) = (
|
||||
player.get("puuid").and_then(|p| p.as_str()),
|
||||
player.get("summonerName").and_then(|n| n.as_str()),
|
||||
) {
|
||||
let champion_id = player
|
||||
.get("championId")
|
||||
.and_then(|id| id.as_u64())
|
||||
.map(|v| v as u32);
|
||||
let champion_name = champion_id.and_then(champion_id_to_name);
|
||||
players.push(super::PlayerIdentity {
|
||||
puuid: puuid.to_string(),
|
||||
summoner_name: summoner_name.to_string(),
|
||||
summoner_id: player
|
||||
.get("summonerId")
|
||||
.and_then(|id| id.as_u64()),
|
||||
champion_name,
|
||||
team: Some(team_id),
|
||||
});
|
||||
}
|
||||
if let Ok(session) = self.get_session_typed().await {
|
||||
if let Some(ref game_data) = session.game_data {
|
||||
// Team one (team ID 100)
|
||||
if let Some(ref team) = game_data.team_one {
|
||||
for player in team {
|
||||
if let (Some(ref puuid), Some(ref summoner_name)) =
|
||||
(&player.puuid, &player.summoner_name)
|
||||
{
|
||||
let champion_id = player.champion_id.map(|id| id as u32);
|
||||
let champion_name = champion_id.and_then(champion_id_to_name);
|
||||
players.push(super::PlayerIdentity {
|
||||
puuid: puuid.clone(),
|
||||
summoner_name: summoner_name.clone(),
|
||||
summoner_id: player.summoner_id,
|
||||
champion_name,
|
||||
team: Some(100),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Team two (team ID 200)
|
||||
if let Some(ref team) = game_data.team_two {
|
||||
for player in team {
|
||||
if let (Some(ref puuid), Some(ref summoner_name)) =
|
||||
(&player.puuid, &player.summoner_name)
|
||||
{
|
||||
let champion_id = player.champion_id.map(|id| id as u32);
|
||||
let champion_name = champion_id.and_then(champion_id_to_name);
|
||||
players.push(super::PlayerIdentity {
|
||||
puuid: puuid.clone(),
|
||||
summoner_name: summoner_name.clone(),
|
||||
summoner_id: player.summoner_id,
|
||||
champion_name,
|
||||
team: Some(200),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -801,34 +760,31 @@ impl LqpClient {
|
||||
pub async fn fetch_final_items(&self) -> Result<Option<ItemBuild>> {
|
||||
info!("[ITEMS] Fetching final items...");
|
||||
|
||||
// First try live client data
|
||||
match self.get_live_client_player_list().await {
|
||||
// First try live client data (typed)
|
||||
match self.get_live_client_player_list_typed().await {
|
||||
Ok(player_list) => {
|
||||
info!(
|
||||
"[ITEMS] Live client player list response: {:?}",
|
||||
player_list
|
||||
"[ITEMS] Live client player list response received with {} players",
|
||||
player_list.0.len()
|
||||
);
|
||||
if let Some(players) = player_list.as_array() {
|
||||
info!(
|
||||
"[ITEMS] Found {} players in live client data",
|
||||
players.len()
|
||||
);
|
||||
for player in players {
|
||||
let is_local = player
|
||||
.get("isLocalPlayer")
|
||||
.and_then(|l| l.as_bool())
|
||||
.unwrap_or(false);
|
||||
if is_local {
|
||||
info!("[ITEMS] Found local player in live client data");
|
||||
if let Some(items) = player.get("items").and_then(|i| i.as_array()) {
|
||||
info!("[ITEMS] Items array has {} items", items.len());
|
||||
let item_build = parse_items_from_live_client(items);
|
||||
if item_build.is_some() {
|
||||
info!(
|
||||
"[ITEMS] Successfully parsed items from live client data"
|
||||
);
|
||||
return Ok(item_build);
|
||||
}
|
||||
for player in &player_list.0 {
|
||||
if player.is_local_player == Some(true) {
|
||||
info!("[ITEMS] Found local player in live client data");
|
||||
if let Some(ref items) = player.items {
|
||||
info!("[ITEMS] Items array has {} items", items.len());
|
||||
// Convert LiveClientItem to serde_json::Value for parsing
|
||||
let items_json: Vec<serde_json::Value> = items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
item.item_id.map(|id| {
|
||||
serde_json::json!({"itemId": id, "displayName": item.display_name})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let item_build = parse_items_from_live_client(&items_json);
|
||||
if item_build.is_some() {
|
||||
info!("[ITEMS] Successfully parsed items from live client data");
|
||||
return Ok(item_build);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,16 +795,22 @@ impl LqpClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try end-of-game stats
|
||||
match self.get_game_stats().await {
|
||||
// Fallback: try end-of-game stats (typed)
|
||||
match self.get_game_stats_typed().await {
|
||||
Ok(stats) => {
|
||||
info!("[ITEMS] Game stats response received");
|
||||
|
||||
if let Some(local_player) = stats.get("localPlayer") {
|
||||
// Try local player first
|
||||
if let Some(local_player) = stats.get_local_player() {
|
||||
info!("[ITEMS] Found localPlayer in game stats");
|
||||
if let Some(items) = local_player.get("items").and_then(|i| i.as_array()) {
|
||||
if let Some(ref items) = local_player.items {
|
||||
info!("[ITEMS] localPlayer.items array has {} items", items.len());
|
||||
let item_build = parse_items_from_game_stats(items);
|
||||
// Convert item IDs to serde_json::Value for parsing
|
||||
let items_json: Vec<serde_json::Value> = items
|
||||
.iter()
|
||||
.map(|id| serde_json::json!({"itemID": id}))
|
||||
.collect();
|
||||
let item_build = parse_items_from_game_stats(&items_json);
|
||||
if item_build.is_some() {
|
||||
info!("[ITEMS] Successfully parsed items from localPlayer");
|
||||
return Ok(item_build);
|
||||
@@ -856,25 +818,24 @@ impl LqpClient {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(teams) = stats.get("teams").and_then(|t| t.as_array()) {
|
||||
// Try teams
|
||||
if let Some(ref teams) = stats.teams {
|
||||
info!("[ITEMS] Found {} teams in game stats", teams.len());
|
||||
for team in teams {
|
||||
if let Some(players) = team.get("players").and_then(|p| p.as_array()) {
|
||||
if let Some(ref players) = team.players {
|
||||
for player in players {
|
||||
let is_local = player
|
||||
.get("isLocalPlayer")
|
||||
.and_then(|l| l.as_bool())
|
||||
.unwrap_or(false);
|
||||
if is_local {
|
||||
if player.is_local_player == Some(true) {
|
||||
info!("[ITEMS] Found local player in teams[].players[]");
|
||||
if let Some(items) =
|
||||
player.get("items").and_then(|i| i.as_array())
|
||||
{
|
||||
if let Some(ref items) = player.items {
|
||||
info!(
|
||||
"[ITEMS] Player items array has {} items",
|
||||
items.len()
|
||||
);
|
||||
let item_build = parse_items_from_game_stats(items);
|
||||
let items_json: Vec<serde_json::Value> = items
|
||||
.iter()
|
||||
.map(|id| serde_json::json!({"itemID": id}))
|
||||
.collect();
|
||||
let item_build = parse_items_from_game_stats(&items_json);
|
||||
if item_build.is_some() {
|
||||
info!("[ITEMS] Successfully parsed items from teams[].players[]");
|
||||
return Ok(item_build);
|
||||
@@ -886,14 +847,19 @@ impl LqpClient {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(players) = stats.get("players").and_then(|p| p.as_array()) {
|
||||
// Try legacy players array
|
||||
if let Some(ref players) = stats.players {
|
||||
info!(
|
||||
"[ITEMS] Found {} players in game stats (legacy)",
|
||||
players.len()
|
||||
);
|
||||
if let Some(player) = players.first() {
|
||||
if let Some(items) = player.get("items").and_then(|i| i.as_array()) {
|
||||
let item_build = parse_items_from_game_stats(items);
|
||||
if let Some(ref items) = player.items {
|
||||
let items_json: Vec<serde_json::Value> = items
|
||||
.iter()
|
||||
.map(|id| serde_json::json!({"itemID": id}))
|
||||
.collect();
|
||||
let item_build = parse_items_from_game_stats(&items_json);
|
||||
if item_build.is_some() {
|
||||
return Ok(item_build);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! This module handles communication with the League of Legends client
|
||||
//! via WebSocket and REST API for game event detection and capture.
|
||||
|
||||
mod api_types;
|
||||
mod auth;
|
||||
mod client;
|
||||
mod endpoints;
|
||||
@@ -14,6 +15,12 @@ mod state;
|
||||
mod tls;
|
||||
mod websocket;
|
||||
|
||||
pub use api_types::{
|
||||
ActivePlayerResponse, ChampionSelectPlayer, ChampionSelectResponse, EndOfGamePlayer,
|
||||
EndOfGameStatsResponse, EndOfGameTeam, GameData, GameflowSessionResponse, LiveClientItem,
|
||||
LiveClientPlayer, PlayerListResponse, PlayerStats, QueueData, RunePageResponse,
|
||||
SummonerResponse, SummonerSpellsData, TeamPlayer, Timers,
|
||||
};
|
||||
pub use auth::{LockfileCredentials, LockfileWatcher};
|
||||
pub use client::LqpClient;
|
||||
pub use endpoints::{
|
||||
|
||||
Reference in New Issue
Block a user