Skip to main content

rpfm_lib/files/asset_variation_db/
xml.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//! XML forms of asset variation databases.
12//!
13//! The XML form in the packs lists the values of a category as repeated `value` attributes of one element, which
14//! XML doesn't allow, so it's read with the small parser of this module instead of a standard one.
15
16use crate::xml::{XmlElement, parse_elements};
17
18use super::*;
19
20/// Categories of the Assembly Kit's environment map, in the order their bits are given.
21const ENVIRONMENT_MAP_CATEGORIES: [&str; 8] = ["map", "climate", "season", "climate_change_index", "precipitation", "time_of_day", "battle_types", "snow_attrition"];
22
23/// Key of the entry the environment map becomes.
24const ENVIRONMENT_KEY: &str = "environment";
25
26//---------------------------------------------------------------------------//
27//                    Implementation of AssetVariationDb
28//---------------------------------------------------------------------------//
29
30impl AssetVariationDb {
31
32    /// Returns the database in the XML form of the packs: UTF-8, CRLF line ends, tab indentation and single-quoted attributes.
33    pub fn to_xml(&self) -> String {
34        let mut xml = String::from("<warscape_asset_variation_db serialise_version='1'>\r\n\t<meta_data_entries>\r\n");
35        for category in &self.categories {
36            xml.push_str(&format!("\t\t<meta_data_entry serialise_version='1' name='{}'>\r\n\t\t\t<values", escape(&category.name)));
37            for value in &category.values {
38                xml.push_str(&format!(" value='{}'", escape(value)));
39            }
40            xml.push_str("/>\r\n\t\t</meta_data_entry>\r\n");
41        }
42
43        xml.push_str("\t</meta_data_entries>\r\n\t<entries>\r\n");
44        for entry in &self.entries {
45            xml.push_str(&format!("\t\t<entry key='{}'>\r\n\t\t\t<value serialise_version='1'>\r\n\t\t\t\t<variations>\r\n", escape(&entry.key)));
46            for variation in &entry.variations {
47                xml.push_str(&format!("\t\t\t\t\t<variation serialise_version='1' filename='{}' flags='{}'/>\r\n", escape(&variation.filename), variation.flags));
48            }
49            xml.push_str("\t\t\t\t</variations>\r\n\t\t\t</value>\r\n\t\t</entry>\r\n");
50        }
51
52        xml.push_str("\t</entries>\r\n</warscape_asset_variation_db>\r\n");
53        xml
54    }
55
56    /// Reads a database from the XML form of the packs. Whitespace between elements doesn't matter.
57    ///
58    /// # Errors
59    ///
60    /// Returns [`RLibError::DecodingAssetVariationDbXml`] if the XML is malformed, or doesn't have the structure of the database.
61    pub fn from_xml(xml: &str) -> Result<Self> {
62        let (elements, roots) = parse_elements(xml).map_err(xml_error)?;
63        let [root] = roots.as_slice() else { return Err(xml_error("there must be one root element".to_owned())) };
64        let root = &elements[*root];
65        if root.name != "warscape_asset_variation_db" {
66            return Err(xml_error("the root element is not warscape_asset_variation_db".to_owned()));
67        }
68
69        check_version(root)?;
70
71        let mut db = Self {
72            serialise_version: 1,
73            ..Default::default()
74        };
75
76        for &index in &child(&elements, root, "meta_data_entries")?.children {
77            let element = &elements[index];
78            if element.name != "meta_data_entry" {
79                return Err(xml_error(format!("unexpected <{}> in meta_data_entries", element.name)));
80            }
81
82            check_version(element)?;
83            let values = child(&elements, element, "values")?;
84            let values = values.attributes.iter().map(|(name, value)| if name == "value" {
85                Ok(value.to_owned())
86            } else {
87                Err(xml_error(format!("<values> attribute '{name}' is not 'value'")))
88            }).collect::<Result<_>>()?;
89
90            db.categories.push(VariationCategory {
91                name: attribute(element, "name")?.to_owned(),
92                values,
93            });
94        }
95
96        for &index in &child(&elements, root, "entries")?.children {
97            let element = &elements[index];
98            if element.name != "entry" {
99                return Err(xml_error(format!("unexpected <{}> in entries", element.name)));
100            }
101
102            let value = child(&elements, element, "value")?;
103            check_version(value)?;
104
105            let mut entry = VariationEntry {
106                key: attribute(element, "key")?.to_owned(),
107                variations: vec![],
108            };
109
110            for &index in &child(&elements, value, "variations")?.children {
111                let variation = &elements[index];
112                if variation.name != "variation" {
113                    return Err(xml_error(format!("unexpected <{}> in variations", variation.name)));
114                }
115
116                check_version(variation)?;
117                let filename = attribute(variation, "filename")?.to_owned();
118                let flags = attribute(variation, "flags")?;
119
120                // Only plain digits: the parser would also accept a sign.
121                let flags = flags.parse::<u32>().ok().filter(|_| flags.bytes().all(|byte| byte.is_ascii_digit()))
122                    .ok_or_else(|| xml_error(format!("flags '{flags}' of {filename} is not an unsigned 32-bit number")))?;
123
124                entry.variations.push(Variation { filename, flags });
125            }
126
127            db.entries.push(entry);
128        }
129
130        Ok(db)
131    }
132
133    /// Builds a database from its source in the Assembly Kit, `raw_data/warscape_asset_variation_db/environment_map.xml`.
134    ///
135    /// Each `environment_file` lists the values it allows per category, comma-separated, and an empty or missing list allows every value.
136    /// The database gets the single entry `environment`. The categories go in a fixed order, with their values sorted:
137    /// the bit order the game's tools use isn't known, but it doesn't change what matches.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`RLibError::DecodingAssetVariationDbXml`] if the XML is malformed, has no `environment_file`, or more values than bits.
142    pub fn from_environment_map(xml: &str) -> Result<Self> {
143        let (elements, _) = parse_elements(xml).map_err(xml_error)?;
144        let files = elements.iter().filter(|element| element.name == "environment_file").collect::<Vec<_>>();
145        if files.is_empty() {
146            return Err(xml_error("no <environment_file> entries".to_owned()));
147        }
148
149        let lists = files.iter().map(|file| {
150            ENVIRONMENT_MAP_CATEGORIES.iter().map(|category| {
151                file.attributes.iter()
152                    .find(|(name, _)| name == category)
153                    .map(|(_, list)| list.split(',').map(|value| value.trim_matches([' ', '\t'])).filter(|value| !value.is_empty()).collect::<Vec<_>>())
154                    .unwrap_or_default()
155            }).collect::<Vec<_>>()
156        }).collect::<Vec<_>>();
157
158        let mut db = Self {
159            serialise_version: 1,
160            ..Default::default()
161        };
162
163        for (index, name) in ENVIRONMENT_MAP_CATEGORIES.iter().enumerate() {
164            let mut values = lists.iter().flat_map(|file| file[index].iter().map(|value| value.to_string())).collect::<Vec<_>>();
165            values.sort();
166            values.dedup();
167            db.categories.push(VariationCategory { name: name.to_string(), values });
168        }
169
170        if db.value_count() > FLAG_BITS {
171            return Err(xml_error(format!("more than {FLAG_BITS} category values, the flags would collide")));
172        }
173
174        let mut entry = VariationEntry {
175            key: ENVIRONMENT_KEY.to_owned(),
176            variations: Vec::with_capacity(files.len()),
177        };
178
179        for (file, lists) in files.iter().zip(&lists) {
180            let allowed = ENVIRONMENT_MAP_CATEGORIES.iter().zip(lists)
181                .filter(|(_, list)| !list.is_empty())
182                .map(|(category, list)| (category.to_string(), list.iter().map(|value| value.to_string()).collect()))
183                .collect();
184
185            entry.variations.push(Variation {
186                filename: file.attributes.iter().find(|(name, _)| name == "filename").map(|(_, value)| value.to_owned()).unwrap_or_default(),
187                flags: db.flags_for(&allowed),
188            });
189        }
190
191        db.entries.push(entry);
192        Ok(db)
193    }
194}
195
196/// Returns the only child of an element with the provided name.
197fn child<'a>(elements: &'a [XmlElement], parent: &XmlElement, name: &str) -> Result<&'a XmlElement> {
198    let mut found = parent.children.iter().map(|&index| &elements[index]).filter(|element| element.name == name);
199    match (found.next(), found.next()) {
200        (Some(element), None) => Ok(element),
201        (Some(_), Some(_)) => Err(xml_error(format!("more than one <{name}> in <{}>", parent.name))),
202        (None, _) => Err(xml_error(format!("missing <{name}> in <{}>", parent.name))),
203    }
204}
205
206/// Returns the value of an attribute of an element.
207fn attribute<'a>(element: &'a XmlElement, name: &str) -> Result<&'a str> {
208    element.attributes.iter()
209        .find(|(attribute, _)| attribute == name)
210        .map(|(_, value)| value.as_str())
211        .ok_or_else(|| xml_error(format!("<{}> has no {name} attribute", element.name)))
212}
213
214/// Fails if the `serialise_version` of an element isn't 1.
215fn check_version(element: &XmlElement) -> Result<()> {
216    if attribute(element, "serialise_version")? != "1" {
217        return Err(xml_error(format!("<{}> serialise_version is not 1", element.name)));
218    }
219
220    Ok(())
221}
222
223/// Escapes a value for a single-quoted attribute.
224fn escape(value: &str) -> String {
225    value.replace('&', "&amp;").replace('<', "&lt;").replace('\'', "&apos;")
226}
227
228/// Error of an XML database that can't be read.
229fn xml_error(reason: String) -> RLibError {
230    RLibError::DecodingAssetVariationDbXml(reason)
231}