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}