Skip to main content

wowlab_parsers/parsers/simc/
types.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
4#[cfg(feature = "wasm")]
5use tsify::Tsify;
6use wowlab_types::{game::GearEntry, sim::FastMap};
7
8/// `WoW` class identifiers.
9#[derive(
10    Clone,
11    Copy,
12    Debug,
13    Deserialize,
14    Eq,
15    Hash,
16    PartialEq,
17    Serialize,
18    strum::EnumString,
19    strum::IntoStaticStr,
20)]
21#[cfg_attr(feature = "wasm", derive(Tsify))]
22#[serde(rename_all = "snake_case")]
23#[strum(serialize_all = "snake_case", ascii_case_insensitive)]
24// #t(rust_non_exhaustive_on_public) WoW class list is game-defined
25pub enum WowClass {
26    #[strum(serialize = "death_knight", serialize = "deathknight")]
27    DeathKnight,
28    #[strum(serialize = "demon_hunter", serialize = "demonhunter")]
29    DemonHunter,
30    Druid,
31    Evoker,
32    Hunter,
33    Mage,
34    Monk,
35    Paladin,
36    Priest,
37    Rogue,
38    Shaman,
39    Warlock,
40    Warrior,
41}
42
43impl WowClass {
44    /// Parse from a `snake_case` string.
45    #[must_use]
46    pub fn parse(s: &str) -> Option<Self> {
47        s.parse().ok()
48    }
49
50    /// `SimC` `snake_case` identifier.
51    #[must_use]
52    pub fn as_simc_str(&self) -> &'static str {
53        (*self).into()
54    }
55}
56
57/// A parsed equipment item.
58#[derive(Clone, Debug, Deserialize, PartialEq)]
59#[cfg_attr(feature = "wasm", derive(Tsify))]
60#[serde(rename_all = "camelCase")]
61pub struct Item {
62    #[serde(flatten)]
63    pub gear: GearEntry,
64    /// `SimC`'s named permanent-enchant token (`enchant=`), retained for data-backed resolution.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    #[cfg_attr(feature = "wasm", tsify(optional))]
67    pub enchant: Option<String>,
68    #[cfg_attr(feature = "wasm", tsify(optional))]
69    pub suffix: Option<i32>,
70    #[cfg_attr(feature = "wasm", tsify(optional))]
71    pub gem_bonus_ids: Option<Vec<u32>>,
72    #[cfg_attr(feature = "wasm", tsify(optional))]
73    pub titan_disc_id: Option<u32>,
74}
75
76const ITEM_FIELD_COUNT_WITHOUT_ENCHANT: usize = 12;
77
78impl Serialize for Item {
79    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
80    where
81        S: Serializer,
82    {
83        let field_count = ITEM_FIELD_COUNT_WITHOUT_ENCHANT + usize::from(self.enchant.is_some());
84        let mut item = serializer.serialize_struct("Item", field_count)?;
85
86        serialize_gear_fields(&mut item, &self.gear)?;
87
88        if let Some(enchant) = &self.enchant {
89            item.serialize_field("enchant", enchant)?;
90        }
91
92        serialize_item_extra_fields(&mut item, self)?;
93
94        item.end()
95    }
96}
97
98fn serialize_gear_fields<S>(item: &mut S, gear: &GearEntry) -> Result<(), S::Error>
99where
100    S: SerializeStruct,
101{
102    item.serialize_field("slot", &gear.slot)?;
103    item.serialize_field("id", &gear.id)?;
104    item.serialize_field("bonusIds", &gear.bonus_ids)?;
105    item.serialize_field("enchantId", &gear.enchant_id)?;
106    item.serialize_field("gemIds", &gear.gem_ids)?;
107    item.serialize_field("craftedStats", &gear.crafted_stats)?;
108    item.serialize_field("craftingQuality", &gear.crafting_quality)?;
109    item.serialize_field("dropLevel", &gear.drop_level)?;
110
111    item.serialize_field("ilevel", &gear.ilevel)
112}
113
114fn serialize_item_extra_fields<S>(item: &mut S, value: &Item) -> Result<(), S::Error>
115where
116    S: SerializeStruct,
117{
118    item.serialize_field("suffix", &value.suffix)?;
119    item.serialize_field("gemBonusIds", &value.gem_bonus_ids)?;
120
121    item.serialize_field("titanDiscId", &value.titan_disc_id)
122}
123
124/// Profession with skill rank.
125#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
126#[cfg_attr(feature = "wasm", derive(Tsify))]
127#[serde(rename_all = "camelCase")]
128pub struct Profession {
129    pub name: String,
130    pub rank: u32,
131}
132
133/// Character information.
134#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
135#[cfg_attr(feature = "wasm", derive(Tsify))]
136#[serde(rename_all = "camelCase")]
137pub struct Character {
138    pub name: String,
139    pub level: u32,
140    pub race: String,
141    pub class: WowClass,
142    #[cfg_attr(feature = "wasm", tsify(optional))]
143    pub spec: Option<String>,
144    #[cfg_attr(feature = "wasm", tsify(optional))]
145    pub region: Option<String>,
146    #[cfg_attr(feature = "wasm", tsify(optional))]
147    pub server: Option<String>,
148    #[cfg_attr(feature = "wasm", tsify(optional))]
149    pub role: Option<String>,
150    pub professions: Vec<Profession>,
151    #[cfg_attr(feature = "wasm", tsify(optional))]
152    pub zandalari_loa: Option<String>,
153    #[cfg_attr(feature = "wasm", tsify(optional))]
154    pub loot_spec: Option<String>,
155}
156
157/// Saved talent loadout.
158#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
159#[cfg_attr(feature = "wasm", derive(Tsify))]
160#[serde(rename_all = "camelCase")]
161pub struct Loadout {
162    pub name: String,
163    pub encoded: String,
164}
165
166/// Parsed talents section: raw encoded string + decoded loadouts.
167#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
168#[cfg_attr(feature = "wasm", derive(Tsify))]
169#[serde(rename_all = "camelCase")]
170pub struct Talents {
171    pub encoded: String,
172    pub loadouts: Vec<Loadout>,
173}
174
175/// Currency entry.
176#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
177#[cfg_attr(feature = "wasm", derive(Tsify))]
178#[serde(rename_all = "camelCase")]
179pub struct CurrencyEntry {
180    pub id: u32,
181    pub amount: u32,
182}
183
184/// Upgrade currency; `currency_type` is 'c' (currency) or 'i' (item).
185#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
186#[cfg_attr(feature = "wasm", derive(Tsify))]
187#[serde(rename_all = "camelCase")]
188pub struct UpgradeCurrency {
189    pub currency_type: String,
190    pub id: u32,
191    pub amount: u32,
192}
193
194/// Slot high watermark.
195#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
196#[cfg_attr(feature = "wasm", derive(Tsify))]
197#[serde(rename_all = "camelCase")]
198pub struct SlotWatermark {
199    pub slot: u8,
200    pub current: u16,
201    pub max: u16,
202}
203
204/// Additional character info.
205#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
206#[cfg_attr(feature = "wasm", derive(Tsify))]
207#[serde(rename_all = "camelCase")]
208pub struct CharacterInfo {
209    pub catalyst_currencies: Vec<CurrencyEntry>,
210    pub upgrade_currencies: Vec<UpgradeCurrency>,
211    pub slot_high_watermarks: Vec<SlotWatermark>,
212    pub upgrade_achievements: Vec<u32>,
213    #[cfg_attr(feature = "wasm", tsify(optional))]
214    pub checksum: Option<String>,
215}
216
217/// A fully parsed `SimC` profile.
218#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
219#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
220#[serde(rename_all = "camelCase")]
221pub struct Profile {
222    pub character: Character,
223    pub equipment: Vec<Item>,
224    pub bag_items: Vec<Item>,
225    pub weekly_rewards: Vec<Item>,
226    pub talents: Talents,
227    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
228    #[cfg_attr(feature = "wasm", tsify(optional))]
229    pub expansion_talents: BTreeMap<String, Vec<String>>,
230    pub extra: FastMap<String, String>,
231    #[cfg_attr(feature = "wasm", tsify(optional))]
232    pub character_info: Option<CharacterInfo>,
233}
234
235#[cfg(test)]
236mod tests {
237    use googletest::prelude::*;
238    use rstest::rstest;
239
240    use super::*;
241
242    #[gtest]
243    #[rstest]
244    #[case::snake("death_knight", Some(WowClass::DeathKnight))]
245    #[case::alias("deathknight", Some(WowClass::DeathKnight))]
246    #[case::case_insensitive("ROGUE", Some(WowClass::Rogue))]
247    #[case::demon_hunter_snake("demon_hunter", Some(WowClass::DemonHunter))]
248    #[case::demon_hunter_alias("demonhunter", Some(WowClass::DemonHunter))]
249    #[case::invalid("banana", None)]
250    fn parse_maps_snake_alias_and_case(
251        #[case] input: &str,
252        #[case] expected: Option<WowClass>,
253    ) -> Result<()> {
254        verify_that!(WowClass::parse(input), eq(expected))
255    }
256
257    #[gtest]
258    #[rstest]
259    #[case::death_knight(WowClass::DeathKnight, "death_knight")]
260    #[case::demon_hunter(WowClass::DemonHunter, "demon_hunter")]
261    #[case::rogue(WowClass::Rogue, "rogue")]
262    fn as_simc_str_round_trips(#[case] class: WowClass, #[case] expected: &str) -> Result<()> {
263        verify_that!(class.as_simc_str(), eq(expected))?;
264
265        verify_that!(WowClass::parse(expected), eq(Some(class)))
266    }
267}