Skip to main content

wowlab_manifest_schema/
scalar.rs

1//! Literal and game-data-backed scalar values.
2
3use serde::Deserialize;
4
5/// A scalar coefficient that is either a literal or a reference resolved from game data.
6#[derive(Debug)]
7// #t(rust_non_exhaustive_on_public) fixed serde untagged variants for manifest scalar values
8pub enum ScalarRef {
9    EffectRef {
10        spell_id: u32,
11        effect: u8,
12        field: ScalarField,
13        multiplier: Option<f64>,
14        offset: Option<f64>,
15        round_decimals: Option<u8>,
16    },
17    SpellRef {
18        spell_id: u32,
19        field: SpellScalarField,
20        multiplier: Option<f64>,
21        offset: Option<f64>,
22        round_decimals: Option<u8>,
23    },
24    Literal(f64),
25}
26
27#[derive(Deserialize)]
28#[serde(untagged)]
29enum ScalarRefRepr {
30    EffectRef {
31        spell_id: u32,
32        effect: u8,
33        field: ScalarField,
34        #[serde(default)]
35        multiplier: Option<f64>,
36        #[serde(default)]
37        offset: Option<f64>,
38        #[serde(default, deserialize_with = "deserialize_round_decimals")]
39        round_decimals: Option<u8>,
40    },
41    SpellRef {
42        spell_id: u32,
43        field: SpellScalarField,
44        #[serde(default)]
45        multiplier: Option<f64>,
46        #[serde(default)]
47        offset: Option<f64>,
48        #[serde(default, deserialize_with = "deserialize_round_decimals")]
49        round_decimals: Option<u8>,
50    },
51    Float(f64),
52    Integer(i64),
53}
54
55impl ScalarRef {
56    #[must_use]
57    pub const fn effect_spell_id(&self) -> Option<u32> {
58        match self {
59            Self::EffectRef { spell_id, .. } | Self::SpellRef { spell_id, .. } => Some(*spell_id),
60            Self::Literal(_) => None,
61        }
62    }
63}
64
65impl<'de> Deserialize<'de> for ScalarRef {
66    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67    where
68        D: serde::Deserializer<'de>,
69    {
70        Ok(match ScalarRefRepr::deserialize(deserializer)? {
71            ScalarRefRepr::EffectRef {
72                spell_id,
73                effect,
74                field,
75                multiplier,
76                offset,
77                round_decimals,
78            } => Self::EffectRef {
79                spell_id,
80                effect,
81                field,
82                multiplier,
83                offset,
84                round_decimals,
85            },
86            ScalarRefRepr::SpellRef {
87                spell_id,
88                field,
89                multiplier,
90                offset,
91                round_decimals,
92            } => Self::SpellRef {
93                spell_id,
94                field,
95                multiplier,
96                offset,
97                round_decimals,
98            },
99            ScalarRefRepr::Float(value) => Self::Literal(value),
100            ScalarRefRepr::Integer(value) => {
101                Self::Literal(wowlab_types::numeric::i64_to_f64(value))
102            }
103        })
104    }
105}
106
107/// Resolved spell property a [`ScalarRef::SpellRef`] reads.
108#[derive(Clone, Copy, Debug, Deserialize)]
109#[serde(rename_all = "snake_case")]
110#[non_exhaustive]
111pub enum SpellScalarField {
112    DurationMs,
113    MaxStacks,
114    Cooldown,
115    Cost,
116    CastTimeMs,
117    Charges,
118    ChargeCd,
119    GcdMs,
120    /// DBC `SpellAuraOptions.ProcCategoryRecovery` in milliseconds.
121    ProcCategoryRecoveryMs,
122}
123
124/// Game-data field a [`ScalarRef::EffectRef`] resolves against.
125#[derive(Clone, Copy, Debug, Deserialize)]
126#[serde(rename_all = "snake_case")]
127#[non_exhaustive]
128pub enum ScalarField {
129    BasePoints,
130    ApCoef,
131    SpCoef,
132    /// DBC `EffectAmplitude`; retained for scalar consumers that explicitly need it.
133    Amplitude,
134    /// DBC `EffectAuraPeriod` (`SpellEffect.period`) in milliseconds.
135    Period,
136    Coefficient,
137}
138
139const MAX_ROUND_DECIMALS: u8 = 15;
140
141fn deserialize_round_decimals<'de, D>(deserializer: D) -> Result<Option<u8>, D::Error>
142where
143    D: serde::Deserializer<'de>,
144{
145    use serde::de::Error as _;
146
147    let value = Option::<u8>::deserialize(deserializer)?;
148
149    if value.is_some_and(|decimals| decimals > MAX_ROUND_DECIMALS) {
150        return Err(D::Error::custom(format!(
151            "round_decimals must be between 0 and {MAX_ROUND_DECIMALS}"
152        )));
153    }
154
155    Ok(value)
156}