prelate_rs/types/
profile.rs

1// SPDX-License-Identifier: Apache-2.0 or MIT
2
3//! API response types for player and profile stats.
4
5pub use isocountry::CountryCode;
6use serde_json::Value;
7
8use std::{
9    collections::{BTreeMap, HashMap},
10    fmt::Display,
11    ops::Deref,
12};
13
14use serde::{Deserialize, Serialize};
15
16use crate::{
17    profile, profile_games,
18    query::{ProfileGamesQuery, ProfileQuery},
19    types::rank::League,
20};
21
22use super::civilization::Civilization;
23
24/// Player profile ID on aoe4world.
25#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone, Copy)]
26#[serde(rename_all = "snake_case")]
27#[cfg_attr(test, derive(arbitrary::Arbitrary))]
28#[cfg_attr(test, serde(deny_unknown_fields))]
29pub struct ProfileId(u64);
30
31impl Display for ProfileId {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        self.0.fmt(f)
34    }
35}
36
37impl AsRef<u64> for ProfileId {
38    fn as_ref(&self) -> &u64 {
39        &self.0
40    }
41}
42
43impl From<u64> for ProfileId {
44    fn from(value: u64) -> Self {
45        ProfileId(value)
46    }
47}
48
49impl From<ProfileId> for u64 {
50    fn from(value: ProfileId) -> Self {
51        value.0
52    }
53}
54
55impl From<&u64> for ProfileId {
56    fn from(value: &u64) -> Self {
57        ProfileId(*value)
58    }
59}
60
61impl From<&ProfileId> for u64 {
62    fn from(value: &ProfileId) -> Self {
63        value.0
64    }
65}
66
67impl ProfileId {
68    /// Returns a [`ProfileQuery`]. Used to get profile for a player.
69    pub fn profile(&self) -> ProfileQuery {
70        profile(self.0)
71    }
72
73    /// Constructs a query for the `/players/{profile_id}/games` endpoint for this [`ProfileId`].
74    pub fn games(&self) -> ProfileGamesQuery {
75        profile_games(self.0)
76    }
77}
78
79/// Player profile and statistics.
80#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
81#[serde(rename_all = "snake_case")]
82#[cfg_attr(test, derive(arbitrary::Arbitrary))]
83#[cfg_attr(test, serde(deny_unknown_fields))]
84pub struct Profile {
85    /// Name of the player.
86    pub name: String,
87    /// Profile ID of the player on aoe4world.
88    pub profile_id: ProfileId,
89    /// Steam ID of the player.
90    pub steam_id: Option<String>,
91    /// URL of the profile on aoe4world.
92    pub site_url: Option<String>,
93    /// Links to avatars used by the player.
94    pub avatars: Option<Avatars>,
95    /// Social information.
96    pub social: Option<Social>,
97    /// Country Code
98    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::option_country))]
99    pub country: Option<CountryCode>,
100    /// Statistics per game mode.
101    #[serde(alias = "leaderboards")]
102    pub modes: Option<GameModes>,
103    /// [`chrono::DateTime`] when last game was played.
104    pub last_game_at: Option<chrono::DateTime<chrono::Utc>>,
105}
106
107impl Deref for Profile {
108    type Target = ProfileId;
109
110    fn deref(&self) -> &Self::Target {
111        &self.profile_id
112    }
113}
114
115/// Links to avatars used by the player.
116#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
117#[serde(rename_all = "snake_case")]
118#[cfg_attr(test, derive(arbitrary::Arbitrary))]
119#[cfg_attr(test, serde(deny_unknown_fields))]
120pub struct Avatars {
121    /// Small size.
122    pub small: Option<String>,
123    /// Medium size.
124    pub medium: Option<String>,
125    /// Full size.
126    pub full: Option<String>,
127}
128
129/// Social information.
130#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
131#[serde(rename_all = "snake_case")]
132#[cfg_attr(test, derive(arbitrary::Arbitrary))]
133#[cfg_attr(test, serde(deny_unknown_fields))]
134pub struct Social {
135    /// URL to the player's Twitch.
136    pub twitch: Option<String>,
137    /// URL to the player's YouTube.
138    pub youtube: Option<String>,
139    /// URL to the player's Liquipedia page.
140    pub liquipedia: Option<String>,
141    /// URL to the player's Twitter.
142    pub twitter: Option<String>,
143    /// URL to the player's Reddit.
144    pub reddit: Option<String>,
145    /// URL to the player's Instagram.
146    pub instagram: Option<String>,
147}
148
149/// Statistics per game mode.
150#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
151#[serde(rename_all = "snake_case")]
152#[cfg_attr(test, derive(arbitrary::Arbitrary))]
153#[cfg_attr(test, serde(deny_unknown_fields))]
154pub struct GameModes {
155    /// Solo ranked stats. Rating is ranked points.
156    pub rm_solo: Option<GameModeStats>,
157    /// Team ranked stats. Rating is ranked points.
158    pub rm_team: Option<GameModeStats>,
159    /// Deprecated.
160    #[deprecated = "Use rm_solo instead."]
161    pub rm_1v1: Option<GameModeStats>,
162    /// 1v1 ranked stats. Rating is ELO.
163    pub rm_1v1_elo: Option<GameModeStats>,
164    /// 2v2 ranked stats. Rating is ELO.
165    #[serde(alias = "rm_2v2")]
166    pub rm_2v2_elo: Option<GameModeStats>,
167    /// 3v3 ranked stats. Rating is ELO.
168    #[serde(alias = "rm_3v3")]
169    pub rm_3v3_elo: Option<GameModeStats>,
170    /// 4v4 ranked stats. Rating is ELO.
171    #[serde(alias = "rm_4v4")]
172    pub rm_4v4_elo: Option<GameModeStats>,
173    /// 1v1 quick match stats. Rating is ELO.
174    pub qm_1v1: Option<GameModeStats>,
175    /// 2v2 quick match stats. Rating is ELO.
176    pub qm_2v2: Option<GameModeStats>,
177    /// 3v3 quick match stats. Rating is ELO.
178    pub qm_3v3: Option<GameModeStats>,
179    /// 4v4 quick match stats. Rating is ELO.
180    pub qm_4v4: Option<GameModeStats>,
181    /// 1v1 Empire Wars quick match stats. Rating is ELO.
182    pub qm_1v1_ew: Option<GameModeStats>,
183    /// 2v2 Empire Wars quick match stats. Rating is ELO.
184    pub qm_2v2_ew: Option<GameModeStats>,
185    /// 3v3 Empire Wars quick match stats. Rating is ELO.
186    pub qm_3v3_ew: Option<GameModeStats>,
187    /// 4v4 Empire Wars quick match stats. Rating is ELO.
188    pub qm_4v4_ew: Option<GameModeStats>,
189    /// Custom stats.
190    pub custom: Option<GameModeStats>,
191}
192
193/// Statistics for a game mode.
194#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
195#[serde(rename_all = "snake_case")]
196#[cfg_attr(test, derive(arbitrary::Arbitrary))]
197#[cfg_attr(test, serde(deny_unknown_fields))]
198pub struct GameModeStats {
199    // Deprecation notice served by the API trips up our deny_unknown_fields attr during tests.
200    #[cfg(test)]
201    _notice_: Option<String>,
202    /// Rating points or ELO.
203    pub rating: Option<i64>,
204    /// Max rating of all time.
205    pub max_rating: Option<i64>,
206    /// Max rating within the last 7 days.
207    pub max_rating_7d: Option<i64>,
208    /// Max rating within the last month.
209    pub max_rating_1m: Option<i64>,
210    /// Position on the leaderboard.
211    pub rank: Option<u32>,
212    /// How many games have been won or lost in a row.
213    pub streak: Option<i64>,
214    /// How many games have been played.
215    pub games_count: Option<u32>,
216    /// How many games have been won.
217    pub wins_count: Option<u32>,
218    /// How many games have been lost.
219    pub losses_count: Option<u32>,
220    /// How many games have been disputed.
221    pub disputes_count: Option<u32>,
222    /// How many games have been dropped.
223    pub drops_count: Option<u32>,
224    /// When the last game was played.
225    pub last_game_at: Option<chrono::DateTime<chrono::Utc>>,
226    /// Win rate as a percentage out of 100.
227    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
228    pub win_rate: Option<f64>,
229    /// The player's league and division.
230    pub rank_level: Option<League>,
231    /// The player's rating history. Maps Game ID to RatingHistoryEntry.
232    #[serde(default)]
233    pub rating_history: BTreeMap<String, RatingHistoryEntry>,
234    /// Stats per-civ.
235    #[serde(default)]
236    pub civilizations: Vec<CivStats>,
237    /// Which season the stats are from.
238    pub season: Option<u32>,
239    /// Previous season stats, if any. Note that this only exists in the context
240    /// of rm_solo and rm_team for the current season.
241    #[serde(default)]
242    pub previous_seasons: Vec<PreviousSeasonStats>,
243}
244
245/// Statistics for previous season.
246#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
247#[serde(rename_all = "snake_case")]
248#[cfg_attr(test, derive(arbitrary::Arbitrary))]
249#[cfg_attr(test, serde(deny_unknown_fields))]
250pub struct PreviousSeasonStats {
251    /// Rating points or ELO.
252    pub rating: Option<u32>,
253    /// Position on the leaderboard.
254    pub rank: Option<u32>,
255    /// How many games have been won or lost in a row.
256    pub streak: Option<i64>,
257    /// How many games have been played.
258    pub games_count: Option<u32>,
259    /// How many games have been won.
260    pub wins_count: Option<u32>,
261    /// How many games have been lost.
262    pub losses_count: Option<u32>,
263    /// How many games have been disputed.
264    pub disputes_count: Option<u32>,
265    /// How many games have been dropped.
266    pub drops_count: Option<u32>,
267    /// When the last game was played.
268    pub last_game_at: Option<chrono::DateTime<chrono::Utc>>,
269    /// Win rate as a percentage out of 100.
270    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
271    pub win_rate: Option<f64>,
272    /// The player's league and division.
273    pub rank_level: Option<League>,
274    /// Which season the stats are from.
275    pub season: Option<u32>,
276}
277
278/// An entry in the player's rating history.
279#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
280#[serde(rename_all = "snake_case")]
281#[cfg_attr(test, derive(arbitrary::Arbitrary))]
282#[cfg_attr(test, serde(deny_unknown_fields))]
283pub struct RatingHistoryEntry {
284    /// Rating points or ELO.
285    pub rating: Option<u32>,
286    /// How many games have been won or lost in a row.
287    pub streak: Option<i64>,
288    /// How many games have been played.
289    pub games_count: Option<u32>,
290    /// How many games have been won.
291    pub wins_count: Option<u32>,
292    /// How many games have been dropped.
293    pub drops_count: Option<u32>,
294    /// How many games have been disputed.
295    pub disputes_count: Option<u32>,
296    /// This field is populated the player has decayed between this match and the previous one. It contains the original rating after the decay but before the match was played.
297    pub orig_rating: Option<u32>,
298}
299
300/// Per-Civilization stats.
301#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
302#[serde(rename_all = "snake_case")]
303#[cfg_attr(test, derive(arbitrary::Arbitrary))]
304#[cfg_attr(test, serde(deny_unknown_fields))]
305pub struct CivStats {
306    /// The civilization.
307    pub civilization: Option<Civilization>,
308    /// Percentage of games won.
309    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
310    pub win_rate: Option<f64>,
311    /// Percentage of games where this civ was picked.
312    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
313    pub pick_rate: Option<f64>,
314    /// Number of games played with this civ.
315    pub games_count: Option<u32>,
316    /// Game length stats.
317    pub game_length: Option<CivGameLengthStats>,
318}
319
320/// Per-Civilization game length stats.
321#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
322#[serde(rename_all = "snake_case")]
323#[cfg_attr(test, derive(arbitrary::Arbitrary))]
324#[cfg_attr(test, serde(deny_unknown_fields))]
325pub struct CivGameLengthStats {
326    /// Average duration in seconds.
327    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
328    pub average: Option<f64>,
329    /// Median duration in seconds.
330    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
331    pub median: Option<f64>,
332    /// Average duration for wins in seconds.
333    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
334    pub wins_average: Option<f64>,
335    /// Median duration for wins in seconds.
336    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
337    pub wins_median: Option<f64>,
338    /// Average duration for losses in seconds.
339    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
340    pub losses_average: Option<f64>,
341    /// Median duration for losses in seconds.
342    #[cfg_attr(test, arbitrary(with = crate::testutils::arbitrary_with::clamped_option_f64(0.0, 100.0)))]
343    pub losses_median: Option<f64>,
344    // TODO: support this field properly
345    #[cfg_attr(test, arbitrary(value = Vec::default()))]
346    breakdown: Vec<HashMap<String, Value>>,
347}
348
349#[cfg(test)]
350mod tests {
351    use crate::testutils::{test_json, test_serde_roundtrip_prop};
352
353    use super::*;
354
355    test_serde_roundtrip_prop!(ProfileId);
356    test_serde_roundtrip_prop!(Profile);
357    test_serde_roundtrip_prop!(Avatars);
358    test_serde_roundtrip_prop!(Social);
359    test_serde_roundtrip_prop!(GameModes);
360    test_serde_roundtrip_prop!(GameModeStats);
361    test_serde_roundtrip_prop!(PreviousSeasonStats);
362    test_serde_roundtrip_prop!(RatingHistoryEntry);
363    test_serde_roundtrip_prop!(CivStats);
364    test_serde_roundtrip_prop!(CivGameLengthStats);
365
366    test_json!(
367        Profile,
368        "../../testdata/profile/neptune.json",
369        neptune_profile
370    );
371
372    test_json!(
373        Profile,
374        "../../testdata/profile/housedhorse.json",
375        housedhorse_profile
376    );
377
378    test_json!(Profile, "../../testdata/profile/jigly.json", jigly_profile);
379}