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 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}