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. A value missing from the lists adds no bit.
39//! - A variation matches when its flags are 0, or when they have every bit of the query.
40//! - If nothing matches the battle's map, the game queries again with the map `default_map`, and picks one of the matches at random.
41
42use getset::*;
43use serde_derive::{Serialize, Deserialize};
44
45use std::collections::BTreeMap;
46
47use crate::error::{Result, RLibError};
48
49/// Path of the database in the packs.
50pub const PATH: &str = "warscape_asset_variation_db/warscape_asset_variation_db.bin";
51
52/// Path of the XML form of the database in the packs.
53pub const PATH_XML: &str = "warscape_asset_variation_db/warscape_asset_variation_db.xml";
54
55/// FASTBIN0 file signature.
56pub const SIGNATURE: &[u8; 8] = b"FASTBIN0";
57
58/// Category of the map, and the value the game uses when nothing matches the battle's map.
59const MAP_CATEGORY: &str = "map";
60const DEFAULT_MAP: &str = "default_map";
61
62/// Amount of bits of the flags: all the values of all categories together can't have more.
63const FLAG_BITS: usize = 32;
64
65mod v1;
66mod xml;
67
68#[cfg(test)] mod asset_variation_db_test;
69
70//---------------------------------------------------------------------------//
71//                              Enum & Structs
72//---------------------------------------------------------------------------//
73
74/// An asset variation database.
75#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
76#[getset(get = "pub", get_mut = "pub", set = "pub")]
77pub struct AssetVariationDb {
78
79    /// Version of the format.
80    serialise_version: u16,
81
82    /// Categories of the conditions of a battle, with their values.
83    categories: Vec<VariationCategory>,
84
85    /// Lists of variations, by key.
86    entries: Vec<VariationEntry>,
87}
88
89/// A category of the conditions of a battle, like `season`, with its values.
90#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
91#[getset(get = "pub", get_mut = "pub", set = "pub")]
92pub struct VariationCategory {
93
94    /// Name of the category.
95    name: String,
96
97    /// Values of the category, in bit order.
98    values: Vec<String>,
99}
100
101/// The variations of a key, like `environment`.
102#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
103#[getset(get = "pub", get_mut = "pub", set = "pub")]
104pub struct VariationEntry {
105
106    /// Key of the entry.
107    key: String,
108
109    /// Variations the game picks from.
110    variations: Vec<Variation>,
111}
112
113/// A file the game may pick, with the values it allows.
114#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
115#[getset(get = "pub", get_mut = "pub", set = "pub")]
116pub struct Variation {
117
118    /// Path of the file, with backslashes.
119    filename: String,
120
121    /// Bits of the values the variation allows. 0 allows everything.
122    flags: u32,
123}
124
125/// The variations of a key that match a query.
126#[derive(Default, PartialEq, Clone, Debug)]
127pub struct VariationMatches<'a> {
128
129    /// The matching variations, in database order.
130    pub variations: Vec<&'a Variation>,
131
132    /// If nothing matched the map of the query, and the game asked again with the default map.
133    pub used_default_map: bool,
134}
135
136/// A query: the value chosen for each category. Categories left out add no bit.
137pub type VariationQuery = BTreeMap<String, String>;
138
139//---------------------------------------------------------------------------//
140//                              Implementations
141//---------------------------------------------------------------------------//
142
143impl AssetVariationDb {
144
145    /// Amount of values of all the categories together.
146    pub fn value_count(&self) -> usize {
147        self.categories.iter().map(|category| category.values.len()).sum()
148    }
149
150    /// Returns the category with the provided name, if any.
151    pub fn category(&self, name: &str) -> Option<&VariationCategory> {
152        self.categories.iter().find(|category| category.name == name)
153    }
154
155    /// Returns the entry with the provided key, if any.
156    pub fn entry(&self, key: &str) -> Option<&VariationEntry> {
157        self.entries.iter().find(|entry| entry.key == key)
158    }
159
160    /// Index of the first bit of a category, or None if there's no category with that name.
161    fn first_bit(&self, name: &str) -> Option<usize> {
162        let mut index = 0;
163        for category in &self.categories {
164            if category.name == name {
165                return Some(index);
166            }
167
168            index += category.values.len();
169        }
170
171        None
172    }
173
174    /// Bit of a value of a category, or 0 if the category or the value don't exist.
175    pub fn bit(&self, category: &str, value: &str) -> u32 {
176        match (self.category(category), self.first_bit(category)) {
177            (Some(category), Some(first)) => category.values.iter()
178                .position(|known| known == value)
179                .map_or(0, |index| 1 << ((first + index) % FLAG_BITS)),
180            _ => 0,
181        }
182    }
183
184    /// Bits of a query.
185    pub fn query_mask(&self, query: &VariationQuery) -> u32 {
186        query.iter().fold(0, |mask, (category, value)| mask | self.bit(category, value))
187    }
188
189    /// Returns the variations of a key that match a query, as the game finds them.
190    ///
191    /// # Arguments
192    ///
193    /// * `key` - Key of the entry, like `environment`.
194    /// * `query` - Value chosen for each category.
195    ///
196    /// # Returns
197    ///
198    /// The matches. If nothing matches, they're the ones of the same query with the map `default_map`.
199    pub fn matches(&self, key: &str, query: &VariationQuery) -> VariationMatches<'_> {
200        let mut matches = VariationMatches::default();
201        let Some(entry) = self.entry(key) else { return matches };
202
203        let find = |query: &VariationQuery| {
204            let mask = self.query_mask(query);
205            entry.variations.iter()
206                .filter(|variation| variation.flags == 0 || variation.flags & mask == mask)
207                .collect::<Vec<_>>()
208        };
209
210        matches.variations = find(query);
211        if matches.variations.is_empty() {
212            let mut query = query.clone();
213            query.insert(MAP_CATEGORY.to_owned(), DEFAULT_MAP.to_owned());
214            matches.variations = find(&query);
215            matches.used_default_map = true;
216        }
217
218        matches
219    }
220
221    /// Returns the variations of a key that match a query, treating a map the database doesn't list as unmatched.
222    ///
223    /// The game adds no bit for a map it doesn't list, so its first query matches every map. Tools that pick from
224    /// a list of maps want the `default_map` query instead, which this does.
225    ///
226    /// # Arguments
227    ///
228    /// * `key` - Key of the entry, like `environment`.
229    /// * `query` - Value chosen for each category.
230    ///
231    /// # Returns
232    ///
233    /// The matches.
234    pub fn matches_listed_map(&self, key: &str, query: &VariationQuery) -> VariationMatches<'_> {
235        let unlisted_map = match (self.category(MAP_CATEGORY), query.get(MAP_CATEGORY)) {
236            (Some(category), Some(map)) => !map.is_empty() && !category.values.contains(map),
237            _ => false,
238        };
239
240        if unlisted_map {
241            let mut query = query.clone();
242            query.insert(MAP_CATEGORY.to_owned(), DEFAULT_MAP.to_owned());
243
244            let mut matches = self.matches(key, &query);
245            matches.used_default_map = true;
246            matches
247        } else {
248            self.matches(key, query)
249        }
250    }
251
252    /// Returns, per category, the values allowed by some flags. Flags 0 allow every value.
253    pub fn allowed_values(&self, flags: u32) -> Vec<(&str, Vec<&str>)> {
254        let mut index = 0;
255        self.categories.iter().map(|category| {
256            let allowed = category.values.iter().enumerate()
257                .filter(|(value, _)| flags == 0 || flags >> ((index + value) % FLAG_BITS) & 1 == 1)
258                .map(|(_, value)| value.as_str())
259                .collect();
260
261            index += category.values.len();
262            (category.name.as_str(), allowed)
263        }).collect()
264    }
265
266    /// Returns the flags that allow exactly the provided values of each category. Categories left out allow all their values.
267    pub fn flags_for(&self, allowed: &BTreeMap<String, Vec<String>>) -> u32 {
268        let mut flags = 0;
269        let mut index = 0;
270        for category in &self.categories {
271            let allowed = allowed.get(&category.name);
272            for (value_index, value) in category.values.iter().enumerate() {
273                if allowed.is_none_or(|allowed| allowed.contains(value)) {
274                    flags |= 1 << ((index + value_index) % FLAG_BITS);
275                }
276            }
277
278            index += category.values.len();
279        }
280
281        flags
282    }
283
284    /// Checks the database for what makes it wrong for the game.
285    ///
286    /// # Returns
287    ///
288    /// A description of each problem: more values than bits, repeated names or values, no `default_map`, flags with bits
289    /// of no value, and variations that can never match.
290    pub fn validate(&self) -> Vec<String> {
291        let mut problems = vec![];
292        let total = self.value_count();
293        if total > FLAG_BITS {
294            problems.push(format!("{total} values in all categories: the flags have {FLAG_BITS} bits, so bits would collide"));
295        }
296
297        for (index, category) in self.categories.iter().enumerate() {
298            if self.categories[index + 1..].iter().any(|other| other.name == category.name) {
299                problems.push(format!("category '{}' appears twice", category.name));
300            }
301
302            for (value_index, value) in category.values.iter().enumerate() {
303                if category.values[value_index + 1..].contains(value) {
304                    problems.push(format!("value '{value}' appears twice in '{}'", category.name));
305                }
306            }
307        }
308
309        if self.category(MAP_CATEGORY).is_some_and(|map| !map.values.iter().any(|value| value == DEFAULT_MAP)) {
310            problems.push(format!("category '{MAP_CATEGORY}' has no value '{DEFAULT_MAP}': the game asks again with it when nothing matched"));
311        }
312
313        let used = if total >= FLAG_BITS { u32::MAX } else { (1 << total) - 1 };
314        for variation in self.entries.iter().flat_map(|entry| &entry.variations) {
315            if variation.flags & !used != 0 {
316                problems.push(format!("{}: flags have bits that belong to no value", variation.filename));
317            }
318
319            // Flags 0 allow everything.
320            if variation.flags != 0 {
321                if let Some((category, _)) = self.allowed_values(variation.flags).into_iter().find(|(_, allowed)| allowed.is_empty()) {
322                    problems.push(format!("{}: no {category} allowed, it can never match", variation.filename));
323                }
324            }
325        }
326
327        problems
328    }
329
330    /// Adds a value to a category, moving the bits of the flags of every variation so they keep allowing the same values.
331    ///
332    /// A variation that allowed every value of the category, without being unconstrained, also allows the new one.
333    ///
334    /// # Errors
335    ///
336    /// Returns [`RLibError::AssetVariationDbEditRefused`] if the category doesn't exist, the value is empty or already
337    /// exists, or every bit of the flags is in use.
338    pub fn add_value(&mut self, category: &str, value: &str) -> Result<()> {
339        let Some(category_index) = self.categories.iter().position(|known| known.name == category) else {
340            return Err(refused(format!("no category '{category}'")));
341        };
342
343        let values = &self.categories[category_index].values;
344        if value.is_empty() {
345            return Err(refused("the value is empty".to_owned()));
346        }
347
348        if values.iter().any(|known| known == value) {
349            return Err(refused(format!("'{value}' already exists in '{category}'")));
350        }
351
352        if self.value_count() >= FLAG_BITS {
353            return Err(refused(format!("all {FLAG_BITS} flag bits are in use: remove a value of any category first")));
354        }
355
356        let first = self.first_bit(category).unwrap_or_default();
357        let count = values.len();
358        let new_bit = first + count;
359        let moved = (0..self.value_count()).map(|bit| Some(if bit < new_bit { bit } else { bit + 1 })).collect::<Vec<_>>();
360        let category_bits = (first..first + count).fold(0u32, |bits, bit| bits | 1 << bit);
361
362        for variation in self.entries.iter_mut().flat_map(|entry| &mut entry.variations) {
363            let allowed_all = variation.flags != 0 && count > 0 && variation.flags & category_bits == category_bits;
364            variation.flags = move_bits(variation.flags, &moved);
365            if allowed_all {
366                variation.flags |= 1 << new_bit;
367            }
368        }
369
370        self.categories[category_index].values.push(value.to_owned());
371        Ok(())
372    }
373
374    /// Removes a value from a category, moving the bits of the flags of every variation so they keep allowing the same values.
375    ///
376    /// # Errors
377    ///
378    /// Returns [`RLibError::AssetVariationDbEditRefused`] if the category or the value don't exist, if the value is the
379    /// `default_map` map, or if a variation would allow nothing else, as its flags would become 0, which allows everything.
380    pub fn remove_value(&mut self, category: &str, value: &str) -> Result<()> {
381        let Some(category_index) = self.categories.iter().position(|known| known.name == category) else {
382            return Err(refused(format!("no category '{category}'")));
383        };
384
385        let Some(value_index) = self.categories[category_index].values.iter().position(|known| known == value) else {
386            return Err(refused(format!("no value '{value}' in '{category}'")));
387        };
388
389        if category == MAP_CATEGORY && value == DEFAULT_MAP {
390            return Err(refused(format!("the game asks again with map '{DEFAULT_MAP}' when nothing matched, so that value must stay")));
391        }
392
393        let removed_bit = self.first_bit(category).unwrap_or_default() + value_index;
394        let moved = (0..self.value_count()).map(|bit| match bit.cmp(&removed_bit) {
395            std::cmp::Ordering::Less => Some(bit),
396            std::cmp::Ordering::Equal => None,
397            std::cmp::Ordering::Greater => Some(bit - 1),
398        }).collect::<Vec<_>>();
399
400        let variations = self.entries.iter().flat_map(|entry| &entry.variations);
401        if let Some(variation) = variations.clone().find(|variation| variation.flags != 0 && move_bits(variation.flags, &moved) == 0) {
402            return Err(refused(format!("{} allows nothing else, removing '{value}' would turn it into an unconstrained variation (flags 0)", variation.filename)));
403        }
404
405        for variation in self.entries.iter_mut().flat_map(|entry| &mut entry.variations) {
406            variation.flags = move_bits(variation.flags, &moved);
407        }
408
409        self.categories[category_index].values.remove(value_index);
410        Ok(())
411    }
412
413    /// Renames a value of a category. The flags don't change.
414    ///
415    /// # Errors
416    ///
417    /// Returns [`RLibError::AssetVariationDbEditRefused`] if the category or the value don't exist, or the new name is
418    /// empty or already exists.
419    pub fn rename_value(&mut self, category: &str, from: &str, to: &str) -> Result<()> {
420        let Some(category) = self.categories.iter_mut().find(|known| known.name == category) else {
421            return Err(refused(format!("no category '{category}'")));
422        };
423
424        if to.is_empty() {
425            return Err(refused("the new name is empty".to_owned()));
426        }
427
428        if from != to && category.values.iter().any(|known| known == to) {
429            return Err(refused(format!("'{to}' already exists in '{}'", category.name)));
430        }
431
432        let Some(value) = category.values.iter_mut().find(|known| *known == from) else {
433            return Err(refused(format!("no value '{from}' in '{}'", category.name)));
434        };
435
436        *value = to.to_owned();
437        Ok(())
438    }
439}
440
441/// Moves every bit of some flags to the index `moved` has for it, dropping the ones it has None for. Flags 0 stay 0.
442fn move_bits(flags: u32, moved: &[Option<usize>]) -> u32 {
443    if flags == 0 {
444        return 0;
445    }
446
447    moved.iter().take(FLAG_BITS).enumerate()
448        .filter(|(bit, _)| flags >> bit & 1 == 1)
449        .filter_map(|(_, to)| *to)
450        .fold(0, |out, to| out | 1 << to)
451}
452
453/// Error of a refused edit.
454fn refused(reason: String) -> RLibError {
455    RLibError::AssetVariationDbEditRefused(reason)
456}