r6tracker/types/
overwolf.rs

1/// Overwolf Lifetime Stats
2pub mod lifetime_stats;
3
4pub use self::lifetime_stats::LifetimeStats;
5use std::collections::HashMap;
6use url::Url;
7
8/// The error that occurs when an OverwolfResponse is in an error state and the data field is accessed.
9/// It optionally contains the reason of failure.
10#[derive(Debug)]
11pub struct InvalidOverwolfResponseError(pub String);
12
13impl std::error::Error for InvalidOverwolfResponseError {}
14
15impl std::fmt::Display for InvalidOverwolfResponseError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        write!(f, "the overwolf response was invalid ({})", self.0)
18    }
19}
20
21/// A Json Overwolf Response
22#[derive(Debug)]
23pub enum OverwolfResponse<T> {
24    /// A Valid response
25    Valid(T),
26
27    /// An Invalid Response
28    Invalid(String),
29}
30
31impl<'de, T> serde::Deserialize<'de> for OverwolfResponse<T>
32where
33    T: serde::Deserialize<'de>,
34{
35    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
36    where
37        D: serde::Deserializer<'de>,
38    {
39        let mut map = serde_json::Map::deserialize(deserializer)?;
40
41        let success = map
42            .remove("success")
43            .ok_or_else(|| serde::de::Error::missing_field("success"))
44            .map(serde::Deserialize::deserialize)?
45            .map_err(serde::de::Error::custom)?;
46        let rest = serde_json::Value::Object(map);
47
48        if success {
49            T::deserialize(rest)
50                .map(OverwolfResponse::Valid)
51                .map_err(serde::de::Error::custom)
52        } else {
53            #[derive(serde::Deserialize)]
54            struct ErrorReason {
55                reason: String,
56            }
57
58            ErrorReason::deserialize(rest)
59                .map(|e| OverwolfResponse::Invalid(e.reason))
60                .map_err(serde::de::Error::custom)
61        }
62    }
63}
64
65impl<T> OverwolfResponse<T> {
66    /// Turn this into a Result.
67    pub fn into_result(self) -> Result<T, InvalidOverwolfResponseError> {
68        match self {
69            Self::Valid(data) => Ok(data),
70            Self::Invalid(reason) => Err(InvalidOverwolfResponseError(reason)),
71        }
72    }
73
74    /// Get the valid member as an Option.
75    pub fn get_valid(&self) -> Option<&T> {
76        match &self {
77            Self::Valid(data) => Some(data),
78            Self::Invalid(_) => None,
79        }
80    }
81
82    /// Take the valid member, consuming this struct. Same function as `get_valid` except it is consuming.
83    pub fn take_valid(self) -> Option<T> {
84        match self {
85            Self::Valid(data) => Some(data),
86            Self::Invalid(_) => None,
87        }
88    }
89}
90
91/// An Overwolf Player
92#[derive(Debug, serde::Deserialize, serde::Serialize)]
93pub struct OverwolfPlayer {
94    /// Player ID
95    #[serde(rename = "playerId")]
96    pub player_id: String,
97
98    /// Player name
99    pub name: String,
100
101    /// Avatar URL
102    pub avatar: Url,
103
104    /// Player Level
105    pub level: u64,
106
107    /// Probably r6tracker premium
108    #[serde(rename = "isPremium")]
109    pub is_premium: bool,
110
111    /// Whether this person is a suspected cheater
112    #[serde(rename = "suspectedCheater")]
113    pub suspected_cheater: bool,
114
115    /// The current season
116    #[serde(rename = "currentSeason")]
117    pub current_season: u64,
118
119    /// Current season best region stats
120    #[serde(rename = "currentSeasonBestRegion")]
121    pub current_season_best_region: Option<OverwolfSeason>,
122
123    /// Lifetime Stats
124    #[serde(rename = "lifetimeStats")]
125    pub lifetime_stats: LifetimeStats,
126
127    /// All seasonal stats
128    pub seasons: Vec<OverwolfSeason>,
129
130    /// Operator Stats
131    pub operators: Vec<OverwolfOperator>,
132
133    /// Seasonal Operator Stats
134    #[serde(rename = "seasonalOperators")]
135    pub seasonal_operators: Option<SeasonalOperators>,
136
137    /// Unknown keys
138    #[serde(flatten)]
139    pub unknown: HashMap<String, serde_json::Value>,
140}
141
142impl OverwolfPlayer {
143    /// Get the current casual season stats for this user
144    pub fn get_current_casual_season(&self) -> Option<&OverwolfSeason> {
145        self.seasons
146            .iter()
147            .find(|season| !season.is_ranked && season.season == self.current_season)
148    }
149
150    /// Iterate over ranked seasons
151    pub fn iter_ranked_seasons(&self) -> impl Iterator<Item = &OverwolfSeason> {
152        self.seasons.iter().filter(|season| season.is_ranked)
153    }
154
155    /// Iterate over ranked seasons where the user placed
156    pub fn iter_placed_ranked_seasons(&self) -> impl Iterator<Item = &OverwolfSeason> {
157        self.iter_ranked_seasons()
158            .filter(|season| season.current_rank.rank_tier != 0)
159    }
160
161    /// Get the season with the max mmr
162    pub fn get_max_season(&self) -> Option<&OverwolfSeason> {
163        self.iter_placed_ranked_seasons()
164            .max_by_key(|season| season.max_mmr)
165    }
166
167    /// Get the lifetime K/D for ranked
168    pub fn get_lifetime_ranked_kd(&self) -> Option<f64> {
169        let mut length = 0;
170        let mut sum = 0.0;
171        for season in self.iter_placed_ranked_seasons() {
172            sum += season.kd;
173            length += 1;
174        }
175
176        if length == 0 {
177            None
178        } else {
179            Some(sum / (length as f64))
180        }
181    }
182
183    /// Get lifetime ranked win %
184    pub fn get_lifetime_ranked_win_pct(&self) -> Option<f64> {
185        let mut length = 0;
186        let mut sum = 0.0f64;
187        for season in self.iter_placed_ranked_seasons() {
188            sum += season.win_pct;
189            length += 1;
190        }
191
192        if length == 0 {
193            None
194        } else {
195            Some(sum / (length as f64))
196        }
197    }
198}
199
200/// Seasonal stats. This may also represent hidden casual rankings.
201#[derive(Debug, serde::Deserialize, serde::Serialize)]
202pub struct OverwolfSeason {
203    /// The rank name
204    #[serde(rename = "rankName")]
205    pub rank_name: String,
206
207    /// Season image URL
208    pub img: Url,
209
210    /// Season #
211    pub season: u64,
212
213    /// Season Region
214    pub region: String,
215
216    /// The label of the region
217    #[serde(rename = "regionLabel")]
218    pub region_label: String,
219
220    /// MMR
221    pub mmr: u64,
222
223    /// Win Percent
224    #[serde(rename = "winPct")]
225    pub win_pct: f64,
226
227    /// The # of wins this season
228    pub wins: u64,
229
230    /// The K/D this season
231    pub kd: f64,
232
233    /// The # of kills this season
234    pub kills: u64,
235
236    /// The # of matches this season
237    pub matches: u64,
238
239    /// Maybe the change in mmr this season?
240    #[serde(rename = "mmrChange")]
241    pub mmr_change: Option<i64>,
242
243    /// Whether this season represents a ranked season
244    #[serde(rename = "isRanked")]
245    pub is_ranked: bool,
246
247    /// The max mmr for this season
248    #[serde(rename = "maxMmr")]
249    pub max_mmr: u64,
250
251    /// Current Rank Info
252    #[serde(rename = "currentRank")]
253    pub current_rank: OverwolfRank,
254
255    /// Max Rank Info
256    #[serde(rename = "maxRank")]
257    pub max_rank: OverwolfRank,
258
259    /// Previous Rank Info
260    #[serde(rename = "prevRank")]
261    pub prev_rank: Option<OverwolfRank>,
262
263    /// Next Rank Info
264    #[serde(rename = "nextRank")]
265    pub next_rank: Option<OverwolfRank>,
266
267    /// Unknown keys
268    #[serde(flatten)]
269    pub unknown: HashMap<String, serde_json::Value>,
270}
271
272/// Overwolf Rank Info
273#[derive(Debug, serde::Deserialize, serde::Serialize)]
274pub struct OverwolfRank {
275    /// Unknown
276    #[serde(rename = "rankTier")]
277    pub rank_tier: u64,
278
279    /// MMR
280    pub mmr: u64,
281
282    /// The icon url for this rank
283    #[serde(rename = "rankIcon")]
284    pub rank_icon: Url,
285
286    /// The name of this rank
287    #[serde(rename = "rankName")]
288    pub rank_name: String,
289
290    /// Unknown keys
291    #[serde(flatten)]
292    pub unknown: HashMap<String, serde_json::Value>,
293}
294
295/// An Overwolf Operator
296#[derive(Debug, serde::Deserialize, serde::Serialize)]
297pub struct OverwolfOperator {
298    /// Operator name
299    pub name: String,
300
301    /// Operator Image
302    pub img: Url,
303
304    /// Whether this operator is attack
305    #[serde(rename = "isAttack")]
306    pub is_attack: bool,
307
308    /// Whether this operator is this user's top operator
309    #[serde(rename = "isTopOperator")]
310    pub is_top_operator: bool,
311
312    /// Win %
313    #[serde(rename = "winpct")]
314    pub win_pct: f64,
315
316    /// The total # of wins with this op
317    pub wins: u64,
318
319    /// The K/D with this op
320    pub kd: f64,
321
322    /// The total # of kills with this op
323    pub kills: u64,
324
325    /// The time played as a user-displayable string
326    #[serde(rename = "timePlayedDisplay")]
327    pub time_played_display: String,
328
329    /// The time played (in seconds?)
330    #[serde(rename = "timePlayed")]
331    pub time_played: u64,
332
333    /// Unknown keys
334    #[serde(flatten)]
335    pub unknown: HashMap<String, serde_json::Value>,
336}
337
338/// Seasonal Operator data
339#[derive(Debug, serde::Deserialize, serde::Serialize)]
340pub struct SeasonalOperators {
341    /// Operator Stats
342    pub operators: Vec<OverwolfOperator>,
343
344    /// Started tracking datetimestamp
345    #[serde(rename = "startedTracking")]
346    pub started_tracking: String,
347
348    /// Unknown keys
349    #[serde(flatten)]
350    pub unknown: HashMap<String, serde_json::Value>,
351}
352
353#[cfg(test)]
354mod test {
355    use super::*;
356
357    const OVERWOLF_PLAYER_1: &str = include_str!("../../test_data/overwolf_player_1.json");
358    const OVERWOLF_PLAYER_2: &str = include_str!("../../test_data/overwolf_player_2.json");
359    const INVALID_OVERWOLF_RESPONSE: &str =
360        include_str!("../../test_data/invalid_overwolf_response.json");
361    const SMACK_ASH_OVERWOLF: &str = include_str!("../../test_data/smack_ash_overwolf.json");
362
363    #[test]
364    fn parse_overwolf_player_1() {
365        let res: OverwolfResponse<OverwolfPlayer> =
366            serde_json::from_str(OVERWOLF_PLAYER_1).unwrap();
367        dbg!(res.take_valid().unwrap());
368    }
369
370    #[test]
371    fn parse_overwolf_player_2() {
372        let res: OverwolfResponse<OverwolfPlayer> =
373            serde_json::from_str(OVERWOLF_PLAYER_2).unwrap();
374        let valid = res.take_valid().unwrap();
375        dbg!(&valid);
376        dbg!(&valid.get_max_season());
377    }
378
379    #[test]
380    fn parse_smack_ash_overwolf() {
381        let res: OverwolfResponse<OverwolfPlayer> =
382            serde_json::from_str(SMACK_ASH_OVERWOLF).expect("failed to parse");
383        let valid = res.take_valid().expect("data is invalid");
384        dbg!(&valid);
385        dbg!(&valid.get_max_season());
386    }
387
388    #[test]
389    fn parse_invalid_overwolf() {
390        let res: OverwolfResponse<serde_json::Value> =
391            serde_json::from_str(INVALID_OVERWOLF_RESPONSE).unwrap();
392        dbg!(res);
393    }
394}