Skip to main content

weiss_core/db/
store.rs

1use anyhow::{Context, Result};
2
3use super::ability::{
4    ability_sort_key, compile_effects_from_def, compile_effects_from_template, AbilitySpec,
5    AbilityTemplate,
6};
7use super::card::CardStatic;
8use super::types::{CardColor, CardId, CardType};
9
10/// Upper bound for dense per-id lookup caches.
11///
12/// The simulator stores several lookup vectors indexed directly by `CardId`.
13/// Rejecting sparse, very large ids keeps attacker-supplied WSDB files from
14/// forcing unbounded allocations while leaving substantial headroom for real
15/// card catalogs.
16pub const MAX_CARD_ID_INDEX: CardId = 1_000_000;
17
18/// Loaded card database with cached per-id lookups and compiled abilities.
19#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
20#[serde(try_from = "CardDbRaw", into = "CardDbRaw")]
21pub struct CardDb {
22    /// Canonical list of static card definitions.
23    pub cards: Vec<CardStatic>,
24    #[serde(skip)]
25    index: Vec<usize>,
26    #[serde(skip)]
27    valid_by_id: Vec<bool>,
28    #[serde(skip)]
29    power_by_id: Vec<i32>,
30    #[serde(skip)]
31    soul_by_id: Vec<u8>,
32    #[serde(skip)]
33    level_by_id: Vec<u8>,
34    #[serde(skip)]
35    cost_by_id: Vec<u8>,
36    #[serde(skip)]
37    color_by_id: Vec<CardColor>,
38    #[serde(skip)]
39    card_type_by_id: Vec<CardType>,
40    #[serde(skip)]
41    ability_specs: Vec<Vec<AbilitySpec>>,
42    #[serde(skip)]
43    compiled_ability_effects: Vec<Vec<Vec<crate::effects::EffectSpec>>>,
44}
45
46#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
47struct CardDbRaw {
48    cards: Vec<CardStatic>,
49}
50
51impl TryFrom<CardDbRaw> for CardDb {
52    type Error = anyhow::Error;
53
54    fn try_from(raw: CardDbRaw) -> Result<Self> {
55        CardDb::new(raw.cards)
56    }
57}
58
59impl From<CardDb> for CardDbRaw {
60    fn from(db: CardDb) -> Self {
61        Self { cards: db.cards }
62    }
63}
64
65impl CardDb {
66    /// Build a new database from raw card definitions.
67    ///
68    /// Validates ids, canonicalizes templates, and builds per-id caches.
69    pub fn new(cards: Vec<CardStatic>) -> Result<Self> {
70        let mut db = Self {
71            cards,
72            index: Vec::new(),
73            valid_by_id: Vec::new(),
74            power_by_id: Vec::new(),
75            soul_by_id: Vec::new(),
76            level_by_id: Vec::new(),
77            cost_by_id: Vec::new(),
78            color_by_id: Vec::new(),
79            card_type_by_id: Vec::new(),
80            ability_specs: Vec::new(),
81            compiled_ability_effects: Vec::new(),
82        };
83        db.build_index()?;
84        Ok(db)
85    }
86
87    /// Look up a card by id. Returns `None` for invalid or zero ids.
88    pub fn get(&self, id: CardId) -> Option<&CardStatic> {
89        if id == 0 {
90            return None;
91        }
92        let idx = *self.index.get(id as usize)?;
93        if idx == usize::MAX {
94            return None;
95        }
96        self.cards.get(idx)
97    }
98
99    #[inline(always)]
100    /// Maximum card id present in the index.
101    pub fn max_card_id(&self) -> CardId {
102        self.index.len().saturating_sub(1).try_into().unwrap_or(0)
103    }
104
105    #[inline(always)]
106    /// Whether an id exists in this database.
107    pub fn is_valid_id(&self, id: CardId) -> bool {
108        self.valid_by_id.get(id as usize).copied().unwrap_or(false)
109    }
110
111    #[inline(always)]
112    /// Cached power value for a card id, or 0 if invalid.
113    pub fn power_by_id(&self, id: CardId) -> i32 {
114        self.power_by_id.get(id as usize).copied().unwrap_or(0)
115    }
116
117    #[inline(always)]
118    /// Cached soul value for a card id, or 0 if invalid.
119    pub fn soul_by_id(&self, id: CardId) -> u8 {
120        self.soul_by_id.get(id as usize).copied().unwrap_or(0)
121    }
122
123    #[inline(always)]
124    /// Cached level value for a card id, or 0 if invalid.
125    pub fn level_by_id(&self, id: CardId) -> u8 {
126        self.level_by_id.get(id as usize).copied().unwrap_or(0)
127    }
128
129    #[inline(always)]
130    /// Cached cost value for a card id, or 0 if invalid.
131    pub fn cost_by_id(&self, id: CardId) -> u8 {
132        self.cost_by_id.get(id as usize).copied().unwrap_or(0)
133    }
134
135    #[inline(always)]
136    /// Cached color value for a card id, defaulting to red.
137    pub fn color_by_id(&self, id: CardId) -> CardColor {
138        self.color_by_id
139            .get(id as usize)
140            .copied()
141            .unwrap_or(CardColor::Red)
142    }
143
144    #[inline(always)]
145    /// Cached card type for a card id, defaulting to character.
146    pub fn card_type_by_id(&self, id: CardId) -> CardType {
147        self.card_type_by_id
148            .get(id as usize)
149            .copied()
150            .unwrap_or(CardType::Character)
151    }
152
153    pub(super) fn build_index(&mut self) -> Result<()> {
154        let mut max_id: usize = 0;
155        for card in &mut self.cards {
156            if card.id == 0 {
157                anyhow::bail!("CardId 0 is reserved for empty and cannot appear in the db");
158            }
159            if card.id > MAX_CARD_ID_INDEX {
160                anyhow::bail!(
161                    "CardId {} exceeds maximum supported id {}",
162                    card.id,
163                    MAX_CARD_ID_INDEX
164                );
165            }
166            if card.counter_timing
167                && !matches!(card.card_type, CardType::Event | CardType::Character)
168            {
169                eprintln!(
170                    "CardId {} has counter timing but card_type {:?} is not eligible; disabling counter timing",
171                    card.id, card.card_type
172                );
173                card.counter_timing = false;
174            }
175            for def in &card.ability_defs {
176                def.validate()
177                    .with_context(|| format!("CardId {} AbilityDef invalid", card.id))?;
178            }
179            max_id = max_id.max(card.id as usize);
180        }
181        let mut index = vec![usize::MAX; max_id + 1];
182        let mut valid_by_id = vec![false; max_id + 1];
183        let mut power_by_id = vec![0i32; max_id + 1];
184        let mut soul_by_id = vec![0u8; max_id + 1];
185        let mut level_by_id = vec![0u8; max_id + 1];
186        let mut cost_by_id = vec![0u8; max_id + 1];
187        let mut color_by_id = vec![CardColor::Red; max_id + 1];
188        let mut card_type_by_id = vec![CardType::Character; max_id + 1];
189        for (i, card) in self.cards.iter().enumerate() {
190            let id = card.id as usize;
191            if index[id] != usize::MAX {
192                anyhow::bail!("Duplicate CardId {id}");
193            }
194            index[id] = i;
195            valid_by_id[id] = true;
196            power_by_id[id] = card.power;
197            soul_by_id[id] = card.soul;
198            level_by_id[id] = card.level;
199            cost_by_id[id] = card.cost;
200            color_by_id[id] = card.color;
201            card_type_by_id[id] = card.card_type;
202        }
203        self.index = index;
204        self.valid_by_id = valid_by_id;
205        self.power_by_id = power_by_id;
206        self.soul_by_id = soul_by_id;
207        self.level_by_id = level_by_id;
208        self.cost_by_id = cost_by_id;
209        self.color_by_id = color_by_id;
210        self.card_type_by_id = card_type_by_id;
211        self.build_ability_specs()?;
212        self.build_compiled_abilities()?;
213        Ok(())
214    }
215
216    fn build_ability_specs(&mut self) -> Result<()> {
217        let mut specs_list: Vec<Vec<AbilitySpec>> = Vec::with_capacity(self.cards.len());
218        for card in &self.cards {
219            for template in &card.abilities {
220                if matches!(
221                    template,
222                    AbilityTemplate::ActivatedPlaceholder | AbilityTemplate::Unsupported { .. }
223                ) {
224                    anyhow::bail!(
225                        "CardId {} uses unsupported ability template; update card db",
226                        card.id
227                    );
228                }
229            }
230            let mut specs: Vec<AbilitySpec> = card
231                .abilities
232                .iter()
233                .map(AbilitySpec::from_template)
234                .collect();
235            for def in &card.ability_defs {
236                specs.push(AbilitySpec::from_template(&AbilityTemplate::AbilityDef(
237                    def.clone(),
238                )));
239            }
240            specs.sort_by_cached_key(ability_sort_key);
241            specs_list.push(specs);
242        }
243        self.ability_specs = specs_list;
244        Ok(())
245    }
246
247    fn build_compiled_abilities(&mut self) -> Result<()> {
248        let mut compiled: Vec<Vec<Vec<crate::effects::EffectSpec>>> =
249            Vec::with_capacity(self.cards.len());
250        for card in &self.cards {
251            let specs = self.iter_card_abilities_in_canonical_order(card.id);
252            let mut per_ability: Vec<Vec<crate::effects::EffectSpec>> =
253                Vec::with_capacity(specs.len());
254            for (ability_index, spec) in specs.iter().enumerate() {
255                let idx = u8::try_from(ability_index).map_err(|_| {
256                    anyhow::anyhow!(
257                        "Ability index out of range for card {}: {}",
258                        card.id,
259                        ability_index
260                    )
261                })?;
262                let effects = match &spec.template {
263                    AbilityTemplate::AbilityDef(def) => compile_effects_from_def(card.id, idx, def),
264                    AbilityTemplate::Vanilla | AbilityTemplate::Unsupported { .. } => Vec::new(),
265                    _ => compile_effects_from_template(card.id, idx, &spec.template),
266                };
267                per_ability.push(effects);
268            }
269            compiled.push(per_ability);
270        }
271        self.compiled_ability_effects = compiled;
272        Ok(())
273    }
274
275    /// Abilities in canonical order for a given card id.
276    pub fn iter_card_abilities_in_canonical_order(&self, card_id: CardId) -> &[AbilitySpec] {
277        let idx = match self.index.get(card_id as usize) {
278            Some(idx) => *idx,
279            None => return &[],
280        };
281        if idx == usize::MAX {
282            return &[];
283        }
284        self.ability_specs
285            .get(idx)
286            .map(|v| v.as_slice())
287            .unwrap_or(&[])
288    }
289
290    /// Compiled effects for a specific ability on a card.
291    pub fn compiled_effects_for_ability(
292        &self,
293        card_id: CardId,
294        ability_index: usize,
295    ) -> &[crate::effects::EffectSpec] {
296        let idx = match self.index.get(card_id as usize) {
297            Some(idx) => *idx,
298            None => return &[],
299        };
300        if idx == usize::MAX {
301            return &[];
302        }
303        self.compiled_ability_effects
304            .get(idx)
305            .and_then(|per_ability| per_ability.get(ability_index))
306            .map(|v| v.as_slice())
307            .unwrap_or(&[])
308    }
309
310    /// Flattened list of all compiled effects for a card.
311    pub fn compiled_effects_flat(&self, card_id: CardId) -> Vec<crate::effects::EffectSpec> {
312        let idx = match self.index.get(card_id as usize) {
313            Some(idx) => *idx,
314            None => return Vec::new(),
315        };
316        if idx == usize::MAX {
317            return Vec::new();
318        }
319        let Some(per_ability) = self.compiled_ability_effects.get(idx) else {
320            return Vec::new();
321        };
322        let mut out = Vec::new();
323        for effects in per_ability {
324            out.extend(effects.iter().cloned());
325        }
326        out
327    }
328}