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