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#[derive(Serialize, Deserialize, Debug)]
25pub struct Auth {
26 token: Option<String>,
27}
28
29#[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#[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#[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}