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