dota/components/
mod.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize, de, de::Error};
5use serde_json::{Value, map};
6
7pub mod abilities;
8pub mod buildings;
9pub mod heroes;
10pub mod items;
11pub mod players;
12pub mod team;
13pub mod wearables;
14
15use abilities::GameAbilities;
16use buildings::Buildings;
17use heroes::{GameHeroes, Hero};
18use items::{GameItems, Items};
19use players::{GamePlayers, PlayerID};
20use team::Team;
21use wearables::GameWearables;
22
23/// Represents Game State Integration authentication via an optional token
24#[derive(Serialize, Deserialize, Debug)]
25pub struct Auth {
26    token: Option<String>,
27}
28
29/// An enum of all possible GAMERULES states
30#[derive(Serialize, Deserialize, Debug)]
31#[serde(from = "String")]
32pub enum DotaGameRulesState {
33    Disconnected,
34    InProgress,
35    HeroSelection,
36    Starting,
37    Ending,
38    PostGame,
39    PreGame,
40    StrategyTime,
41    WaitingForMap,
42    WaitingForPlayers,
43    CustomGameSetup,
44    Undefined(String),
45}
46
47impl From<String> for DotaGameRulesState {
48    fn from(s: String) -> Self {
49        match s.as_str() {
50            "DOTA_GAMERULES_STATE_DISCONNECT" => DotaGameRulesState::Disconnected,
51            "DOTA_GAMERULES_STATE_GAME_IN_PROGRESS" => DotaGameRulesState::InProgress,
52            "DOTA_GAMERULES_STATE_HERO_SELECTION" => DotaGameRulesState::HeroSelection,
53            "DOTA_GAMERULES_STATE_INIT" => DotaGameRulesState::Starting,
54            "DOTA_GAMERULES_STATE_LAST" => DotaGameRulesState::Ending,
55            "DOTA_GAMERULES_STATE_POST_GAME" => DotaGameRulesState::PostGame,
56            "DOTA_GAMERULES_STATE_PRE_GAME" => DotaGameRulesState::PreGame,
57            "DOTA_GAMERULES_STATE_STRATEGY_TIME" => DotaGameRulesState::StrategyTime,
58            "DOTA_GAMERULES_STATE_WAIT_FOR_MAP_TO_LOAD" => DotaGameRulesState::WaitingForMap,
59            "DOTA_GAMERULES_STATE_WAIT_FOR_PLAYERS_TO_LOAD" => {
60                DotaGameRulesState::WaitingForPlayers
61            }
62            "DOTA_GAMERULES_STATE_CUSTOM_GAME_SETUP" => DotaGameRulesState::CustomGameSetup,
63            _ => DotaGameRulesState::Undefined(s),
64        }
65    }
66}
67
68impl fmt::Display for DotaGameRulesState {
69    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70        match self {
71            DotaGameRulesState::Disconnected => write!(f, "Disconnected"),
72            DotaGameRulesState::InProgress => write!(f, "In Progress"),
73            DotaGameRulesState::HeroSelection => write!(f, "Hero Selection"),
74            DotaGameRulesState::Starting => write!(f, "Starting"),
75            DotaGameRulesState::Ending => write!(f, "Ending"),
76            DotaGameRulesState::PostGame => write!(f, "Post Game"),
77            DotaGameRulesState::PreGame => write!(f, "Pre Game"),
78            DotaGameRulesState::StrategyTime => write!(f, "Strategy Time"),
79            DotaGameRulesState::WaitingForMap => write!(f, "Waiting For Map"),
80            DotaGameRulesState::WaitingForPlayers => write!(f, "Waiting For Players"),
81            DotaGameRulesState::CustomGameSetup => write!(f, "Custom Game Setup"),
82            DotaGameRulesState::Undefined(s) => write!(f, "Undefined: {}", s),
83        }
84    }
85}
86
87/// The Game State Integration provider, will be Dota
88#[derive(Serialize, Deserialize, Debug)]
89pub struct Provider {
90    name: String,
91    #[serde(alias = "appid")]
92    app_id: u32,
93    version: u32,
94    timestamp: u32,
95}
96
97impl fmt::Display for Provider {
98    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99        write!(f, "{} {}", self.name, self.version)
100    }
101}
102
103/// Represents a Dota Game State Integration map
104#[derive(Serialize, Deserialize, Debug)]
105pub struct Map {
106    name: String,
107    #[serde(alias = "matchid")]
108    match_id: String,
109    game_time: u32,
110    clock_time: i32,
111    daytime: bool,
112    nightstalker_night: bool,
113    game_state: DotaGameRulesState,
114    paused: bool,
115    win_team: Team,
116    #[serde(alias = "customgamename")]
117    custom_game_name: String,
118    ward_purchase_cooldown: Option<u16>,
119}
120
121impl fmt::Display for Map {
122    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
123        write!(
124            f,
125            "Match ID: {}\nState: {}\nMap: {}\nTime: {}\n",
126            self.match_id, self.game_state, self.name, self.game_time
127        )
128    }
129}
130
131fn empty_map_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
132where
133    D: de::Deserializer<'de>,
134    T: de::DeserializeOwned + std::fmt::Debug,
135{
136    let opt = Option::<map::Map<String, Value>>::deserialize(de)?;
137
138    match opt {
139        None => Ok(None),
140        Some(m) => {
141            if m.is_empty() {
142                Ok(None)
143            } else {
144                let res: T = serde_json::from_value(Value::Object(m)).map_err(D::Error::custom)?;
145                Ok(Some(res))
146            }
147        }
148    }
149}
150
151#[derive(Serialize, Deserialize, Debug)]
152pub struct GameState {
153    provider: Provider,
154    #[serde(default, deserialize_with = "empty_map_as_none")]
155    buildings: Option<HashMap<Team, Buildings>>,
156    map: Option<Map>,
157    #[serde(alias = "player", default, deserialize_with = "empty_map_as_none")]
158    players: Option<GamePlayers>,
159    #[serde(alias = "hero", default, deserialize_with = "empty_map_as_none")]
160    heroes: Option<GameHeroes>,
161    #[serde(default, deserialize_with = "empty_map_as_none")]
162    abilities: Option<GameAbilities>,
163    #[serde(default, deserialize_with = "empty_map_as_none")]
164    items: Option<GameItems>,
165    draft: Option<HashMap<Team, HashMap<PlayerID, Value>>>,
166    #[serde(default, deserialize_with = "empty_map_as_none")]
167    wearables: Option<GameWearables>,
168}
169
170impl GameState {
171    pub fn get_items(&self) -> Option<&Items> {
172        if let Some(items) = &self.items {
173            match items {
174                GameItems::Playing(i) => Some(i),
175                _ => None,
176            }
177        } else {
178            None
179        }
180    }
181
182    pub fn get_heroes(&self) -> Option<&GameHeroes> {
183        self.heroes.as_ref()
184    }
185
186    pub fn get_players(&self) -> Option<&GameHeroes> {
187        self.heroes.as_ref()
188    }
189
190    pub fn get_hero(&self) -> Option<&Hero> {
191        if let Some(heroes) = &self.heroes {
192            match heroes {
193                GameHeroes::Playing(h) => Some(h),
194                _ => None,
195            }
196        } else {
197            None
198        }
199    }
200
201    pub fn get_team_player_items(&self, team: &Team, id: &PlayerID) -> Option<&Items> {
202        if let Some(items) = &self.items {
203            match items {
204                GameItems::Spectating(m) => match m.get(team) {
205                    Some(t) => t.get(id),
206                    None => None,
207                },
208                _ => None,
209            }
210        } else {
211            None
212        }
213    }
214
215    pub fn get_team_player_hero(&self, team: &Team, id: &PlayerID) -> Option<&Hero> {
216        if let Some(heroes) = &self.heroes {
217            match heroes {
218                GameHeroes::Spectating(m) => match m.get(team) {
219                    Some(t) => t.get(id),
220                    None => None,
221                },
222                _ => None,
223            }
224        } else {
225            None
226        }
227    }
228}
229
230impl fmt::Display for GameState {
231    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
232        writeln!(f, "{}", self.provider)?;
233
234        if let Some(map) = &self.map {
235            writeln!(f, "{}", map)?;
236        }
237
238        if let Some(players) = &self.players {
239            match players {
240                GamePlayers::Playing(p) => {
241                    writeln!(f, "{}\n{}", p.team_name, p.name)?;
242
243                    if let Some(hero) = self.get_hero() {
244                        writeln!(f, "{}", hero)?;
245                    }
246
247                    if let Some(items) = self.get_items() {
248                        writeln!(f, "{}", items)?;
249                    }
250                }
251                GamePlayers::Spectating(i) => {
252                    for (team, players) in i.iter() {
253                        writeln!(f, "{}", team)?;
254                        for (id, player) in players.iter() {
255                            writeln!(f, "{}", player.name)?;
256
257                            if let Some(hero) = self.get_team_player_hero(team, id) {
258                                writeln!(f, "{}", hero)?;
259                            }
260
261                            if let Some(items) = self.get_team_player_items(team, id) {
262                                writeln!(f, "{}", items)?;
263                            }
264                        }
265                    }
266                }
267            }
268        }
269
270        Ok(())
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_idle_game_state_deserialize() {
280        let json_str = r#"{
281            "provider": {
282                "name": "Dota 2",
283                "appid": 570,
284                "version": 47,
285                "timestamp": 1658690112
286            },
287            "player": {},
288            "draft": {},
289            "auth": {
290                "token": "1234"
291            }
292        }"#;
293        let gs: GameState =
294            serde_json::from_str(json_str).expect("Failed to deserialize GameState");
295
296        assert!(gs.players.is_none());
297        assert!(gs.map.is_none());
298        assert!(gs.heroes.is_none());
299        assert_eq!(gs.provider.name, "Dota 2".to_owned());
300    }
301
302    #[test]
303    fn test_inititalizing_game_state_deserialize() {
304        let json_str = r#"{
305    "buildings": {
306        "radiant": {
307            "dota_goodguys_tower1_mid": {
308                "health": 1800,
309                "max_health": 1800
310            }
311        },
312        "dire": {
313            "dota_badguys_tower1_mid": {
314                "health": 1800,
315                "max_health": 1800
316            }
317        }
318    },
319    "provider": {
320        "name": "Dota 2",
321        "appid": 570,
322        "version": 47,
323        "timestamp": 1659017150
324    },
325    "map": {
326        "name": "hero_demo_main",
327        "matchid": "0",
328        "game_time": 1,
329        "clock_time": 1,
330        "daytime": true,
331        "nightstalker_night": false,
332        "game_state": "DOTA_GAMERULES_STATE_INIT",
333        "paused": false,
334        "win_team": "none",
335        "customgamename": "/.local/share/Steam/steamapps/common/dota 2 beta/game/dota_addons/hero_demo"
336    },
337    "player": {},
338    "hero": {},
339    "abilities": {},
340    "items": {},
341    "draft": {},
342    "wearables": {},
343    "auth": {
344        "token": "hello1234"
345    }
346}"#;
347        let gs: GameState =
348            serde_json::from_str(json_str).expect("Failed to deserialize GameState starting");
349        let buildings = gs.buildings.unwrap();
350
351        assert!(matches!(
352            gs.map.unwrap().game_state,
353            DotaGameRulesState::Starting
354        ));
355        assert_eq!(buildings.is_empty(), false);
356        assert_eq!(buildings.len(), 2);
357    }
358
359    #[test]
360    fn test_strategy_time_game_state_deserialize() {
361        let json_str = r#"{
362    "buildings": {
363        "radiant": {
364            "dota_goodguys_tower1_mid": {
365                "health": 1800,
366                "max_health": 1800
367            }
368        }
369    },
370    "provider": {
371        "name": "Dota 2",
372        "appid": 570,
373        "version": 47,
374        "timestamp": 1659033793
375    },
376    "map": {
377        "name": "hero_demo_main",
378        "matchid": "0",
379        "game_time": 1,
380        "clock_time": 0,
381        "daytime": true,
382        "nightstalker_night": false,
383        "game_state": "DOTA_GAMERULES_STATE_STRATEGY_TIME",
384        "paused": false,
385        "win_team": "none",
386        "customgamename": "/home/tomasfarias/.local/share/Steam/steamapps/common/dota 2 beta/game/dota_addons/hero_demo",
387        "ward_purchase_cooldown": 0
388    },
389    "player": {
390        "steamid": "76561197996881999",
391        "name": "farxc3xadas",
392        "activity": "playing",
393        "kills": 0,
394        "deaths": 0,
395        "assists": 0,
396        "last_hits": 0,
397        "denies": 0,
398        "kill_streak": 0,
399        "commands_issued": 0,
400        "kill_list": {},
401        "team_name": "radiant",
402        "gold": 600,
403        "gold_reliable": 0,
404        "gold_unreliable": 600,
405        "gold_from_hero_kills": 0,
406        "gold_from_creep_kills": 0,
407        "gold_from_income": 0,
408        "gold_from_shared": 0,
409        "gpm": 0,
410        "xpm": 0
411    },
412    "hero": {
413        "id": 90,
414        "name": "npc_dota_hero_keeper_of_the_light"
415    },
416    "abilities": {},
417    "items": {},
418    "draft": {},
419    "wearables": {
420        "wearable0": 13773,
421        "wearable1": 14451,
422        "wearable2": 14452,
423        "wearable3": 14450,
424        "wearable4": 12433,
425        "wearable5": 528
426    },
427    "auth": {"token": "hello1234"}
428}"#;
429        let gs: GameState =
430            serde_json::from_str(json_str).expect("Failed to deserialize GameState Strategy Time");
431
432        assert!(matches!(
433            gs.map.unwrap().game_state,
434            DotaGameRulesState::StrategyTime
435        ));
436    }
437
438    #[test]
439    fn test_in_progress_game_state_deserialize() {
440        let json_str = r#"{
441  "buildings": {
442    "radiant": {
443      "dota_goodguys_tower1_mid": {
444        "health": 1800,
445        "max_health": 1800
446      }
447    }
448  },
449  "provider": {
450    "name": "Dota 2",
451    "appid": 570,
452    "version": 47,
453    "timestamp": 1659035016
454  },
455  "map": {
456    "name": "hero_demo_main",
457    "matchid": "0",
458    "game_time": 1,
459    "clock_time": 0,
460    "daytime": true,
461    "nightstalker_night": false,
462    "game_state": "DOTA_GAMERULES_STATE_GAME_IN_PROGRESS",
463    "paused": false,
464    "win_team": "none",
465    "customgamename": "/home/tomasfarias/.local/share/Steam/steamapps/common/dota 2 beta/game/dota_addons/hero_demo",
466    "ward_purchase_cooldown": 0
467  },
468  "player": {
469    "steamid": "76561197996881999",
470    "name": "farxc3xadas",
471    "activity": "playing",
472    "kills": 0,
473    "deaths": 0,
474    "assists": 0,
475    "last_hits": 0,
476    "denies": 0,
477    "kill_streak": 0,
478    "commands_issued": 0,
479    "kill_list": {},
480    "team_name": "radiant",
481    "gold": 600,
482    "gold_reliable": 0,
483    "gold_unreliable": 600,
484    "gold_from_hero_kills": 0,
485    "gold_from_creep_kills": 0,
486    "gold_from_income": 0,
487    "gold_from_shared": 0,
488    "gpm": 0,
489    "xpm": 0
490  },
491  "hero": {
492    "xpos": -1664,
493    "ypos": -1216,
494    "id": 42,
495    "name": "npc_dota_hero_skeleton_king",
496    "level": 0,
497    "xp": 0,
498    "alive": false,
499    "respawn_seconds": 0,
500    "buyback_cost": 200,
501    "buyback_cooldown": 0,
502    "health": 640,
503    "max_health": 640,
504    "health_percent": 100,
505    "mana": 291,
506    "max_mana": 291,
507    "mana_percent": 100,
508    "silenced": false,
509    "stunned": false,
510    "disarmed": false,
511    "magicimmune": false,
512    "hexed": false,
513    "muted": false,
514    "break": false,
515    "aghanims_scepter": false,
516    "aghanims_shard": false,
517    "smoked": false,
518    "has_debuff": false,
519    "talent_1": false,
520    "talent_2": false,
521    "talent_3": false,
522    "talent_4": false,
523    "talent_5": false,
524    "talent_6": false,
525    "talent_7": false,
526    "talent_8": false
527  },
528  "abilities": {
529    "ability0": {
530      "name": "skeleton_king_hellfire_blast",
531      "level": 0,
532      "can_cast": false,
533      "passive": false,
534      "ability_active": true,
535      "cooldown": 0,
536      "ultimate": false
537    },
538    "ability1": {
539      "name": "skeleton_king_vampiric_aura",
540      "level": 0,
541      "can_cast": false,
542      "passive": false,
543      "ability_active": true,
544      "cooldown": 0,
545      "ultimate": false
546    },
547    "ability2": {
548      "name": "skeleton_king_mortal_strike",
549      "level": 0,
550      "can_cast": false,
551      "passive": true,
552      "ability_active": true,
553      "cooldown": 0,
554      "ultimate": false
555    },
556    "ability3": {
557      "name": "skeleton_king_reincarnation",
558      "level": 0,
559      "can_cast": false,
560      "passive": true,
561      "ability_active": true,
562      "cooldown": 0,
563      "ultimate": true
564    },
565    "ability4": {
566      "name": "plus_high_five",
567      "level": 1,
568      "can_cast": true,
569      "passive": false,
570      "ability_active": true,
571      "cooldown": 0,
572      "ultimate": false
573    },
574    "ability5": {
575      "name": "plus_guild_banner",
576      "level": 1,
577      "can_cast": true,
578      "passive": false,
579      "ability_active": true,
580      "cooldown": 0,
581      "ultimate": false
582    }
583  },
584  "items": {
585    "slot0": {
586      "name": "empty"
587    },
588    "slot1": {
589        "name": "item_manta",
590        "purchaser": 0,
591        "can_cast": true,
592        "cooldown": 0,
593        "passive": false
594    },
595    "slot2": {
596      "name": "item_ultimate_orb",
597      "purchaser": 0,
598      "passive": true
599    },
600    "slot3": {
601      "name": "empty"
602    },
603    "slot4": {
604      "name": "empty"
605    },
606    "slot5": {
607      "name": "empty"
608    },
609    "slot6": {
610      "name": "empty"
611    },
612    "slot7": {
613      "name": "empty"
614    },
615    "slot8": {
616      "name": "empty"
617    },
618    "stash0": {
619      "name": "empty"
620    },
621    "stash1": {
622      "name": "empty"
623    },
624    "stash2": {
625      "name": "empty"
626    },
627    "stash3": {
628      "name": "empty"
629    },
630    "stash4": {
631      "name": "empty"
632    },
633    "stash5": {
634      "name": "empty"
635    },
636    "teleport0": {
637      "name": "item_tpscroll",
638      "purchaser": 0,
639      "can_cast": false,
640      "cooldown": 100,
641      "passive": false,
642      "charges": 1
643    },
644    "neutral0": {
645      "name": "empty"
646    }
647  },
648  "draft": {},
649  "wearables": {
650    "wearable0": 9747,
651    "wearable1": 8780,
652    "wearable2": 8623,
653    "wearable3": 8622,
654    "wearable4": 8624,
655    "wearable5": 14942,
656    "wearable6": 483,
657    "wearable7": 8621,
658    "wearable8": 790,
659    "wearable9": 792,
660    "wearable10": 791,
661    "wearable11": 14912
662  },
663  "auth": {
664    "token": "hello1234"
665  }
666}"#;
667
668        let gs: GameState =
669            serde_json::from_str(json_str).expect("Failed to deserialize GameState In Progress");
670        let heroes = gs.heroes.as_ref().unwrap();
671        let wearables = gs.wearables.as_ref().unwrap();
672        let players = gs.players.as_ref().unwrap();
673
674        assert!(matches!(
675            gs.map.as_ref().unwrap().game_state,
676            DotaGameRulesState::InProgress,
677        ));
678
679        assert!(matches!(heroes, GameHeroes::Playing(_)));
680        if let GameHeroes::Playing(hero) = heroes {
681            assert_eq!(hero.id, 42);
682        } else {
683            panic!("Failed to deserialize single hero");
684        }
685
686        assert!(matches!(wearables, GameWearables::Playing(_)));
687        if let GameWearables::Playing(wearables_map) = wearables {
688            assert_eq!(wearables_map.len(), 12);
689        } else {
690            panic!("Failed to deserialize wearables");
691        }
692
693        assert!(matches!(players, GamePlayers::Playing(_)));
694        assert!(gs.get_items().is_some());
695    }
696
697    #[test]
698    fn test_map_deserialize() {
699        let json_str = r#"{
700            "name": "hero_demo_main",
701            "matchid": "0",
702            "game_time": 5,
703            "clock_time": 4,
704            "daytime": true,
705            "nightstalker_night": false,
706            "game_state": "DOTA_GAMERULES_STATE_GAME_IN_PROGRESS",
707            "paused": false,
708            "win_team": "none",
709            "customgamename": "common/dota 2 beta/game/dota_addons/hero_demo",
710            "ward_purchase_cooldown": 0
711        }"#;
712
713        let map: Map = serde_json::from_str(json_str).expect("Failed to deserialize Map");
714
715        assert_eq!(map.name, "hero_demo_main");
716        assert_eq!(map.match_id, "0");
717        assert_eq!(map.game_time, 5);
718        assert_eq!(map.clock_time, 4);
719        assert_eq!(map.daytime, true);
720        assert_eq!(map.nightstalker_night, false);
721        assert!(matches!(map.game_state, DotaGameRulesState::InProgress));
722        assert_eq!(map.paused, false);
723    }
724}