dota/components/
items.rs

1use std::collections::HashMap;
2use std::convert::TryFrom;
3use std::fmt;
4use std::num::ParseIntError;
5
6use serde::{Deserialize, Serialize, de};
7use thiserror::Error;
8
9use super::{PlayerID, Team};
10
11#[derive(Error, Debug)]
12pub enum ItemsError {
13    #[error("the container `{0}` has no slot")]
14    MissingSlotInContainer(String),
15    #[error("failed to parse slot number")]
16    ParseSlotError(#[from] ParseIntError),
17    #[error("the filed `{0}` is missing in `{1}`")]
18    MissingRequiredField(String, ItemContainer),
19    #[error("an unknown item container was found: `{0}`")]
20    UnknownItemContainer(String),
21}
22
23#[derive(Serialize, Deserialize, Debug)]
24#[serde(from = "String")]
25pub enum Rune {
26    Arcane,
27    Bounty,
28    DoubleDamage,
29    Empty,
30    Haste,
31    Illusion,
32    Invisibility,
33    Regeneration,
34    Shield,
35    Undefined(String),
36}
37
38impl From<String> for Rune {
39    fn from(s: String) -> Self {
40        match s.as_str() {
41            "arcane" => Rune::Arcane,
42            "bounty" => Rune::Bounty,
43            "double_damage" => Rune::DoubleDamage,
44            "empty" => Rune::Empty,
45            "haste" => Rune::Haste,
46            "illusion" => Rune::Illusion,
47            "invisibility" => Rune::Invisibility,
48            "regen" => Rune::Regeneration,
49            "shield" => Rune::Shield,
50            _ => Rune::Undefined(s),
51        }
52    }
53}
54
55impl fmt::Display for Rune {
56    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57        match self {
58            Rune::Arcane => write!(f, "Arcane"),
59            Rune::Bounty => write!(f, "Bounty"),
60            Rune::DoubleDamage => write!(f, "Double damage"),
61            Rune::Empty => write!(f, "Empty"),
62            Rune::Haste => write!(f, "Haste"),
63            Rune::Illusion => write!(f, "Illusion"),
64            Rune::Invisibility => write!(f, "Invisibility"),
65            Rune::Regeneration => write!(f, "Regeneration"),
66            Rune::Shield => write!(f, "Shield"),
67            Rune::Undefined(s) => write!(f, "Rune {}", s),
68        }
69    }
70}
71
72#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
73#[serde(try_from = "String")]
74pub enum ItemContainer {
75    Inventory(u8),
76    Stash(u8),
77    Teleport,
78    Neutral,
79    PreservedNeutral,
80}
81
82impl ItemContainer {
83    fn index(&self) -> u8 {
84        match self {
85            ItemContainer::Inventory(n) | ItemContainer::Stash(n) => *n,
86            ItemContainer::Teleport | ItemContainer::Neutral | ItemContainer::PreservedNeutral => 0,
87        }
88    }
89}
90
91impl fmt::Display for ItemContainer {
92    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93        match self {
94            ItemContainer::Inventory(n) => write!(f, "Inventory: {}", n),
95            ItemContainer::Stash(n) => write!(f, "Stash: {}", n),
96            ItemContainer::Teleport => write!(f, "Teleport"),
97            ItemContainer::Neutral => write!(f, "Neutral"),
98            ItemContainer::PreservedNeutral => write!(f, "Preserved neutral"),
99        }
100    }
101}
102
103impl TryFrom<String> for ItemContainer {
104    type Error = ItemsError;
105
106    fn try_from(s: String) -> Result<Self, Self::Error> {
107        let index = match find_first_numeric(&s) {
108            Some(i) => i,
109            None => {
110                return Err(ItemsError::MissingSlotInContainer(s.to_string()));
111            }
112        };
113
114        let (container, slot) = s.split_at(index);
115        let numeric_slot = slot.parse::<u8>()?;
116
117        match container {
118            "slot" => Ok(ItemContainer::Inventory(numeric_slot)),
119            "stash" => Ok(ItemContainer::Stash(numeric_slot)),
120            "teleport" => Ok(ItemContainer::Teleport),
121            "neutral" => Ok(ItemContainer::Neutral),
122            "preserved_neutral" => Ok(ItemContainer::PreservedNeutral),
123            s => Err(ItemsError::UnknownItemContainer(s.to_owned())),
124        }
125    }
126}
127
128fn find_first_numeric(s: &str) -> Option<usize> {
129    for (i, c) in s.chars().enumerate() {
130        if c.is_numeric() {
131            return Some(i);
132        }
133    }
134
135    None
136}
137
138#[derive(Serialize, Deserialize, Debug)]
139pub struct Item {
140    name: String,
141    purchaser: i16,
142    item_level: Option<u16>,
143    contains_rune: Option<Rune>,
144    can_cast: Option<bool>,
145    cooldown: Option<u16>,
146    passive: bool,
147    charges: Option<u16>,
148    item_charges: Option<u16>,
149}
150
151#[derive(Deserialize, Serialize, Debug)]
152pub enum ItemSlot {
153    Empty { index: u8 },
154    Full { index: u8, item: Item },
155}
156
157impl fmt::Display for ItemSlot {
158    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
159        match self {
160            ItemSlot::Full { index, item } => write!(f, "Slot {}: {}", index, item.name),
161            ItemSlot::Empty { index } => write!(f, "Slot {}: Empty", index),
162        }
163    }
164}
165
166#[derive(Deserialize, Debug, Serialize)]
167#[serde(untagged)]
168pub enum GameItems {
169    Playing(Items),
170    Spectating(HashMap<Team, HashMap<PlayerID, Items>>),
171}
172
173#[derive(Serialize, Debug)]
174pub struct Items {
175    inventory: Vec<ItemSlot>,
176    stash: Vec<ItemSlot>,
177    teleport: ItemSlot,
178    neutrals: Vec<ItemSlot>,
179    preserved_neutrals: Vec<ItemSlot>,
180}
181
182impl Items {
183    pub fn is_inventory_empty(&self) -> bool {
184        self.inventory.iter().all(|item| match item {
185            ItemSlot::Empty { index: _ } => true,
186            ItemSlot::Full { index: _, item: _ } => false,
187        })
188    }
189
190    pub fn is_stash_empty(&self) -> bool {
191        self.stash.iter().all(|item| match item {
192            ItemSlot::Empty { index: _ } => true,
193            ItemSlot::Full { index: _, item: _ } => false,
194        })
195    }
196
197    pub fn is_teleport_empty(&self) -> bool {
198        match self.teleport {
199            ItemSlot::Empty { index: _ } => true,
200            ItemSlot::Full { index: _, item: _ } => false,
201        }
202    }
203
204    pub fn is_neutrals_empty(&self) -> bool {
205        self.neutrals.iter().all(|item| match item {
206            ItemSlot::Empty { index: _ } => true,
207            ItemSlot::Full { index: _, item: _ } => false,
208        })
209    }
210
211    pub fn is_preserved_neutrals_empty(&self) -> bool {
212        self.preserved_neutrals.iter().all(|item| match item {
213            ItemSlot::Empty { index: _ } => true,
214            ItemSlot::Full { index: _, item: _ } => false,
215        })
216    }
217}
218
219impl fmt::Display for Items {
220    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
221        write!(f, "Inventory: ")?;
222
223        if self.is_inventory_empty() {
224            writeln!(f, "Empty")?;
225        } else {
226            for (index, slot) in self.inventory.iter().enumerate() {
227                writeln!(f, "{{ {} }}", slot)?;
228
229                if (index + 1) != self.inventory.len() {
230                    write!(f, "{:11}", "")?;
231                }
232            }
233        };
234
235        write!(f, "Stash: ")?;
236
237        if self.is_stash_empty() {
238            writeln!(f, "Empty")?;
239        } else {
240            for (index, slot) in self.stash.iter().enumerate() {
241                writeln!(f, "{{ {} }}", slot)?;
242
243                if (index + 1) != self.stash.len() {
244                    write!(f, "{:7}", "")?;
245                }
246            }
247        };
248
249        if self.is_teleport_empty() {
250            writeln!(f, "Teleport: Empty")?;
251        } else {
252            writeln!(f, "Teleport: {}", self.teleport)?;
253        }
254
255        if self.is_neutrals_empty() {
256            writeln!(f, "Neutral: Empty")?;
257        } else {
258            for (index, slot) in self.neutrals.iter().enumerate() {
259                writeln!(f, "{{ {} }}", slot)?;
260
261                if (index + 1) != self.inventory.len() {
262                    write!(f, "{:11}", "")?;
263                }
264            }
265        }
266
267        if self.is_preserved_neutrals_empty() {
268            writeln!(f, "Preserved neutral: Empty")?;
269        } else {
270            for (index, slot) in self.preserved_neutrals.iter().enumerate() {
271                writeln!(f, "{{ {} }}", slot)?;
272
273                if (index + 1) != self.preserved_neutrals.len() {
274                    write!(f, "{:11}", "")?;
275                }
276            }
277        }
278
279        Ok(())
280    }
281}
282
283impl<'de> Deserialize<'de> for Items {
284    /// Deserialize Items by flattening JSON of ItemContainers.
285    /// Items can be contained in Inventory, Stash, Teleport slot, or Neutral slot.
286    fn deserialize<D>(deserializer: D) -> Result<Items, D::Error>
287    where
288        D: de::Deserializer<'de>,
289    {
290        #[derive(Deserialize)]
291        struct Helper {
292            #[serde(flatten)]
293            items: HashMap<String, NestedItem>,
294        }
295
296        #[derive(Deserialize)]
297        struct NestedItem {
298            name: String,
299            purchaser: Option<i16>,
300            item_level: Option<u16>,
301            contains_rune: Option<Rune>,
302            can_cast: Option<bool>,
303            cooldown: Option<u16>,
304            passive: Option<bool>,
305            item_charges: Option<u16>,
306            charges: Option<u16>,
307        }
308
309        let helper = Helper::deserialize(deserializer)?;
310        let mut inventory: Vec<ItemSlot> = Vec::new();
311        let mut stash: Vec<ItemSlot> = Vec::new();
312        let mut teleport: ItemSlot = ItemSlot::Empty { index: 0 };
313        let mut neutrals: Vec<ItemSlot> = Vec::new();
314        let mut preserved_neutrals: Vec<ItemSlot> = Vec::new();
315
316        for (k, v) in helper.items.into_iter() {
317            let container = ItemContainer::try_from(k).map_err(de::Error::custom)?;
318
319            let item = if v.name == "empty" {
320                ItemSlot::Empty {
321                    index: container.index(),
322                }
323            } else {
324                ItemSlot::Full {
325                    index: container.index(),
326                    item: Item {
327                        name: v.name,
328                        purchaser: v
329                            .purchaser
330                            .ok_or_else(|| {
331                                ItemsError::MissingRequiredField("purchaser".to_owned(), container)
332                            })
333                            .map_err(de::Error::custom)?,
334                        item_level: v.item_level,
335                        contains_rune: v.contains_rune,
336                        can_cast: v.can_cast,
337                        cooldown: v.cooldown,
338                        passive: v
339                            .passive
340                            .ok_or_else(|| {
341                                ItemsError::MissingRequiredField("passive".to_owned(), container)
342                            })
343                            .map_err(de::Error::custom)?,
344                        item_charges: v.item_charges,
345                        charges: v.charges,
346                    },
347                }
348            };
349
350            match container {
351                ItemContainer::Inventory(_) => inventory.push(item),
352                ItemContainer::Stash(_) => stash.push(item),
353                ItemContainer::Teleport => {
354                    teleport = item;
355                }
356                ItemContainer::Neutral => neutrals.push(item),
357                ItemContainer::PreservedNeutral => preserved_neutrals.push(item),
358            }
359        }
360
361        Ok(Items {
362            inventory,
363            stash,
364            teleport,
365            neutrals,
366            preserved_neutrals,
367        })
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_items_deserialize() {
377        let json_str = r#"{
378          "slot0": {
379              "name": "empty"
380          },
381          "slot1": {
382              "name": "empty"
383          },
384          "slot2": {
385              "name": "empty"
386          },
387          "slot3": {
388              "name": "empty"
389          },
390          "slot4": {
391              "name": "empty"
392          },
393          "slot5": {
394              "name": "empty"
395          },
396          "slot6": {
397              "name": "empty"
398          },
399          "slot7": {
400              "name": "empty"
401          },
402          "slot8": {
403              "name": "empty"
404          },
405          "stash0": {
406              "name": "empty"
407          },
408          "stash1": {
409              "name": "empty"
410          },
411          "stash2": {
412              "name": "empty"
413          },
414          "stash3": {
415              "name": "empty"
416          },
417          "stash4": {
418              "name": "empty"
419          },
420          "stash5": {
421              "name": "empty"
422          },
423          "teleport0": {
424              "name": "item_tpscroll",
425              "purchaser": 0,
426              "item_level": 1,
427              "can_cast": false,
428              "cooldown": 96,
429              "passive": false,
430              "item_charges": 1,
431              "charges": 1
432          },
433          "neutral0": {
434            "name": "empty"
435          },
436          "neutral1": {
437            "name": "empty"
438          },
439          "preserved_neutral6": {
440            "name": "empty"
441          },
442          "preserved_neutral7": {
443            "name": "empty"
444          },
445          "preserved_neutral8": {
446            "name": "empty"
447          },
448          "preserved_neutral9": {
449            "name": "empty"
450          },
451          "preserved_neutral10": {
452            "name": "empty"
453          }
454        }"#;
455
456        let items: Items = serde_json::from_str(json_str).expect("Failed to deserialize items");
457
458        assert!(matches!(
459            items.teleport,
460            ItemSlot::Full { index: 0, item: _ }
461        ));
462
463        assert!(items.is_inventory_empty());
464        assert!(items.is_stash_empty());
465        assert!(items.is_neutrals_empty());
466        assert!(items.is_preserved_neutrals_empty());
467    }
468}