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 std::sync::Arc;
|
||||||
|
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
use tokio::sync::{broadcast, RwLock};
|
use tokio::sync::{broadcast, RwLock};
|
||||||
use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::protocol::Message};
|
use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::protocol::Message};
|
||||||
use tracing::{debug, error, info, trace, warn};
|
use tracing::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
|
use super::api_types::{
|
||||||
|
ActivePlayerResponse, ChampionSelectResponse, EndOfGameStatsResponse, GameflowSessionResponse,
|
||||||
|
PlayerListResponse, RunePageResponse, SummonerResponse,
|
||||||
|
};
|
||||||
use super::auth::LockfileCredentials;
|
use super::auth::LockfileCredentials;
|
||||||
use super::endpoints;
|
use super::endpoints;
|
||||||
use super::events::{GameEvent, ItemBuild};
|
use super::events::{GameEvent, ItemBuild};
|
||||||
@@ -353,6 +358,13 @@ impl LqpClient {
|
|||||||
Ok(json)
|
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.
|
/// Get the current gameflow phase.
|
||||||
pub async fn get_gameflow_phase(&self) -> Result<GameflowPhase> {
|
pub async fn get_gameflow_phase(&self) -> Result<GameflowPhase> {
|
||||||
let json = self.request("GET", endpoints::GAMEFLOW_PHASE).await?;
|
let json = self.request("GET", endpoints::GAMEFLOW_PHASE).await?;
|
||||||
@@ -362,22 +374,42 @@ impl LqpClient {
|
|||||||
Ok(GameflowPhase::from(phase_str))
|
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> {
|
pub async fn get_session(&self) -> Result<serde_json::Value> {
|
||||||
self.request("GET", endpoints::SESSION).await
|
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> {
|
pub async fn get_summoner(&self) -> Result<serde_json::Value> {
|
||||||
self.request("GET", endpoints::SUMMONER).await
|
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> {
|
pub async fn get_champion_select(&self) -> Result<serde_json::Value> {
|
||||||
self.request("GET", endpoints::CHAMPION_SELECT).await
|
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> {
|
pub async fn get_game_stats(&self) -> Result<serde_json::Value> {
|
||||||
self.request("GET", endpoints::GAME_STATS).await
|
self.request("GET", endpoints::GAME_STATS).await
|
||||||
}
|
}
|
||||||
@@ -387,7 +419,12 @@ impl LqpClient {
|
|||||||
self.request("GET", endpoints::CHAMPION_SUMMARY).await
|
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> {
|
pub async fn get_rune_page(&self) -> Result<serde_json::Value> {
|
||||||
self.request("GET", endpoints::RUNE_PAGES).await
|
self.request("GET", endpoints::RUNE_PAGES).await
|
||||||
}
|
}
|
||||||
@@ -407,13 +444,25 @@ impl LqpClient {
|
|||||||
self.request("GET", endpoints::LIVE_CLIENT_DATA).await
|
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> {
|
pub async fn get_live_client_active_player(&self) -> Result<serde_json::Value> {
|
||||||
self.request("GET", endpoints::LIVE_CLIENT_DATA_ACTIVE_PLAYER)
|
self.request("GET", endpoints::LIVE_CLIENT_DATA_ACTIVE_PLAYER)
|
||||||
.await
|
.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> {
|
pub async fn get_live_client_player_list(&self) -> Result<serde_json::Value> {
|
||||||
self.request("GET", endpoints::LIVE_CLIENT_DATA_PLAYER_LIST)
|
self.request("GET", endpoints::LIVE_CLIENT_DATA_PLAYER_LIST)
|
||||||
.await
|
.await
|
||||||
@@ -434,62 +483,33 @@ impl LqpClient {
|
|||||||
let mut metadata = PreGameMetadata::default();
|
let mut metadata = PreGameMetadata::default();
|
||||||
|
|
||||||
// Get session info for queue type and game mode
|
// Get session info for queue type and game mode
|
||||||
if let Ok(session) = self.get_session().await {
|
if let Ok(session) = self.get_session_typed().await {
|
||||||
if let Some(map) = session.get("map").and_then(|m| m.as_str()) {
|
metadata.map_name = session.map;
|
||||||
metadata.map_name = Some(map.to_string());
|
metadata.game_mode = session.game_mode;
|
||||||
}
|
metadata.queue_id = session.queue_id.map(|id| id as u32);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get summoner info (including puuid)
|
// Get summoner info (including puuid)
|
||||||
if let Ok(summoner) = self.get_summoner().await {
|
if let Ok(summoner) = self.get_summoner_typed().await {
|
||||||
if let Some(name) = summoner.get("displayName").and_then(|n| n.as_str()) {
|
metadata.summoner_name = summoner.display_name;
|
||||||
metadata.summoner_name = Some(name.to_string());
|
if let Some(puuid) = &summoner.puuid {
|
||||||
}
|
self.state.write().await.local_puuid = Some(puuid.clone());
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
metadata.local_puuid = summoner.puuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get champion select info
|
// Get champion select info
|
||||||
if let Ok(champ_select) = self.get_champion_select().await {
|
if let Ok(champ_select) = self.get_champion_select_typed().await {
|
||||||
if let Some(local_player_cell_id) = champ_select
|
if let Some(player) = champ_select.get_local_player_selection() {
|
||||||
.get("localPlayerCellId")
|
metadata.champion_id = player.champion_id.map(|id| id as u32);
|
||||||
.and_then(|id| id.as_i64())
|
metadata.team = player.team.map(|id| id as u32);
|
||||||
{
|
metadata.skin_id = player.skin_id.map(|id| id as u32);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rune page
|
// Get rune page
|
||||||
if let Ok(rune_page) = self.get_rune_page().await {
|
if let Ok(rune_page) = self.get_rune_page_typed().await {
|
||||||
if let Some(name) = rune_page.get("name").and_then(|n| n.as_str()) {
|
metadata.rune_page_name = rune_page.name;
|
||||||
metadata.rune_page_name = Some(name.to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(metadata)
|
Ok(metadata)
|
||||||
@@ -499,57 +519,27 @@ impl LqpClient {
|
|||||||
pub async fn fetch_game_end_stats(&self) -> Result<GameEndMetadata> {
|
pub async fn fetch_game_end_stats(&self) -> Result<GameEndMetadata> {
|
||||||
let mut metadata = GameEndMetadata::default();
|
let mut metadata = GameEndMetadata::default();
|
||||||
|
|
||||||
if let Ok(stats) = self.get_game_stats().await {
|
if let Ok(stats) = self.get_game_stats_typed().await {
|
||||||
if let Some(victory) = stats.get("gameResult").and_then(|r| r.as_str()) {
|
metadata.victory = Some(stats.is_victory());
|
||||||
metadata.victory = Some(victory == "WIN");
|
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()) {
|
// Extract player stats
|
||||||
if let Some(player) = players.first() {
|
if let Some(player) = stats.get_local_player() {
|
||||||
if let Some(stats_obj) = player.get("stats") {
|
if let Some(player_stats) = &player.stats {
|
||||||
metadata.kills =
|
metadata.kills = player_stats.champions_killed.unwrap_or(0) as u32;
|
||||||
stats_obj.get("kills").and_then(|k| k.as_u64()).unwrap_or(0) as u32;
|
metadata.deaths = player_stats.num_deaths.unwrap_or(0) as u32;
|
||||||
metadata.deaths = stats_obj
|
metadata.assists = player_stats.assists.unwrap_or(0) as u32;
|
||||||
.get("deaths")
|
metadata.creep_score = player_stats.minions_killed.unwrap_or(0) as u32;
|
||||||
.and_then(|d| d.as_u64())
|
metadata.gold_earned = player_stats.gold_earned.unwrap_or(0) as u32;
|
||||||
.unwrap_or(0) as u32;
|
metadata.damage_dealt =
|
||||||
metadata.assists = stats_obj
|
player_stats.total_damage_dealt_to_champions.unwrap_or(0);
|
||||||
.get("assists")
|
metadata.damage_taken = player_stats.total_damage_taken.unwrap_or(0);
|
||||||
.and_then(|a| a.as_u64())
|
metadata.vision_score = player_stats.vision_score.unwrap_or(0.0);
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
Ok(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,39 +549,25 @@ impl LqpClient {
|
|||||||
|
|
||||||
let mut metadata = super::PlayerGameMetadata::default();
|
let mut metadata = super::PlayerGameMetadata::default();
|
||||||
|
|
||||||
// Get summoner info
|
// Get summoner info (typed)
|
||||||
if let Ok(summoner) = self.get_summoner().await {
|
if let Ok(summoner) = self.get_summoner_typed().await {
|
||||||
metadata.puuid = summoner
|
metadata.puuid = summoner.puuid;
|
||||||
.get("puuid")
|
|
||||||
.and_then(|p| p.as_str())
|
|
||||||
.map(|s| s.to_string());
|
|
||||||
metadata.summoner_name = summoner
|
metadata.summoner_name = summoner
|
||||||
.get("displayName")
|
.display_name
|
||||||
.or_else(|| summoner.get("name"))
|
.or(summoner.name)
|
||||||
.or_else(|| summoner.get("internalName"))
|
.or(summoner.internal_name);
|
||||||
.and_then(|n| n.as_str())
|
|
||||||
.map(|s| s.to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get rune page
|
// Get rune page (typed)
|
||||||
if let Ok(rune_page) = self.get_rune_page().await {
|
if let Ok(rune_page) = self.get_rune_page_typed().await {
|
||||||
let primary_style_id = rune_page
|
let primary_style_id = rune_page.primary_style_id.unwrap_or(0) as u32;
|
||||||
.get("primaryStyleId")
|
let secondary_style_id = rune_page.sub_style_id.unwrap_or(0) as u32;
|
||||||
.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;
|
|
||||||
let selected_perks = rune_page
|
let selected_perks = rune_page
|
||||||
.get("selectedPerkIds")
|
.selected_perk_ids
|
||||||
.and_then(|ids| ids.as_array())
|
.unwrap_or_default()
|
||||||
.map(|arr| {
|
.iter()
|
||||||
arr.iter()
|
.map(|id| *id as u32)
|
||||||
.filter_map(|id| id.as_u64().map(|v| v as u32))
|
.collect();
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if primary_style_id > 0 {
|
if primary_style_id > 0 {
|
||||||
metadata.runes = Some(RunePage {
|
metadata.runes = Some(RunePage {
|
||||||
@@ -599,47 +575,30 @@ impl LqpClient {
|
|||||||
secondary_style_id,
|
secondary_style_id,
|
||||||
selected_perks,
|
selected_perks,
|
||||||
stat_modifiers: Vec::new(),
|
stat_modifiers: Vec::new(),
|
||||||
name: rune_page
|
name: rune_page.name,
|
||||||
.get("name")
|
current: rune_page.current.unwrap_or(true),
|
||||||
.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),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get summoner spells from live client data
|
// Get summoner spells from live client data (typed)
|
||||||
if let Ok(active_player) = self.get_live_client_active_player().await {
|
if let Ok(active_player) = self.get_live_client_active_player_typed().await {
|
||||||
debug!(
|
debug!("[METADATA] Live client active player data received");
|
||||||
"[METADATA] Live client active player data: {:?}",
|
|
||||||
active_player
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(summoner_spells) = active_player.get("summonerSpells") {
|
if let Some(ref spells) = active_player.summoner_spells {
|
||||||
let spell1_id = summoner_spells
|
let spell1_id = spells
|
||||||
.get("summonerSpellOne")
|
.summoner_spell_one
|
||||||
.and_then(|s| s.get("spellId"))
|
.as_ref()
|
||||||
.and_then(|id| id.as_u64())
|
.and_then(|s| s.spell_id)
|
||||||
.unwrap_or_else(|| {
|
.or(spells.spell1_id)
|
||||||
summoner_spells
|
.unwrap_or(0) as u32;
|
||||||
.get("spell1Id")
|
|
||||||
.and_then(|id| id.as_u64())
|
|
||||||
.unwrap_or(0)
|
|
||||||
}) as u32;
|
|
||||||
|
|
||||||
let spell2_id = summoner_spells
|
let spell2_id = spells
|
||||||
.get("summonerSpellTwo")
|
.summoner_spell_two
|
||||||
.and_then(|s| s.get("spellId"))
|
.as_ref()
|
||||||
.and_then(|id| id.as_u64())
|
.and_then(|s| s.spell_id)
|
||||||
.unwrap_or_else(|| {
|
.or(spells.spell2_id)
|
||||||
summoner_spells
|
.unwrap_or(0) as u32;
|
||||||
.get("spell2Id")
|
|
||||||
.and_then(|id| id.as_u64())
|
|
||||||
.unwrap_or(0)
|
|
||||||
}) as u32;
|
|
||||||
|
|
||||||
if spell1_id > 0 || spell2_id > 0 {
|
if spell1_id > 0 || spell2_id > 0 {
|
||||||
metadata.summoner_spells = Some(SummonerSpells {
|
metadata.summoner_spells = Some(SummonerSpells {
|
||||||
@@ -655,35 +614,23 @@ impl LqpClient {
|
|||||||
|| metadata.summoner_name.as_ref().is_none_or(|n| n.is_empty())
|
|| metadata.summoner_name.as_ref().is_none_or(|n| n.is_empty())
|
||||||
{
|
{
|
||||||
metadata.summoner_name = active_player
|
metadata.summoner_name = active_player
|
||||||
.get("summonerName")
|
.summoner_name
|
||||||
.or_else(|| active_player.get("displayName"))
|
.or(active_player.display_name)
|
||||||
.or_else(|| active_player.get("riotId"))
|
.or(active_player.riot_id);
|
||||||
.and_then(|n| n.as_str())
|
|
||||||
.map(|s| s.to_string());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Get summoner spells from session gameData
|
// Fallback: Get summoner spells from session gameData (typed)
|
||||||
if metadata.summoner_spells.is_none() {
|
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(local_puuid) = &metadata.puuid {
|
||||||
if let Some(game_data) = session.get("gameData") {
|
if let Some(ref game_data) = session.game_data {
|
||||||
for team_key in &["teamOne", "teamTwo"] {
|
// Check team one
|
||||||
if let Some(team) = game_data.get(team_key).and_then(|t| t.as_array()) {
|
if let Some(ref team) = game_data.team_one {
|
||||||
for player in team {
|
for player in team {
|
||||||
if player.get("puuid").and_then(|p| p.as_str())
|
if player.puuid.as_deref() == Some(local_puuid.as_str()) {
|
||||||
== 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;
|
||||||
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 spell1_id > 0 || spell2_id > 0 {
|
if spell1_id > 0 || spell2_id > 0 {
|
||||||
metadata.summoner_spells = Some(SummonerSpells {
|
metadata.summoner_spells = Some(SummonerSpells {
|
||||||
@@ -694,14 +641,32 @@ impl LqpClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata.champion_id = player
|
metadata.champion_id = player.champion_id.map(|id| id as u32);
|
||||||
.get("championId")
|
metadata.team = player.team_id.map(|id| id as u32);
|
||||||
.and_then(|id| id.as_u64())
|
break;
|
||||||
.map(|v| v as u32);
|
}
|
||||||
metadata.team = player
|
}
|
||||||
.get("teamId")
|
}
|
||||||
.and_then(|id| id.as_u64())
|
// Check team two if not found
|
||||||
.map(|v| v as u32);
|
if metadata.summoner_spells.is_none() {
|
||||||
|
if let Some(ref team) = game_data.team_two {
|
||||||
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -723,72 +688,66 @@ impl LqpClient {
|
|||||||
pub async fn fetch_all_players_identities(&self) -> Result<Vec<super::PlayerIdentity>> {
|
pub async fn fetch_all_players_identities(&self) -> Result<Vec<super::PlayerIdentity>> {
|
||||||
let mut players = Vec::new();
|
let mut players = Vec::new();
|
||||||
|
|
||||||
// Try live client data first
|
// Try live client data first (typed)
|
||||||
if let Ok(player_list) = self.get_live_client_player_list().await {
|
if let Ok(player_list) = self.get_live_client_player_list_typed().await {
|
||||||
if let Some(arr) = player_list.as_array() {
|
for player in &player_list.0 {
|
||||||
for player in arr {
|
|
||||||
let summoner_name = player
|
let summoner_name = player
|
||||||
.get("summonerName")
|
.summoner_name
|
||||||
.or_else(|| player.get("riotId"))
|
.as_deref()
|
||||||
.and_then(|n| n.as_str())
|
.or(player.riot_id.as_deref())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
|
|
||||||
let team = player
|
if let Some(ref puuid) = player.puuid {
|
||||||
.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 {
|
players.push(super::PlayerIdentity {
|
||||||
puuid: puuid.to_string(),
|
puuid: puuid.clone(),
|
||||||
summoner_name: summoner_name.to_string(),
|
summoner_name: summoner_name.to_string(),
|
||||||
summoner_id: player.get("summonerId").and_then(|id| id.as_u64()),
|
summoner_id: player.summoner_id,
|
||||||
champion_name,
|
champion_name: player.champion_name.clone(),
|
||||||
team,
|
team: player.team.map(|id| id as u32),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: try from gameflow session
|
// Fallback: try from gameflow session (typed)
|
||||||
if players.is_empty() {
|
if players.is_empty() {
|
||||||
if let Ok(session) = self.get_session().await {
|
if let Ok(session) = self.get_session_typed().await {
|
||||||
if let Some(game_data) = session.get("gameData") {
|
if let Some(ref game_data) = session.game_data {
|
||||||
for team_key in &["teamOne", "teamTwo"] {
|
// Team one (team ID 100)
|
||||||
let team_id = if *team_key == "teamOne" {
|
if let Some(ref team) = game_data.team_one {
|
||||||
100u32
|
|
||||||
} else {
|
|
||||||
200u32
|
|
||||||
};
|
|
||||||
if let Some(team) = game_data.get(team_key).and_then(|t| t.as_array()) {
|
|
||||||
for player in team {
|
for player in team {
|
||||||
if let (Some(puuid), Some(summoner_name)) = (
|
if let (Some(ref puuid), Some(ref summoner_name)) =
|
||||||
player.get("puuid").and_then(|p| p.as_str()),
|
(&player.puuid, &player.summoner_name)
|
||||||
player.get("summonerName").and_then(|n| n.as_str()),
|
{
|
||||||
) {
|
let champion_id = player.champion_id.map(|id| id as u32);
|
||||||
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);
|
let champion_name = champion_id.and_then(champion_id_to_name);
|
||||||
players.push(super::PlayerIdentity {
|
players.push(super::PlayerIdentity {
|
||||||
puuid: puuid.to_string(),
|
puuid: puuid.clone(),
|
||||||
summoner_name: summoner_name.to_string(),
|
summoner_name: summoner_name.clone(),
|
||||||
summoner_id: player
|
summoner_id: player.summoner_id,
|
||||||
.get("summonerId")
|
|
||||||
.and_then(|id| id.as_u64()),
|
|
||||||
champion_name,
|
champion_name,
|
||||||
team: Some(team_id),
|
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,54 +760,57 @@ impl LqpClient {
|
|||||||
pub async fn fetch_final_items(&self) -> Result<Option<ItemBuild>> {
|
pub async fn fetch_final_items(&self) -> Result<Option<ItemBuild>> {
|
||||||
info!("[ITEMS] Fetching final items...");
|
info!("[ITEMS] Fetching final items...");
|
||||||
|
|
||||||
// First try live client data
|
// First try live client data (typed)
|
||||||
match self.get_live_client_player_list().await {
|
match self.get_live_client_player_list_typed().await {
|
||||||
Ok(player_list) => {
|
Ok(player_list) => {
|
||||||
info!(
|
info!(
|
||||||
"[ITEMS] Live client player list response: {:?}",
|
"[ITEMS] Live client player list response received with {} players",
|
||||||
player_list
|
player_list.0.len()
|
||||||
);
|
);
|
||||||
if let Some(players) = player_list.as_array() {
|
for player in &player_list.0 {
|
||||||
info!(
|
if player.is_local_player == Some(true) {
|
||||||
"[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");
|
info!("[ITEMS] Found local player in live client data");
|
||||||
if let Some(items) = player.get("items").and_then(|i| i.as_array()) {
|
if let Some(ref items) = player.items {
|
||||||
info!("[ITEMS] Items array has {} items", items.len());
|
info!("[ITEMS] Items array has {} items", items.len());
|
||||||
let item_build = parse_items_from_live_client(items);
|
// 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() {
|
if item_build.is_some() {
|
||||||
info!(
|
info!("[ITEMS] Successfully parsed items from live client data");
|
||||||
"[ITEMS] Successfully parsed items from live client data"
|
|
||||||
);
|
|
||||||
return Ok(item_build);
|
return Ok(item_build);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
info!("[ITEMS] Failed to get live client player list: {:?}", e);
|
info!("[ITEMS] Failed to get live client player list: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: try end-of-game stats
|
// Fallback: try end-of-game stats (typed)
|
||||||
match self.get_game_stats().await {
|
match self.get_game_stats_typed().await {
|
||||||
Ok(stats) => {
|
Ok(stats) => {
|
||||||
info!("[ITEMS] Game stats response received");
|
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");
|
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());
|
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() {
|
if item_build.is_some() {
|
||||||
info!("[ITEMS] Successfully parsed items from localPlayer");
|
info!("[ITEMS] Successfully parsed items from localPlayer");
|
||||||
return Ok(item_build);
|
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());
|
info!("[ITEMS] Found {} teams in game stats", teams.len());
|
||||||
for team in teams {
|
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 {
|
for player in players {
|
||||||
let is_local = player
|
if player.is_local_player == Some(true) {
|
||||||
.get("isLocalPlayer")
|
|
||||||
.and_then(|l| l.as_bool())
|
|
||||||
.unwrap_or(false);
|
|
||||||
if is_local {
|
|
||||||
info!("[ITEMS] Found local player in teams[].players[]");
|
info!("[ITEMS] Found local player in teams[].players[]");
|
||||||
if let Some(items) =
|
if let Some(ref items) = player.items {
|
||||||
player.get("items").and_then(|i| i.as_array())
|
|
||||||
{
|
|
||||||
info!(
|
info!(
|
||||||
"[ITEMS] Player items array has {} items",
|
"[ITEMS] Player items array has {} items",
|
||||||
items.len()
|
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() {
|
if item_build.is_some() {
|
||||||
info!("[ITEMS] Successfully parsed items from teams[].players[]");
|
info!("[ITEMS] Successfully parsed items from teams[].players[]");
|
||||||
return Ok(item_build);
|
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!(
|
info!(
|
||||||
"[ITEMS] Found {} players in game stats (legacy)",
|
"[ITEMS] Found {} players in game stats (legacy)",
|
||||||
players.len()
|
players.len()
|
||||||
);
|
);
|
||||||
if let Some(player) = players.first() {
|
if let Some(player) = players.first() {
|
||||||
if let Some(items) = player.get("items").and_then(|i| i.as_array()) {
|
if let Some(ref items) = player.items {
|
||||||
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() {
|
if item_build.is_some() {
|
||||||
return Ok(item_build);
|
return Ok(item_build);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
//! This module handles communication with the League of Legends client
|
//! This module handles communication with the League of Legends client
|
||||||
//! via WebSocket and REST API for game event detection and capture.
|
//! via WebSocket and REST API for game event detection and capture.
|
||||||
|
|
||||||
|
mod api_types;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod client;
|
mod client;
|
||||||
mod endpoints;
|
mod endpoints;
|
||||||
@@ -14,6 +15,12 @@ mod state;
|
|||||||
mod tls;
|
mod tls;
|
||||||
mod websocket;
|
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 auth::{LockfileCredentials, LockfileWatcher};
|
||||||
pub use client::LqpClient;
|
pub use client::LqpClient;
|
||||||
pub use endpoints::{
|
pub use endpoints::{
|
||||||
|
|||||||
Reference in New Issue
Block a user