Skip to main content

rpfm_lib/files/asset_variation_db/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Asset variation databases (`warscape_asset_variation_db.bin`): which environment file a battle uses.
12//!
13//! The database has a list of categories (map, season, precipitation...) with their values, and per key
14//! (`environment`) a list of files with the flags of the category values each one allows. When a battle starts,
15//! the game picks one of the files whose flags allow the battle's values.
16//!
17//! # File Format
18//!
19//! FASTBIN0, little endian, with `str` being a u16 size and that many bytes:
20//!
21//! ```text
22//! [8 bytes]  FASTBIN0 signature
23//! [u16]      serialise_version (1)
24//! [u32]      category count, then per category: u16 version (1), str name, u32 value count, str values
25//! [u32]      entry count, then per entry: str key, u16 version (1), u32 variation count,
26//!            and per variation: u16 version (1), str filename, u32 flags
27//! ```
28//!
29//! The packs also have the database in XML form, next to the binary one, and the Assembly Kit has its source
30//! (`environment_map.xml`). See [`AssetVariationDb::to_xml`], [`AssetVariationDb::from_xml`] and
31//! [`AssetVariationDb::from_environment_map`].
32//!
33//! # Selection
34//!
35//! As the game does it:
36//!
37//! - Every value has a bit, counting through the values of all categories in order: `1 << (index & 31)`.
38//! - A query ORs the bit of the value chosen for each category. The game finds a value by its name alone, searching
39//!   the values of all categories in order, so the first category that has the name gives the bit.
40//! - A query with a value missing from every list matches nothing.
41//! - A variation matches when its flags are 0, or when they have every bit of the query.
42//! - If nothing matches, the game queries again with the map `default_map`, and picks one of the matches at random.
43
44use getset::*;
45use serde_derive::{Serialize, Deserialize};
46
47use std::collections::BTreeMap;
48
49use crate::error::{Result, RLibError};
50
51/// Path of the database in the packs.
52pub const PATH: &str = "warscape_asset_variation_db/warscape_asset_variation_db.bin";
53
54/// Path of the XML form of the database in the packs.
55pub const PATH_XML: &str = "warscape_asset_variation_db/warscape_asset_variation_db.xml";
56
57/// FASTBIN0 file signature.
58pub const SIGNATURE: &[u8; 8] = b"FASTBIN0";
59
60/// Category of the map, and the value the game uses when nothing matches the battle's map.
61const MAP_CATEGORY: &str = "map";
62const DEFAULT_MAP: &str = "default_map";
63
64/// Amount of bits of the flags: all the values of all categories together can't have more.
65const FLAG_BITS: usize = 32;
66
67mod v1;
68mod xml;
69
70#[cfg(test)] mod asset_variation_db_test;
71
72//---------------------------------------------------------------------------//
73//                              Enum & Structs
74//---------------------------------------------------------------------------//
75
76/// An asset variation database.
77#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
78#[getset(get = "pub", get_mut = "pub", set = "pub")]
79pub struct AssetVariationDb {
80
81    /// Version of the format.
82    serialise_version: u16,
83
84    /// Categories of the conditions of a battle, with their values.
85    categories: Vec<VariationCategory>,
86
87    /// Lists of variations, by key.
88    entries: Vec<VariationEntry>,
89}
90
91/// A category of the conditions of a battle, like `season`, with its values.
92#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
93#[getset(get = "pub", get_mut = "pub", set = "pub")]
94pub struct VariationCategory {
95
96    /// Name of the category.
97    name: String,
98
99    /// Values of the category, in bit order.
100    values: Vec<String>,
101}
102
103/// The variations of a key, like `environment`.
104#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
105#[getset(get = "pub", get_mut = "pub", set = "pub")]
106pub struct VariationEntry {
107
108    /// Key of the entry.
109    key: String,
110
111    /// Variations the game picks from.
112    variations: Vec<Variation>,
113}
114
115/// A file the game may pick, with the values it allows.
116#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
117#[getset(get = "pub", get_mut = "pub", set = "pub")]
118pub struct Variation {
119
120    /// Path of the file, with backslashes.
121    filename: String,
122
123    /// Bits of the values the variation allows. 0 allows everything.
124    flags: u32,
125}
126
127/// The variations of a key that match a query.
128#[derive(Default, PartialEq, Clone, Debug)]
129pub struct VariationMatches<'a> {
130
131    /// The matching variations, in database order.
132    pub variations: Vec<&'a Variation>,
133
134    /// If nothing matched the map of the query, and the game asked again with the default map.
135    pub used_default_map: bool,
136}
137
138/// A query: the value chosen for each category. Categories left out, or with an empty value, add no bit.
139pub type VariationQuery = BTreeMap<String, String>;
140
141//---------------------------------------------------------------------------//
142//                              Implementations
143//---------------------------------------------------------------------------//
144
145impl AssetVariationDb {
146
147    /// Amount of values of all the categories together.
148    pub fn value_count(&self) -> usize {
149        self.categories.iter().map(|category| category.values.len()).sum()
150    }
151
152    /// Returns the category with the provided name, if any.
153    pub fn category(&self, name: &str) -> Option<&VariationCategory> {
154        self.categories.iter().find(|category| category.name == name)
155    }
156
157    /// Returns the entry with the provided key, if any.
158    pub fn entry(&self, key: &str) -> Option<&VariationEntry> {
159        self.entries.iter().find(|entry| entry.key == key)
160    }
161
162    /// Index of the first bit of a category, or None if there's no category with that name.
163    fn first_bit(&self, name: &str) -> Option<usize> {
164        let mut index = 0;
165        for category in &self.categories {
166            if category.name == name {
167                return Some(index);
168            }
169
170            index += category.values.len();
171        }
172
173        None
174    }
175
176    /// Bit of a value of a category, or 0 if the category or the value don't exist.
177    pub fn bit(&self, category: &str, value: &str) -> u32 {
178        match (self.category(category), self.first_bit(category)) {
179            (Some(category), Some(first)) => category.values.iter()
180                .position(|known| known == value)
181                .map_or(0, |index| 1 << ((first + index) % FLAG_BITS)),
182            _ => 0,
183        }
184    }
185
186    /// Bit the game gives a value: that of the first value with this name in any category, or None if no category has it.
187    pub fn value_bit(&self, value: &str) -> Option<u32> {
188        self.categories.iter()
189            .flat_map(|category| &category.values)
190            .position(|known| known == value)
191            .map(|index| 1 << (index % FLAG_BITS))
192    }
193
194    /// Bits of a query, or None if one of its values is in no category, which makes the game match nothing.
195    pub fn query_mask(&self, query: &VariationQuery) -> Option<u32> {
196        query.values()
197            .filter(|value| !value.is_empty())
198            .try_fold(0, |mask, value| self.value_bit(value).map(|bit| mask | bit))
199    }
200
201    /// Returns the variations of a key that match a query, as the game finds them.
202    ///
203    /// # Arguments
204    ///
205    /// * `key` - Key of the entry, like `environment`.
206    /// * `query` - Value chosen for each category.
207    ///
208    /// # Returns
209    ///
210    /// The matches. If nothing matches, they're the ones of the same query with the map `default_map`.
211    pub fn matches(&self, key: &str, query: &VariationQuery) -> VariationMatches<'_> {
212        let mut matches = VariationMatches::default();
213        let Some(entry) = self.entry(key) else { return matches };
214
215        let find = |query: &VariationQuery| match self.query_mask(query) {
216            Some(mask) => entry.variations.iter()
217                .filter(|variation| variation.flags == 0 || variation.flags & mask == mask)
218                .collect::<Vec<_>>(),
219            None => vec![],
220        };
221
222        matches.variations = find(query);
223        if matches.variations.is_empty() {
224            let mut query = query.clone();
225            query.insert(MAP_CATEGORY.to_owned(), DEFAULT_MAP.to_owned());
226            matches.variations = find(&query);
227            matches.used_default_map = true;
228        }
229
230        matches
231    }
232
233    /// Returns, per category, the values allowed by some flags. Flags 0 allow every value.
234    pub fn allowed_values(&self, flags: u32) -> Vec<(&str, Vec<&str>)> {
235        let mut index = 0;
236        self.categories.iter().map(|category| {
237            let allowed = category.values.iter().enumerate()
238                .filter(|(value, _)| flags == 0 || flags >> ((index + value) % FLAG_BITS) & 1 == 1)
239                .map(|(_, value)| value.as_str())
240                .collect();
241
242            index += category.values.len();
243            (category.name.as_str(), allowed)
244        }).collect()
245    }
246
247    /// Returns the flags that allow exactly the provided values of each category. Categories left out allow all their values.
248    pub fn flags_for(&self, allowed: &BTreeMap<String, Vec<String>>) -> u32 {
249        let mut flags = 0;
250        let mut index = 0;
251        for category in &self.categories {
252            let allowed = allowed.get(&category.name);
253            for (value_index, value) in category.values.iter().enumerate() {
254                if allowed.is_none_or(|allowed| allowed.contains(value)) {
255                    flags |= 1 << ((index + value_index) % FLAG_BITS);
256                }
257            }
258
259            index += category.values.len();
260        }
261
262        flags
263    }
264
265    /// Checks the database for what makes it wrong for the game.
266    ///
267    /// # Returns
268    ///
269    /// A description of each problem: more values than bits, repeated names or values, no `default_map`, flags with bits
270    /// of no value, and variations that can never match.
271    pub fn validate(&self) -> Vec<String> {
272        let mut problems = vec![];
273        let total = self.value_count();
274        if total > FLAG_BITS {
275            problems.push(format!("{total} values in all categories: the flags have {FLAG_BITS} bits, so bits would collide"));
276        }
277
278        for (index, category) in self.categories.iter().enumerate() {
279            if self.categories[index + 1..].iter().any(|other| other.name == category.name) {
280                problems.push(format!("category '{}' appears twice", category.name));
281            }
282
283            for (value_index, value) in category.values.iter().enumerate() {
284                if category.values[value_index + 1..].contains(value) {
285                    problems.push(format!("value '{value}' appears twice in '{}'", category.name));
286                }
287
288                if let Some(first) = self.categories[..index].iter().find(|other| other.values.contains(value)) {
289                    problems.push(format!(
290                        "value '{value}' is in '{}' and '{}': the game finds a value by its name in all categories, so the one in '{}' is never used",
291                        first.name, category.name, category.name
292                    ));
293                }
294            }
295        }
296
297        if self.category(MAP_CATEGORY).is_some_and(|map| !map.values.iter().any(|value| value == DEFAULT_MAP)) {
298            problems.push(format!("category '{MAP_CATEGORY}' has no value '{DEFAULT_MAP}': the game asks again with it when nothing matched"));
299        }
300
301        let used = if total >= FLAG_BITS { u32::MAX } else { (1 << total) - 1 };
302        for variation in self.entries.iter().flat_map(|entry| &entry.variations) {
303            if variation.flags & !used != 0 {
304                problems.push(format!("{}: flags have bits that belong to no value", variation.filename));
305            }
306
307            // Flags 0 allow everything.
308            if variation.flags != 0 {
309                if let Some((category, _)) = self.allowed_values(variation.flags).into_iter().find(|(_, allowed)| allowed.is_empty()) {
310                    problems.push(format!("{}: no {category} allowed, it can never match", variation.filename));
311                }
312            }
313        }
314
315        problems
316    }
317
318    /// Adds a value to a category, moving the bits of the flags of every variation so they keep allowing the same values.
319    ///
320    /// A variation that allowed every value of the category, without being unconstrained, also allows the new one.
321    ///
322    /// # Errors
323    ///
324    /// Returns [`RLibError::AssetVariationDbEditRefused`] if the category doesn't exist, the value is empty or already
325    /// exists in any category (the game finds values by name alone), or every bit of the flags is in use.
326    pub fn add_value(&mut self, category: &str, value: &str) -> Result<()> {
327        let Some(category_index) = self.categories.iter().position(|known| known.name == category) else {
328            return Err(refused(format!("no category '{category}'")));
329        };
330
331        let values = &self.categories[category_index].values;
332        if value.is_empty() {
333            return Err(refused("the value is empty".to_owned()));
334        }
335
336        if let Some(owner) = self.category_of(value) {
337            return Err(refused(format!("'{value}' already exists in '{owner}'")));
338        }
339
340        if self.value_count() >= FLAG_BITS {
341            return Err(refused(format!("all {FLAG_BITS} flag bits are in use: remove a value of any category first")));
342        }
343
344        let first = self.first_bit(category).unwrap_or_default();
345        let count = values.len();
346        let new_bit = first + count;
347        let moved = (0..self.value_count()).map(|bit| Some(if bit < new_bit { bit } else { bit + 1 })).collect::<Vec<_>>();
348        let category_bits = (first..first + count).fold(0u32, |bits, bit| bits | 1 << bit);
349
350        for variation in self.entries.iter_mut().flat_map(|entry| &mut entry.variations) {
351            let allowed_all = variation.flags != 0 && count > 0 && variation.flags & category_bits == category_bits;
352            variation.flags = move_bits(variation.flags, &moved);
353            if allowed_all {
354                variation.flags |= 1 << new_bit;
355            }
356        }
357
358        self.categories[category_index].values.push(value.to_owned());
359        Ok(())
360    }
361
362    /// Removes a value from a category, moving the bits of the flags of every variation so they keep allowing the same values.
363    ///
364    /// # Errors
365    ///
366    /// Returns [`RLibError::AssetVariationDbEditRefused`] if the category or the value don't exist, if the value is the
367    /// `default_map` map, or if a variation would allow nothing else, as its flags would become 0, which allows everything.
368    pub fn remove_value(&mut self, category: &str, value: &str) -> Result<()> {
369        let Some(category_index) = self.categories.iter().position(|known| known.name == category) else {
370            return Err(refused(format!("no category '{category}'")));
371        };
372
373        let Some(value_index) = self.categories[category_index].values.iter().position(|known| known == value) else {
374            return Err(refused(format!("no value '{value}' in '{category}'")));
375        };
376
377        if category == MAP_CATEGORY && value == DEFAULT_MAP {
378            return Err(refused(format!("the game asks again with map '{DEFAULT_MAP}' when nothing matched, so that value must stay")));
379        }
380
381        let removed_bit = self.first_bit(category).unwrap_or_default() + value_index;
382        let moved = (0..self.value_count()).map(|bit| match bit.cmp(&removed_bit) {
383            std::cmp::Ordering::Less => Some(bit),
384            std::cmp::Ordering::Equal => None,
385            std::cmp::Ordering::Greater => Some(bit - 1),
386        }).collect::<Vec<_>>();
387
388        let variations = self.entries.iter().flat_map(|entry| &entry.variations);
389        if let Some(variation) = variations.clone().find(|variation| variation.flags != 0 && move_bits(variation.flags, &moved) == 0) {
390            return Err(refused(format!("{} allows nothing else, removing '{value}' would turn it into an unconstrained variation (flags 0)", variation.filename)));
391        }
392
393        for variation in self.entries.iter_mut().flat_map(|entry| &mut entry.variations) {
394            variation.flags = move_bits(variation.flags, &moved);
395        }
396
397        self.categories[category_index].values.remove(value_index);
398        Ok(())
399    }
400
401    /// Renames a value of a category. The flags don't change.
402    ///
403    /// # Errors
404    ///
405    /// Returns [`RLibError::AssetVariationDbEditRefused`] if the category or the value don't exist, or the new name is
406    /// empty or already exists in any category.
407    pub fn rename_value(&mut self, category: &str, from: &str, to: &str) -> Result<()> {
408        if to.is_empty() {
409            return Err(refused("the new name is empty".to_owned()));
410        }
411
412        if let Some(owner) = self.category_of(to).filter(|_| from != to) {
413            return Err(refused(format!("'{to}' already exists in '{owner}'")));
414        }
415
416        let Some(category) = self.categories.iter_mut().find(|known| known.name == category) else {
417            return Err(refused(format!("no category '{category}'")));
418        };
419
420        let Some(value) = category.values.iter_mut().find(|known| *known == from) else {
421            return Err(refused(format!("no value '{from}' in '{}'", category.name)));
422        };
423
424        *value = to.to_owned();
425        Ok(())
426    }
427
428    /// Name of the first category that has a value, if any.
429    pub fn category_of(&self, value: &str) -> Option<&str> {
430        self.categories.iter().find(|category| category.values.iter().any(|known| known == value)).map(|category| category.name.as_str())
431    }
432
433    /// Adds a value to a category that every variation allows exactly when it allows another value of the same category,
434    /// like a new climate with the weather of an existing one.
435    ///
436    /// # Errors
437    ///
438    /// Returns [`RLibError::AssetVariationDbEditRefused`] if `from` isn't a value of the category, or for the reasons of
439    /// [`AssetVariationDb::add_value`].
440    pub fn clone_value(&mut self, category: &str, from: &str, value: &str) -> Result<()> {
441        if self.category(category).is_none_or(|known| !known.values.iter().any(|known| known == from)) {
442            return Err(refused(format!("no value '{from}' in '{category}'")));
443        }
444
445        self.add_value(category, value)?;
446        let from_bit = self.bit(category, from);
447        let new_bit = self.bit(category, value);
448        for variation in self.entries.iter_mut().flat_map(|entry| &mut entry.variations).filter(|variation| variation.flags != 0) {
449            if variation.flags & from_bit != 0 {
450                variation.flags |= new_bit;
451            } else {
452                variation.flags &= !new_bit;
453            }
454        }
455
456        Ok(())
457    }
458
459    /// Adds an empty category after the others. It takes no bit until it has values.
460    ///
461    /// # Errors
462    ///
463    /// Returns [`RLibError::AssetVariationDbEditRefused`] if the name is empty or already used.
464    pub fn add_category(&mut self, name: &str) -> Result<()> {
465        if name.is_empty() {
466            return Err(refused("the category name is empty".to_owned()));
467        }
468
469        if self.category(name).is_some() {
470            return Err(refused(format!("category '{name}' already exists")));
471        }
472
473        let mut category = VariationCategory::default();
474        category.name = name.to_owned();
475        self.categories.push(category);
476        Ok(())
477    }
478
479    /// Removes a category with its values, moving the bits of the flags of every variation so they keep allowing the
480    /// same values of the other categories.
481    ///
482    /// # Errors
483    ///
484    /// Returns [`RLibError::AssetVariationDbEditRefused`] if the category doesn't exist, or if a variation only
485    /// restricts this category, as its flags would become 0, which allows everything.
486    pub fn remove_category(&mut self, name: &str) -> Result<()> {
487        let Some(category_index) = self.categories.iter().position(|known| known.name == name) else {
488            return Err(refused(format!("no category '{name}'")));
489        };
490
491        let first = self.first_bit(name).unwrap_or_default();
492        let count = self.categories[category_index].values.len();
493        let moved = (0..self.value_count()).map(|bit| match bit {
494            bit if bit < first => Some(bit),
495            bit if bit < first + count => None,
496            bit => Some(bit - count),
497        }).collect::<Vec<_>>();
498
499        let variations = self.entries.iter().flat_map(|entry| &entry.variations);
500        if let Some(variation) = variations.clone().find(|variation| variation.flags != 0 && move_bits(variation.flags, &moved) == 0) {
501            return Err(refused(format!("{} only has bits in '{name}', removing it would turn it into an unconstrained variation (flags 0)", variation.filename)));
502        }
503
504        for variation in self.entries.iter_mut().flat_map(|entry| &mut entry.variations) {
505            variation.flags = move_bits(variation.flags, &moved);
506        }
507
508        self.categories.remove(category_index);
509        Ok(())
510    }
511
512    /// Renames a category. The flags don't change.
513    ///
514    /// # Errors
515    ///
516    /// Returns [`RLibError::AssetVariationDbEditRefused`] if the category doesn't exist, or the new name is empty or already used.
517    pub fn rename_category(&mut self, from: &str, to: &str) -> Result<()> {
518        if to.is_empty() {
519            return Err(refused("the new name is empty".to_owned()));
520        }
521
522        if from != to && self.category(to).is_some() {
523            return Err(refused(format!("category '{to}' already exists")));
524        }
525
526        let Some(category) = self.categories.iter_mut().find(|known| known.name == from) else {
527            return Err(refused(format!("no category '{from}'")));
528        };
529
530        category.name = to.to_owned();
531        Ok(())
532    }
533}
534
535/// Moves every bit of some flags to the index `moved` has for it, dropping the ones it has None for. Flags 0 stay 0.
536fn move_bits(flags: u32, moved: &[Option<usize>]) -> u32 {
537    if flags == 0 {
538        return 0;
539    }
540
541    moved.iter().take(FLAG_BITS).enumerate()
542        .filter(|(bit, _)| flags >> bit & 1 == 1)
543        .filter_map(|(_, to)| *to)
544        .fold(0, |out, to| out | 1 << to)
545}
546
547/// Error of a refused edit.
548fn refused(reason: String) -> RLibError {
549    RLibError::AssetVariationDbEditRefused(reason)
550}