Skip to main content

rpfm_lib/integrations/assembly_kit/
table_definition.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//! Assembly Kit table definition parsing and schema generation.
12//!
13//! This module handles the parsing of Assembly Kit schema files (table structure definitions)
14//! and their conversion to RPFM's internal schema format. It supports three different Assembly
15//! Kit versions used across Total War games.
16//!
17//! # Assembly Kit Schema Formats
18//!
19//! Different Total War games use different schema file formats:
20//!
21//! - **Version 0** (Empire, Napoleon): `.xsd` XML schema files with basic type and constraint information
22//! - **Version 1** (Shogun 2): `TWaD_*.xml` files with enhanced metadata
23//! - **Version 2** (Rome 2+): `TWaD_*.xml` files with full relationship data and field descriptions
24//!
25//! # Main Types
26//!
27//! ## Version 1 & 2 Formats
28//!
29//! - [`RawDefinition`]: Represents a complete table definition with all fields
30//! - [`RawField`]: Individual field definition with type, constraints, and relationship info
31//! - [`RawRelationshipsTable`]: Foreign key relationships between tables
32//! - [`RawRelationship`]: Single foreign key relationship
33//!
34//! ## Version 0 Format (Legacy)
35//!
36//! - [`RawDefinitionV0`]: XSD schema root structure
37//! - [`Element`]: XSD element with type and constraint information
38//! - [`Index`]: Database index definition (used to derive relationships)
39//!
40//! # Functionality
41//!
42//! The main operations this module provides:
43//!
44//! 1. **Batch Reading**: [`RawDefinition::read_all()`] reads all table definitions from a directory
45//! 2. **Individual Reading**: [`RawDefinition::read()`] parses a single definition file
46//! 3. **Field Filtering**: [`RawDefinition::get_non_localisable_fields()`] separates translatable fields
47//! 4. **Schema Conversion**: `From<&RawDefinition>` for [`Definition`] converts to RPFM format
48//!
49//! # Version 0 Processing
50//!
51//! Version 0 (Empire/Napoleon) uses a two-pass approach:
52//! 1. First pass: Parse XSD files and extract basic field information and primary keys
53//! 2. Second pass: Analyze index definitions to derive foreign key relationships
54//!
55//! This is necessary because Version 0 uses database-style indexes rather than explicit
56//! foreign key declarations.
57//!
58//! # Type Mapping
59//!
60//! Assembly Kit types are mapped to RPFM field types:
61//! - `yesno` → `Boolean`
62//! - `single` → `F32`, `double` → `F64`
63//! - `integer` → `I32`, `autonumber`/`card64` → `I64`
64//! - `colour` → `ColourRGB`
65//! - `text`/`expression` → `StringU8`/`StringU16` (or optional variants)
66
67use itertools::Itertools;
68use serde_derive::Deserialize;
69use serde_xml_rs::from_reader;
70
71use std::fs::File;
72use std::io::{BufReader, Read};
73use std::path::Path;
74
75use crate::error::{Result, RLibError};
76
77use super::*;
78use super::get_raw_definition_paths;
79use super::localisable_fields::RawLocalisableField;
80use super::table_data::RawTableRow;
81
82//---------------------------------------------------------------------------//
83// Types for parsing the Assembly Kit Schema Files into.
84//---------------------------------------------------------------------------//
85
86/// Raw table definition parsed from Assembly Kit schema files.
87///
88/// This is the raw equivalent to RPFM's [`Definition`] struct. In Assembly Kit files,
89/// this corresponds to a `TWaD_*.xml` file (versions 1-2) or `.xsd` file (version 0).
90///
91/// # Fields
92///
93/// * `name` - Table name with `.xml` extension (e.g., `"units_tables.xml"`)
94/// * `fields` - All field definitions for this table
95///
96/// # Example Structure
97///
98/// A `TWaD_units_tables.xml` file contains field definitions like:
99/// ```xml
100/// <root>
101///   <field primary_key="1" name="key" field_type="text" required="1"/>
102///   <field primary_key="0" name="category" field_type="text" required="0"
103///          column_source_table="unit_categories_tables"
104///          column_source_column="key"/>
105/// </root>
106/// ```
107#[derive(Clone, Debug, Default, Deserialize)]
108#[serde(rename = "root")]
109pub struct RawDefinition {
110
111    /// Table name with `.xml` extension (e.g., `"units_tables.xml"`) and without the 'TWaD_' prefix.
112    pub name: Option<String>,
113
114    /// All the field definitions within this table definition.
115    #[serde(rename = "field")]
116    pub fields: Vec<RawField>,
117}
118
119/// Individual field definition from Assembly Kit schema.
120///
121/// This is the raw equivalent to RPFM's [`Field`] struct, containing all metadata
122/// about a single table column.
123///
124/// # Type Information
125///
126/// Assembly Kit uses string-based type names:
127/// - `"yesno"` - Boolean value
128/// - `"single"`, `"double"` - Floating point numbers
129/// - `"integer"` - 32-bit integer
130/// - `"autonumber"`, `"card64"` - 64-bit integer (often auto-incrementing)
131/// - `"text"`, `"expression"` - String data
132/// - `"colour"` - RGB color value
133///
134/// # Foreign Key Relationships
135///
136/// Relationships are defined via `column_source_table` and `column_source_column`:
137/// - First element in `column_source_column` is the referenced primary key
138/// - Additional elements (if present) are lookup columns for concatenated display
139#[derive(Clone, Debug, Default, Deserialize)]
140#[serde(rename = "field")]
141pub struct RawField {
142
143    /// Primary key flag (`"1"` = true, `"0"` = false).
144    pub primary_key: String,
145
146    /// Field name (column name in the table).
147    pub name: String,
148
149    /// Assembly Kit type name (see struct documentation for type mapping).
150    pub field_type: String,
151
152    /// Required field flag (`"1"` = required, `"0"` = optional).
153    pub required: String,
154
155    /// Default value for this field when creating new rows.
156    pub default_value: Option<String>,
157
158    /// Maximum allowed string length for text fields.
159    pub max_length: Option<String>,
160
161    /// Filename flag - indicates this field contains a game file path.
162    pub is_filename: Option<String>,
163
164    /// Relative path where referenced files should be located.
165    ///
166    /// Multiple paths can be specified, separated by semicolons.
167    pub filename_relative_path: Option<String>,
168
169    /// Fragment path (internal use, not useful for modders).
170    pub fragment_path: Option<String>,
171
172    /// Referenced column names for foreign key relationships.
173    ///
174    /// First element is the referenced primary key column.
175    /// Additional elements are lookup columns for composite display.
176    pub column_source_column: Option<Vec<String>>,
177
178    /// Referenced table name for foreign key relationships.
179    pub column_source_table: Option<String>,
180
181    /// Human-readable description of the field's purpose.
182    pub field_description: Option<String>,
183
184    /// Encyclopaedia export flag (`"1"` = export, `"0"` = don't export).
185    ///
186    /// Indicates if this field should be included in game encyclopaedia exports.
187    pub encyclopaedia_export: Option<String>,
188
189    /// Highlight color flag for marking unused/deprecated fields.
190    ///
191    /// `"#c8c8c8"` (gray) indicates an unused field in Warhammer 3.
192    pub highlight_flag: Option<String>,
193
194    /// Custom flag for old game (Empire/Napoleon/Shogun 2) type handling.
195    ///
196    /// When true, uses UTF-16 strings instead of UTF-8.
197    pub is_old_game: Option<bool>,
198}
199
200/// Version 0 (Empire/Napoleon) XSD schema root structure.
201///
202/// Empire and Napoleon use `.xsd` XML Schema Definition files instead of
203/// the `TWaD_` format used in later games. This struct represents the root
204/// of such a schema file.
205#[derive(Clone, Debug, Default, Deserialize)]
206#[serde(rename = "xsd_schema")]
207pub struct RawDefinitionV0 {
208    /// XSD elements defining the table structure.
209    pub xsd_element: Vec<Element>,
210}
211
212/// Represents an XSD element definition from Assembly Kit v0 schema files.
213///
214/// Elements are the core building blocks of XSD schemas, representing individual
215/// fields in database tables. Each element can have type constraints (via `SimpleType`),
216/// nested structures (via `ComplexType`), and metadata annotations.
217///
218/// # Field Mapping
219///
220/// - `name`: Column name in the database table
221/// - `jet_type`: Microsoft Jet database type (e.g., "Text", "Long", "Boolean")
222/// - `min_occurs`: Minimum occurrences (0 = optional, 1 = required)
223/// - `xsd_annotation`: Contains metadata like index definitions
224/// - `xsd_simple_type`: Type constraints (e.g., string max length)
225/// - `xsd_complex_type`: Nested element sequences for complex types
226#[derive(Clone, Debug, Default, Deserialize)]
227#[serde(rename = "xsd_element")]
228pub struct Element {
229    /// The name of this element (field/column name).
230    #[serde(rename = "@name")]
231    pub name: Option<String>,
232
233    /// Microsoft Jet database type identifier.
234    ///
235    /// Common values: "Text" (string), "Long" (i32), "Boolean", "Single" (f32), "Double" (f64).
236    #[serde(rename = "@od_jetType")]
237    pub jet_type: Option<String>,
238
239    /// Minimum number of occurrences for this element.
240    ///
241    /// - `0`: Field is optional
242    /// - `1` or higher: Field is required
243    #[serde(rename = "@minOccurs")]
244    pub min_occurs: Option<i32>,
245
246    /// Annotation containing metadata like index definitions.
247    #[serde(rename = "xsd_annotation")]
248    pub xsd_annotation: Option<Annotation>,
249
250    /// Simple type definition with constraints (e.g., max string length).
251    #[serde(rename = "xsd_simpleType")]
252    pub xsd_simple_type: Option<Vec<SimpleType>>,
253
254    /// Complex type definition for nested element sequences.
255    #[serde(rename = "xsd_complexType")]
256    pub xsd_complex_type: Option<Vec<ComplexType>>,
257}
258
259/// Defines a simple type with restrictions in XSD schemas.
260///
261/// Simple types are used to apply constraints to basic data types, such as
262/// limiting the maximum length of a string field.
263#[derive(Clone, Debug, Default, Deserialize)]
264#[serde(rename = "xsd_simpleType")]
265pub struct SimpleType {
266    /// The restriction applied to this simple type (e.g., max length).
267    pub xsd_restriction: Option<Restriction>,
268}
269
270/// Defines a complex type containing nested element sequences.
271///
272/// Complex types are used when a field contains multiple sub-elements organized
273/// in a specific order. In Assembly Kit schemas, these are typically used for
274/// nested table structures, though most tables use simple flat structures.
275#[derive(Clone, Debug, Default, Deserialize)]
276#[serde(rename = "xsd_complexType")]
277pub struct ComplexType {
278    /// The ordered sequence of elements within this complex type.
279    #[serde(rename = "xsd_sequence")]
280    pub xsd_sequence: Sequence,
281}
282
283/// Represents an ordered sequence of XSD elements.
284///
285/// Sequences define the order in which child elements must appear within
286/// a complex type. Each element in the sequence can itself be a simple or
287/// complex type.
288#[derive(Clone, Debug, Default, Deserialize)]
289#[serde(rename = "xsd_sequence")]
290pub struct Sequence {
291    /// The ordered list of elements in this sequence.
292    pub xsd_element: Vec<Element>,
293}
294
295/// Defines restrictions/constraints on an XSD simple type.
296///
297/// Restrictions are used to constrain the values of a simple type, such as
298/// limiting the maximum length of a string. The `base` field specifies which
299/// base type the restriction applies to.
300#[derive(Clone, Debug, Default, Deserialize)]
301#[serde(rename = "xsd_restriction")]
302pub struct Restriction {
303    /// The base XSD type being restricted (e.g., "xsd:string", "xsd:int").
304    #[serde(rename = "@base")]
305    pub base: String,
306
307    /// Maximum length constraint for string types.
308    #[serde(rename = "xsd_maxLength")]
309    pub max_lenght: Option<MaxLength>
310}
311
312/// Specifies the maximum length constraint for a string field.
313///
314/// This constraint limits how many characters a string field can contain.
315/// Used in XSD restrictions to define database column size limits.
316#[derive(Clone, Debug, Default, Deserialize)]
317#[serde(rename = "xsd_maxLength")]
318pub struct MaxLength {
319    /// The maximum number of characters allowed.
320    #[serde(rename = "@value")]
321    pub value: i32
322}
323
324/// Contains annotation metadata for XSD elements.
325///
326/// Annotations provide additional information about schema elements that isn't
327/// part of the core validation rules. In Assembly Kit schemas, annotations are
328/// primarily used to store database index definitions via the `AppInfo` structure.
329#[derive(Clone, Debug, Default, Deserialize)]
330#[serde(rename = "xsd_annotation")]
331pub struct Annotation {
332    /// Application-specific information, containing index definitions.
333    #[serde(rename = "xsd_appinfo")]
334    pub xsd_appinfo: Option<AppInfo>
335}
336
337/// Contains application-specific information within XSD annotations.
338///
339/// This structure holds database-specific metadata that extends the base XSD schema.
340/// In Assembly Kit schemas, it primarily contains index definitions that describe
341/// primary keys, foreign keys, and unique constraints on table columns.
342#[derive(Clone, Debug, Default, Deserialize)]
343#[serde(rename = "xsd_appinfo")]
344pub struct AppInfo {
345    /// List of database index definitions for this element.
346    #[serde(rename = "od_index")]
347    pub od_index: Option<Vec<Index>>
348}
349
350/// Defines a database index on a table column.
351///
352/// Indexes are used to derive foreign key relationships in Assembly Kit v0 schemas.
353/// Since v0 schemas don't explicitly define relationships between tables, RPFM
354/// infers them by matching index names across tables.
355///
356/// # Relationship Inference
357///
358/// When an index name appears in multiple tables, RPFM creates a foreign key
359/// relationship between them. For example:
360///
361/// - Table A has index "fk_building" on column "building_key"
362/// - Table B has index "fk_building" on column "key"
363/// - RPFM infers: A.building_key → B.key
364///
365/// # Boolean String Fields
366///
367/// The `primary`, `unique`, and `clustered` fields use string values "true"/"false"
368/// instead of booleans due to the XSD format.
369#[derive(Clone, Debug, Default, Deserialize)]
370#[serde(rename = "od_index")]
371pub struct Index {
372    /// The name of this index.
373    ///
374    /// Index names are used to match relationships across tables. Identical names
375    /// in different tables indicate a foreign key relationship.
376    #[serde(rename = "@index-name")]
377    pub name: String,
378
379    /// The column(s) this index applies to.
380    ///
381    /// Multiple columns are separated by semicolons (e.g., "col1;col2").
382    #[serde(rename = "@index-key")]
383    pub key: String,
384
385    /// Whether this is a primary key index ("true"/"false").
386    #[serde(rename = "@primary")]
387    pub primary: String,
388
389    /// Whether this index enforces uniqueness ("true"/"false").
390    #[serde(rename = "@unique")]
391    pub unique: String,
392
393    /// Whether this is a clustered index ("true"/"false").
394    #[serde(rename = "@clustered")]
395    pub clustered: String,
396}
397
398/// Foreign key relationships table from Assembly Kit.
399///
400/// This corresponds to the `TWaD_relationships.xml` file found in Version 2
401/// Assembly Kits (Rome 2+). It defines all foreign key relationships between tables.
402#[derive(Clone, Debug, Default, Deserialize)]
403#[serde(rename = "root")]
404pub struct RawRelationshipsTable {
405    /// Table name (should be "relationships").
406    pub name: Option<String>,
407
408    /// All foreign key relationships defined in the Assembly Kit.
409    #[serde(rename = "relationship")]
410    pub relationships: Vec<RawRelationship>,
411}
412
413/// Single foreign key relationship definition.
414///
415/// Defines a foreign key constraint from one table's column to another table's column.
416///
417/// # Example
418///
419/// A relationship from `units_tables.category` to `unit_categories_tables.key`:
420/// ```xml
421/// <relationship>
422///   <table_name>units_tables</table_name>
423///   <column_name>category</column_name>
424///   <foreign_table_name>unit_categories_tables</foreign_table_name>
425///   <foreign_column_name>key</foreign_column_name>
426/// </relationship>
427/// ```
428#[derive(Clone, Debug, Default, Deserialize)]
429pub struct RawRelationship {
430    /// Source table name containing the foreign key column.
431    pub table_name: String,
432
433    /// Source column name (the foreign key field).
434    pub column_name: String,
435
436    /// Referenced table name.
437    pub foreign_table_name: String,
438
439    /// Referenced column name (typically a primary key).
440    pub foreign_column_name: String
441}
442
443//---------------------------------------------------------------------------//
444// Implementations
445//---------------------------------------------------------------------------//
446
447/// Implementation of `RawDefinition`.
448impl RawDefinition {
449
450    /// Reads all table definitions from an Assembly Kit directory.
451    ///
452    /// This function scans the provided directory for Assembly Kit definition files
453    /// and parses them into [`RawDefinition`] structs. The parsing logic varies
454    /// significantly by version.
455    ///
456    /// # Version-Specific Behavior
457    ///
458    /// ## Version 1 & 2 (Shogun 2, Rome 2+)
459    /// - Reads `TWaD_*.xml` files directly
460    /// - Each file is a complete, self-contained definition
461    ///
462    /// ## Version 0 (Empire, Napoleon)
463    /// - Reads `.xsd` XML Schema files
464    /// - Uses two-pass processing:
465    ///   1. Parse all XSD files and extract field info + primary keys
466    ///   2. Analyze index definitions to derive foreign key relationships
467    /// - This is necessary because Version 0 uses database-style indexes rather than
468    ///   explicit foreign key declarations
469    ///
470    /// # Arguments
471    ///
472    /// * `raw_definitions_folder` - Directory containing Assembly Kit definition files
473    /// * `version` - Assembly Kit version (0 = Empire/Napoleon, 1 = Shogun 2, 2 = Rome 2+)
474    /// * `tables_to_skip` - Table names (without extension) to exclude from parsing
475    ///
476    /// # Returns
477    ///
478    /// Returns a vector of successfully parsed table definitions. Tables in the
479    /// blacklist or skip list are excluded.
480    ///
481    /// # Errors
482    ///
483    /// Returns an error if:
484    /// - The version is unsupported (not 0, 1, or 2)
485    /// - The directory cannot be read
486    /// - Any definition file has malformed XML
487    pub fn read_all(raw_definitions_folder: &Path, version: i16, tables_to_skip: &[&str]) -> Result<Vec<Self>> {
488        let definitions = get_raw_definition_paths(raw_definitions_folder, version)?;
489        match version {
490            2 | 1 => {
491                definitions.iter()
492                    .filter(|x| !BLACKLISTED_TABLES.contains(&x.file_name().unwrap().to_str().unwrap()))
493                    .filter(|x| {
494                        let table_name = x.file_stem().unwrap().to_str().unwrap().split_at(5).1;
495                        !tables_to_skip.par_iter().any(|vanilla_name| vanilla_name == &table_name)
496                    })
497                    .map(|x| Self::read(x, version))
498                    .collect::<Result<Vec<Self>>>()
499            }
500            0 => {
501                let v0s = definitions.iter()
502                    .filter(|x| !BLACKLISTED_TABLES.contains(&x.file_name().unwrap().to_str().unwrap()))
503                    .filter(|x| {
504                        let table_name = x.file_stem().unwrap().to_str().unwrap();
505                        !tables_to_skip.par_iter().any(|vanilla_name| vanilla_name == &table_name)
506                    })
507                    .filter_map(|x| RawDefinitionV0::read(x).transpose())
508                    .map(|def_v0| {
509
510                        // NOTE: This from processes the primary keys already.
511                        let raw = match def_v0 {
512                            Ok(ref def_v0) => Self::from(def_v0),
513                            Err(_) => Self::default(),
514                        };
515                        def_v0.map(|def_v0| (def_v0, raw))
516                    })
517                    .collect::<Result<Vec<(RawDefinitionV0, RawDefinition)>>>()?;
518
519                // We need to do a second pass because without the entire set available we cannot figure out the references.
520                Ok(v0s.iter()
521                    .map(|(def_v0, new_def)| {
522                        let mut new_def = new_def.clone();
523
524                        if let Some(elements) = def_v0.xsd_element.get(1) {
525                            if let Some(ref table_name) = elements.name {
526                                if let Some(ref ann) = elements.xsd_annotation {
527                                    if let Some(ref app) = ann.xsd_appinfo {
528                                        if let Some(ref od_index) = app.od_index {
529                                            for index in od_index {
530
531                                                // Ignore indexes of unused fields, the primary key, and field-specific indexes.
532                                                if index.name == "PrimaryKey" || index.name == index.key.trim() {
533                                                    continue;
534                                                }
535
536                                                // Indexes follow the format "remotetablelocaltable", with a 61 char limit. To find the remote table,
537                                                // we need to remove the local one, and to do so, we need to find what part of the local one is actually in the index name.
538                                                let remote_table_name = if index.name.chars().count() == 61 {
539                                                    let mut table_name = table_name.clone();
540                                                    let mut remote_table_name = String::new();
541                                                    loop {
542                                                        if index.name.ends_with(&*table_name) {
543                                                            remote_table_name = index.name.clone();
544                                                            if let Some(sub) = index.name.len().checked_sub(table_name.len()) {
545                                                                remote_table_name.truncate(sub);
546                                                            } else {
547                                                                remote_table_name = String::new();
548                                                            }
549                                                            break;
550                                                        } else {
551                                                            if table_name.is_empty() {
552                                                                break;
553                                                            }
554
555                                                            table_name.pop();
556                                                        }
557                                                    }
558
559                                                    remote_table_name
560                                                } else {
561                                                    let mut remote_table_name = index.name.clone();
562                                                    if let Some(sub) = index.name.len().checked_sub(table_name.len()) {
563                                                        remote_table_name.truncate(sub);
564                                                    } else {
565                                                        remote_table_name = String::new();
566                                                    }
567                                                    remote_table_name
568                                                };
569
570                                                // Now we need to find the primary key of the remote table, if any.
571                                                if !remote_table_name.is_empty() {
572                                                    if let Some(remote_def) = v0s.par_iter().find_map_first(|(def_v0, new_def)| {
573                                                        if let Some(elements) = def_v0.xsd_element.get(1) {
574                                                            if let Some(ref table_name) = elements.name {
575                                                                if table_name == &remote_table_name {
576                                                                    Some(new_def)
577                                                                } else { None }
578                                                            } else { None }
579                                                        } else { None }
580                                                    }) {
581
582                                                        // No fucking clue if ANY reference is to a multikey table, but if is, we'll use the first key as ref key, and the rest as lookups.
583                                                        let primary_keys = remote_def.fields.iter().filter(|x| x.primary_key == "1" || x.name == "key").collect::<Vec<_>>();
584                                                        if !primary_keys.is_empty() {
585                                                            for field in &mut new_def.fields {
586                                                                if field.name == index.key.trim() {
587                                                                    field.column_source_table = Some(remote_table_name.to_string());
588                                                                    field.column_source_column = Some(primary_keys.iter().map(|x| x.name.to_string()).collect());
589                                                                }
590                                                            }
591                                                        }
592                                                    }
593                                                }
594                                            }
595                                        }
596                                    }
597                                }
598                            }
599                        }
600                        new_def
601                    })
602                    .collect())
603            }
604            _ => Err(RLibError::AssemblyKitUnsupportedVersion(version))
605        }
606    }
607
608    /// Parses a single Assembly Kit definition file.
609    ///
610    /// Reads and parses one table definition file from the Assembly Kit.
611    ///
612    /// # Arguments
613    ///
614    /// * `raw_definition_path` - Path to the definition file (e.g., `TWaD_units_tables.xml`)
615    /// * `version` - Assembly Kit version (1 = Shogun 2, 2 = Rome 2+)
616    ///
617    /// # Returns
618    ///
619    /// Returns the parsed [`RawDefinition`] with the table name set to the filename
620    /// without the `TWaD_` prefix (e.g., `"units_tables.xml"`).
621    ///
622    /// # Errors
623    ///
624    /// Returns an error if:
625    /// - The version is not 1 or 2 (use [`RawDefinitionV0::read()`] for version 0)
626    /// - The file cannot be opened (returns [`RLibError::AssemblyKitNotFound`])
627    /// - The XML is malformed
628    ///
629    /// # Note
630    ///
631    /// For Version 0 (Empire/Napoleon), use [`RawDefinitionV0::read()`] instead as the
632    /// file format is completely different (.xsd vs .xml).
633    pub fn read(raw_definition_path: &Path, version: i16) -> Result<Self> {
634        match version {
635            2 | 1 => {
636                let definition_file = BufReader::new(File::open(raw_definition_path).map_err(|_| RLibError::AssemblyKitNotFound)?);
637                let mut definition: Self = from_reader(definition_file)?;
638                definition.name = Some(raw_definition_path.file_name().unwrap().to_str().unwrap().split_at(5).1.to_string());
639                Ok(definition)
640            }
641
642            _ => Err(RLibError::AssemblyKitUnsupportedVersion(version))
643        }
644    }
645
646    /// Filters out localisable fields from the definition.
647    ///
648    /// Returns only the fields that are not marked as localisable (translatable) and
649    /// are present in the test row data. This is used when processing Assembly Kit
650    /// table data to separate regular fields from translation fields.
651    ///
652    /// # Arguments
653    ///
654    /// * `raw_localisable_fields` - List of all localisable fields from `TExc_LocalisableFields.xml`
655    /// * `test_row` - Sample row data used to verify field presence
656    ///
657    /// # Returns
658    ///
659    /// Returns a vector of [`Field`] instances for non-localisable fields that exist
660    /// in the test data.
661    ///
662    /// # Note
663    ///
664    /// Fields are excluded if:
665    /// - They're listed in `raw_localisable_fields` for this table
666    /// - They don't appear in the test row
667    /// - They have a "state" attribute (marked as modified/deprecated)
668    pub fn get_non_localisable_fields(&self, raw_localisable_fields: &[RawLocalisableField], test_row: &RawTableRow) -> Vec<Field> {
669        let raw_table_name = &self.name.as_ref().unwrap()[..self.name.as_ref().unwrap().len() - 4];
670        let localisable_fields_names = raw_localisable_fields.iter()
671            .filter(|x| x.table_name == raw_table_name)
672            .map(|x| &*x.field)
673            .collect::<Vec<&str>>();
674
675        self.fields.iter()
676            .filter(|x| match test_row.fields.iter().find(|y| x.name == y.field_name) {
677                Some(y) => y.state.is_none(),
678                None => false,
679            })
680            .filter(|x| !localisable_fields_names.contains(&&*x.name))
681            .map(From::from)
682            .collect::<Vec<Field>>()
683    }
684}
685
686impl From<&RawDefinition> for Definition {
687    fn from(raw_definition: &RawDefinition) -> Self {
688        let fields = raw_definition.fields.iter().map(From::from).collect::<Vec<_>>();
689        Self::new_with_fields(-100, &fields, &[], None)
690    }
691}
692
693
694impl From<&RawField> for Field {
695    fn from(raw_field: &RawField) -> Self {
696
697        let is_old_game = raw_field.is_old_game.unwrap_or(false);
698
699        let field_type = match &*raw_field.field_type {
700            "yesno" => FieldType::Boolean,
701            "single" => FieldType::F32,
702            "double" => FieldType::F64,
703            "integer" => FieldType::I32,
704            "autonumber" | "card64" => FieldType::I64,
705            "colour" => FieldType::ColourRGB,
706            "expression" | "text" => {
707                if raw_field.required == "1" {
708                    if is_old_game {
709                        FieldType::StringU16
710                    } else {
711                        FieldType::StringU8
712                    }
713                }
714                else if is_old_game {
715                    FieldType::OptionalStringU16
716                } else {
717                    FieldType::OptionalStringU8
718                }
719            },
720            _ => if is_old_game {
721                FieldType::StringU16
722            } else {
723                FieldType::StringU8
724            },
725        };
726
727        let (is_reference, lookup) = if let Some(x) = &raw_field.column_source_table {
728            if let Some(y) = &raw_field.column_source_column {
729                if y.len() > 1 { (Some((x.to_owned(), y[0].to_owned())), Some(y[1..].to_vec()))}
730                else { (Some((x.to_owned(), y[0].to_owned())), None) }
731            } else { (None, None) }
732        }
733        else { (None, None) };
734
735        // CA sometimes uses comma as separator, and has random spaces between paths.
736        let filename_relative_path = raw_field.filename_relative_path.clone().map(|x| {
737            x.split(',').map(|y| y.trim()).join(";")
738        });
739
740        // Some fields are marked as filename, but only have fragment paths, which do not seem to correlate to game file paths.
741        // We need to disable those to avoid false positives on diagnostics.
742        let is_filename = match raw_field.is_filename {
743            Some(_) => !(raw_field.fragment_path.is_some() && raw_field.filename_relative_path.is_none()),
744            None => false,
745        };
746
747        Self {
748            name: raw_field.name.to_owned(),
749            field_type,
750            is_key: raw_field.primary_key == "1",
751            default_value: raw_field.default_value.clone(),
752            is_filename,
753            filename_relative_path,
754            is_reference,
755            lookup,
756            description: if let Some(x) = &raw_field.field_description { x.to_owned() } else { String::new() },
757            ..Default::default()
758        }
759    }
760}
761
762impl RawDefinitionV0 {
763
764    /// Parses a Version 0 (Empire/Napoleon) XSD schema file.
765    ///
766    /// Reads and parses an XSD (XML Schema Definition) file from the Empire or
767    /// Napoleon Assembly Kit. The XSD format is significantly different from the
768    /// `TWaD_` format used in later games.
769    ///
770    /// # Arguments
771    ///
772    /// * `raw_definition_path` - Path to the `.xsd` file
773    ///
774    /// # Returns
775    ///
776    /// Returns `Ok(Some(definition))` if the file was parsed successfully, `Ok(None)`
777    /// if the file was empty, or an error if parsing failed.
778    ///
779    /// # Errors
780    ///
781    /// Returns an error if:
782    /// - The file cannot be opened (returns [`RLibError::AssemblyKitNotFound`])
783    /// - The XML/XSD is malformed
784    ///
785    /// # Implementation Note
786    ///
787    /// Due to limitations in `serde_xml_rs`, this function performs extensive string
788    /// replacements on the XSD content before parsing to normalize XML namespace
789    /// prefixes (`xsd:` and `xs:` → `xsd_`, `od:` → `od_`).
790    pub fn read(raw_definition_path: &Path) -> Result<Option<Self>> {
791        let mut definition_file = BufReader::new(File::open(raw_definition_path).map_err(|_| RLibError::AssemblyKitNotFound)?);
792
793        // Before deserializing the data, due to limitations of serde_xml_rs, we have to rename all rows, because unique names for
794        // rows in each file is not supported for deserializing. Same for the fields, we have to change them to something more generic.
795        let mut buffer = String::new();
796        definition_file.read_to_string(&mut buffer)?;
797
798        if buffer.is_empty() {
799            return Ok(None)
800        }
801
802        // Rust doesn't like : in variable names when deserializing.
803        buffer = buffer.replace("xsd:schema", "xsd_schema");
804        buffer = buffer.replace("xsd:element", "xsd_element");
805        buffer = buffer.replace("xsd:complexType", "xsd_complexType");
806        buffer = buffer.replace("xsd:sequence", "xsd_sequence");
807        buffer = buffer.replace("xsd:attribute", "xsd_attribute");
808        buffer = buffer.replace("xsd:annotation", "xsd_annotation");
809        buffer = buffer.replace("xsd:appinfo", "xsd_appinfo");
810        buffer = buffer.replace("od:index", "od_index");
811        buffer = buffer.replace("xsd:sequence", "xsd_sequence");
812        buffer = buffer.replace("xsd:simpleType", "xsd_simpleType");
813        buffer = buffer.replace("xsd:restriction", "xsd_restriction");
814        buffer = buffer.replace("xsd:maxLength", "xsd_maxLength");
815        buffer = buffer.replace("od:jetType", "od_jetType");
816
817        buffer = buffer.replace("xs:schema", "xsd_schema");
818        buffer = buffer.replace("xs:element", "xsd_element");
819        buffer = buffer.replace("xs:complexType", "xsd_complexType");
820        buffer = buffer.replace("xs:sequence", "xsd_sequence");
821        buffer = buffer.replace("xs:attribute", "xsd_attribute");
822        buffer = buffer.replace("xs:annotation", "xsd_annotation");
823        buffer = buffer.replace("xs:appinfo", "xsd_appinfo");
824        buffer = buffer.replace("xs:sequence", "xsd_sequence");
825        buffer = buffer.replace("xs:simpleType", "xsd_simpleType");
826        buffer = buffer.replace("xs:restriction", "xsd_restriction");
827        buffer = buffer.replace("xs:maxLength", "xsd_maxLength");
828
829        // Only if the table has data we deserialize it. If not, we just create an empty one.
830        let definition: RawDefinitionV0 = from_reader(buffer.as_bytes())?;
831
832        //dbg!(&definition);
833        Ok(Some(definition))
834    }
835}
836
837/// Old games don't use references, but rather indexes like a database. This means we're unable to find
838/// the referenced column without having the reference definition. So ref data needs to be calculated after this.
839impl From<&RawDefinitionV0> for RawDefinition {
840    fn from(value: &RawDefinitionV0) -> Self {
841        let mut definition = Self::default();
842
843        // Second element has the fields.
844        if let Some(elements) = value.xsd_element.get(1) {
845            definition.name = elements.name.clone().map(|x| format!("{x}.xml"));
846
847            // Try to get the indexes to check what do we need to mark as key.
848            let primary_keys = if let Some(ref ann) = elements.xsd_annotation {
849                if let Some(ref app) = ann.xsd_appinfo {
850                    if let Some(ref od_index) = app.od_index {
851                        od_index.iter().find_map(|index| {
852                            if index.name == "PrimaryKey" {
853
854                                // Always trim to remove the final space, then split by space to find all the keys of the table.
855                                let keys = index.key.trim().split(' ').collect::<Vec<_>>();
856                                if keys.is_empty() {
857                                    None
858                                } else {
859                                    Some(keys)
860                                }
861                            } else {
862                                None
863                            }
864                        }).unwrap_or(vec![])
865                    } else { vec![] }
866                } else { vec![] }
867            } else { vec![] };
868
869            if let Some(complex) = &elements.xsd_complex_type {
870                if let Some(elements) = complex.first() {
871                    for element in &elements.xsd_sequence.xsd_element {
872
873                        // For a field to be valid we need name and type.
874                        if let Some(ref name) = element.name {
875                            if let Some(ref jet_type) = element.jet_type {
876
877                                let mut field = RawField {
878                                    name: name.to_owned(),
879                                    field_type: match &**jet_type {
880                                        "yesno" => "yesno".to_owned(),
881                                        "integer" => "integer".to_owned(),
882                                        "longinteger" | "autonumber" => "autonumber".to_owned(),
883                                        "decimal" | "single" => "single".to_owned(),
884                                        "double" => "double".to_owned(),
885                                        "text" | "memo" | "oleobject" | "replicationid" => "text".to_owned(),
886
887                                        // These are dates as in a DateTime format. Treat them as text for now.
888                                        "datetime" => "text".to_owned(),
889
890                                        _ => todo!("{}", jet_type),
891                                    },
892                                    ..Default::default()
893                                };
894
895                                if primary_keys.contains(&&*field.name) {
896                                    field.primary_key = "1".to_owned();
897                                } else {
898                                    field.primary_key = "0".to_owned();
899                                }
900
901                                field.is_old_game = Some(true);
902
903                                definition.fields.push(field);
904                            }
905                        }
906                    }
907                }
908            }
909        }
910
911        definition
912    }
913}