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    pub name: String,
335
336    /// Data type of the field.
337    ///
338    /// Determines how the field's binary data is interpreted.
339    pub 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    pub 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    pub 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    pub 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    pub 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    pub 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    pub 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    pub 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    pub 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    pub 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    pub 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    pub 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    pub 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, Default, 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    #[default]
482    StringU8,
483
484    /// UTF-16 encoded string with [`u16`] length prefix (max 65535 characters).
485    StringU16,
486
487    /// Optional 16-bit signed integer (1-byte flag + value if present).
488    OptionalI16,
489
490    /// Optional 32-bit signed integer (1-byte flag + value if present).
491    OptionalI32,
492
493    /// Optional 64-bit signed integer (1-byte flag + value if present).
494    OptionalI64,
495
496    /// Optional UTF-8 encoded string (1-byte flag + [`u16`] length prefix + string if present).
497    OptionalStringU8,
498
499    /// Optional UTF-16 encoded string (1-byte flag + [`u16`] length prefix + string if present).
500    OptionalStringU16,
501
502    /// Array with [`u16`] element count followed by elements matching the nested definition.
503    SequenceU16(Box<Definition>),
504
505    /// Array with [`u32`] element count followed by elements matching the nested definition.
506    SequenceU32(Box<Definition>)
507}
508
509//---------------------------------------------------------------------------//
510//                       Enum & Structs Implementations
511//---------------------------------------------------------------------------//
512
513/// Implementation of [`Schema`].
514impl Schema {
515
516    /// Saves patches to a local patches file, merging with existing patches.
517    ///
518    /// This function loads existing patches from the file, merges the provided patches with them,
519    /// and writes the combined patch set back to the file in RON format.
520    ///
521    /// # Arguments
522    ///
523    /// * `patches` - The patches to add or update
524    /// * `path` - Path to the local patches file (must exist)
525    ///
526    /// # Returns
527    ///
528    /// Returns [`Ok`] if successful, or an error if:
529    /// - The file cannot be read or written
530    /// - The file contains invalid patch data
531    ///
532    /// # Example
533    ///
534    /// ```no_run
535    /// use std::collections::HashMap;
536    /// use std::path::Path;
537    /// use rpfm_lib::schema::{Schema, DefinitionPatch};
538    ///
539    /// let mut patches: HashMap<String, DefinitionPatch> = HashMap::new();
540    /// // Add patches...
541    ///
542    /// Schema::save_patches(&patches, Path::new("my_patches.ron"))?;
543    /// # Ok::<(), rpfm_lib::error::RLibError>(())
544    /// ```
545    pub fn save_patches(patches: &HashMap<String, DefinitionPatch>, path: &Path) -> Result<()> {
546        let mut file = BufReader::new(File::open(path)?);
547        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
548        file.read_to_end(&mut data)?;
549        let mut local_patches: HashMap<String, DefinitionPatch> = from_bytes(&data)?;
550
551        Self::add_patches_to_patch_set(&mut local_patches, patches);
552
553        let mut file = BufWriter::new(File::create(path)?);
554        let config = PrettyConfig::default();
555        file.write_all(to_string_pretty(&local_patches, config)?.as_bytes())?;
556
557        Ok(())
558    }
559
560    /// Removes all local patches for a specific table.
561    ///
562    /// This function loads the patches file, removes all patches for the specified table,
563    /// and writes the updated patch set back to the file.
564    ///
565    /// # Arguments
566    ///
567    /// * `table_name` - Name of the table to remove patches for
568    /// * `path` - Path to the local patches file
569    ///
570    /// # Returns
571    ///
572    /// Returns [`Ok`] if successful, even if no there were no patches to remove, or an error
573    /// if file I/O fails.
574    pub fn remove_patches_for_table(table_name: &str, path: &Path) -> Result<()> {
575        let mut file = BufReader::new(File::open(path)?);
576        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
577        file.read_to_end(&mut data)?;
578        let mut local_patches: HashMap<String, DefinitionPatch> = from_bytes(&data)?;
579
580        local_patches.remove(table_name);
581
582        let mut file = BufWriter::new(File::create(path)?);
583        let config = PrettyConfig::default();
584        file.write_all(to_string_pretty(&local_patches, config)?.as_bytes())?;
585
586        Ok(())
587    }
588
589    /// Removes all local patches for a specific field in a table.
590    ///
591    /// This function loads the patches file, removes all patches for the specified table's field,
592    /// and writes the updated patch set back to the file. Other fields in the table are unaffected.
593    ///
594    /// # Arguments
595    ///
596    /// * `table_name` - Name of the table containing the field
597    /// * `field_name` - Name of the field to remove patches for
598    /// * `path` - Path to the local patches file
599    ///
600    /// # Returns
601    ///
602    /// Returns [`Ok`] if successful, even if no there were no patches to remove, or an error
603    /// if file I/O fails.
604    pub fn remove_patches_for_table_and_field(table_name: &str, field_name: &str, path: &Path) -> Result<()> {
605        let mut file = BufReader::new(File::open(path)?);
606        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
607        file.read_to_end(&mut data)?;
608        let mut local_patches: HashMap<String, DefinitionPatch> = from_bytes(&data)?;
609
610        if let Some(table_patches) = local_patches.get_mut(table_name) {
611            table_patches.remove(field_name);
612        }
613
614        let mut file = BufWriter::new(File::create(path)?);
615        let config = PrettyConfig::default();
616        file.write_all(to_string_pretty(&local_patches, config)?.as_bytes())?;
617
618        Ok(())
619    }
620
621    /// Retrieves a specific patch value for a table's column.
622    ///
623    /// # Arguments
624    ///
625    /// * `table_name` - Name of the table
626    /// * `column_name` - Name of the column
627    /// * `key` - Patch key (e.g., "is_key", "default_value")
628    ///
629    /// # Returns
630    ///
631    /// Returns the patch value if found, or [`None`] otherwise.
632    pub fn patch_value(&self, table_name: &str, column_name: &str, key: &str) -> Option<&String> {
633        self.patches.get(table_name)?.get(column_name)?.get(key)
634    }
635
636    /// Retrieves all patches for a specific table.
637    ///
638    /// # Arguments
639    ///
640    /// * `table_name` - Name of the table
641    ///
642    /// # Returns
643    ///
644    /// Returns the table's patches if found, or [`None`] otherwise.
645    pub fn patches_for_table(&self, table_name: &str) -> Option<&DefinitionPatch> {
646        self.patches.get(table_name)
647    }
648
649    /// Merges patches into an existing patch set.
650    ///
651    /// This function adds the provided patches to the patch set, merging them with any
652    /// existing patches. If a patch already exists for a table/column/key combination,
653    /// it will be extended with the new values.
654    ///
655    /// # Arguments
656    ///
657    /// * `patch_set` - The patch set to merge into (modified in place)
658    /// * `patches` - The patches to add
659    ///
660    /// # Note
661    ///
662    /// After adding patches, you must re-retrieve any definitions you've already retrieved
663    /// for the patches to take effect, as patches are applied when retrieving definitions.
664    pub fn add_patches_to_patch_set(patch_set: &mut HashMap<String, DefinitionPatch>, patches: &HashMap<String, DefinitionPatch>) {
665        patches.iter().for_each(|(table_name, column_patch)| {
666            match patch_set.get_mut(table_name) {
667                Some(column_patch_current) => {
668                    column_patch.iter().for_each(|(column_name, patch)| {
669                        match column_patch_current.get_mut(column_name) {
670                            Some(patch_current) => patch_current.extend(patch.clone()),
671                            None => {
672                                column_patch_current.insert(column_name.to_owned(), patch.clone());
673                            }
674                        }
675                    });
676                }
677                None => {
678                    patch_set.insert(table_name.to_owned(), column_patch.clone());
679                }
680            }
681        });
682    }
683
684    /// Adds or updates a table definition in the schema.
685    ///
686    /// If a definition with the same version already exists for this table, it will be replaced.
687    /// Otherwise, the definition is added to the table's version list.
688    ///
689    /// # Arguments
690    ///
691    /// * `table_name` - Name of the table
692    /// * `definition` - The definition to add or update
693    pub fn add_definition(&mut self, table_name: &str, definition: &Definition) {
694        match self.definitions.get_mut(table_name) {
695            Some(definitions) => {
696                match definitions.iter_mut().find(|def| def.version() == definition.version()) {
697                    Some(def) => *def = definition.to_owned(),
698                    None => definitions.push(definition.to_owned()),
699                }
700            },
701            None => { self.definitions.insert(table_name.to_owned(), vec![definition.to_owned()]); },
702        }
703    }
704
705    /// Removes a specific table definition version from the schema.
706    ///
707    /// # Arguments
708    ///
709    /// * `table_name` - Name of the table
710    /// * `version` - Version number of the definition to remove
711    pub fn remove_definition(&mut self, table_name: &str, version: i32) {
712        if let Some(definitions) = self.definitions.get_mut(table_name) {
713            let mut index_to_delete = vec![];
714            for (index, definition) in definitions.iter().enumerate() {
715                if definition.version == version {
716                    index_to_delete.push(index);
717                }
718            }
719
720            index_to_delete.iter().rev().for_each(|index| { definitions.remove(*index); });
721        }
722    }
723
724    /// Returns a cloned copy of all definitions for a table.
725    ///
726    /// # Arguments
727    ///
728    /// * `table_name` - Name of the table
729    ///
730    /// # Returns
731    ///
732    /// Returns a cloned vector of all definitions for the table, or [`None`] if not found.
733    pub fn definitions_by_table_name_cloned(&self, table_name: &str) -> Option<Vec<Definition>> {
734        self.definitions.get(table_name).cloned()
735    }
736
737    /// Returns a reference to all definitions for a table.
738    ///
739    /// # Arguments
740    ///
741    /// * `table_name` - Name of the table
742    ///
743    /// # Returns
744    ///
745    /// Returns a reference to the vector of definitions, or [`None`] if not found.
746    pub fn definitions_by_table_name(&self, table_name: &str) -> Option<&Vec<Definition>>  {
747        self.definitions.get(table_name)
748    }
749
750    /// Returns a mutable reference to all definitions for a table.
751    ///
752    /// # Arguments
753    ///
754    /// * `table_name` - Name of the table
755    ///
756    /// # Returns
757    ///
758    /// Returns a mutable reference to the vector of definitions, or [`None`] if not found.
759    pub fn definitions_by_table_name_mut(&mut self, table_name: &str) -> Option<&mut Vec<Definition>>  {
760        self.definitions.get_mut(table_name)
761    }
762
763    /// Returns the newest compatible definition for a table based on candidate versions.
764    ///
765    /// This function first tries to find a definition matching the highest version number
766    /// from the candidates (typically from a dependency database). If that fails, it
767    /// falls back to the first (newest) definition in the schema.
768    ///
769    /// # Arguments
770    ///
771    /// * `table_name` - Name of the table
772    /// * `candidates` - List of candidate definitions (typically from dependencies)
773    ///
774    /// # Returns
775    ///
776    /// Returns the best matching definition, or [`None`] if the table is not found.
777    pub fn definition_newer(&self, table_name: &str, candidates: &[Definition]) -> Option<&Definition> {
778
779        // Version is... complicated. We don't really want the last one, but the last one compatible with our game.
780        // So we have to try to get it first from the Dependency Database first. If that fails, we fall back to the schema.
781        if let Some(definition) = candidates.iter().max_by(|x, y| x.version().cmp(y.version())) {
782            self.definition_by_name_and_version(table_name, *definition.version())
783        }
784
785        // If there was no coincidence in the dependency database... we risk ourselves getting the last definition we have for
786        // that db from the schema.
787        else{
788            self.definitions.get(table_name)?.first()
789        }
790    }
791
792    /// Returns a reference to a specific table definition by name and version.
793    ///
794    /// # Arguments
795    ///
796    /// * `table_name` - Name of the table
797    /// * `table_version` - Version number of the definition
798    ///
799    /// # Returns
800    ///
801    /// Returns the definition if found, or [`None`] otherwise.
802    pub fn definition_by_name_and_version(&self, table_name: &str, table_version: i32) -> Option<&Definition>  {
803        self.definitions.get(table_name)?.iter().find(|definition| *definition.version() == table_version)
804    }
805
806    /// Returns a mutable reference to a specific table definition by name and version.
807    ///
808    /// # Arguments
809    ///
810    /// * `table_name` - Name of the table
811    /// * `table_version` - Version number of the definition
812    ///
813    /// # Returns
814    ///
815    /// Returns the definition if found, or [`None`] otherwise.
816    pub fn definition_by_name_and_version_mut(&mut self, table_name: &str, table_version: i32) -> Option<&mut Definition>  {
817        self.definitions.get_mut(table_name)?.iter_mut().find(|definition| *definition.version() == table_version)
818    }
819
820    /// Loads a [`Schema`] from a RON file, optionally merging local patches.
821    ///
822    /// This function loads a schema from a `.ron` file and applies any patches from both
823    /// the schema itself and an optional local patches file. Patches from the local file
824    /// are merged with schema patches and applied to all definitions.
825    ///
826    /// # Arguments
827    ///
828    /// * `path` - Path to the schema `.ron` file
829    /// * `local_patches` - Optional path to a local patches file
830    ///
831    /// # Returns
832    ///
833    /// Returns the loaded schema with all patches applied, or an error if loading fails.
834    pub fn load(path: &Path, local_patches: Option<&Path>) -> Result<Self> {
835        let mut file = BufReader::new(File::open(path)?);
836        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
837        file.read_to_end(&mut data)?;
838        let mut schema: Self = from_bytes(&data)?;
839        let mut patches = schema.patches().clone();
840
841        // If we got local patches, add them to the patches list.
842        //
843        // NOTE: we separate the patches from the schemas because otherwise an schema edit will save local patches into the schema,
844        // and we want them to remain local.
845        if let Some(path) = local_patches {
846            if let Ok(file) = File::open(path) {
847                let mut file = BufReader::new(file);
848                let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
849                file.read_to_end(&mut data)?;
850                if let Ok(local_patches) = from_bytes::<HashMap<String, DefinitionPatch>>(&data) {
851                    Self::add_patches_to_patch_set(&mut patches, &local_patches);
852                }
853            }
854        }
855
856        // Preload all patches to their respective definitions.
857        for (table_name, patches) in &patches {
858            if let Some(definitions) = schema.definitions_by_table_name_mut(table_name) {
859                for definition in definitions {
860                    definition.set_patches(patches.clone());
861                }
862            }
863        }
864
865        Ok(schema)
866    }
867
868    /// Loads a [`Schema`] from a JSON file.
869    ///
870    /// Similar to [`load()`], but reads from a JSON file instead of RON. Applies all
871    /// patches from the schema to the definitions.
872    ///
873    /// # Arguments
874    ///
875    /// * `path` - Path to the schema `.json` file
876    ///
877    /// # Returns
878    ///
879    /// Returns the loaded schema with patches applied, or an error if loading fails.
880    ///
881    /// [`load()`]: Schema::load
882    pub fn load_json(path: &Path) -> Result<Self> {
883        let mut file = BufReader::new(File::open(path)?);
884        let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
885        file.read_to_end(&mut data)?;
886        let mut schema: Self = serde_json::from_slice(&data)?;
887
888        // Preload all patches to their respective definitions.
889        for (table_name, patches) in schema.patches().clone() {
890            if let Some(definitions) = schema.definitions_by_table_name_mut(&table_name) {
891                for definition in definitions {
892                    definition.set_patches(patches.clone());
893                }
894            }
895        }
896
897        Ok(schema)
898    }
899
900    /// Saves the schema to a RON file.
901    ///
902    /// This function saves the schema to a `.ron` file, automatically:
903    /// - Creating parent directories if needed
904    /// - Sorting definitions by version (newest first)
905    /// - Cleaning up invalid references
906    /// - Moving certain patches from definitions to schema patches
907    ///
908    /// # Arguments
909    ///
910    /// * `path` - Path where the schema file should be saved
911    ///
912    /// # Returns
913    ///
914    /// Returns [`Ok`] if saved successfully, or an error if file I/O fails.
915    pub fn save(&mut self, path: &Path) -> Result<()> {
916
917        // Make sure the path exists to avoid problems with updating schemas.
918        if let Some(parent_folder) = path.parent() {
919            DirBuilder::new().recursive(true).create(parent_folder)?;
920        }
921
922        let mut file = BufWriter::new(File::create(path)?);
923        let config = PrettyConfig::default();
924
925        let mut patches = HashMap::new();
926
927        // Make sure all definitions are properly sorted by version number.
928        self.definitions.iter_mut().for_each(|(table_name, definitions)| {
929            definitions.sort_by(|a, b| b.version().cmp(a.version()));
930
931            // Fix for empty dependencies, again.
932            definitions.iter_mut().for_each(|definition| {
933                definition.fields.iter_mut().for_each(|field| {
934                    if let Some((ref_table, ref_column)) = field.is_reference(None) {
935                        if ref_table.trim().is_empty() || ref_column.trim().is_empty() {
936                            field.is_reference = None;
937                        }
938                    }
939                });
940
941                // Move any lookup_hardcoded patches to schema patches.
942                if definition.patches.values().any(|x| x.keys().any(|y| y == "lookup_hardcoded")) {
943                    let mut def_patches = definition.patches().clone();
944                    def_patches.retain(|_, value| {
945                        value.retain(|key, _| key == "lookup_hardcoded");
946                        !value.is_empty()
947                    });
948                    patches.insert(table_name.to_owned(), def_patches);
949                }
950
951                // Move any unused patches to schema patches.
952                if definition.patches.values().any(|x| x.keys().any(|y| y == "unused")) {
953                    let mut def_patches = definition.patches().clone();
954                    def_patches.retain(|_, value| {
955                        value.retain(|key, _| key == "unused");
956                        !value.is_empty()
957                    });
958                    patches.insert(table_name.to_owned(), def_patches);
959                }
960            })
961        });
962
963        Self::add_patches_to_patch_set(self.patches_mut(), &patches);
964
965        file.write_all(to_string_pretty(&self, config)?.as_bytes())?;
966        Ok(())
967    }
968
969    /// Saves the schema to a JSON file.
970    ///
971    /// This function saves the schema to a `.json` file at the specified path, automatically:
972    /// - Creating parent directories if needed
973    /// - Changing the extension to `.json`
974    /// - Sorting definitions by version (newest first)
975    /// - Pretty-printing the JSON output
976    ///
977    /// # Arguments
978    ///
979    /// * `path` - Path where the schema file should be saved (extension will be changed to `.json`)
980    ///
981    /// # Returns
982    ///
983    /// Returns [`Ok`] if saved successfully, or an error if file I/O or serialization fails.
984    pub fn save_json(&mut self, path: &Path) -> Result<()> {
985        let mut path = path.to_path_buf();
986        path.set_extension("json");
987
988        // Make sure the path exists to avoid problems with updating schemas.
989        if let Some(parent_folder) = path.parent() {
990            DirBuilder::new().recursive(true).create(parent_folder)?;
991        }
992
993        let mut file = BufWriter::new(File::create(&path)?);
994
995        // Make sure all definitions are properly sorted by version number.
996        self.definitions.iter_mut().for_each(|(_, definitions)| {
997            definitions.sort_by(|a, b| b.version().cmp(a.version()));
998        });
999
1000        file.write_all(serde_json::to_string_pretty(&self)?.as_bytes())?;
1001        Ok(())
1002    }
1003
1004    /// Exports all schema files in a folder to JSON format.
1005    ///
1006    /// This function loads all schema files (`.ron`) for supported games from the specified folder
1007    /// and saves them as `.json` files in the same location. This is primarily used for
1008    /// compatibility with external tools that prefer JSON.
1009    ///
1010    /// # Arguments
1011    ///
1012    /// * `schema_folder_path` - Path to the folder containing schema `.ron` files
1013    ///
1014    /// # Returns
1015    ///
1016    /// Returns [`Ok`] if all schemas are successfully exported, or an error if any operation fails.
1017    ///
1018    /// # Note
1019    ///
1020    /// This function processes schemas in parallel for better performance.
1021    pub fn export_to_json(schema_folder_path: &Path) -> Result<()> {
1022        let games = SupportedGames::default();
1023
1024        games.games_sorted().par_iter().map(|x| x.schema_file_name()).try_for_each(|schema_file| {
1025            let mut schema_path = schema_folder_path.to_owned();
1026            schema_path.push(schema_file);
1027
1028            let mut schema = Schema::load(&schema_path, None)?;
1029            schema_path.set_extension("json");
1030            schema.save_json(&schema_path)?;
1031            Ok(())
1032        })
1033    }
1034
1035    /// Updates a schema from a legacy format to the current format.
1036    ///
1037    /// This function handles migration of schema files from older structural versions (e.g., v4)
1038    /// to the current structural version (v5). It automatically detects the schema version and
1039    /// applies the necessary transformations.
1040    ///
1041    /// # Arguments
1042    ///
1043    /// * `schema_path` - Path to the schema file to update
1044    /// * `schema_patches_path` - Path to the schema patches file
1045    /// * `game_name` - Name of the game this schema is for
1046    ///
1047    /// # Returns
1048    ///
1049    /// Returns [`Ok`] if the update succeeds, or an error if the update process fails.
1050    pub fn update(schema_path: &Path, schema_patches_path: &Path, game_name: &str) -> Result<()>{
1051        v4::SchemaV4::update(schema_path, schema_patches_path, game_name)
1052    }
1053
1054    /// Returns all columns that reference fields in the specified table.
1055    ///
1056    /// This function searches through all table definitions in the schema to find fields
1057    /// that have foreign key references pointing to the provided table's fields.
1058    ///
1059    /// # Arguments
1060    ///
1061    /// * `table_name` - Name of the table to find references to
1062    /// * `definition` - Definition of the table (used to get the field list)
1063    ///
1064    /// # Returns
1065    ///
1066    /// Returns a map where:
1067    /// - Keys are local field names from the provided definition
1068    /// - Values are maps of `table_name -> Vec<field_name>` containing all referencing fields
1069    ///
1070    /// # Example
1071    ///
1072    /// For a `factions_tables` table, this might return:
1073    /// ```text
1074    /// {
1075    ///   "key": {
1076    ///     "units_tables": ["faction_key"],
1077    ///     "characters_tables": ["faction_key", "home_faction_key"]
1078    ///   }
1079    /// }
1080    /// ```
1081    pub fn referencing_columns_for_table(&self, table_name: &str, definition: &Definition) -> HashMap<String, HashMap<String, Vec<String>>> {
1082
1083        // Iterate over all definitions and find the ones referencing our table/field.
1084        let fields_processed = definition.fields_processed();
1085        let definitions = self.definitions();
1086        let table_name_no_tables = table_name.to_owned().drain(..table_name.len() - 7).collect::<String>();
1087
1088        fields_processed.iter().filter_map(|field| {
1089
1090            let references = definitions.par_iter().filter_map(|(ver_name, ver_definitions)| {
1091                let mut references = ver_definitions.iter().filter_map(|ver_definition| {
1092                    let ver_patches = Some(ver_definition.patches());
1093                    let references = ver_definition.fields_processed().iter().filter_map(|ver_field| {
1094                        if let Some((source_table_name, source_column_name)) = ver_field.is_reference(ver_patches) {
1095                            if table_name_no_tables == source_table_name && field.name() == source_column_name {
1096                                Some(ver_field.name().to_owned())
1097                            } else { None }
1098                        } else { None }
1099                    }).collect::<Vec<String>>();
1100                    if references.is_empty() {
1101                        None
1102                    } else {
1103                        Some(references)
1104                    }
1105                }).flatten().collect::<Vec<String>>();
1106                if references.is_empty() {
1107                    None
1108                } else {
1109                    references.sort();
1110                    references.dedup();
1111                    Some((ver_name.to_owned(), references))
1112                }
1113            }).collect::<HashMap<String, Vec<String>>>();
1114            if references.is_empty() {
1115                None
1116            } else {
1117                Some((field.name().to_owned(), references))
1118            }
1119        }).collect()
1120    }
1121
1122    /// Returns all tables and columns that reference the specified column, and whether LOC files may be affected.
1123    ///
1124    /// This function performs a recursive search to find all fields that reference the specified column,
1125    /// including indirect references (fields that reference fields that reference the target column).
1126    /// It also checks if changing the column would affect localisation keys.
1127    ///
1128    /// # Arguments
1129    ///
1130    /// * `table_name` - Name of the table containing the column (with or without `_tables` suffix)
1131    /// * `column_name` - Name of the column to find references to
1132    /// * `fields` - The table's field list
1133    /// * `localised_fields` - The table's localised field list
1134    ///
1135    /// # Returns
1136    ///
1137    /// Returns a tuple of:
1138    /// - A map of `table_name -> Vec<field_name>` containing all referencing fields (recursively)
1139    /// - A boolean indicating if LOC files may need updates (true if the column is a key field and the table has localised fields)
1140    ///
1141    /// # Note
1142    ///
1143    /// Recursion is supported for table references, but not for LOC field detection.
1144    pub fn tables_and_columns_referencing_our_own(
1145        &self,
1146        table_name: &str,
1147        column_name: &str,
1148        fields: &[Field],
1149        localised_fields: &[Field]
1150    ) -> (BTreeMap<String, Vec<String>>, bool) {
1151
1152        // Make sure the table name is correct.
1153        let short_table_name = if table_name.ends_with("_tables") { table_name.split_at(table_name.len() - 7).0 } else { table_name };
1154        let mut tables: BTreeMap<String, Vec<String>> = BTreeMap::new();
1155
1156        // We get all the db definitions from the schema, then iterate all of them to find what tables/columns reference our own.
1157        for (ref_table_name, ref_definition) in self.definitions() {
1158            let mut columns: Vec<String> = vec![];
1159            for ref_version in ref_definition {
1160                let ref_fields = ref_version.fields_processed();
1161                let ref_patches = Some(ref_version.patches());
1162                let ref_fields_localised = ref_version.localised_fields();
1163                for ref_field in &ref_fields {
1164                    if let Some((ref_ref_table, ref_ref_field)) = ref_field.is_reference(ref_patches) {
1165
1166                        // As this applies to all versions of a table, skip repeated fields.
1167                        if ref_ref_table == short_table_name && ref_ref_field == column_name && !columns.iter().any(|x| x == ref_field.name()) {
1168                            columns.push(ref_field.name().to_owned());
1169
1170                            // If we find a referencing column, get recursion working to check if there is any column referencing this one that needs to be edited.
1171                            let (ref_of_ref, _) = self.tables_and_columns_referencing_our_own(ref_table_name, ref_field.name(), &ref_fields, ref_fields_localised);
1172                            for refs in &ref_of_ref {
1173                                match tables.get_mut(refs.0) {
1174                                    Some(columns) => for value in refs.1 {
1175                                        if !columns.contains(value) {
1176                                            columns.push(value.to_owned());
1177                                        }
1178                                    }
1179                                    None => { tables.insert(refs.0.to_owned(), refs.1.to_vec()); },
1180                                }
1181                            }
1182                        }
1183                    }
1184                }
1185            }
1186
1187            // Only add them if we actually found columns.
1188            if !columns.is_empty() {
1189                tables.insert(ref_table_name.to_owned(), columns);
1190            }
1191        }
1192
1193        // Also, check if we have to be careful about localised fields.
1194        let patches = self.patches().get(table_name);
1195        let has_loc_fields = if let Some(field) = fields.iter().find(|x| x.name() == column_name) {
1196            (field.is_key(patches) || field.name() == "key") && !localised_fields.is_empty()
1197        } else { false };
1198
1199        (tables, has_loc_fields)
1200    }
1201    /// Loads patches from a RON-formatted string.
1202    ///
1203    /// # Arguments
1204    ///
1205    /// * `patch` - RON-formatted string containing patches
1206    ///
1207    /// # Returns
1208    ///
1209    /// Returns the parsed patches, or an error if the string is not valid RON.
1210    pub fn load_patches_from_str(patch: &str) -> Result<HashMap<String, DefinitionPatch>> {
1211        from_str(patch).map_err(From::from)
1212    }
1213
1214    /// Loads definitions from a RON-formatted string.
1215    ///
1216    /// # Arguments
1217    ///
1218    /// * `definition` - RON-formatted string containing table definitions
1219    ///
1220    /// # Returns
1221    ///
1222    /// Returns the parsed definitions, or an error if the string is not valid RON.
1223    pub fn load_definitions_from_str(definition: &str) -> Result<HashMap<String, Definition>> {
1224        from_str(definition).map_err(From::from)
1225    }
1226
1227    /// Exports patches to a RON-formatted string.
1228    ///
1229    /// # Arguments
1230    ///
1231    /// * `patches` - The patches to export
1232    ///
1233    /// # Returns
1234    ///
1235    /// Returns the RON-formatted string, or an error if serialization fails.
1236    pub fn export_patches_to_str(patches: &HashMap<String, DefinitionPatch>) -> Result<String> {
1237        let config = PrettyConfig::default();
1238        ron::ser::to_string_pretty(&patches, config).map_err(From::from)
1239    }
1240
1241    /// Exports definitions to a RON-formatted string.
1242    ///
1243    /// # Arguments
1244    ///
1245    /// * `definitions` - The definitions to export
1246    ///
1247    /// # Returns
1248    ///
1249    /// Returns the RON-formatted string, or an error if serialization fails.
1250    pub fn export_definitions_to_str(definitions: &HashMap<String, Definition>) -> Result<String> {
1251        let config = PrettyConfig::default();
1252        ron::ser::to_string_pretty(&definitions, config).map_err(From::from)
1253    }
1254}
1255
1256/// Implementation of [`Definition`].
1257impl Definition {
1258
1259    /// Creates a new empty definition for a specific version.
1260    ///
1261    /// # Arguments
1262    ///
1263    /// * `version` - The version number for this definition
1264    /// * `schema_patches` - Optional patches to apply to this definition
1265    ///
1266    /// # Returns
1267    ///
1268    /// Returns a new empty definition with no fields.
1269    pub fn new(version: i32, schema_patches: Option<&DefinitionPatch>) -> Definition {
1270        Definition {
1271            version,
1272            localised_fields: vec![],
1273            fields: vec![],
1274            localised_key_order: vec![],
1275            patches: schema_patches.cloned().unwrap_or_default(),
1276        }
1277    }
1278
1279    /// Creates a new definition with the specified fields.
1280    ///
1281    /// # Arguments
1282    ///
1283    /// * `version` - The version number for this definition
1284    /// * `fields` - The table's field list
1285    /// * `loc_fields` - The localised fields list
1286    /// * `schema_patches` - Optional patches to apply to this definition
1287    ///
1288    /// # Returns
1289    ///
1290    /// Returns a new definition with the provided fields.
1291    pub fn new_with_fields(version: i32, fields: &[Field], loc_fields: &[Field], schema_patches: Option<&DefinitionPatch>) -> Definition {
1292        Definition {
1293            version,
1294            localised_fields: loc_fields.to_vec(),
1295            fields: fields.to_vec(),
1296            localised_key_order: vec![],
1297            patches: schema_patches.cloned().unwrap_or_default(),
1298        }
1299    }
1300
1301    /// Returns reference and lookup information for all fields with foreign key references.
1302    ///
1303    /// This function extracts foreign key information from all fields in the definition
1304    /// that have a reference to another table.
1305    ///
1306    /// # Returns
1307    ///
1308    /// Returns a map where:
1309    /// - Keys are field indices (as [`i32`])
1310    /// - Values are tuples of `(referenced_table, referenced_column, optional_lookup_columns)`
1311    ///
1312    /// Only fields with `is_reference` set are included in the result.
1313    pub fn reference_data(&self) -> BTreeMap<i32, (String, String, Option<Vec<String>>)> {
1314        self.fields.iter()
1315            .enumerate()
1316            .filter(|x| x.1.is_reference.is_some())
1317            .map(|x| (x.0 as i32, (x.1.is_reference.clone().unwrap().0, x.1.is_reference.clone().unwrap().1, x.1.lookup.clone())))
1318            .collect()
1319    }
1320
1321    /// Returns the processed field list with transformations applied.
1322    ///
1323    /// This function processes the raw field list and applies various transformations:
1324    /// - **Bitwise fields**: Expanded into multiple boolean fields (e.g., `flags` → `flags_1`, `flags_2`, etc.)
1325    /// - **Enum fields**: Converted to StringU8 fields
1326    /// - **Colour fields**: RGB triplets merged into single ColourRGB fields
1327    /// - **Numeric fields**: Converted to I32 fields (with patches)
1328    ///
1329    /// This is the field list that should be used for UI display and data editing.
1330    ///
1331    /// # Returns
1332    ///
1333    /// Returns the processed field list with all transformations applied.
1334    pub fn fields_processed(&self) -> Vec<Field> {
1335        let mut split_colour_fields: BTreeMap<u8, Field> = BTreeMap::new();
1336        let patches = Some(self.patches());
1337        let mut fields = self.fields().iter()
1338            .filter_map(|x|
1339                if x.is_bitwise() > 1 {
1340                    let unused = x.unused(patches);
1341                    let mut fields = vec![x.clone(); x.is_bitwise() as usize];
1342                    fields.iter_mut().enumerate().for_each(|(index, field)| {
1343                        field.set_name(format!("{}_{}", field.name(), index + 1));
1344                        field.set_field_type(FieldType::Boolean);
1345                        field.set_unused(unused);
1346                    });
1347                    Some(fields)
1348                }
1349
1350                else if !x.enum_values().is_empty() {
1351                    let mut field = x.clone();
1352                    field.set_field_type(FieldType::StringU8);
1353                    Some(vec![field; 1])
1354                }
1355
1356                else if let Some(colour_index) = x.is_part_of_colour() {
1357                    match split_colour_fields.get_mut(&colour_index) {
1358
1359                        // If found, add the default value to the other previously known default value.
1360                        Some(field) => {
1361                            let default_value = match x.default_value(None) {
1362                                Some(default_value) => {
1363                                    if x.name.ends_with("_r") || x.name.ends_with("_red") || x.name == "r" || x.name == "red" {
1364                                        field.default_value.clone().map(|df| {
1365                                            format!("{:X}{}", default_value.parse::<i32>().unwrap_or(0), &df[2..])
1366                                        })
1367                                    } else if x.name.ends_with("_g") || x.name.ends_with("_green") || x.name == "g" || x.name == "green" {
1368                                        field.default_value.clone().map(|df| {
1369                                            format!("{}{:X}{}", &df[..2], default_value.parse::<i32>().unwrap_or(0), &df[4..])
1370                                        })
1371                                    } else if x.name.ends_with("_b") || x.name.ends_with("_blue") || x.name == "b" || x.name == "blue" {
1372                                        field.default_value.clone().map(|df| {
1373                                            format!("{}{:X}", &df[..4], default_value.parse::<i32>().unwrap_or(0))
1374                                        })
1375                                    } else {
1376                                        Some("000000".to_owned())
1377                                    }
1378                                }
1379                                None => Some("000000".to_owned())
1380                            };
1381
1382                            // Update the default value with the one for this colour.
1383                            field.set_default_value(default_value);
1384
1385                            if !field.unused(patches) {
1386                                field.set_unused(x.unused(patches));
1387                            }
1388                        },
1389                        None => {
1390                            let unused = x.unused(patches);
1391                            let colour_split = x.name().rsplitn(2, '_').collect::<Vec<&str>>();
1392                            let colour_field_name = if colour_split.len() == 2 {
1393                                format!("{}{}", colour_split[1].to_lowercase(), MERGE_COLOUR_POST)
1394                            } else {
1395                                format!("{}_{}", MERGE_COLOUR_NO_NAME.to_lowercase(), colour_index)
1396                            };
1397
1398                            let mut field = x.clone();
1399                            field.set_name(colour_field_name);
1400                            field.set_field_type(FieldType::ColourRGB);
1401                            field.set_unused(unused);
1402
1403                            // We need to fix the default value so it's a ColourRGB one.
1404                            let default_value = match field.default_value(None) {
1405                                Some(default_value) => {
1406                                    if x.name.ends_with("_r") || x.name.ends_with("_red") || x.name == "r" || x.name == "red" {
1407                                        Some(format!("{:X}0000", default_value.parse::<i32>().unwrap_or(0)))
1408                                    } else if x.name.ends_with("_g") || x.name.ends_with("_green") || x.name == "g" || x.name == "green" {
1409                                        Some(format!("00{:X}00", default_value.parse::<i32>().unwrap_or(0)))
1410                                    } else if x.name.ends_with("_b") || x.name.ends_with("_blue") || x.name == "b" || x.name == "blue" {
1411                                        Some(format!("0000{:X}", default_value.parse::<i32>().unwrap_or(0)))
1412                                    } else {
1413                                        Some("000000".to_owned())
1414                                    }
1415                                }
1416                                None => Some("000000".to_owned())
1417                            };
1418
1419                            field.set_default_value(default_value);
1420
1421                            split_colour_fields.insert(colour_index, field);
1422                        }
1423                    }
1424
1425                    None
1426                }
1427
1428                else if x.is_numeric(patches) {
1429                    let mut field = x.clone();
1430                    field.set_field_type(FieldType::I32);
1431                    Some(vec![field; 1])
1432                }
1433
1434                else {
1435                    Some(vec![x.clone(); 1])
1436                }
1437            )
1438            .flatten()
1439            .collect::<Vec<Field>>();
1440
1441        // Second pass to add the combined colour fields.
1442        fields.append(&mut split_colour_fields.values().cloned().collect::<Vec<Field>>());
1443        fields
1444    }
1445
1446    /// Returns the original raw field corresponding to a processed field index.
1447    ///
1448    /// This function maps a field from the processed field list back to its original
1449    /// raw field definition. This is useful when you need to access the underlying
1450    /// field data before transformations like bitwise expansion.
1451    ///
1452    /// # Arguments
1453    ///
1454    /// * `index` - Index in the processed field list
1455    ///
1456    /// # Returns
1457    ///
1458    /// Returns the original field from the raw field list.
1459    ///
1460    /// # Panics
1461    ///
1462    /// Panics if the field is not found (which should never happen for valid indices).
1463    ///
1464    /// # Note
1465    ///
1466    /// This function does not work correctly with combined colour fields, as they don't
1467    /// have a direct 1:1 mapping to a single raw field.
1468    pub fn original_field_from_processed(&self, index: usize) -> Field {
1469        let fields = self.fields();
1470        let processed = self.fields_processed();
1471
1472        let field_processed = &processed[index];
1473        let name = if field_processed.is_bitwise() > 1 {
1474            let mut name = field_processed.name().to_owned();
1475            name.drain(..name.rfind('_').unwrap()).collect::<String>()
1476        }
1477        else {field_processed.name().to_owned() };
1478
1479        fields.iter().find(|x| *x.name() == name).unwrap().clone()
1480    }
1481
1482    /// Returns the processed field list sorted by either key fields or CA order.
1483    ///
1484    /// This function returns the processed fields sorted according to the specified criteria.
1485    ///
1486    /// # Arguments
1487    ///
1488    /// * `key_first` - If `true`, sorts key fields first, then non-key fields. If `false`, sorts by CA order.
1489    ///
1490    /// # Returns
1491    ///
1492    /// Returns the sorted field list. Fields with `ca_order == -1` are left in their original order
1493    /// when sorting by CA order.
1494    pub fn fields_processed_sorted(&self, key_first: bool) -> Vec<Field> {
1495        let mut fields = self.fields_processed();
1496        let patches = Some(self.patches());
1497        fields.sort_by(|a, b| {
1498            if key_first {
1499                if a.is_key(patches) && b.is_key(patches) { Ordering::Equal }
1500                else if a.is_key(patches) && !b.is_key(patches) { Ordering::Less }
1501                else if !a.is_key(patches) && b.is_key(patches) { Ordering::Greater }
1502                else { Ordering::Equal }
1503            }
1504            else if a.ca_order() == -1 || b.ca_order() == -1 { Ordering::Equal }
1505            else { a.ca_order().cmp(&b.ca_order()) }
1506        });
1507        fields
1508    }
1509
1510    /// Returns the position of a column in the processed field list by name.
1511    ///
1512    /// # Arguments
1513    ///
1514    /// * `column_name` - Name of the column to find
1515    ///
1516    /// # Returns
1517    ///
1518    /// Returns the column's index in the processed field list, or [`None`] if not found.
1519    pub fn column_position_by_name(&self, column_name: &str) -> Option<usize> {
1520        self.fields_processed()
1521            .iter()
1522            .position(|x| x.name() == column_name)
1523    }
1524
1525    /// Returns the positions of all key columns in the processed field list.
1526    ///
1527    /// # Returns
1528    ///
1529    /// Returns a vector of indices for all fields marked as key fields.
1530    pub fn key_column_positions(&self) -> Vec<usize> {
1531        self.fields_processed()
1532            .iter()
1533            .enumerate()
1534            .filter(|(_, x)| x.is_key(Some(self.patches())))
1535            .map(|(x, _)| x)
1536            .collect::<Vec<_>>()
1537    }
1538
1539    /// Returns the positions of all key columns sorted by CA order.
1540    ///
1541    /// This function returns key column positions in the same order as they appear in
1542    /// CA's Assembly Kit, rather than the binary order. This is primarily needed for
1543    /// `twad_key_deletes` functionality, which uses CA's ordering.
1544    ///
1545    /// # Returns
1546    ///
1547    /// Returns a vector of key column indices sorted by their `ca_order` value.
1548    pub fn key_column_positions_by_ca_order(&self) -> Vec<usize> {
1549        let fields_processed = self.fields_processed();
1550        let mut keys = fields_processed
1551            .iter()
1552            .enumerate()
1553            .filter(|(_, x)| x.is_key(Some(self.patches())))
1554            .map(|(x, _)| x)
1555            .collect::<Vec<_>>();
1556
1557        keys.sort_by_key(|x| fields_processed[*x].ca_order);
1558        keys
1559    }
1560
1561    /// Generates a SQL `CREATE TABLE` statement for this definition.
1562    ///
1563    /// This function creates a SQL statement suitable for creating a table in SQLite
1564    /// with the structure defined by this definition. The table includes additional
1565    /// metadata columns (`pack_name`, `file_name`, `is_vanilla`) for tracking data sources.
1566    ///
1567    /// # Arguments
1568    ///
1569    /// * `table_name` - Name for the SQL table
1570    ///
1571    /// # Returns
1572    ///
1573    /// Returns the SQL `CREATE TABLE` statement as a string.
1574    ///
1575    /// # Note
1576    ///
1577    /// Foreign key constraints are intentionally disabled because Total War tables
1578    /// (especially in mods) often have referential integrity issues. The function
1579    /// only creates a primary key constraint on the key fields.
1580    ///
1581    /// # Feature
1582    ///
1583    /// This function requires the `integration_sqlite` feature.
1584    #[cfg(feature = "integration_sqlite")]
1585    pub fn map_to_sql_create_table_string(&self, table_name: &str) -> String {
1586        let patches = Some(self.patches());
1587        let fields_sorted = self.fields_processed();
1588        let fields_query = fields_sorted.iter().map(|field| field.map_to_sql_string(patches)).collect::<Vec<_>>().join(",");
1589
1590        let local_keys_join = fields_sorted.iter().filter_map(|field| if field.is_key(patches) { Some(format!("\"{}\"", field.name()))} else { None }).collect::<Vec<_>>().join(",");
1591        let local_keys = format!("CONSTRAINT unique_key PRIMARY KEY (\"pack_name\", \"file_name\", {local_keys_join})");
1592        //let foreign_keys = fields_sorted.iter()
1593        //    .filter_map(|field| field.is_reference(patches).clone().map(|(ref_table, ref_column)| (field.name(), ref_table, ref_column)))
1594        //    .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"))
1595        //    .collect::<Vec<_>>()
1596        //    .join(",");
1597
1598        //if foreign_keys.is_empty() {
1599            if local_keys_join.is_empty() {
1600                format!("CREATE TABLE \"{}_v{}\" (\"pack_name\" STRING NOT NULL, \"file_name\" STRING NOT NULL, \"is_vanilla\" INTEGER DEFAULT 0, {})",
1601                    table_name.replace('\"', "'"),
1602                    self.version(),
1603                    fields_query
1604                )
1605            } else {
1606                format!("CREATE TABLE \"{}_v{}\" (\"pack_name\" STRING NOT NULL, \"file_name\" STRING NOT NULL, \"is_vanilla\" INTEGER DEFAULT 0, {}, {})",
1607                    table_name.replace('\"', "'"),
1608                    self.version(),
1609                    fields_query,
1610                    local_keys
1611                )
1612            }
1613        /*} else if local_keys_join.is_empty() {
1614            format!("CREATE TABLE \"{}_v{}\" (\"table_unique_id\" INTEGER DEFAULT 0, {}, {})",
1615                table_name.replace('\"', "'"),
1616                self.version(),
1617                fields_query,
1618                foreign_keys
1619            )
1620        } else {
1621            format!("CREATE TABLE \"{}_v{}\" (\"table_unique_id\" INTEGER DEFAULT 0, {}, {}, {})",
1622                table_name.replace('\"', "'"),
1623                self.version(),
1624                fields_query,
1625                local_keys,
1626                foreign_keys
1627            )
1628        }*/
1629    }
1630
1631    /// Generates the column list for a SQL `INSERT INTO` statement.
1632    ///
1633    /// This function creates the column name list portion of an `INSERT INTO` statement,
1634    /// including the metadata columns and all processed fields.
1635    ///
1636    /// # Returns
1637    ///
1638    /// Returns a string like `("pack_name", "file_name", "is_vanilla", "field1", "field2", ...)`.
1639    ///
1640    /// # Feature
1641    ///
1642    /// This function requires the `integration_sqlite` feature.
1643    #[cfg(feature = "integration_sqlite")]
1644    pub fn map_to_sql_insert_into_string(&self) -> String {
1645        let fields_sorted = self.fields_processed();
1646        let fields_query = fields_sorted.iter().map(|field| format!("\"{}\"", field.name())).collect::<Vec<_>>().join(",");
1647        let fields_query = format!("(\"pack_name\", \"file_name\", \"is_vanilla\", {fields_query})");
1648
1649        fields_query
1650    }
1651
1652    /// Updates field properties from Assembly Kit raw definition data.
1653    ///
1654    /// This function updates the definition's fields with data extracted from the Assembly Kit,
1655    /// matching fields by name and updating specific properties. Fields not found in the
1656    /// Assembly Kit are added to the `unfound_fields` list for reporting.
1657    ///
1658    /// # Updated Properties
1659    ///
1660    /// - `is_key`: Primary key status
1661    /// - `default_value`: Default value for new rows
1662    /// - `filename_relative_path`: Path hints for filename fields
1663    /// - `is_filename`: Whether the field contains a filename
1664    /// - `is_reference`: Foreign key reference information
1665    /// - `lookup`: Lookup column information
1666    /// - `description`: Field description
1667    /// - `ca_order`: Visual position in Assembly Kit
1668    /// - `is_part_of_colour`: Auto-detected RGB colour field grouping
1669    ///
1670    /// # Arguments
1671    ///
1672    /// * `raw_definition` - The Assembly Kit definition data
1673    /// * `unfound_fields` - List to append unfound field names to (format: `"table_name/field_name"`)
1674    ///
1675    /// # Note
1676    ///
1677    /// Fields in `IGNORABLE_FIELDS` are automatically skipped and not reported as unfound.
1678    ///
1679    /// # Feature
1680    ///
1681    /// This function requires the `integration_assembly_kit` feature.
1682    #[cfg(feature = "integration_assembly_kit")]
1683    pub fn update_from_raw_definition(&mut self, raw_definition: &RawDefinition, unfound_fields: &mut Vec<String>) {
1684        let raw_table_name = &raw_definition.name.as_ref().unwrap()[..raw_definition.name.as_ref().unwrap().len() - 4];
1685        let mut combined_fields = BTreeMap::new();
1686        for (index, raw_field) in raw_definition.fields.iter().enumerate() {
1687
1688            let mut found = false;
1689            for field in &mut self.fields {
1690                if field.name == raw_field.name {
1691                    if (raw_field.primary_key == "1" && !field.is_key) || (raw_field.primary_key == "0" && field.is_key) {
1692                        field.is_key = raw_field.primary_key == "1";
1693                    }
1694
1695                    if raw_field.default_value.is_some() {
1696                        field.default_value = raw_field.default_value.clone();
1697                    }
1698
1699                    if let Some(ref path) = raw_field.filename_relative_path {
1700                        let mut new_path = path.to_owned();
1701                        if path.contains(",") {
1702                            new_path = path.split(',').map(|x| x.trim()).join(";");
1703                        }
1704
1705                        field.filename_relative_path = Some(new_path);
1706                    }
1707
1708                    // Some fields are marked as filename, but only have fragment paths, which do not seem to correlate to game file paths.
1709                    // We need to disable those to avoid false positives on diagnostics.
1710                    field.is_filename = match raw_field.is_filename {
1711                        Some(_) => !(raw_field.fragment_path.is_some() && raw_field.filename_relative_path.is_none()),
1712                        None => false,
1713                    };
1714
1715                    // Make sure to cleanup any old invalid definition.
1716                    if let Some(ref description) = raw_field.field_description {
1717                        field.description = description.to_owned();
1718                    } else {
1719                        field.description = String::new();
1720                    }
1721
1722                    // We reset these so we don't inherit wrong references from older tables.
1723                    field.is_reference = Default::default();
1724                    field.lookup = Default::default();
1725                    if let Some(ref table) = raw_field.column_source_table {
1726                        if let Some(ref columns) = raw_field.column_source_column {
1727                            if !table.is_empty() && !columns.is_empty() && !columns[0].is_empty() {
1728                                field.is_reference = Some((table.to_owned(), columns[0].to_owned()));
1729                                if columns.len() > 1 {
1730                                    field.lookup = Some(columns[1..].to_vec());
1731                                }
1732                            }
1733                        }
1734                    }
1735
1736                    field.ca_order = index as i16;
1737
1738                    // Detect and group colour fields.
1739                    let is_numeric = matches!(field.field_type, FieldType::I16 | FieldType::I32 | FieldType::I64 | FieldType::F32 | FieldType::F64);
1740
1741                    if is_numeric && (
1742                        field.name.ends_with("_r") ||
1743                        field.name.ends_with("_g") ||
1744                        field.name.ends_with("_b") ||
1745                        field.name.ends_with("_red") ||
1746                        field.name.ends_with("_green") ||
1747                        field.name.ends_with("_blue") ||
1748                        field.name == "r" ||
1749                        field.name == "g" ||
1750                        field.name == "b" ||
1751                        field.name == "red" ||
1752                        field.name == "green" ||
1753                        field.name == "blue"
1754                    ) {
1755                        let colour_split = field.name.rsplitn(2, '_').collect::<Vec<&str>>();
1756                        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() };
1757
1758                        match combined_fields.get(&colour_field_name) {
1759                            Some(group_key) => field.is_part_of_colour = Some(*group_key),
1760                            None => {
1761                                let group_key = combined_fields.keys().len() as u8 + 1;
1762                                combined_fields.insert(colour_field_name.to_owned(), group_key);
1763                                field.is_part_of_colour = Some(group_key);
1764                            }
1765                        }
1766                    }
1767                    found = true;
1768                    break;
1769                }
1770            }
1771
1772            if !found {
1773
1774                // We need to check if it's a loc field before reporting it as unfound.
1775                for loc_field in self.localised_fields() {
1776                    if loc_field.name == raw_field.name {
1777                        found = true;
1778                        break;
1779                    }
1780                }
1781
1782                // We automatically ignore certain old fields that have nothing to do with the game's data.
1783                if !found && !IGNORABLE_FIELDS.contains(&&*raw_field.name) {
1784                    unfound_fields.push(format!("{}/{}", raw_table_name, raw_field.name));
1785                }
1786            }
1787        }
1788    }
1789
1790    /// Populates the `localised_fields` list from Assembly Kit data.
1791    ///
1792    /// This function identifies fields that should be extracted to LOC files based on
1793    /// Assembly Kit localisable field data and updates the definition's `localised_fields` list.
1794    /// All identified localised fields are set to [`FieldType::StringU8`] for consistency.
1795    ///
1796    /// # Arguments
1797    ///
1798    /// * `raw_definition` - The Assembly Kit table definition
1799    /// * `raw_localisable_fields` - List of all localisable fields from the Assembly Kit
1800    ///
1801    /// # Feature
1802    ///
1803    /// This function requires the `integration_assembly_kit` feature.
1804    #[cfg(feature = "integration_assembly_kit")]
1805    pub fn update_from_raw_localisable_fields(&mut self, raw_definition: &RawDefinition, raw_localisable_fields: &[RawLocalisableField]) {
1806        let raw_table_name = &raw_definition.name.as_ref().unwrap()[..raw_definition.name.as_ref().unwrap().len() - 4];
1807        let localisable_fields_names = raw_localisable_fields.iter()
1808            .filter(|x| x.table_name == raw_table_name)
1809            .map(|x| &*x.field)
1810            .collect::<Vec<&str>>();
1811
1812        if !localisable_fields_names.is_empty() {
1813            let localisable_fields = raw_definition.fields.iter()
1814                .filter(|x| localisable_fields_names.contains(&&*x.name))
1815                .collect::<Vec<&RawField>>();
1816
1817            self.localised_fields = localisable_fields.iter().map(|x| From::from(*x)).collect();
1818
1819            // Set their type to StringU8 for consistency.
1820            self.localised_fields.iter_mut().for_each(|field| field.field_type = FieldType::StringU8);
1821        }
1822    }
1823}
1824
1825/// Implementation of `Field`.
1826impl Field {
1827
1828    //----------------------------------------------------------------------//
1829    // Manual getter implementations with patch support
1830    //----------------------------------------------------------------------//
1831
1832    /// Returns the field name.
1833    pub fn name(&self) -> &str {
1834        &self.name
1835    }
1836
1837    /// Returns the field's data type.
1838    pub fn field_type(&self) -> &FieldType {
1839        &self.field_type
1840    }
1841
1842    /// Returns whether this field is a key field, applying patches if provided.
1843    ///
1844    /// # Arguments
1845    ///
1846    /// * `schema_patches` - Optional patches to check for overrides
1847    ///
1848    /// # Returns
1849    ///
1850    /// Returns `true` if the field is a key field (either by base definition or patch).
1851    pub fn is_key(&self, schema_patches: Option<&DefinitionPatch>) -> bool {
1852        if let Some(schema_patches) = schema_patches {
1853            if let Some(patch) = schema_patches.get(self.name()) {
1854                if let Some(field_patch) = patch.get("is_key") {
1855                    return field_patch.parse().unwrap_or(false);
1856                }
1857            }
1858        }
1859
1860        self.is_key
1861    }
1862
1863    /// Returns the field's default value, applying patches if provided.
1864    ///
1865    /// # Arguments
1866    ///
1867    /// * `schema_patches` - Optional patches to check for overrides
1868    ///
1869    /// # Returns
1870    ///
1871    /// Returns the default value if set (either by base definition or patch).
1872    pub fn default_value(&self, schema_patches: Option<&DefinitionPatch>) -> Option<String> {
1873        if let Some(schema_patches) = schema_patches {
1874            if let Some(patch) = schema_patches.get(self.name()) {
1875                if let Some(field_patch) = patch.get("default_value") {
1876                    return Some(field_patch.to_string());
1877                }
1878            }
1879        }
1880
1881        self.default_value.clone()
1882    }
1883
1884    /// Returns whether this field contains a filename, applying patches if provided.
1885    ///
1886    /// # Arguments
1887    ///
1888    /// * `schema_patches` - Optional patches to check for overrides
1889    ///
1890    /// # Returns
1891    ///
1892    /// Returns `true` if the field contains a filename path.
1893    pub fn is_filename(&self, schema_patches: Option<&DefinitionPatch>) -> bool {
1894        if let Some(schema_patches) = schema_patches {
1895            if let Some(patch) = schema_patches.get(self.name()) {
1896                if let Some(field_patch) = patch.get("is_filename") {
1897                    return field_patch.parse().unwrap_or(false);
1898                }
1899            }
1900        }
1901
1902        self.is_filename
1903    }
1904
1905    /// Returns the filename relative paths, applying patches if provided.
1906    ///
1907    /// The paths are split by semicolons and backslashes are converted to forward slashes.
1908    ///
1909    /// # Arguments
1910    ///
1911    /// * `schema_patches` - Optional patches to check for overrides
1912    ///
1913    /// # Returns
1914    ///
1915    /// Returns a vector of relative path strings, or [`None`] if no paths are defined.
1916    pub fn filename_relative_path(&self, schema_patches: Option<&DefinitionPatch>) -> Option<Vec<String>> {
1917        if let Some(schema_patches) = schema_patches {
1918            if let Some(patch) = schema_patches.get(self.name()) {
1919                if let Some(field_patch) = patch.get("filename_relative_path") {
1920                    return Some(field_patch.replace('\\', "/").split(';').map(|x| x.to_string()).collect::<Vec<String>>());
1921                }
1922            }
1923        }
1924
1925        self.filename_relative_path.clone().map(|x| x.replace('\\', "/").split(';').map(|x| x.to_string()).collect::<Vec<String>>())
1926    }
1927
1928    /// Returns the foreign key reference information, applying patches if provided.
1929    ///
1930    /// # Arguments
1931    ///
1932    /// * `schema_patches` - Optional patches to check for overrides
1933    ///
1934    /// # Returns
1935    ///
1936    /// Returns `Some((table_name, column_name))` if this field references another table,
1937    /// or [`None`] if it doesn't. The table name does not include the `_tables` suffix.
1938    pub fn is_reference(&self, schema_patches: Option<&DefinitionPatch>) -> Option<(String,String)> {
1939        if let Some(schema_patches) = schema_patches {
1940            if let Some(patch) = schema_patches.get(self.name()) {
1941                if let Some(field_patch) = patch.get("is_reference") {
1942                    let split = field_patch.splitn(2, ';').collect::<Vec<_>>();
1943                    if split.len() == 2 {
1944                        return Some((split[0].to_string(), split[1].to_string()));
1945                    }
1946                }
1947            }
1948        }
1949
1950        self.is_reference.clone()
1951    }
1952
1953    /// Returns the lookup column list, applying patches if provided.
1954    ///
1955    /// Lookup columns are additional columns from the referenced table that should
1956    /// be displayed in the UI alongside the referenced field.
1957    ///
1958    /// # Arguments
1959    ///
1960    /// * `schema_patches` - Optional patches to check for overrides
1961    ///
1962    /// # Returns
1963    ///
1964    /// Returns a vector of column names to look up, or [`None`] if no lookups are defined.
1965    pub fn lookup(&self, schema_patches: Option<&DefinitionPatch>) -> Option<Vec<String>> {
1966        if let Some(schema_patches) = schema_patches {
1967            if let Some(patch) = schema_patches.get(self.name()) {
1968                if let Some(field_patch) = patch.get("lookup") {
1969                    return Some(field_patch.split(';').map(|x| x.to_string()).collect());
1970                }
1971            }
1972        }
1973
1974        self.lookup.clone()
1975    }
1976
1977    /// Returns the lookup column list without applying patches.
1978    ///
1979    /// # Returns
1980    ///
1981    /// Returns a vector of column names from the base definition, ignoring any patches.
1982    pub fn lookup_no_patch(&self) -> Option<Vec<String>> {
1983        self.lookup.clone()
1984    }
1985
1986    /// Returns hardcoded lookup values from patches.
1987    ///
1988    /// Hardcoded lookups provide predefined value mappings that don't require
1989    /// querying the referenced table. This is useful for performance or when
1990    /// the referenced table is not available.
1991    ///
1992    /// # Arguments
1993    ///
1994    /// * `schema_patches` - Optional patches to check for hardcoded values
1995    ///
1996    /// # Returns
1997    ///
1998    /// Returns a map of key values to their display strings. Returns an empty
1999    /// map if no hardcoded lookups are defined.
2000    pub fn lookup_hardcoded(&self, schema_patches: Option<&DefinitionPatch>) -> HashMap<String, String> {
2001        if let Some(schema_patches) = schema_patches {
2002            if let Some(patch) = schema_patches.get(self.name()) {
2003                if let Some(field_patch) = patch.get("lookup_hardcoded") {
2004                    let entries = field_patch.split(":::::").map(|x| x.split(";;;;;").collect::<Vec<_>>()).collect::<Vec<_>>();
2005                    let mut hashmap = HashMap::new();
2006                    for entry in entries {
2007                        hashmap.insert(entry[0].to_owned(), entry[1].to_owned());
2008                    }
2009                    return hashmap;
2010                }
2011            }
2012        }
2013
2014        HashMap::new()
2015    }
2016
2017    /// Returns the field description, applying patches if provided.
2018    ///
2019    /// # Arguments
2020    ///
2021    /// * `schema_patches` - Optional patches to check for overrides
2022    ///
2023    /// # Returns
2024    ///
2025    /// Returns the field's description text. May be empty if no description is set.
2026    pub fn description(&self, schema_patches: Option<&DefinitionPatch>) -> String {
2027        if let Some(schema_patches) = schema_patches {
2028            if let Some(patch) = schema_patches.get(self.name()) {
2029                if let Some(field_patch) = patch.get("description") {
2030                    return field_patch.to_owned();
2031                }
2032            }
2033        }
2034
2035        self.description.to_owned()
2036    }
2037
2038    /// Returns the CA order value.
2039    ///
2040    /// This represents the visual position of the field in CA's Assembly Kit.
2041    /// A value of `-1` indicates the position is unknown.
2042    pub fn ca_order(&self) ->  i16 {
2043        self.ca_order
2044    }
2045
2046    /// Returns the bitwise expansion count.
2047    ///
2048    /// # Returns
2049    ///
2050    /// - `0` or `1`: No bitwise expansion
2051    /// - `> 1`: Number of boolean columns this field should be expanded into
2052    pub fn is_bitwise(&self) -> i32 {
2053        self.is_bitwise
2054    }
2055
2056    /// Returns the enum value mappings.
2057    ///
2058    /// # Returns
2059    ///
2060    /// Returns a reference to the map of integer values to their string names.
2061    /// Empty if this field is not an enum.
2062    pub fn enum_values(&self) -> &BTreeMap<i32,String> {
2063        &self.enum_values
2064    }
2065
2066    /// Returns the enum values as an [`Option`].
2067    pub fn enum_values_to_option(&self) -> Option<BTreeMap<i32, String>> {
2068        if self.enum_values.is_empty() { None }
2069        else { Some(self.enum_values.clone()) }
2070    }
2071
2072    /// Returns the enum values as a semicolon-separated string.
2073    ///
2074    /// # Returns
2075    ///
2076    /// Returns a string in the format `"value1,name1;value2,name2;..."`.
2077    pub fn enum_values_to_string(&self) -> String {
2078        self.enum_values.iter().map(|(x, y)| format!("{x},{y}")).collect::<Vec<String>>().join(";")
2079    }
2080
2081    /// Returns the RGB colour group index.
2082    ///
2083    /// # Returns
2084    ///
2085    /// Returns the colour group index if this field is part of an RGB triplet,
2086    /// or [`None`] if it's not a colour field.
2087    pub fn is_part_of_colour(&self) -> Option<u8>{
2088        self.is_part_of_colour
2089    }
2090
2091    /// Returns whether this field should be treated as numeric (currently always `false`).
2092    ///
2093    /// This is a placeholder for future functionality and currently always returns `false`.
2094    ///
2095    /// # Arguments
2096    ///
2097    /// * `_schema_patches` - Unused (reserved for future use)
2098    pub fn is_numeric(&self, _schema_patches: Option<&DefinitionPatch>) -> bool {
2099        false
2100        /*
2101        if let Some(schema_patches) = schema_patches {
2102            if let Some(patch) = schema_patches.get(self.name()) {
2103                if let Some(is_numeric) = patch.get("is_numeric") {
2104                    return is_numeric.parse::<bool>().unwrap_or(false);
2105                }
2106            }
2107        }
2108
2109        false*/
2110    }
2111
2112    /// Returns whether this field cannot be empty, checking patches.
2113    ///
2114    /// # Arguments
2115    ///
2116    /// * `schema_patches` - Optional patches to check for the `not_empty` flag
2117    ///
2118    /// # Returns
2119    ///
2120    /// Returns `true` if the field is marked as "cannot be empty" via a patch.
2121    pub fn cannot_be_empty(&self, schema_patches: Option<&DefinitionPatch>) -> bool {
2122        if let Some(schema_patches) = schema_patches {
2123            if let Some(patch) = schema_patches.get(self.name()) {
2124                if let Some(cannot_be_empty) = patch.get("not_empty") {
2125                    return cannot_be_empty.parse::<bool>().unwrap_or(false);
2126                }
2127            }
2128        }
2129
2130        false
2131    }
2132
2133    /// Returns whether this field is unused by the game.
2134    ///
2135    /// Fields marked as unused are still present in the binary format but are not
2136    /// actually used by the game logic. This information is primarily determined via patches.
2137    ///
2138    /// # Arguments
2139    ///
2140    /// * `schema_patches` - Optional patches to check for the `unused` flag
2141    ///
2142    /// # Returns
2143    ///
2144    /// Returns `true` if the field is marked as unused (either in the base definition or via patch).
2145    pub fn unused(&self, schema_patches: Option<&DefinitionPatch>) -> bool {
2146
2147        // By default all fields are used, except the ones set through patches. If it's already marked unused, return early.
2148        self.unused || {
2149
2150            if let Some(schema_patches) = schema_patches {
2151                if let Some(patch) = schema_patches.get(self.name()) {
2152                    if let Some(cannot_be_empty) = patch.get("unused") {
2153                        return cannot_be_empty.parse::<bool>().unwrap_or(false);
2154                    }
2155                }
2156            }
2157
2158            false
2159        }
2160    }
2161
2162    /// Generates a SQL column definition string for this field.
2163    ///
2164    /// This function creates the SQL column definition portion for use in a
2165    /// `CREATE TABLE` statement, including the data type and optional default value.
2166    ///
2167    /// # Arguments
2168    ///
2169    /// * `schema_patches` - Optional patches to apply when getting the default value
2170    ///
2171    /// # Returns
2172    ///
2173    /// Returns a string like `"field_name" INTEGER DEFAULT "value"`.
2174    ///
2175    /// # Feature
2176    ///
2177    /// This function requires the `integration_sqlite` feature.
2178    #[cfg(feature = "integration_sqlite")]
2179    pub fn map_to_sql_string(&self, schema_patches: Option<&DefinitionPatch>) -> String {
2180        let mut string = format!(" \"{}\" {:?} ", self.name(), self.field_type().map_to_sql_type());
2181
2182        if let Some(default_value) = self.default_value(schema_patches) {
2183            string.push_str(&format!(" DEFAULT \"{}\"", default_value.replace("\"", "\"\"")));
2184        }
2185
2186        string
2187    }
2188}
2189
2190impl FieldType {
2191
2192    /// Maps this field type to its corresponding SQLite type.
2193    ///
2194    /// This function converts RPFM's field types to their appropriate SQLite equivalents
2195    /// for database operations.
2196    ///
2197    /// # Returns
2198    ///
2199    /// Returns the SQLite [`Type`] that best represents this field type:
2200    /// - Numeric types → [`Type::Integer`] or [`Type::Real`]
2201    /// - String types → [`Type::Text`]
2202    /// - Sequence types → [`Type::Blob`]
2203    ///
2204    /// # Feature
2205    ///
2206    /// This function requires the `integration_sqlite` feature.
2207    #[cfg(feature = "integration_sqlite")]
2208    pub fn map_to_sql_type(&self) -> Type {
2209        match self {
2210            FieldType::Boolean => Type::Integer,
2211            FieldType::F32 => Type::Real,
2212            FieldType::F64 => Type::Real,
2213            FieldType::I16 => Type::Integer,
2214            FieldType::I32 => Type::Integer,
2215            FieldType::I64 => Type::Integer,
2216            FieldType::ColourRGB => Type::Text,
2217            FieldType::StringU8 => Type::Text,
2218            FieldType::StringU16 => Type::Text,
2219            FieldType::OptionalI16 => Type::Integer,
2220            FieldType::OptionalI32 => Type::Integer,
2221            FieldType::OptionalI64 => Type::Integer,
2222            FieldType::OptionalStringU8 => Type::Text,
2223            FieldType::OptionalStringU16 => Type::Text,
2224            FieldType::SequenceU16(_) => Type::Blob,
2225            FieldType::SequenceU32(_) => Type::Blob,
2226        }
2227    }
2228}
2229//---------------------------------------------------------------------------//
2230//                         Extra Implementations
2231//---------------------------------------------------------------------------//
2232
2233/// Default implementation of `Schema`.
2234impl Default for Schema {
2235    fn default() -> Self {
2236        Self {
2237            version: CURRENT_STRUCTURAL_VERSION,
2238            definitions: HashMap::new(),
2239            patches: HashMap::new()
2240        }
2241    }
2242}
2243
2244/// Default implementation of `FieldType`.
2245impl Default for Field {
2246    fn default() -> Self {
2247        Self {
2248            name: String::from("new_field"),
2249            field_type: FieldType::StringU8,
2250            is_key: false,
2251            default_value: None,
2252            is_filename: false,
2253            filename_relative_path: None,
2254            is_reference: None,
2255            lookup: None,
2256            description: String::new(),
2257            ca_order: -1,
2258            is_bitwise: 0,
2259            enum_values: BTreeMap::new(),
2260            is_part_of_colour: None,
2261            unused: false,
2262        }
2263    }
2264}
2265
2266/// Display implementation of `FieldType`.
2267impl Display for FieldType {
2268    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2269        match self {
2270            FieldType::Boolean => write!(f, "Boolean"),
2271            FieldType::F32 => write!(f, "F32"),
2272            FieldType::F64 => write!(f, "F64"),
2273            FieldType::I16 => write!(f, "I16"),
2274            FieldType::I32 => write!(f, "I32"),
2275            FieldType::I64 => write!(f, "I64"),
2276            FieldType::ColourRGB => write!(f, "ColourRGB"),
2277            FieldType::StringU8 => write!(f, "StringU8"),
2278            FieldType::StringU16 => write!(f, "StringU16"),
2279            FieldType::OptionalI16 => write!(f, "OptionalI16"),
2280            FieldType::OptionalI32 => write!(f, "OptionalI32"),
2281            FieldType::OptionalI64 => write!(f, "OptionalI64"),
2282            FieldType::OptionalStringU8 => write!(f, "OptionalStringU8"),
2283            FieldType::OptionalStringU16 => write!(f, "OptionalStringU16"),
2284            FieldType::SequenceU16(_) => write!(f, "SequenceU16"),
2285            FieldType::SequenceU32(_) => write!(f, "SequenceU32"),
2286        }
2287    }
2288}
2289
2290/// Implementation of `From<&RawDefinition>` for `Definition.
2291impl From<&DecodedData> for FieldType {
2292    fn from(data: &DecodedData) -> Self {
2293        match data {
2294            DecodedData::Boolean(_) => FieldType::Boolean,
2295            DecodedData::F32(_) => FieldType::F32,
2296            DecodedData::F64(_) => FieldType::F64,
2297            DecodedData::I16(_) => FieldType::I16,
2298            DecodedData::I32(_) => FieldType::I32,
2299            DecodedData::I64(_) => FieldType::I64,
2300            DecodedData::ColourRGB(_) => FieldType::ColourRGB,
2301            DecodedData::StringU8(_) => FieldType::StringU8,
2302            DecodedData::StringU16(_) => FieldType::StringU16,
2303            DecodedData::OptionalI16(_) => FieldType::OptionalI16,
2304            DecodedData::OptionalI32(_) => FieldType::OptionalI32,
2305            DecodedData::OptionalI64(_) => FieldType::OptionalI64,
2306            DecodedData::OptionalStringU8(_) => FieldType::OptionalStringU8,
2307            DecodedData::OptionalStringU16(_) => FieldType::OptionalStringU16,
2308            DecodedData::SequenceU16(_) => FieldType::SequenceU16(Box::new(Definition::new(INVALID_VERSION, None))),
2309            DecodedData::SequenceU32(_) => FieldType::SequenceU32(Box::new(Definition::new(INVALID_VERSION, None))),
2310        }
2311    }
2312}
2313
2314/// Special serializer function to sort the definitions HashMap before serializing.
2315fn ordered_map_definitions<S>(value: &HashMap<String, Vec<Definition>>, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, {
2316    let ordered: BTreeMap<_, _> = value.iter().collect();
2317    ordered.serialize(serializer)
2318}
2319
2320/// Special serializer function to sort the patches HashMap before serializing.
2321fn ordered_map_patches<S>(value: &HashMap<String, HashMap<String, HashMap<String, String>>>, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, {
2322    let ordered: BTreeMap<_, BTreeMap<_, BTreeMap<_, _>>> = value.iter().map(|(a, x)| (a, x.iter().map(|(b, y)| (b, y.iter().collect())).collect())).collect();
2323    ordered.serialize(serializer)
2324}