Skip to main content

rpfm_lib/schema/
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//! Schema system for defining Total War file formats.
12//!
13//! This module provides the infrastructure for defining and managing schemas that describe the binary
14//! structure of Total War game files, primarily database tables and localization files.
15//!
16//! # Overview
17//!
18//! A [`Schema`] contains [`Definition`]s that specify the exact binary layout of different file types.
19//! Each table can have multiple definitions to support different versions across game patches. The schema
20//! system also supports runtime patches to override field properties without modifying the base schema.
21//!
22//! # Key Components
23//!
24//! - [`Schema`]: The main container holding all table definitions and patches for a game
25//! - [`Definition`]: Describes one version of a table's structure (fields, types, constraints)
26//! - [`Field`]: Represents a single column in a table with its type and metadata
27//! - [`FieldType`]: The data type of a field (integers, strings, booleans, sequences, etc.)
28//! - [`DefinitionPatch`]: Runtime modifications to field properties
29//!
30//! # Schema Versioning
31//!
32//! - Each game has its own schema file (e.g., `warhammer_3.ron`)
33//! - The schema format version (currently v5) is tracked separately from table versions
34//! - Legacy schema formats (like v4) can be automatically upgraded via [`Schema::update()`]
35//!
36//! # Loading and Saving
37//!
38//! Schemas are typically stored in RON format but can also be exported to JSON:
39//!
40//! ```no_run
41//! use rpfm_lib::schema::Schema;
42//! use std::path::Path;
43//!
44//! // Load a schema
45//! let schema_path = Path::new("schemas/warhammer_3.ron");
46//! let schema = Schema::load(schema_path, None)?;
47//!
48//! // Access table definitions
49//! if let Some(defs) = schema.definitions_by_table_name("units_tables") {
50//!     for def in defs {
51//!         println!("Version {}: {} fields", def.version(), def.fields().len());
52//!     }
53//! }
54//! # Ok::<(), rpfm_lib::error::RLibError>(())
55//! ```
56//!
57//! # Patches
58//!
59//! Patches allow modifying field properties at runtime without changing the schema:
60//!
61//! ```no_run
62//! use rpfm_lib::schema::Schema;
63//! use std::path::Path;
64//!
65//! let schema = Schema::load(Path::new("schema.ron"), Some(Path::new("patches.ron")))?;
66//!
67//! // Check if a field has a patched value
68//! if let Some(value) = schema.patch_value("units_tables", "key", "is_key") {
69//!     println!("Patched is_key value: {}", value);
70//! }
71//! # Ok::<(), rpfm_lib::error::RLibError>(())
72//! ```
73//!
74//! # Schema Repository
75//!
76//! Schemas are maintained in a separate Git repository and can be updated independently from RPFM itself.
77//! The repository URL and branch are defined as constants in this module.
78
79use getset::*;
80use itertools::Itertools;
81use rayon::prelude::*;
82use ron::de::{from_bytes, from_str};
83use ron::ser::{to_string_pretty, PrettyConfig};
84use serde::{Serialize as SerdeSerialize, Serializer};
85use serde_derive::{Serialize, Deserialize};
86
87use std::cmp::Ordering;
88use std::collections::{BTreeMap, HashMap};
89use std::{fmt, fmt::Display};
90use std::fs::{DirBuilder, File};
91use std::io::{BufReader, BufWriter, Read, Write};
92use std::path::Path;
93
94#[cfg(feature = "integration_assembly_kit")]use crate::integrations::assembly_kit::localisable_fields::RawLocalisableField;
95#[cfg(feature = "integration_assembly_kit")]use crate::integrations::assembly_kit::table_definition::RawDefinition;
96#[cfg(feature = "integration_assembly_kit")]use crate::integrations::assembly_kit::table_definition::RawField;
97#[cfg(feature = "integration_sqlite")] use rusqlite::types::Type;
98
99use crate::error::Result;
100use crate::files::table::DecodedData;
101use crate::games::supported_games::SupportedGames;
102
103// Legacy Schemas, to keep backwards compatibility during updates.
104pub(crate) mod v4;
105
106/// Name of the folder containing all the schemas.
107///
108/// This folder is located within the application's config directory and stores schema files
109/// for each supported Total War game.
110pub const SCHEMA_FOLDER: &str = "schemas";
111
112/// URL of the remote Git repository containing the schema files.
113///
114/// This repository is used to fetch and update schema definitions for all supported games.
115pub const SCHEMA_REPO: &str = "https://github.com/Frodo45127/rpfm-schemas";
116
117/// Name of the Git remote to use when fetching schemas.
118pub const SCHEMA_REMOTE: &str = "origin";
119
120/// Name of the Git branch to use when fetching schemas.
121pub const SCHEMA_BRANCH: &str = "master";
122
123/// Current structural version of the Schema, for compatibility purposes.
124///
125/// This version number is incremented when the schema format itself changes
126/// in a backwards-incompatible way.
127const CURRENT_STRUCTURAL_VERSION: u16 = 5;
128
129/// Invalid version marker for internal use.
130///
131/// This value is used for temporary or fake [`Definition`] instances that don't
132/// represent actual file versions.
133const INVALID_VERSION: i32 = -100;
134
135/// Name for unnamed colour groups.
136///
137/// When RGB colour fields are merged but have no common prefix, this name is used
138/// as the base name for the combined field.
139pub const MERGE_COLOUR_NO_NAME: &str = "Unnamed Colour Group";
140
141/// Suffix for merged colour field names.
142///
143/// This string is appended to the base name when creating a merged RGB colour field.
144/// For example, `banner_colour` fields would become `banner_colour_hex`.
145pub const MERGE_COLOUR_POST: &str = "_hex";
146
147/// Fields that can be ignored in missing field checks.
148///
149/// These fields are legacy fields from older Assembly Kit versions that are not
150/// actually used by the games and can be safely ignored during schema updates.
151const IGNORABLE_FIELDS: [&str; 4] = ["s_ColLineage", "s_Generation", "s_GUID", "s_Lineage"];
152
153//---------------------------------------------------------------------------//
154//                              Enum & Structs
155//---------------------------------------------------------------------------//
156
157/// This type defines patches for specific table definitions, in a ColumnName -> [key -> value] format.
158///
159/// Patches allow runtime modification of schema fields without changing the base schema files.
160/// They are stored separately and applied when loading definitions.
161///
162/// # Structure
163///
164/// The outer [`HashMap`] maps column names to their patches. The inner [`HashMap`] maps patch keys
165/// to their values. For table-wide patches (not specific to a column), use the special column name `"-1"`.
166///
167/// # Example Patch Keys
168///
169/// - `"is_key"`: Override whether a field is a key field
170/// - `"default_value"`: Override the default value
171/// - `"is_filename"`: Override whether a field is a filename
172/// - `"filename_relative_path"`: Override the relative filename path
173/// - `"is_reference"`: Override reference information
174/// - `"lookup"`: Override lookup columns
175/// - `"lookup_hardcoded"`: Add hardcoded lookup values
176/// - `"description"`: Override the field description
177/// - `"not_empty"`: Mark the field as "cannot be empty"
178/// - `"unused"`: Mark a field as unused
179pub type DefinitionPatch = HashMap<String, HashMap<String, String>>;
180
181/// Represents a complete schema file containing table definitions for a Total War game.
182///
183/// A [`Schema`] stores the structural definitions for all database tables in a Total War game.
184/// Each table can have multiple [`Definition`] versions, allowing the schema to support
185/// different versions of the same table across game patches.
186///
187/// # Structure
188///
189/// - `version`: The structural version of the schema format itself (currently 5)
190/// - `definitions`: A map of table names to their version history
191/// - `patches`: Runtime modifications to field properties
192///
193/// # Usage
194///
195/// ```no_run
196/// use rpfm_lib::schema::Schema;
197/// use std::path::Path;
198///
199/// let schema_path = Path::new("schemas/warhammer_3.ron");
200/// let schema = Schema::load(schema_path, None)?;
201///
202/// // Get definitions for a specific table
203/// if let Some(definitions) = schema.definitions_by_table_name("units_tables") {
204///     println!("Found {} versions of units_tables", definitions.len());
205/// }
206/// # Ok::<(), rpfm_lib::error::RLibError>(())
207/// ```
208#[derive(Clone, PartialEq, Eq, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
209#[getset(get = "pub", get_mut = "pub", set = "pub")]
210pub struct Schema {
211
212    /// The structural version of the schema format.
213    ///
214    /// This is incremented when the schema format itself changes in backwards-incompatible ways.
215    version: u16,
216
217    /// Map of table names to their version definitions.
218    ///
219    /// Each table can have multiple versions, stored as a [`Vec`] of [`Definition`] instances.
220    /// Serialization orders this map alphabetically for consistent output.
221    #[serde(serialize_with = "ordered_map_definitions")]
222    definitions: HashMap<String, Vec<Definition>>,
223
224    /// Map of table names to their patches.
225    ///
226    /// Patches allow runtime modification of field properties without changing the base schema.
227    /// See [`DefinitionPatch`] for the patch structure.
228    #[serde(serialize_with = "ordered_map_patches")]
229    patches: HashMap<String, DefinitionPatch>,
230}
231
232/// Defines the structure of a specific version of a database table.
233///
234/// A [`Definition`] specifies the exact binary layout and field properties for one version
235/// of a table. Tables can have multiple definitions in a schema to support different versions
236/// across game patches.
237///
238/// # Version Numbers
239///
240/// - `-1`: Fake definition used internally for dependency resolution
241/// - `0`: Unversioned files (tables without version markers in their binary format)
242/// - `1+`: Versioned files with explicit version numbers
243///
244/// # Fields Processing
245///
246/// The raw [`fields`] list may undergo processing when accessed via [`fields_processed()`]:
247/// - Bitwise fields are expanded into multiple boolean fields
248/// - Enum fields are converted to string fields
249/// - RGB colour triplets are merged into single ColourRGB fields
250///
251/// Unless you have a specific reason to do so, it is recommended to use [`fields_processed()`] instead of [`fields`].
252///
253/// # Localisation
254///
255/// Some tables have fields that are moved to separate LOC files during export:
256/// - [`localised_fields`] lists these fields
257/// - [`localised_key_order`] defines the key field order for LOC keys
258///
259/// [`fields_processed()`]: Definition::fields_processed
260/// [`fields`]: Definition::fields
261/// [`localised_fields`]: Definition::localised_fields
262/// [`localised_key_order`]: Definition::localised_key_order
263#[derive(Clone, PartialEq, Eq, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
264#[getset(get = "pub", get_mut = "pub", set = "pub")]
265pub struct Definition {
266
267    /// The version number of this table definition.
268    ///
269    /// See type-level documentation for version number meanings.
270    version: i32,
271
272    /// List of fields in the order they appear in the binary format.
273    ///
274    /// This is the raw field list. For the processed version (with bitwise expansion,
275    /// enum conversion, etc.), use [`fields_processed()`].
276    ///
277    /// [`fields_processed()`]: Definition::fields_processed
278    fields: Vec<Field>,
279
280    /// Fields that are extracted to LOC files during export.
281    ///
282    /// These fields contain localisable text that gets separated from the main table data
283    /// when exporting said table to binary format.
284    localised_fields: Vec<Field>,
285
286    /// Order of key fields when constructing localisation keys.
287    ///
288    /// This specifies the order in which key fields should be concatenated when
289    /// creating LOC entry keys. Only applies to processed fields.
290    localised_key_order: Vec<u32>,
291
292    /// Runtime patches applied to this definition.
293    ///
294    /// These are loaded from the schema's patch set and applied when retrieving
295    /// the definition. Not serialized - they come from the schema's patches field.
296    #[serde(skip)]
297    patches: DefinitionPatch
298}
299
300/// Defines a single field within a table definition.
301///
302/// A [`Field`] describes one column in a database table, including its data type, constraints,
303/// and metadata. Fields can be modified at runtime via schema patches.
304///
305/// # Field Types
306///
307/// See [`FieldType`] for the supported data types (integers, strings, sequences, etc.).
308///
309/// # Field Attributes and Constraints
310///
311/// - **Key Fields**: When `is_key` is true, the field is part of the table's primary key
312/// - **References**: Fields can reference columns in other tables for foreign key relationships
313/// - **Lookups**: Additional columns from referenced tables to display in the UI
314/// - **Filenames**: Fields that contain file paths within the game's VFS
315/// - **Bitwise**: Numeric fields that should be split into multiple boolean columns
316/// - **Enums**: Numeric fields with named values
317/// - **Colours**: Fields that are part of an RGB triplet
318///
319/// # Patching
320///
321/// Most field properties can be overridden via schema patches. Use the accessor methods
322/// (e.g., [`is_key()`], [`default_value()`]) rather than direct field access to ensure
323/// patches are applied.
324///
325/// [`is_key()`]: Field::is_key
326/// [`default_value()`]: Field::default_value
327#[derive(Clone, PartialEq, Eq, Debug, Setters, Serialize, Deserialize)]
328#[getset(set = "pub")]
329pub struct Field {
330
331    /// Name of the field.
332    ///
333    /// Must match the field name from the Assembly Kit table definition (usually snake_case, but not always).
334    name: String,
335
336    /// Data type of the field.
337    ///
338    /// Determines how the field's binary data is interpreted.
339    field_type: FieldType,
340
341    /// Whether this field is part of the table's primary key.
342    ///
343    /// Can be overridden via patches. Use [`is_key()`] to get the patched value.
344    ///
345    /// [`is_key()`]: Field::is_key
346    is_key: bool,
347
348    /// Default value for this field when creating new rows.
349    ///
350    /// Can be overridden via patches. Use [`default_value()`] to get the patched value.
351    ///
352    /// [`default_value()`]: Field::default_value
353    default_value: Option<String>,
354
355    /// Whether this field contains a filename/path.
356    ///
357    /// Can be overridden via patches. Use [`is_filename()`] to get the patched value.
358    ///
359    /// [`is_filename()`]: Field::is_filename
360    is_filename: bool,
361
362    /// Semicolon-separated list of relative paths where files for this field can be found.
363    ///
364    /// Only applicable when `is_filename` is true. Can be overridden via patches.
365    /// Use [`filename_relative_path()`] to get the parsed, patched value.
366    ///
367    /// [`filename_relative_path()`]: Field::filename_relative_path
368    filename_relative_path: Option<String>,
369
370    /// Foreign key reference to another table.
371    ///
372    /// Format: `Some((table_name, column_name))` where `table_name` doesn't include
373    /// the `_tables` suffix. Can be overridden via patches.
374    /// Use [`is_reference()`] to get the patched value.
375    ///
376    /// [`is_reference()`]: Field::is_reference
377    is_reference: Option<(String, String)>,
378
379    /// Additional columns from the referenced table to show in lookups.
380    ///
381    /// Only applicable when `is_reference` is Some. Can be overridden via patches.
382    /// Use [`lookup()`] to get the patched value.
383    ///
384    /// [`lookup()`]: Field::lookup
385    lookup: Option<Vec<String>>,
386
387    /// Human-readable description of the field's purpose.
388    ///
389    /// Can be overridden via patches. Use [`description()`] to get the patched value.
390    ///
391    /// [`description()`]: Field::description
392    description: String,
393
394    /// Visual position in CA's Assembly Kit table editor.
395    ///
396    /// `-1` means the position is unknown. This is used to maintain column order
397    /// consistency with the Assembly Kit.
398    ca_order: i16,
399
400    /// Number of boolean columns this field should be split into.
401    ///
402    /// Only applicable to numeric fields. A value > 1 means the field should be
403    /// expanded into that many boolean columns when processed.
404    is_bitwise: i32,
405
406    /// Named values for this field when treated as an enum.
407    ///
408    /// Maps integer values to their string names. When non-empty, the field
409    /// is treated as a string enum in processed fields.
410    ///
411    /// NOTE: When possible, prefer using lookups instead of enum_values.
412    enum_values: BTreeMap<i32, String>,
413
414    /// Index of the RGB colour group this field belongs to.
415    ///
416    /// When set, this field is part of a 3-field RGB triplet that should be
417    /// merged into a single ColourRGB field when processed.
418    is_part_of_colour: Option<u8>,
419
420    /// Whether this field is unused by the game.
421    ///
422    /// Not serialized - determined via patches at runtime.
423    /// Use [`unused()`] to get the patched value.
424    ///
425    /// [`unused()`]: Field::unused
426    #[serde(skip_serializing, skip_deserializing)]
427    unused: bool,
428}
429
430/// Supported data types for table fields.
431///
432/// This enum defines all field types that can be encoded/decoded from Total War database tables.
433/// Each variant corresponds to a specific binary representation in the game files.
434///
435/// # Basic Types
436///
437/// - **Boolean**: 1-byte boolean value
438/// - **Integers**: Signed integers of various sizes (I16, I32, I64)
439/// - **Floats**: Floating-point numbers (F32, F64)
440/// - **Strings**: Length-prefixed strings with [`u8`] or [`u16`] length markers
441///
442/// # Optional Types
443///
444/// Optional types use a 1-byte flag followed by the value if present:
445/// - **OptionalI16**, **OptionalI32**, **OptionalI64**: Optional integers
446/// - **OptionalStringU8**, **OptionalStringU16**: Optional strings
447///
448/// # Complex Types
449///
450/// - **ColourRGB**: 6-character hexadecimal RGB colour (e.g., "FF0000" for red)
451/// - **SequenceU16**, **SequenceU32**: Arrays with [`u16`] or [`u32`] length prefix
452///
453/// # Sequences
454///
455/// Sequence types contain a nested [`Definition`] that describes the structure of each
456/// array element. The length prefix determines how many elements follow.
457#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
458pub enum FieldType {
459    /// 1-byte boolean value (0 = false, 1 = true).
460    Boolean,
461
462    /// 32-bit floating-point number.
463    F32,
464
465    /// 64-bit floating-point number.
466    F64,
467
468    /// 16-bit signed integer.
469    I16,
470
471    /// 32-bit signed integer.
472    I32,
473
474    /// 64-bit signed integer.
475    I64,
476
477    /// RGB colour as a 6-character hexadecimal string (e.g., "FF0000").
478    ColourRGB,
479
480    /// UTF-8 encoded string with [`u16`] length prefix (max 65535 bytes).
481    StringU8,
482
483    /// UTF-16 encoded string with [`u16`] length prefix (max 65535 characters).
484    StringU16,
485
486    /// Optional 16-bit signed integer (1-byte flag + value if present).
487    OptionalI16,
488
489    /// Optional 32-bit signed integer (1-byte flag + value if present).
490    OptionalI32,
491
492    /// Optional 64-bit signed integer (1-byte flag + value if present).
493    OptionalI64,
494
495    /// Optional UTF-8 encoded string (1-byte flag + [`u16`] length prefix + string if present).
496    OptionalStringU8,
497
498    /// Optional UTF-16 encoded string (1-byte flag + [`u16`] length prefix + string if present).
499    OptionalStringU16,
500
501    /// Array with [`u16`] element count followed by elements matching the nested definition.
502    SequenceU16(Box<Definition>),
503
504    /// Array with [`u32`] element count followed by elements matching the nested definition.
505    SequenceU32(Box<Definition>)
506}
507
508//---------------------------------------------------------------------------//
509//                       Enum & Structs Implementations
510//---------------------------------------------------------------------------//
511
512/// Implementation of [`Schema`].
513impl Schema {
514
515    /// Saves patches to a local patches file, merging with existing patches.
516    ///
517    /// This function loads existing patches from the file, merges the provided patches with them,
518    /// and writes the combined patch set back to the file in RON format.
519    ///
520    /// # Arguments
521    ///
522    /// * `patches` - The patches to add or update
523    /// * `path` - Path to the local patches file (must exist)
524    ///
525    /// # Returns
526    ///
527    /// Returns [`Ok`] if successful, or an error if:
528    /// - The file cannot be read or written
529    /// - The file contains invalid patch data
530    ///
531    /// # Example
532    ///
533    /// ```no_run
534    /// use std::collections::HashMap;
535    /// use std::path::Path;
536    /// use rpfm_lib::schema::{Schema, DefinitionPatch};
537    ///
538    /// let mut patches: HashMap<String, DefinitionPatch> = HashMap::new();
539    /// // Add patches...
540    ///
541    /// Schema::save_patches(&patches, Path::new("my_patches.ron"))?;
542    /// # Ok::<(), rpfm_lib::error::RLibError>(())
543    /// ```
544    pub fn save_patches(patches: &HashMap<String, DefinitionPatch>, path: &Path) -> Result<()> {
545        let mut file = BufReader::new(File::open(path)?);
546        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
547        file.read_to_end(&mut data)?;
548        let mut local_patches: HashMap<String, DefinitionPatch> = from_bytes(&data)?;
549
550        Self::add_patches_to_patch_set(&mut local_patches, patches);
551
552        let mut file = BufWriter::new(File::create(path)?);
553        let config = PrettyConfig::default();
554        file.write_all(to_string_pretty(&local_patches, config)?.as_bytes())?;
555
556        Ok(())
557    }
558
559    /// Removes all local patches for a specific table.
560    ///
561    /// This function loads the patches file, removes all patches for the specified table,
562    /// and writes the updated patch set back to the file.
563    ///
564    /// # Arguments
565    ///
566    /// * `table_name` - Name of the table to remove patches for
567    /// * `path` - Path to the local patches file
568    ///
569    /// # Returns
570    ///
571    /// Returns [`Ok`] if successful, even if no there were no patches to remove, or an error
572    /// if file I/O fails.
573    pub fn remove_patches_for_table(table_name: &str, path: &Path) -> Result<()> {
574        let mut file = BufReader::new(File::open(path)?);
575        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
576        file.read_to_end(&mut data)?;
577        let mut local_patches: HashMap<String, DefinitionPatch> = from_bytes(&data)?;
578
579        local_patches.remove(table_name);
580
581        let mut file = BufWriter::new(File::create(path)?);
582        let config = PrettyConfig::default();
583        file.write_all(to_string_pretty(&local_patches, config)?.as_bytes())?;
584
585        Ok(())
586    }
587
588    /// Removes all local patches for a specific field in a table.
589    ///
590    /// This function loads the patches file, removes all patches for the specified table's field,
591    /// and writes the updated patch set back to the file. Other fields in the table are unaffected.
592    ///
593    /// # Arguments
594    ///
595    /// * `table_name` - Name of the table containing the field
596    /// * `field_name` - Name of the field to remove patches for
597    /// * `path` - Path to the local patches file
598    ///
599    /// # Returns
600    ///
601    /// Returns [`Ok`] if successful, even if no there were no patches to remove, or an error
602    /// if file I/O fails.
603    pub fn remove_patches_for_table_and_field(table_name: &str, field_name: &str, path: &Path) -> Result<()> {
604        let mut file = BufReader::new(File::open(path)?);
605        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
606        file.read_to_end(&mut data)?;
607        let mut local_patches: HashMap<String, DefinitionPatch> = from_bytes(&data)?;
608
609        if let Some(table_patches) = local_patches.get_mut(table_name) {
610            table_patches.remove(field_name);
611        }
612
613        let mut file = BufWriter::new(File::create(path)?);
614        let config = PrettyConfig::default();
615        file.write_all(to_string_pretty(&local_patches, config)?.as_bytes())?;
616
617        Ok(())
618    }
619
620    /// Retrieves a specific patch value for a table's column.
621    ///
622    /// # Arguments
623    ///
624    /// * `table_name` - Name of the table
625    /// * `column_name` - Name of the column
626    /// * `key` - Patch key (e.g., "is_key", "default_value")
627    ///
628    /// # Returns
629    ///
630    /// Returns the patch value if found, or [`None`] otherwise.
631    pub fn patch_value(&self, table_name: &str, column_name: &str, key: &str) -> Option<&String> {
632        self.patches.get(table_name)?.get(column_name)?.get(key)
633    }
634
635    /// Retrieves all patches for a specific table.
636    ///
637    /// # Arguments
638    ///
639    /// * `table_name` - Name of the table
640    ///
641    /// # Returns
642    ///
643    /// Returns the table's patches if found, or [`None`] otherwise.
644    pub fn patches_for_table(&self, table_name: &str) -> Option<&DefinitionPatch> {
645        self.patches.get(table_name)
646    }
647
648    /// Merges patches into an existing patch set.
649    ///
650    /// This function adds the provided patches to the patch set, merging them with any
651    /// existing patches. If a patch already exists for a table/column/key combination,
652    /// it will be extended with the new values.
653    ///
654    /// # Arguments
655    ///
656    /// * `patch_set` - The patch set to merge into (modified in place)
657    /// * `patches` - The patches to add
658    ///
659    /// # Note
660    ///
661    /// After adding patches, you must re-retrieve any definitions you've already retrieved
662    /// for the patches to take effect, as patches are applied when retrieving definitions.
663    pub fn add_patches_to_patch_set(patch_set: &mut HashMap<String, DefinitionPatch>, patches: &HashMap<String, DefinitionPatch>) {
664        patches.iter().for_each(|(table_name, column_patch)| {
665            match patch_set.get_mut(table_name) {
666                Some(column_patch_current) => {
667                    column_patch.iter().for_each(|(column_name, patch)| {
668                        match column_patch_current.get_mut(column_name) {
669                            Some(patch_current) => patch_current.extend(patch.clone()),
670                            None => {
671                                column_patch_current.insert(column_name.to_owned(), patch.clone());
672                            }
673                        }
674                    });
675                }
676                None => {
677                    patch_set.insert(table_name.to_owned(), column_patch.clone());
678                }
679            }
680        });
681    }
682
683    /// Adds or updates a table definition in the schema.
684    ///
685    /// If a definition with the same version already exists for this table, it will be replaced.
686    /// Otherwise, the definition is added to the table's version list.
687    ///
688    /// # Arguments
689    ///
690    /// * `table_name` - Name of the table
691    /// * `definition` - The definition to add or update
692    pub fn add_definition(&mut self, table_name: &str, definition: &Definition) {
693        match self.definitions.get_mut(table_name) {
694            Some(definitions) => {
695                match definitions.iter_mut().find(|def| def.version() == definition.version()) {
696                    Some(def) => *def = definition.to_owned(),
697                    None => definitions.push(definition.to_owned()),
698                }
699            },
700            None => { self.definitions.insert(table_name.to_owned(), vec![definition.to_owned()]); },
701        }
702    }
703
704    /// Removes a specific table definition version from the schema.
705    ///
706    /// # Arguments
707    ///
708    /// * `table_name` - Name of the table
709    /// * `version` - Version number of the definition to remove
710    pub fn remove_definition(&mut self, table_name: &str, version: i32) {
711        if let Some(definitions) = self.definitions.get_mut(table_name) {
712            let mut index_to_delete = vec![];
713            for (index, definition) in definitions.iter().enumerate() {
714                if definition.version == version {
715                    index_to_delete.push(index);
716                }
717            }
718
719            index_to_delete.iter().rev().for_each(|index| { definitions.remove(*index); });
720        }
721    }
722
723    /// Returns a cloned copy of all definitions for a table.
724    ///
725    /// # Arguments
726    ///
727    /// * `table_name` - Name of the table
728    ///
729    /// # Returns
730    ///
731    /// Returns a cloned vector of all definitions for the table, or [`None`] if not found.
732    pub fn definitions_by_table_name_cloned(&self, table_name: &str) -> Option<Vec<Definition>> {
733        self.definitions.get(table_name).cloned()
734    }
735
736    /// Returns a reference to all definitions for a table.
737    ///
738    /// # Arguments
739    ///
740    /// * `table_name` - Name of the table
741    ///
742    /// # Returns
743    ///
744    /// Returns a reference to the vector of definitions, or [`None`] if not found.
745    pub fn definitions_by_table_name(&self, table_name: &str) -> Option<&Vec<Definition>>  {
746        self.definitions.get(table_name)
747    }
748
749    /// Returns a mutable reference to all definitions for a table.
750    ///
751    /// # Arguments
752    ///
753    /// * `table_name` - Name of the table
754    ///
755    /// # Returns
756    ///
757    /// Returns a mutable reference to the vector of definitions, or [`None`] if not found.
758    pub fn definitions_by_table_name_mut(&mut self, table_name: &str) -> Option<&mut Vec<Definition>>  {
759        self.definitions.get_mut(table_name)
760    }
761
762    /// Returns the newest compatible definition for a table based on candidate versions.
763    ///
764    /// This function first tries to find a definition matching the highest version number
765    /// from the candidates (typically from a dependency database). If that fails, it
766    /// falls back to the first (newest) definition in the schema.
767    ///
768    /// # Arguments
769    ///
770    /// * `table_name` - Name of the table
771    /// * `candidates` - List of candidate definitions (typically from dependencies)
772    ///
773    /// # Returns
774    ///
775    /// Returns the best matching definition, or [`None`] if the table is not found.
776    pub fn definition_newer(&self, table_name: &str, candidates: &[Definition]) -> Option<&Definition> {
777
778        // Version is... complicated. We don't really want the last one, but the last one compatible with our game.
779        // So we have to try to get it first from the Dependency Database first. If that fails, we fall back to the schema.
780        if let Some(definition) = candidates.iter().max_by(|x, y| x.version().cmp(y.version())) {
781            self.definition_by_name_and_version(table_name, *definition.version())
782        }
783
784        // If there was no coincidence in the dependency database... we risk ourselves getting the last definition we have for
785        // that db from the schema.
786        else{
787            self.definitions.get(table_name)?.first()
788        }
789    }
790
791    /// Returns a reference to a specific table definition by name and version.
792    ///
793    /// # Arguments
794    ///
795    /// * `table_name` - Name of the table
796    /// * `table_version` - Version number of the definition
797    ///
798    /// # Returns
799    ///
800    /// Returns the definition if found, or [`None`] otherwise.
801    pub fn definition_by_name_and_version(&self, table_name: &str, table_version: i32) -> Option<&Definition>  {
802        self.definitions.get(table_name)?.iter().find(|definition| *definition.version() == table_version)
803    }
804
805    /// Returns a mutable reference to a specific table definition by name and version.
806    ///
807    /// # Arguments
808    ///
809    /// * `table_name` - Name of the table
810    /// * `table_version` - Version number of the definition
811    ///
812    /// # Returns
813    ///
814    /// Returns the definition if found, or [`None`] otherwise.
815    pub fn definition_by_name_and_version_mut(&mut self, table_name: &str, table_version: i32) -> Option<&mut Definition>  {
816        self.definitions.get_mut(table_name)?.iter_mut().find(|definition| *definition.version() == table_version)
817    }
818
819    /// Loads a [`Schema`] from a RON file, optionally merging local patches.
820    ///
821    /// This function loads a schema from a `.ron` file and applies any patches from both
822    /// the schema itself and an optional local patches file. Patches from the local file
823    /// are merged with schema patches and applied to all definitions.
824    ///
825    /// # Arguments
826    ///
827    /// * `path` - Path to the schema `.ron` file
828    /// * `local_patches` - Optional path to a local patches file
829    ///
830    /// # Returns
831    ///
832    /// Returns the loaded schema with all patches applied, or an error if loading fails.
833    pub fn load(path: &Path, local_patches: Option<&Path>) -> Result<Self> {
834        let mut file = BufReader::new(File::open(path)?);
835        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
836        file.read_to_end(&mut data)?;
837        let mut schema: Self = from_bytes(&data)?;
838        let mut patches = schema.patches().clone();
839
840        // If we got local patches, add them to the patches list.
841        //
842        // NOTE: we separate the patches from the schemas because otherwise an schema edit will save local patches into the schema,
843        // and we want them to remain local.
844        if let Some(path) = local_patches {
845            if let Ok(file) = File::open(path) {
846                let mut file = BufReader::new(file);
847                let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
848                file.read_to_end(&mut data)?;
849                if let Ok(local_patches) = from_bytes::<HashMap<String, DefinitionPatch>>(&data) {
850                    Self::add_patches_to_patch_set(&mut patches, &local_patches);
851                }
852            }
853        }
854
855        // Preload all patches to their respective definitions.
856        for (table_name, patches) in &patches {
857            if let Some(definitions) = schema.definitions_by_table_name_mut(table_name) {
858                for definition in definitions {
859                    definition.set_patches(patches.clone());
860                }
861            }
862        }
863
864        Ok(schema)
865    }
866
867    /// Loads a [`Schema`] from a JSON file.
868    ///
869    /// Similar to [`load()`], but reads from a JSON file instead of RON. Applies all
870    /// patches from the schema to the definitions.
871    ///
872    /// # Arguments
873    ///
874    /// * `path` - Path to the schema `.json` file
875    ///
876    /// # Returns
877    ///
878    /// Returns the loaded schema with patches applied, or an error if loading fails.
879    ///
880    /// [`load()`]: Schema::load
881    pub fn load_json(path: &Path) -> Result<Self> {
882        let mut file = BufReader::new(File::open(path)?);
883        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
884        file.read_to_end(&mut data)?;
885        let mut schema: Self = serde_json::from_slice(&data)?;
886
887        // Preload all patches to their respective definitions.
888        for (table_name, patches) in schema.patches().clone() {
889            if let Some(definitions) = schema.definitions_by_table_name_mut(&table_name) {
890                for definition in definitions {
891                    definition.set_patches(patches.clone());
892                }
893            }
894        }
895
896        Ok(schema)
897    }
898
899    /// Saves the schema to a RON file.
900    ///
901    /// This function saves the schema to a `.ron` file, automatically:
902    /// - Creating parent directories if needed
903    /// - Sorting definitions by version (newest first)
904    /// - Cleaning up invalid references
905    /// - Moving certain patches from definitions to schema patches
906    ///
907    /// # Arguments
908    ///
909    /// * `path` - Path where the schema file should be saved
910    ///
911    /// # Returns
912    ///
913    /// Returns [`Ok`] if saved successfully, or an error if file I/O fails.
914    pub fn save(&mut self, path: &Path) -> Result<()> {
915
916        // Make sure the path exists to avoid problems with updating schemas.
917        if let Some(parent_folder) = path.parent() {
918            DirBuilder::new().recursive(true).create(parent_folder)?;
919        }
920
921        let mut file = BufWriter::new(File::create(path)?);
922        let config = PrettyConfig::default();
923
924        let mut patches = HashMap::new();
925
926        // Make sure all definitions are properly sorted by version number.
927        self.definitions.iter_mut().for_each(|(table_name, definitions)| {
928            definitions.sort_by(|a, b| b.version().cmp(a.version()));
929
930            // Fix for empty dependencies, again.
931            definitions.iter_mut().for_each(|definition| {
932                definition.fields.iter_mut().for_each(|field| {
933                    if let Some((ref_table, ref_column)) = field.is_reference(None) {
934                        if ref_table.trim().is_empty() || ref_column.trim().is_empty() {
935                            field.is_reference = None;
936                        }
937                    }
938                });
939
940                // Move any lookup_hardcoded patches to schema patches.
941                if definition.patches.values().any(|x| x.keys().any(|y| y == "lookup_hardcoded")) {
942                    let mut def_patches = definition.patches().clone();
943                    def_patches.retain(|_, value| {
944                        value.retain(|key, _| key == "lookup_hardcoded");
945                        !value.is_empty()
946                    });
947                    patches.insert(table_name.to_owned(), def_patches);
948                }
949
950                // Move any unused patches to schema patches.
951                if definition.patches.values().any(|x| x.keys().any(|y| y == "unused")) {
952                    let mut def_patches = definition.patches().clone();
953                    def_patches.retain(|_, value| {
954                        value.retain(|key, _| key == "unused");
955                        !value.is_empty()
956                    });
957                    patches.insert(table_name.to_owned(), def_patches);
958                }
959            })
960        });
961
962        Self::add_patches_to_patch_set(self.patches_mut(), &patches);
963
964        file.write_all(to_string_pretty(&self, config)?.as_bytes())?;
965        Ok(())
966    }
967
968    /// Saves the schema to a JSON file.
969    ///
970    /// This function saves the schema to a `.json` file at the specified path, automatically:
971    /// - Creating parent directories if needed
972    /// - Changing the extension to `.json`
973    /// - Sorting definitions by version (newest first)
974    /// - Pretty-printing the JSON output
975    ///
976    /// # Arguments
977    ///
978    /// * `path` - Path where the schema file should be saved (extension will be changed to `.json`)
979    ///
980    /// # Returns
981    ///
982    /// Returns [`Ok`] if saved successfully, or an error if file I/O or serialization fails.
983    pub fn save_json(&mut self, path: &Path) -> Result<()> {
984        let mut path = path.to_path_buf();
985        path.set_extension("json");
986
987        // Make sure the path exists to avoid problems with updating schemas.
988        if let Some(parent_folder) = path.parent() {
989            DirBuilder::new().recursive(true).create(parent_folder)?;
990        }
991
992        let mut file = BufWriter::new(File::create(&path)?);
993
994        // Make sure all definitions are properly sorted by version number.
995        self.definitions.iter_mut().for_each(|(_, definitions)| {
996            definitions.sort_by(|a, b| b.version().cmp(a.version()));
997        });
998
999        file.write_all(serde_json::to_string_pretty(&self)?.as_bytes())?;
1000        Ok(())
1001    }
1002
1003    /// Exports all schema files in a folder to JSON format.
1004    ///
1005    /// This function loads all schema files (`.ron`) for supported games from the specified folder
1006    /// and saves them as `.json` files in the same location. This is primarily used for
1007    /// compatibility with external tools that prefer JSON.
1008    ///
1009    /// # Arguments
1010    ///
1011    /// * `schema_folder_path` - Path to the folder containing schema `.ron` files
1012    ///
1013    /// # Returns
1014    ///
1015    /// Returns [`Ok`] if all schemas are successfully exported, or an error if any operation fails.
1016    ///
1017    /// # Note
1018    ///
1019    /// This function processes schemas in parallel for better performance.
1020    pub fn export_to_json(schema_folder_path: &Path) -> Result<()> {
1021        let games = SupportedGames::default();
1022
1023        games.games_sorted().par_iter().map(|x| x.schema_file_name()).try_for_each(|schema_file| {
1024            let mut schema_path = schema_folder_path.to_owned();
1025            schema_path.push(schema_file);
1026
1027            let mut schema = Schema::load(&schema_path, None)?;
1028            schema_path.set_extension("json");
1029            schema.save_json(&schema_path)?;
1030            Ok(())
1031        })
1032    }
1033
1034    /// Updates a schema from a legacy format to the current format.
1035    ///
1036    /// This function handles migration of schema files from older structural versions (e.g., v4)
1037    /// to the current structural version (v5). It automatically detects the schema version and
1038    /// applies the necessary transformations.
1039    ///
1040    /// # Arguments
1041    ///
1042    /// * `schema_path` - Path to the schema file to update
1043    /// * `schema_patches_path` - Path to the schema patches file
1044    /// * `game_name` - Name of the game this schema is for
1045    ///
1046    /// # Returns
1047    ///
1048    /// Returns [`Ok`] if the update succeeds, or an error if the update process fails.
1049    pub fn update(schema_path: &Path, schema_patches_path: &Path, game_name: &str) -> Result<()>{
1050        v4::SchemaV4::update(schema_path, schema_patches_path, game_name)
1051    }
1052
1053    /// Returns all columns that reference fields in the specified table.
1054    ///
1055    /// This function searches through all table definitions in the schema to find fields
1056    /// that have foreign key references pointing to the provided table's fields.
1057    ///
1058    /// # Arguments
1059    ///
1060    /// * `table_name` - Name of the table to find references to
1061    /// * `definition` - Definition of the table (used to get the field list)
1062    ///
1063    /// # Returns
1064    ///
1065    /// Returns a map where:
1066    /// - Keys are local field names from the provided definition
1067    /// - Values are maps of `table_name -> Vec<field_name>` containing all referencing fields
1068    ///
1069    /// # Example
1070    ///
1071    /// For a `factions_tables` table, this might return:
1072    /// ```text
1073    /// {
1074    ///   "key": {
1075    ///     "units_tables": ["faction_key"],
1076    ///     "characters_tables": ["faction_key", "home_faction_key"]
1077    ///   }
1078    /// }
1079    /// ```
1080    pub fn referencing_columns_for_table(&self, table_name: &str, definition: &Definition) -> HashMap<String, HashMap<String, Vec<String>>> {
1081
1082        // Iterate over all definitions and find the ones referencing our table/field.
1083        let fields_processed = definition.fields_processed();
1084        let definitions = self.definitions();
1085        let table_name_no_tables = table_name.to_owned().drain(..table_name.len() - 7).collect::<String>();
1086
1087        fields_processed.iter().filter_map(|field| {
1088
1089            let references = definitions.par_iter().filter_map(|(ver_name, ver_definitions)| {
1090                let mut references = ver_definitions.iter().filter_map(|ver_definition| {
1091                    let ver_patches = Some(ver_definition.patches());
1092                    let references = ver_definition.fields_processed().iter().filter_map(|ver_field| {
1093                        if let Some((source_table_name, source_column_name)) = ver_field.is_reference(ver_patches) {
1094                            if table_name_no_tables == source_table_name && field.name() == source_column_name {
1095                                Some(ver_field.name().to_owned())
1096                            } else { None }
1097                        } else { None }
1098                    }).collect::<Vec<String>>();
1099                    if references.is_empty() {
1100                        None
1101                    } else {
1102                        Some(references)
1103                    }
1104                }).flatten().collect::<Vec<String>>();
1105                if references.is_empty() {
1106                    None
1107                } else {
1108                    references.sort();
1109                    references.dedup();
1110                    Some((ver_name.to_owned(), references))
1111                }
1112            }).collect::<HashMap<String, Vec<String>>>();
1113            if references.is_empty() {
1114                None
1115            } else {
1116                Some((field.name().to_owned(), references))
1117            }
1118        }).collect()
1119    }
1120
1121    /// Returns all tables and columns that reference the specified column, and whether LOC files may be affected.
1122    ///
1123    /// This function performs a recursive search to find all fields that reference the specified column,
1124    /// including indirect references (fields that reference fields that reference the target column).
1125    /// It also checks if changing the column would affect localisation keys.
1126    ///
1127    /// # Arguments
1128    ///
1129    /// * `table_name` - Name of the table containing the column (with or without `_tables` suffix)
1130    /// * `column_name` - Name of the column to find references to
1131    /// * `fields` - The table's field list
1132    /// * `localised_fields` - The table's localised field list
1133    ///
1134    /// # Returns
1135    ///
1136    /// Returns a tuple of:
1137    /// - A map of `table_name -> Vec<field_name>` containing all referencing fields (recursively)
1138    /// - A boolean indicating if LOC files may need updates (true if the column is a key field and the table has localised fields)
1139    ///
1140    /// # Note
1141    ///
1142    /// Recursion is supported for table references, but not for LOC field detection.
1143    pub fn tables_and_columns_referencing_our_own(
1144        &self,
1145        table_name: &str,
1146        column_name: &str,
1147        fields: &[Field],
1148        localised_fields: &[Field]
1149    ) -> (BTreeMap<String, Vec<String>>, bool) {
1150
1151        // Make sure the table name is correct.
1152        let short_table_name = if table_name.ends_with("_tables") { table_name.split_at(table_name.len() - 7).0 } else { table_name };
1153        let mut tables: BTreeMap<String, Vec<String>> = BTreeMap::new();
1154
1155        // We get all the db definitions from the schema, then iterate all of them to find what tables/columns reference our own.
1156        for (ref_table_name, ref_definition) in self.definitions() {
1157            let mut columns: Vec<String> = vec![];
1158            for ref_version in ref_definition {
1159                let ref_fields = ref_version.fields_processed();
1160                let ref_patches = Some(ref_version.patches());
1161                let ref_fields_localised = ref_version.localised_fields();
1162                for ref_field in &ref_fields {
1163                    if let Some((ref_ref_table, ref_ref_field)) = ref_field.is_reference(ref_patches) {
1164
1165                        // As this applies to all versions of a table, skip repeated fields.
1166                        if ref_ref_table == short_table_name && ref_ref_field == column_name && !columns.iter().any(|x| x == ref_field.name()) {
1167                            columns.push(ref_field.name().to_owned());
1168
1169                            // If we find a referencing column, get recursion working to check if there is any column referencing this one that needs to be edited.
1170                            let (ref_of_ref, _) = self.tables_and_columns_referencing_our_own(ref_table_name, ref_field.name(), &ref_fields, ref_fields_localised);
1171                            for refs in &ref_of_ref {
1172                                match tables.get_mut(refs.0) {
1173                                    Some(columns) => for value in refs.1 {
1174                                        if !columns.contains(value) {
1175                                            columns.push(value.to_owned());
1176                                        }
1177                                    }
1178                                    None => { tables.insert(refs.0.to_owned(), refs.1.to_vec()); },
1179                                }
1180                            }
1181                        }
1182                    }
1183                }
1184            }
1185
1186            // Only add them if we actually found columns.
1187            if !columns.is_empty() {
1188                tables.insert(ref_table_name.to_owned(), columns);
1189            }
1190        }
1191
1192        // Also, check if we have to be careful about localised fields.
1193        let patches = self.patches().get(table_name);
1194        let has_loc_fields = if let Some(field) = fields.iter().find(|x| x.name() == column_name) {
1195            (field.is_key(patches) || field.name() == "key") && !localised_fields.is_empty()
1196        } else { false };
1197
1198        (tables, has_loc_fields)
1199    }
1200    /// Loads patches from a RON-formatted string.
1201    ///
1202    /// # Arguments
1203    ///
1204    /// * `patch` - RON-formatted string containing patches
1205    ///
1206    /// # Returns
1207    ///
1208    /// Returns the parsed patches, or an error if the string is not valid RON.
1209    pub fn load_patches_from_str(patch: &str) -> Result<HashMap<String, DefinitionPatch>> {
1210        from_str(patch).map_err(From::from)
1211    }
1212
1213    /// Loads definitions from a RON-formatted string.
1214    ///
1215    /// # Arguments
1216    ///
1217    /// * `definition` - RON-formatted string containing table definitions
1218    ///
1219    /// # Returns
1220    ///
1221    /// Returns the parsed definitions, or an error if the string is not valid RON.
1222    pub fn load_definitions_from_str(definition: &str) -> Result<HashMap<String, Definition>> {
1223        from_str(definition).map_err(From::from)
1224    }
1225
1226    /// Exports patches to a RON-formatted string.
1227    ///
1228    /// # Arguments
1229    ///
1230    /// * `patches` - The patches to export
1231    ///
1232    /// # Returns
1233    ///
1234    /// Returns the RON-formatted string, or an error if serialization fails.
1235    pub fn export_patches_to_str(patches: &HashMap<String, DefinitionPatch>) -> Result<String> {
1236        let config = PrettyConfig::default();
1237        ron::ser::to_string_pretty(&patches, config).map_err(From::from)
1238    }
1239
1240    /// Exports definitions to a RON-formatted string.
1241    ///
1242    /// # Arguments
1243    ///
1244    /// * `definitions` - The definitions to export
1245    ///
1246    /// # Returns
1247    ///
1248    /// Returns the RON-formatted string, or an error if serialization fails.
1249    pub fn export_definitions_to_str(definitions: &HashMap<String, Definition>) -> Result<String> {
1250        let config = PrettyConfig::default();
1251        ron::ser::to_string_pretty(&definitions, config).map_err(From::from)
1252    }
1253}
1254
1255/// Implementation of [`Definition`].
1256impl Definition {
1257
1258    /// Creates a new empty definition for a specific version.
1259    ///
1260    /// # Arguments
1261    ///
1262    /// * `version` - The version number for this definition
1263    /// * `schema_patches` - Optional patches to apply to this definition
1264    ///
1265    /// # Returns
1266    ///
1267    /// Returns a new empty definition with no fields.
1268    pub fn new(version: i32, schema_patches: Option<&DefinitionPatch>) -> Definition {
1269        Definition {
1270            version,
1271            localised_fields: vec![],
1272            fields: vec![],
1273            localised_key_order: vec![],
1274            patches: schema_patches.cloned().unwrap_or_default(),
1275        }
1276    }
1277
1278    /// Creates a new definition with the specified fields.
1279    ///
1280    /// # Arguments
1281    ///
1282    /// * `version` - The version number for this definition
1283    /// * `fields` - The table's field list
1284    /// * `loc_fields` - The localised fields list
1285    /// * `schema_patches` - Optional patches to apply to this definition
1286    ///
1287    /// # Returns
1288    ///
1289    /// Returns a new definition with the provided fields.
1290    pub fn new_with_fields(version: i32, fields: &[Field], loc_fields: &[Field], schema_patches: Option<&DefinitionPatch>) -> Definition {
1291        Definition {
1292            version,
1293            localised_fields: loc_fields.to_vec(),
1294            fields: fields.to_vec(),
1295            localised_key_order: vec![],
1296            patches: schema_patches.cloned().unwrap_or_default(),
1297        }
1298    }
1299
1300    /// Returns reference and lookup information for all fields with foreign key references.
1301    ///
1302    /// This function extracts foreign key information from all fields in the definition
1303    /// that have a reference to another table.
1304    ///
1305    /// # Returns
1306    ///
1307    /// Returns a map where:
1308    /// - Keys are field indices (as [`i32`])
1309    /// - Values are tuples of `(referenced_table, referenced_column, optional_lookup_columns)`
1310    ///
1311    /// Only fields with `is_reference` set are included in the result.
1312    pub fn reference_data(&self) -> BTreeMap<i32, (String, String, Option<Vec<String>>)> {
1313        self.fields.iter()
1314            .enumerate()
1315            .filter(|x| x.1.is_reference.is_some())
1316            .map(|x| (x.0 as i32, (x.1.is_reference.clone().unwrap().0, x.1.is_reference.clone().unwrap().1, x.1.lookup.clone())))
1317            .collect()
1318    }
1319
1320    /// Returns the processed field list with transformations applied.
1321    ///
1322    /// This function processes the raw field list and applies various transformations:
1323    /// - **Bitwise fields**: Expanded into multiple boolean fields (e.g., `flags` → `flags_1`, `flags_2`, etc.)
1324    /// - **Enum fields**: Converted to StringU8 fields
1325    /// - **Colour fields**: RGB triplets merged into single ColourRGB fields
1326    /// - **Numeric fields**: Converted to I32 fields (with patches)
1327    ///
1328    /// This is the field list that should be used for UI display and data editing.
1329    ///
1330    /// # Returns
1331    ///
1332    /// Returns the processed field list with all transformations applied.
1333    pub fn fields_processed(&self) -> Vec<Field> {
1334        let mut split_colour_fields: BTreeMap<u8, Field> = BTreeMap::new();
1335        let patches = Some(self.patches());
1336        let mut fields = self.fields().iter()
1337            .filter_map(|x|
1338                if x.is_bitwise() > 1 {
1339                    let unused = x.unused(patches);
1340                    let mut fields = vec![x.clone(); x.is_bitwise() as usize];
1341                    fields.iter_mut().enumerate().for_each(|(index, field)| {
1342                        field.set_name(format!("{}_{}", field.name(), index + 1));
1343                        field.set_field_type(FieldType::Boolean);
1344                        field.set_unused(unused);
1345                    });
1346                    Some(fields)
1347                }
1348
1349                else if !x.enum_values().is_empty() {
1350                    let mut field = x.clone();
1351                    field.set_field_type(FieldType::StringU8);
1352                    Some(vec![field; 1])
1353                }
1354
1355                else if let Some(colour_index) = x.is_part_of_colour() {
1356                    match split_colour_fields.get_mut(&colour_index) {
1357
1358                        // If found, add the default value to the other previously known default value.
1359                        Some(field) => {
1360                            let default_value = match x.default_value(None) {
1361                                Some(default_value) => {
1362                                    if x.name.ends_with("_r") || x.name.ends_with("_red") || x.name == "r" || x.name == "red" {
1363                                        field.default_value.clone().map(|df| {
1364                                            format!("{:X}{}", default_value.parse::<i32>().unwrap_or(0), &df[2..])
1365                                        })
1366                                    } else if x.name.ends_with("_g") || x.name.ends_with("_green") || x.name == "g" || x.name == "green" {
1367                                        field.default_value.clone().map(|df| {
1368                                            format!("{}{:X}{}", &df[..2], default_value.parse::<i32>().unwrap_or(0), &df[4..])
1369                                        })
1370                                    } else if x.name.ends_with("_b") || x.name.ends_with("_blue") || x.name == "b" || x.name == "blue" {
1371                                        field.default_value.clone().map(|df| {
1372                                            format!("{}{:X}", &df[..4], default_value.parse::<i32>().unwrap_or(0))
1373                                        })
1374                                    } else {
1375                                        Some("000000".to_owned())
1376                                    }
1377                                }
1378                                None => Some("000000".to_owned())
1379                            };
1380
1381                            // Update the default value with the one for this colour.
1382                            field.set_default_value(default_value);
1383
1384                            if !field.unused(patches) {
1385                                field.set_unused(x.unused(patches));
1386                            }
1387                        },
1388                        None => {
1389                            let unused = x.unused(patches);
1390                            let colour_split = x.name().rsplitn(2, '_').collect::<Vec<&str>>();
1391                            let colour_field_name = if colour_split.len() == 2 {
1392                                format!("{}{}", colour_split[1].to_lowercase(), MERGE_COLOUR_POST)
1393                            } else {
1394                                format!("{}_{}", MERGE_COLOUR_NO_NAME.to_lowercase(), colour_index)
1395                            };
1396
1397                            let mut field = x.clone();
1398                            field.set_name(colour_field_name);
1399                            field.set_field_type(FieldType::ColourRGB);
1400                            field.set_unused(unused);
1401
1402                            // We need to fix the default value so it's a ColourRGB one.
1403                            let default_value = match field.default_value(None) {
1404                                Some(default_value) => {
1405                                    if x.name.ends_with("_r") || x.name.ends_with("_red") || x.name == "r" || x.name == "red" {
1406                                        Some(format!("{:X}0000", default_value.parse::<i32>().unwrap_or(0)))
1407                                    } else if x.name.ends_with("_g") || x.name.ends_with("_green") || x.name == "g" || x.name == "green" {
1408                                        Some(format!("00{:X}00", default_value.parse::<i32>().unwrap_or(0)))
1409                                    } else if x.name.ends_with("_b") || x.name.ends_with("_blue") || x.name == "b" || x.name == "blue" {
1410                                        Some(format!("0000{:X}", default_value.parse::<i32>().unwrap_or(0)))
1411                                    } else {
1412                                        Some("000000".to_owned())
1413                                    }
1414                                }
1415                                None => Some("000000".to_owned())
1416                            };
1417
1418                            field.set_default_value(default_value);
1419
1420                            split_colour_fields.insert(colour_index, field);
1421                        }
1422                    }
1423
1424                    None
1425                }
1426
1427                else if x.is_numeric(patches) {
1428                    let mut field = x.clone();
1429                    field.set_field_type(FieldType::I32);
1430                    Some(vec![field; 1])
1431                }
1432
1433                else {
1434                    Some(vec![x.clone(); 1])
1435                }
1436            )
1437            .flatten()
1438            .collect::<Vec<Field>>();
1439
1440        // Second pass to add the combined colour fields.
1441        fields.append(&mut split_colour_fields.values().cloned().collect::<Vec<Field>>());
1442        fields
1443    }
1444
1445    /// Returns the original raw field corresponding to a processed field index.
1446    ///
1447    /// This function maps a field from the processed field list back to its original
1448    /// raw field definition. This is useful when you need to access the underlying
1449    /// field data before transformations like bitwise expansion.
1450    ///
1451    /// # Arguments
1452    ///
1453    /// * `index` - Index in the processed field list
1454    ///
1455    /// # Returns
1456    ///
1457    /// Returns the original field from the raw field list.
1458    ///
1459    /// # Panics
1460    ///
1461    /// Panics if the field is not found (which should never happen for valid indices).
1462    ///
1463    /// # Note
1464    ///
1465    /// This function does not work correctly with combined colour fields, as they don't
1466    /// have a direct 1:1 mapping to a single raw field.
1467    pub fn original_field_from_processed(&self, index: usize) -> Field {
1468        let fields = self.fields();
1469        let processed = self.fields_processed();
1470
1471        let field_processed = &processed[index];
1472        let name = if field_processed.is_bitwise() > 1 {
1473            let mut name = field_processed.name().to_owned();
1474            name.drain(..name.rfind('_').unwrap()).collect::<String>()
1475        }
1476        else {field_processed.name().to_owned() };
1477
1478        fields.iter().find(|x| *x.name() == name).unwrap().clone()
1479    }
1480
1481    /// Returns the processed field list sorted by either key fields or CA order.
1482    ///
1483    /// This function returns the processed fields sorted according to the specified criteria.
1484    ///
1485    /// # Arguments
1486    ///
1487    /// * `key_first` - If `true`, sorts key fields first, then non-key fields. If `false`, sorts by CA order.
1488    ///
1489    /// # Returns
1490    ///
1491    /// Returns the sorted field list. Fields with `ca_order == -1` are left in their original order
1492    /// when sorting by CA order.
1493    pub fn fields_processed_sorted(&self, key_first: bool) -> Vec<Field> {
1494        let mut fields = self.fields_processed();
1495        let patches = Some(self.patches());
1496        fields.sort_by(|a, b| {
1497            if key_first {
1498                if a.is_key(patches) && b.is_key(patches) { Ordering::Equal }
1499                else if a.is_key(patches) && !b.is_key(patches) { Ordering::Less }
1500                else if !a.is_key(patches) && b.is_key(patches) { Ordering::Greater }
1501                else { Ordering::Equal }
1502            }
1503            else if a.ca_order() == -1 || b.ca_order() == -1 { Ordering::Equal }
1504            else { a.ca_order().cmp(&b.ca_order()) }
1505        });
1506        fields
1507    }
1508
1509    /// Returns the position of a column in the processed field list by name.
1510    ///
1511    /// # Arguments
1512    ///
1513    /// * `column_name` - Name of the column to find
1514    ///
1515    /// # Returns
1516    ///
1517    /// Returns the column's index in the processed field list, or [`None`] if not found.
1518    pub fn column_position_by_name(&self, column_name: &str) -> Option<usize> {
1519        self.fields_processed()
1520            .iter()
1521            .position(|x| x.name() == column_name)
1522    }
1523
1524    /// Returns the positions of all key columns in the processed field list.
1525    ///
1526    /// # Returns
1527    ///
1528    /// Returns a vector of indices for all fields marked as key fields.
1529    pub fn key_column_positions(&self) -> Vec<usize> {
1530        self.fields_processed()
1531            .iter()
1532            .enumerate()
1533            .filter(|(_, x)| x.is_key(Some(self.patches())))
1534            .map(|(x, _)| x)
1535            .collect::<Vec<_>>()
1536    }
1537
1538    /// Returns the positions of all key columns sorted by CA order.
1539    ///
1540    /// This function returns key column positions in the same order as they appear in
1541    /// CA's Assembly Kit, rather than the binary order. This is primarily needed for
1542    /// `twad_key_deletes` functionality, which uses CA's ordering.
1543    ///
1544    /// # Returns
1545    ///
1546    /// Returns a vector of key column indices sorted by their `ca_order` value.
1547    pub fn key_column_positions_by_ca_order(&self) -> Vec<usize> {
1548        let fields_processed = self.fields_processed();
1549        let mut keys = fields_processed
1550            .iter()
1551            .enumerate()
1552            .filter(|(_, x)| x.is_key(Some(self.patches())))
1553            .map(|(x, _)| x)
1554            .collect::<Vec<_>>();
1555
1556        keys.sort_by_key(|x| fields_processed[*x].ca_order);
1557        keys
1558    }
1559
1560    /// Generates a SQL `CREATE TABLE` statement for this definition.
1561    ///
1562    /// This function creates a SQL statement suitable for creating a table in SQLite
1563    /// with the structure defined by this definition. The table includes additional
1564    /// metadata columns (`pack_name`, `file_name`, `is_vanilla`) for tracking data sources.
1565    ///
1566    /// # Arguments
1567    ///
1568    /// * `table_name` - Name for the SQL table
1569    ///
1570    /// # Returns
1571    ///
1572    /// Returns the SQL `CREATE TABLE` statement as a string.
1573    ///
1574    /// # Note
1575    ///
1576    /// Foreign key constraints are intentionally disabled because Total War tables
1577    /// (especially in mods) often have referential integrity issues. The function
1578    /// only creates a primary key constraint on the key fields.
1579    ///
1580    /// # Feature
1581    ///
1582    /// This function requires the `integration_sqlite` feature.
1583    #[cfg(feature = "integration_sqlite")]
1584    pub fn map_to_sql_create_table_string(&self, table_name: &str) -> String {
1585        let patches = Some(self.patches());
1586        let fields_sorted = self.fields_processed();
1587        let fields_query = fields_sorted.iter().map(|field| field.map_to_sql_string(patches)).collect::<Vec<_>>().join(",");
1588
1589        let local_keys_join = fields_sorted.iter().filter_map(|field| if field.is_key(patches) { Some(format!("\"{}\"", field.name()))} else { None }).collect::<Vec<_>>().join(",");
1590        let local_keys = format!("CONSTRAINT unique_key PRIMARY KEY (\"pack_name\", \"file_name\", {local_keys_join})");
1591        //let foreign_keys = fields_sorted.iter()
1592        //    .filter_map(|field| field.is_reference(patches).clone().map(|(ref_table, ref_column)| (field.name(), ref_table, ref_column)))
1593        //    .map(|(loc_name, ref_table, ref_field)| format!("CONSTRAINT fk_{table_name} FOREIGN KEY (\"{loc_name}\") REFERENCES {ref_table}(\"{ref_field}\") ON UPDATE CASCADE ON DELETE CASCADE"))
1594        //    .collect::<Vec<_>>()
1595        //    .join(",");
1596
1597        //if foreign_keys.is_empty() {
1598            if local_keys_join.is_empty() {
1599                format!("CREATE TABLE \"{}_v{}\" (\"pack_name\" STRING NOT NULL, \"file_name\" STRING NOT NULL, \"is_vanilla\" INTEGER DEFAULT 0, {})",
1600                    table_name.replace('\"', "'"),
1601                    self.version(),
1602                    fields_query
1603                )
1604            } else {
1605                format!("CREATE TABLE \"{}_v{}\" (\"pack_name\" STRING NOT NULL, \"file_name\" STRING NOT NULL, \"is_vanilla\" INTEGER DEFAULT 0, {}, {})",
1606                    table_name.replace('\"', "'"),
1607                    self.version(),
1608                    fields_query,
1609                    local_keys
1610                )
1611            }
1612        /*} else if local_keys_join.is_empty() {
1613            format!("CREATE TABLE \"{}_v{}\" (\"table_unique_id\" INTEGER DEFAULT 0, {}, {})",
1614                table_name.replace('\"', "'"),
1615                self.version(),
1616                fields_query,
1617                foreign_keys
1618            )
1619        } else {
1620            format!("CREATE TABLE \"{}_v{}\" (\"table_unique_id\" INTEGER DEFAULT 0, {}, {}, {})",
1621                table_name.replace('\"', "'"),
1622                self.version(),
1623                fields_query,
1624                local_keys,
1625                foreign_keys
1626            )
1627        }*/
1628    }
1629
1630    /// Generates the column list for a SQL `INSERT INTO` statement.
1631    ///
1632    /// This function creates the column name list portion of an `INSERT INTO` statement,
1633    /// including the metadata columns and all processed fields.
1634    ///
1635    /// # Returns
1636    ///
1637    /// Returns a string like `("pack_name", "file_name", "is_vanilla", "field1", "field2", ...)`.
1638    ///
1639    /// # Feature
1640    ///
1641    /// This function requires the `integration_sqlite` feature.
1642    #[cfg(feature = "integration_sqlite")]
1643    pub fn map_to_sql_insert_into_string(&self) -> String {
1644        let fields_sorted = self.fields_processed();
1645        let fields_query = fields_sorted.iter().map(|field| format!("\"{}\"", field.name())).collect::<Vec<_>>().join(",");
1646        let fields_query = format!("(\"pack_name\", \"file_name\", \"is_vanilla\", {fields_query})");
1647
1648        fields_query
1649    }
1650
1651    /// Updates field properties from Assembly Kit raw definition data.
1652    ///
1653    /// This function updates the definition's fields with data extracted from the Assembly Kit,
1654    /// matching fields by name and updating specific properties. Fields not found in the
1655    /// Assembly Kit are added to the `unfound_fields` list for reporting.
1656    ///
1657    /// # Updated Properties
1658    ///
1659    /// - `is_key`: Primary key status
1660    /// - `default_value`: Default value for new rows
1661    /// - `filename_relative_path`: Path hints for filename fields
1662    /// - `is_filename`: Whether the field contains a filename
1663    /// - `is_reference`: Foreign key reference information
1664    /// - `lookup`: Lookup column information
1665    /// - `description`: Field description
1666    /// - `ca_order`: Visual position in Assembly Kit
1667    /// - `is_part_of_colour`: Auto-detected RGB colour field grouping
1668    ///
1669    /// # Arguments
1670    ///
1671    /// * `raw_definition` - The Assembly Kit definition data
1672    /// * `unfound_fields` - List to append unfound field names to (format: `"table_name/field_name"`)
1673    ///
1674    /// # Note
1675    ///
1676    /// Fields in `IGNORABLE_FIELDS` are automatically skipped and not reported as unfound.
1677    ///
1678    /// # Feature
1679    ///
1680    /// This function requires the `integration_assembly_kit` feature.
1681    #[cfg(feature = "integration_assembly_kit")]
1682    pub fn update_from_raw_definition(&mut self, raw_definition: &RawDefinition, unfound_fields: &mut Vec<String>) {
1683        let raw_table_name = &raw_definition.name.as_ref().unwrap()[..raw_definition.name.as_ref().unwrap().len() - 4];
1684        let mut combined_fields = BTreeMap::new();
1685        for (index, raw_field) in raw_definition.fields.iter().enumerate() {
1686
1687            let mut found = false;
1688            for field in &mut self.fields {
1689                if field.name == raw_field.name {
1690                    if (raw_field.primary_key == "1" && !field.is_key) || (raw_field.primary_key == "0" && field.is_key) {
1691                        field.is_key = raw_field.primary_key == "1";
1692                    }
1693
1694                    if raw_field.default_value.is_some() {
1695                        field.default_value = raw_field.default_value.clone();
1696                    }
1697
1698                    if let Some(ref path) = raw_field.filename_relative_path {
1699                        let mut new_path = path.to_owned();
1700                        if path.contains(",") {
1701                            new_path = path.split(',').map(|x| x.trim()).join(";");
1702                        }
1703
1704                        field.filename_relative_path = Some(new_path);
1705                    }
1706
1707                    // Some fields are marked as filename, but only have fragment paths, which do not seem to correlate to game file paths.
1708                    // We need to disable those to avoid false positives on diagnostics.
1709                    field.is_filename = match raw_field.is_filename {
1710                        Some(_) => !(raw_field.fragment_path.is_some() && raw_field.filename_relative_path.is_none()),
1711                        None => false,
1712                    };
1713
1714                    // Make sure to cleanup any old invalid definition.
1715                    if let Some(ref description) = raw_field.field_description {
1716                        field.description = description.to_owned();
1717                    } else {
1718                        field.description = String::new();
1719                    }
1720
1721                    // We reset these so we don't inherit wrong references from older tables.
1722                    field.is_reference = Default::default();
1723                    field.lookup = Default::default();
1724                    if let Some(ref table) = raw_field.column_source_table {
1725                        if let Some(ref columns) = raw_field.column_source_column {
1726                            if !table.is_empty() && !columns.is_empty() && !columns[0].is_empty() {
1727                                field.is_reference = Some((table.to_owned(), columns[0].to_owned()));
1728                                if columns.len() > 1 {
1729                                    field.lookup = Some(columns[1..].to_vec());
1730                                }
1731                            }
1732                        }
1733                    }
1734
1735                    field.ca_order = index as i16;
1736
1737                    // Detect and group colour fields.
1738                    let is_numeric = matches!(field.field_type, FieldType::I16 | FieldType::I32 | FieldType::I64 | FieldType::F32 | FieldType::F64);
1739
1740                    if is_numeric && (
1741                        field.name.ends_with("_r") ||
1742                        field.name.ends_with("_g") ||
1743                        field.name.ends_with("_b") ||
1744                        field.name.ends_with("_red") ||
1745                        field.name.ends_with("_green") ||
1746                        field.name.ends_with("_blue") ||
1747                        field.name == "r" ||
1748                        field.name == "g" ||
1749                        field.name == "b" ||
1750                        field.name == "red" ||
1751                        field.name == "green" ||
1752                        field.name == "blue"
1753                    ) {
1754                        let colour_split = field.name.rsplitn(2, '_').collect::<Vec<&str>>();
1755                        let colour_field_name = if colour_split.len() == 2 { format!("{}{}", colour_split[1].to_lowercase(), MERGE_COLOUR_POST) } else { MERGE_COLOUR_NO_NAME.to_lowercase() };
1756
1757                        match combined_fields.get(&colour_field_name) {
1758                            Some(group_key) => field.is_part_of_colour = Some(*group_key),
1759                            None => {
1760                                let group_key = combined_fields.keys().len() as u8 + 1;
1761                                combined_fields.insert(colour_field_name.to_owned(), group_key);
1762                                field.is_part_of_colour = Some(group_key);
1763                            }
1764                        }
1765                    }
1766                    found = true;
1767                    break;
1768                }
1769            }
1770
1771            if !found {
1772
1773                // We need to check if it's a loc field before reporting it as unfound.
1774                for loc_field in self.localised_fields() {
1775                    if loc_field.name == raw_field.name {
1776                        found = true;
1777                        break;
1778                    }
1779                }
1780
1781                // We automatically ignore certain old fields that have nothing to do with the game's data.
1782                if !found && !IGNORABLE_FIELDS.contains(&&*raw_field.name) {
1783                    unfound_fields.push(format!("{}/{}", raw_table_name, raw_field.name));
1784                }
1785            }
1786        }
1787    }
1788
1789    /// Populates the `localised_fields` list from Assembly Kit data.
1790    ///
1791    /// This function identifies fields that should be extracted to LOC files based on
1792    /// Assembly Kit localisable field data and updates the definition's `localised_fields` list.
1793    /// All identified localised fields are set to [`FieldType::StringU8`] for consistency.
1794    ///
1795    /// # Arguments
1796    ///
1797    /// * `raw_definition` - The Assembly Kit table definition
1798    /// * `raw_localisable_fields` - List of all localisable fields from the Assembly Kit
1799    ///
1800    /// # Feature
1801    ///
1802    /// This function requires the `integration_assembly_kit` feature.
1803    #[cfg(feature = "integration_assembly_kit")]
1804    pub fn update_from_raw_localisable_fields(&mut self, raw_definition: &RawDefinition, raw_localisable_fields: &[RawLocalisableField]) {
1805        let raw_table_name = &raw_definition.name.as_ref().unwrap()[..raw_definition.name.as_ref().unwrap().len() - 4];
1806        let localisable_fields_names = raw_localisable_fields.iter()
1807            .filter(|x| x.table_name == raw_table_name)
1808            .map(|x| &*x.field)
1809            .collect::<Vec<&str>>();
1810
1811        if !localisable_fields_names.is_empty() {
1812            let localisable_fields = raw_definition.fields.iter()
1813                .filter(|x| localisable_fields_names.contains(&&*x.name))
1814                .collect::<Vec<&RawField>>();
1815
1816            self.localised_fields = localisable_fields.iter().map(|x| From::from(*x)).collect();
1817
1818            // Set their type to StringU8 for consistency.
1819            self.localised_fields.iter_mut().for_each(|field| field.field_type = FieldType::StringU8);
1820        }
1821    }
1822}
1823
1824/// Implementation of `Field`.
1825impl Field {
1826
1827    /// Creates a new field with the specified properties.
1828    ///
1829    /// # Arguments
1830    ///
1831    /// * `name` - Field name
1832    /// * `field_type` - Data type of the field
1833    /// * `is_key` - Whether this field is part of the primary key
1834    /// * `default_value` - Optional default value
1835    /// * `is_filename` - Whether this field contains a filename
1836    /// * `filename_relative_path` - Optional path hints for filename fields
1837    /// * `is_reference` - Optional foreign key reference `(table, column)`
1838    /// * `lookup` - Optional lookup columns
1839    /// * `description` - Field description
1840    /// * `ca_order` - Visual position in Assembly Kit
1841    /// * `is_bitwise` - Number of boolean columns to expand into (0 or 1 = no expansion)
1842    /// * `enum_values` - Map of integer values to string names for enum fields
1843    /// * `is_part_of_colour` - Optional RGB colour group index
1844    ///
1845    /// # Returns
1846    ///
1847    /// Returns a new [`Field`] instance with the specified properties.
1848    pub fn new(
1849        name: String,
1850        field_type: FieldType,
1851        is_key: bool,
1852        default_value: Option<String>,
1853        is_filename: bool,
1854        filename_relative_path: Option<String>,
1855        is_reference: Option<(String, String)>,
1856        lookup: Option<Vec<String>>,
1857        description: String,
1858        ca_order: i16,
1859        is_bitwise: i32,
1860        enum_values: BTreeMap<i32, String>,
1861        is_part_of_colour: Option<u8>,
1862    ) -> Self {
1863        Self {
1864            name,
1865            field_type,
1866            is_key,
1867            default_value,
1868            is_filename,
1869            filename_relative_path,
1870            is_reference,
1871            lookup,
1872            description,
1873            ca_order,
1874            is_bitwise,
1875            enum_values,
1876            is_part_of_colour,
1877            unused: false
1878        }
1879    }
1880
1881    //----------------------------------------------------------------------//
1882    // Manual getter implementations with patch support
1883    //----------------------------------------------------------------------//
1884
1885    /// Returns the field name.
1886    pub fn name(&self) -> &str {
1887        &self.name
1888    }
1889
1890    /// Returns the field's data type.
1891    pub fn field_type(&self) -> &FieldType {
1892        &self.field_type
1893    }
1894
1895    /// Returns whether this field is a key field, applying patches if provided.
1896    ///
1897    /// # Arguments
1898    ///
1899    /// * `schema_patches` - Optional patches to check for overrides
1900    ///
1901    /// # Returns
1902    ///
1903    /// Returns `true` if the field is a key field (either by base definition or patch).
1904    pub fn is_key(&self, schema_patches: Option<&DefinitionPatch>) -> bool {
1905        if let Some(schema_patches) = schema_patches {
1906            if let Some(patch) = schema_patches.get(self.name()) {
1907                if let Some(field_patch) = patch.get("is_key") {
1908                    return field_patch.parse().unwrap_or(false);
1909                }
1910            }
1911        }
1912
1913        self.is_key
1914    }
1915
1916    /// Returns the field's default value, applying patches if provided.
1917    ///
1918    /// # Arguments
1919    ///
1920    /// * `schema_patches` - Optional patches to check for overrides
1921    ///
1922    /// # Returns
1923    ///
1924    /// Returns the default value if set (either by base definition or patch).
1925    pub fn default_value(&self, schema_patches: Option<&DefinitionPatch>) -> Option<String> {
1926        if let Some(schema_patches) = schema_patches {
1927            if let Some(patch) = schema_patches.get(self.name()) {
1928                if let Some(field_patch) = patch.get("default_value") {
1929                    return Some(field_patch.to_string());
1930                }
1931            }
1932        }
1933
1934        self.default_value.clone()
1935    }
1936
1937    /// Returns whether this field contains a filename, applying patches if provided.
1938    ///
1939    /// # Arguments
1940    ///
1941    /// * `schema_patches` - Optional patches to check for overrides
1942    ///
1943    /// # Returns
1944    ///
1945    /// Returns `true` if the field contains a filename path.
1946    pub fn is_filename(&self, schema_patches: Option<&DefinitionPatch>) -> bool {
1947        if let Some(schema_patches) = schema_patches {
1948            if let Some(patch) = schema_patches.get(self.name()) {
1949                if let Some(field_patch) = patch.get("is_filename") {
1950                    return field_patch.parse().unwrap_or(false);
1951                }
1952            }
1953        }
1954
1955        self.is_filename
1956    }
1957
1958    /// Returns the filename relative paths, applying patches if provided.
1959    ///
1960    /// The paths are split by semicolons and backslashes are converted to forward slashes.
1961    ///
1962    /// # Arguments
1963    ///
1964    /// * `schema_patches` - Optional patches to check for overrides
1965    ///
1966    /// # Returns
1967    ///
1968    /// Returns a vector of relative path strings, or [`None`] if no paths are defined.
1969    pub fn filename_relative_path(&self, schema_patches: Option<&DefinitionPatch>) -> Option<Vec<String>> {
1970        if let Some(schema_patches) = schema_patches {
1971            if let Some(patch) = schema_patches.get(self.name()) {
1972                if let Some(field_patch) = patch.get("filename_relative_path") {
1973                    return Some(field_patch.replace('\\', "/").split(';').map(|x| x.to_string()).collect::<Vec<String>>());
1974                }
1975            }
1976        }
1977
1978        self.filename_relative_path.clone().map(|x| x.replace('\\', "/").split(';').map(|x| x.to_string()).collect::<Vec<String>>())
1979    }
1980
1981    /// Returns the foreign key reference information, applying patches if provided.
1982    ///
1983    /// # Arguments
1984    ///
1985    /// * `schema_patches` - Optional patches to check for overrides
1986    ///
1987    /// # Returns
1988    ///
1989    /// Returns `Some((table_name, column_name))` if this field references another table,
1990    /// or [`None`] if it doesn't. The table name does not include the `_tables` suffix.
1991    pub fn is_reference(&self, schema_patches: Option<&DefinitionPatch>) -> Option<(String,String)> {
1992        if let Some(schema_patches) = schema_patches {
1993            if let Some(patch) = schema_patches.get(self.name()) {
1994                if let Some(field_patch) = patch.get("is_reference") {
1995                    let split = field_patch.splitn(2, ';').collect::<Vec<_>>();
1996                    if split.len() == 2 {
1997                        return Some((split[0].to_string(), split[1].to_string()));
1998                    }
1999                }
2000            }
2001        }
2002
2003        self.is_reference.clone()
2004    }
2005
2006    /// Returns the lookup column list, applying patches if provided.
2007    ///
2008    /// Lookup columns are additional columns from the referenced table that should
2009    /// be displayed in the UI alongside the referenced field.
2010    ///
2011    /// # Arguments
2012    ///
2013    /// * `schema_patches` - Optional patches to check for overrides
2014    ///
2015    /// # Returns
2016    ///
2017    /// Returns a vector of column names to look up, or [`None`] if no lookups are defined.
2018    pub fn lookup(&self, schema_patches: Option<&DefinitionPatch>) -> Option<Vec<String>> {
2019        if let Some(schema_patches) = schema_patches {
2020            if let Some(patch) = schema_patches.get(self.name()) {
2021                if let Some(field_patch) = patch.get("lookup") {
2022                    return Some(field_patch.split(';').map(|x| x.to_string()).collect());
2023                }
2024            }
2025        }
2026
2027        self.lookup.clone()
2028    }
2029
2030    /// Returns the lookup column list without applying patches.
2031    ///
2032    /// # Returns
2033    ///
2034    /// Returns a vector of column names from the base definition, ignoring any patches.
2035    pub fn lookup_no_patch(&self) -> Option<Vec<String>> {
2036        self.lookup.clone()
2037    }
2038
2039    /// Returns hardcoded lookup values from patches.
2040    ///
2041    /// Hardcoded lookups provide predefined value mappings that don't require
2042    /// querying the referenced table. This is useful for performance or when
2043    /// the referenced table is not available.
2044    ///
2045    /// # Arguments
2046    ///
2047    /// * `schema_patches` - Optional patches to check for hardcoded values
2048    ///
2049    /// # Returns
2050    ///
2051    /// Returns a map of key values to their display strings. Returns an empty
2052    /// map if no hardcoded lookups are defined.
2053    pub fn lookup_hardcoded(&self, schema_patches: Option<&DefinitionPatch>) -> HashMap<String, String> {
2054        if let Some(schema_patches) = schema_patches {
2055            if let Some(patch) = schema_patches.get(self.name()) {
2056                if let Some(field_patch) = patch.get("lookup_hardcoded") {
2057                    let entries = field_patch.split(":::::").map(|x| x.split(";;;;;").collect::<Vec<_>>()).collect::<Vec<_>>();
2058                    let mut hashmap = HashMap::new();
2059                    for entry in entries {
2060                        hashmap.insert(entry[0].to_owned(), entry[1].to_owned());
2061                    }
2062                    return hashmap;
2063                }
2064            }
2065        }
2066
2067        HashMap::new()
2068    }
2069
2070    /// Returns the field description, applying patches if provided.
2071    ///
2072    /// # Arguments
2073    ///
2074    /// * `schema_patches` - Optional patches to check for overrides
2075    ///
2076    /// # Returns
2077    ///
2078    /// Returns the field's description text. May be empty if no description is set.
2079    pub fn description(&self, schema_patches: Option<&DefinitionPatch>) -> String {
2080        if let Some(schema_patches) = schema_patches {
2081            if let Some(patch) = schema_patches.get(self.name()) {
2082                if let Some(field_patch) = patch.get("description") {
2083                    return field_patch.to_owned();
2084                }
2085            }
2086        }
2087
2088        self.description.to_owned()
2089    }
2090
2091    /// Returns the CA order value.
2092    ///
2093    /// This represents the visual position of the field in CA's Assembly Kit.
2094    /// A value of `-1` indicates the position is unknown.
2095    pub fn ca_order(&self) ->  i16 {
2096        self.ca_order
2097    }
2098
2099    /// Returns the bitwise expansion count.
2100    ///
2101    /// # Returns
2102    ///
2103    /// - `0` or `1`: No bitwise expansion
2104    /// - `> 1`: Number of boolean columns this field should be expanded into
2105    pub fn is_bitwise(&self) -> i32 {
2106        self.is_bitwise
2107    }
2108
2109    /// Returns the enum value mappings.
2110    ///
2111    /// # Returns
2112    ///
2113    /// Returns a reference to the map of integer values to their string names.
2114    /// Empty if this field is not an enum.
2115    pub fn enum_values(&self) -> &BTreeMap<i32,String> {
2116        &self.enum_values
2117    }
2118
2119    /// Returns the enum values as an [`Option`].
2120    pub fn enum_values_to_option(&self) -> Option<BTreeMap<i32, String>> {
2121        if self.enum_values.is_empty() { None }
2122        else { Some(self.enum_values.clone()) }
2123    }
2124
2125    /// Returns the enum values as a semicolon-separated string.
2126    ///
2127    /// # Returns
2128    ///
2129    /// Returns a string in the format `"value1,name1;value2,name2;..."`.
2130    pub fn enum_values_to_string(&self) -> String {
2131        self.enum_values.iter().map(|(x, y)| format!("{x},{y}")).collect::<Vec<String>>().join(";")
2132    }
2133
2134    /// Returns the RGB colour group index.
2135    ///
2136    /// # Returns
2137    ///
2138    /// Returns the colour group index if this field is part of an RGB triplet,
2139    /// or [`None`] if it's not a colour field.
2140    pub fn is_part_of_colour(&self) -> Option<u8>{
2141        self.is_part_of_colour
2142    }
2143
2144    /// Returns whether this field should be treated as numeric (currently always `false`).
2145    ///
2146    /// This is a placeholder for future functionality and currently always returns `false`.
2147    ///
2148    /// # Arguments
2149    ///
2150    /// * `_schema_patches` - Unused (reserved for future use)
2151    pub fn is_numeric(&self, _schema_patches: Option<&DefinitionPatch>) -> bool {
2152        false
2153        /*
2154        if let Some(schema_patches) = schema_patches {
2155            if let Some(patch) = schema_patches.get(self.name()) {
2156                if let Some(is_numeric) = patch.get("is_numeric") {
2157                    return is_numeric.parse::<bool>().unwrap_or(false);
2158                }
2159            }
2160        }
2161
2162        false*/
2163    }
2164
2165    /// Returns whether this field cannot be empty, checking patches.
2166    ///
2167    /// # Arguments
2168    ///
2169    /// * `schema_patches` - Optional patches to check for the `not_empty` flag
2170    ///
2171    /// # Returns
2172    ///
2173    /// Returns `true` if the field is marked as "cannot be empty" via a patch.
2174    pub fn cannot_be_empty(&self, schema_patches: Option<&DefinitionPatch>) -> bool {
2175        if let Some(schema_patches) = schema_patches {
2176            if let Some(patch) = schema_patches.get(self.name()) {
2177                if let Some(cannot_be_empty) = patch.get("not_empty") {
2178                    return cannot_be_empty.parse::<bool>().unwrap_or(false);
2179                }
2180            }
2181        }
2182
2183        false
2184    }
2185
2186    /// Returns whether this field is unused by the game.
2187    ///
2188    /// Fields marked as unused are still present in the binary format but are not
2189    /// actually used by the game logic. This information is primarily determined via patches.
2190    ///
2191    /// # Arguments
2192    ///
2193    /// * `schema_patches` - Optional patches to check for the `unused` flag
2194    ///
2195    /// # Returns
2196    ///
2197    /// Returns `true` if the field is marked as unused (either in the base definition or via patch).
2198    pub fn unused(&self, schema_patches: Option<&DefinitionPatch>) -> bool {
2199
2200        // By default all fields are used, except the ones set through patches. If it's already marked unused, return early.
2201        self.unused || {
2202
2203            if let Some(schema_patches) = schema_patches {
2204                if let Some(patch) = schema_patches.get(self.name()) {
2205                    if let Some(cannot_be_empty) = patch.get("unused") {
2206                        return cannot_be_empty.parse::<bool>().unwrap_or(false);
2207                    }
2208                }
2209            }
2210
2211            false
2212        }
2213    }
2214
2215    /// Generates a SQL column definition string for this field.
2216    ///
2217    /// This function creates the SQL column definition portion for use in a
2218    /// `CREATE TABLE` statement, including the data type and optional default value.
2219    ///
2220    /// # Arguments
2221    ///
2222    /// * `schema_patches` - Optional patches to apply when getting the default value
2223    ///
2224    /// # Returns
2225    ///
2226    /// Returns a string like `"field_name" INTEGER DEFAULT "value"`.
2227    ///
2228    /// # Feature
2229    ///
2230    /// This function requires the `integration_sqlite` feature.
2231    #[cfg(feature = "integration_sqlite")]
2232    pub fn map_to_sql_string(&self, schema_patches: Option<&DefinitionPatch>) -> String {
2233        let mut string = format!(" \"{}\" {:?} ", self.name(), self.field_type().map_to_sql_type());
2234
2235        if let Some(default_value) = self.default_value(schema_patches) {
2236            string.push_str(&format!(" DEFAULT \"{}\"", default_value.replace("\"", "\"\"")));
2237        }
2238
2239        string
2240    }
2241}
2242
2243impl FieldType {
2244
2245    /// Maps this field type to its corresponding SQLite type.
2246    ///
2247    /// This function converts RPFM's field types to their appropriate SQLite equivalents
2248    /// for database operations.
2249    ///
2250    /// # Returns
2251    ///
2252    /// Returns the SQLite [`Type`] that best represents this field type:
2253    /// - Numeric types → [`Type::Integer`] or [`Type::Real`]
2254    /// - String types → [`Type::Text`]
2255    /// - Sequence types → [`Type::Blob`]
2256    ///
2257    /// # Feature
2258    ///
2259    /// This function requires the `integration_sqlite` feature.
2260    #[cfg(feature = "integration_sqlite")]
2261    pub fn map_to_sql_type(&self) -> Type {
2262        match self {
2263            FieldType::Boolean => Type::Integer,
2264            FieldType::F32 => Type::Real,
2265            FieldType::F64 => Type::Real,
2266            FieldType::I16 => Type::Integer,
2267            FieldType::I32 => Type::Integer,
2268            FieldType::I64 => Type::Integer,
2269            FieldType::ColourRGB => Type::Text,
2270            FieldType::StringU8 => Type::Text,
2271            FieldType::StringU16 => Type::Text,
2272            FieldType::OptionalI16 => Type::Integer,
2273            FieldType::OptionalI32 => Type::Integer,
2274            FieldType::OptionalI64 => Type::Integer,
2275            FieldType::OptionalStringU8 => Type::Text,
2276            FieldType::OptionalStringU16 => Type::Text,
2277            FieldType::SequenceU16(_) => Type::Blob,
2278            FieldType::SequenceU32(_) => Type::Blob,
2279        }
2280    }
2281}
2282//---------------------------------------------------------------------------//
2283//                         Extra Implementations
2284//---------------------------------------------------------------------------//
2285
2286/// Default implementation of `Schema`.
2287impl Default for Schema {
2288    fn default() -> Self {
2289        Self {
2290            version: CURRENT_STRUCTURAL_VERSION,
2291            definitions: HashMap::new(),
2292            patches: HashMap::new()
2293        }
2294    }
2295}
2296
2297/// Default implementation of `FieldType`.
2298impl Default for Field {
2299    fn default() -> Self {
2300        Self {
2301            name: String::from("new_field"),
2302            field_type: FieldType::StringU8,
2303            is_key: false,
2304            default_value: None,
2305            is_filename: false,
2306            filename_relative_path: None,
2307            is_reference: None,
2308            lookup: None,
2309            description: String::from(""),
2310            ca_order: -1,
2311            is_bitwise: 0,
2312            enum_values: BTreeMap::new(),
2313            is_part_of_colour: None,
2314            unused: false,
2315        }
2316    }
2317}
2318
2319/// Display implementation of `FieldType`.
2320impl Display for FieldType {
2321    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2322        match self {
2323            FieldType::Boolean => write!(f, "Boolean"),
2324            FieldType::F32 => write!(f, "F32"),
2325            FieldType::F64 => write!(f, "F64"),
2326            FieldType::I16 => write!(f, "I16"),
2327            FieldType::I32 => write!(f, "I32"),
2328            FieldType::I64 => write!(f, "I64"),
2329            FieldType::ColourRGB => write!(f, "ColourRGB"),
2330            FieldType::StringU8 => write!(f, "StringU8"),
2331            FieldType::StringU16 => write!(f, "StringU16"),
2332            FieldType::OptionalI16 => write!(f, "OptionalI16"),
2333            FieldType::OptionalI32 => write!(f, "OptionalI32"),
2334            FieldType::OptionalI64 => write!(f, "OptionalI64"),
2335            FieldType::OptionalStringU8 => write!(f, "OptionalStringU8"),
2336            FieldType::OptionalStringU16 => write!(f, "OptionalStringU16"),
2337            FieldType::SequenceU16(_) => write!(f, "SequenceU16"),
2338            FieldType::SequenceU32(_) => write!(f, "SequenceU32"),
2339        }
2340    }
2341}
2342
2343/// Implementation of `From<&RawDefinition>` for `Definition.
2344impl From<&DecodedData> for FieldType {
2345    fn from(data: &DecodedData) -> Self {
2346        match data {
2347            DecodedData::Boolean(_) => FieldType::Boolean,
2348            DecodedData::F32(_) => FieldType::F32,
2349            DecodedData::F64(_) => FieldType::F64,
2350            DecodedData::I16(_) => FieldType::I16,
2351            DecodedData::I32(_) => FieldType::I32,
2352            DecodedData::I64(_) => FieldType::I64,
2353            DecodedData::ColourRGB(_) => FieldType::ColourRGB,
2354            DecodedData::StringU8(_) => FieldType::StringU8,
2355            DecodedData::StringU16(_) => FieldType::StringU16,
2356            DecodedData::OptionalI16(_) => FieldType::OptionalI16,
2357            DecodedData::OptionalI32(_) => FieldType::OptionalI32,
2358            DecodedData::OptionalI64(_) => FieldType::OptionalI64,
2359            DecodedData::OptionalStringU8(_) => FieldType::OptionalStringU8,
2360            DecodedData::OptionalStringU16(_) => FieldType::OptionalStringU16,
2361            DecodedData::SequenceU16(_) => FieldType::SequenceU16(Box::new(Definition::new(INVALID_VERSION, None))),
2362            DecodedData::SequenceU32(_) => FieldType::SequenceU32(Box::new(Definition::new(INVALID_VERSION, None))),
2363        }
2364    }
2365}
2366
2367/// Special serializer function to sort the definitions HashMap before serializing.
2368fn ordered_map_definitions<S>(value: &HashMap<String, Vec<Definition>>, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, {
2369    let ordered: BTreeMap<_, _> = value.iter().collect();
2370    ordered.serialize(serializer)
2371}
2372
2373/// Special serializer function to sort the patches HashMap before serializing.
2374fn ordered_map_patches<S>(value: &HashMap<String, HashMap<String, HashMap<String, String>>>, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, {
2375    let ordered: BTreeMap<_, BTreeMap<_, BTreeMap<_, _>>> = value.iter().map(|(a, x)| (a, x.iter().map(|(b, y)| (b, y.iter().collect())).collect())).collect();
2376    ordered.serialize(serializer)
2377}