rakata_formats/
gff_schema.rs

1//! Schema types for GFF field validation.
2//!
3//! Defines [`GffType`], [`FieldSchema`], and the [`GffSchema`] trait used to
4//! associate engine-derived field schemas with typed GFF resource wrappers.
5//!
6//! These types live in `rakata-formats` (next to [`GffValue`](crate::gff::GffValue))
7//! so both `rakata-generics` (schema provider) and `rakata-lint` (schema consumer)
8//! can depend on them without circular imports.
9
10use crate::gff::GffValue;
11
12/// Expected GFF field type, mirroring the variants of [`GffValue`].
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum GffType {
15    /// `BYTE` - unsigned 8-bit integer.
16    UInt8,
17    /// `CHAR` - signed 8-bit integer.
18    Int8,
19    /// `WORD` - unsigned 16-bit integer.
20    UInt16,
21    /// `SHORT` - signed 16-bit integer.
22    Int16,
23    /// `DWORD` - unsigned 32-bit integer.
24    UInt32,
25    /// `INT` - signed 32-bit integer.
26    Int32,
27    /// `DWORD64` - unsigned 64-bit integer.
28    UInt64,
29    /// `INT64` - signed 64-bit integer.
30    Int64,
31    /// `FLOAT` - 32-bit float.
32    Single,
33    /// `DOUBLE` - 64-bit float.
34    Double,
35    /// `CExoString` - variable-length string.
36    String,
37    /// `CResRef` - resource reference (max 16 chars).
38    ResRef,
39    /// `CExoLocString` - localized string with optional StrRef.
40    LocalizedString,
41    /// `VOID` - raw binary data.
42    Binary,
43    /// Nested struct.
44    Struct,
45    /// List of structs.
46    List,
47    /// Vector3 - 3 packed f32 values (position).
48    Vector3,
49    /// Vector4 - 4 packed f32 values (orientation/quaternion).
50    Vector4,
51}
52
53impl GffType {
54    /// Human-readable name matching the KotOR GFF wire-format type names.
55    pub fn name(self) -> &'static str {
56        match self {
57            GffType::UInt8 => "BYTE",
58            GffType::Int8 => "CHAR",
59            GffType::UInt16 => "WORD",
60            GffType::Int16 => "SHORT",
61            GffType::UInt32 => "DWORD",
62            GffType::Int32 => "INT",
63            GffType::UInt64 => "DWORD64",
64            GffType::Int64 => "INT64",
65            GffType::Single => "FLOAT",
66            GffType::Double => "DOUBLE",
67            GffType::String => "CExoString",
68            GffType::ResRef => "CResRef",
69            GffType::LocalizedString => "CExoLocString",
70            GffType::Binary => "VOID",
71            GffType::Struct => "Struct",
72            GffType::List => "List",
73            GffType::Vector3 => "Vector",
74            GffType::Vector4 => "Quaternion",
75        }
76    }
77}
78
79/// Schema definition for a single GFF field.
80///
81/// Describes a field the K1 engine reads for a particular GFF resource type,
82/// as determined by Phase 0 Ghidra audits.
83#[derive(Debug, Clone)]
84pub struct FieldSchema {
85    /// GFF field label (e.g., `"Tag"`, `"Appearance_Type"`).
86    pub label: &'static str,
87    /// Expected GFF value type.
88    pub expected_type: GffType,
89    /// Whether the field is required for the engine to function correctly.
90    ///
91    /// Missing required fields produce warnings (the engine typically falls
92    /// back to a default, but the modder likely intended to set the field).
93    pub required: bool,
94    /// Sub-schema for List elements or Struct children. `None` for leaf fields.
95    ///
96    /// When present, validation recurses into each list element or the inner
97    /// struct, checking fields against this child schema.
98    pub children: Option<&'static [FieldSchema]>,
99    /// Optional bounds check for numeric types.
100    ///
101    /// Defines min/max constraints that the engine actually honors before truncating.
102    pub constraint: Option<FieldConstraint>,
103}
104
105/// A numeric boundary constraint for engine value truncation/clamping.
106#[derive(Debug, Clone, PartialEq)]
107pub enum FieldConstraint {
108    /// Integer inclusive range `(min, max)`.
109    RangeInt(i64, i64),
110    /// Floating-point inclusive range `(min, max)`.
111    RangeFloat(f64, f64),
112}
113
114/// Trait providing the engine-derived field schema for a GFF resource type.
115///
116/// Implemented by typed generics (e.g., `Utw`, `Utc`) to expose the full
117/// engine schema - including fields not modeled as struct fields. The schema
118/// is the single source of truth for GFF field validation.
119///
120/// # Example
121///
122/// ```
123/// use rakata_formats::gff_schema::{FieldSchema, GffSchema, GffType};
124///
125/// struct MyType;
126///
127/// impl GffSchema for MyType {
128///     fn schema() -> &'static [FieldSchema] {
129///         &[
130///             FieldSchema {
131///                 label: "Tag",
132///                 expected_type: GffType::String,
133///                 required: false,
134///                 children: None,
135///                 constraint: None,
136///             },
137///         ]
138///     }
139/// }
140///
141/// assert_eq!(MyType::schema().len(), 1);
142/// ```
143pub trait GffSchema {
144    /// Returns the root field schema for this GFF resource type.
145    fn schema() -> &'static [FieldSchema];
146}
147
148/// Map a [`GffValue`] variant to its corresponding [`GffType`].
149///
150/// Extension variants not part of the standard GFF V3.2 type set are mapped
151/// to their logical equivalents:
152/// - [`GffValue::Vector3`] -> [`GffType::Vector3`]
153/// - [`GffValue::Vector4`] -> [`GffType::Vector4`]
154/// - [`GffValue::StrRef`] -> [`GffType::UInt32`]
155pub fn gff_value_type(value: &GffValue) -> GffType {
156    match value {
157        GffValue::UInt8(_) => GffType::UInt8,
158        GffValue::Int8(_) => GffType::Int8,
159        GffValue::UInt16(_) => GffType::UInt16,
160        GffValue::Int16(_) => GffType::Int16,
161        GffValue::UInt32(_) => GffType::UInt32,
162        GffValue::Int32(_) => GffType::Int32,
163        GffValue::UInt64(_) => GffType::UInt64,
164        GffValue::Int64(_) => GffType::Int64,
165        GffValue::Single(_) => GffType::Single,
166        GffValue::Double(_) => GffType::Double,
167        GffValue::String(_) => GffType::String,
168        GffValue::ResRef(_) => GffType::ResRef,
169        GffValue::LocalizedString(_) => GffType::LocalizedString,
170        GffValue::Binary(_) => GffType::Binary,
171        GffValue::Struct(_) => GffType::Struct,
172        GffValue::List(_) => GffType::List,
173        GffValue::Vector4(_) => GffType::Vector4,
174        GffValue::Vector3(_) => GffType::Vector3,
175        GffValue::StrRef(_) => GffType::UInt32,
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::gff::GffLocalizedString;
183    use rakata_core::StrRef;
184
185    #[test]
186    fn gff_type_name_roundtrip() {
187        assert_eq!(GffType::UInt8.name(), "BYTE");
188        assert_eq!(GffType::String.name(), "CExoString");
189        assert_eq!(GffType::ResRef.name(), "CResRef");
190        assert_eq!(GffType::LocalizedString.name(), "CExoLocString");
191        assert_eq!(GffType::Binary.name(), "VOID");
192        assert_eq!(GffType::List.name(), "List");
193    }
194
195    #[test]
196    fn gff_value_type_maps_standard_types() {
197        assert_eq!(gff_value_type(&GffValue::UInt8(0)), GffType::UInt8);
198        assert_eq!(gff_value_type(&GffValue::Int32(0)), GffType::Int32);
199        assert_eq!(
200            gff_value_type(&GffValue::String("x".into())),
201            GffType::String
202        );
203        assert_eq!(gff_value_type(&GffValue::resref_lit("x")), GffType::ResRef);
204        assert_eq!(
205            gff_value_type(&GffValue::LocalizedString(GffLocalizedString::new(
206                StrRef::invalid()
207            ))),
208            GffType::LocalizedString
209        );
210        assert_eq!(gff_value_type(&GffValue::Binary(vec![])), GffType::Binary);
211    }
212
213    #[test]
214    fn gff_value_type_maps_extension_types() {
215        assert_eq!(
216            gff_value_type(&GffValue::Vector3([0.0; 3])),
217            GffType::Vector3
218        );
219        assert_eq!(
220            gff_value_type(&GffValue::Vector4([0.0; 4])),
221            GffType::Vector4
222        );
223        assert_eq!(
224            gff_value_type(&GffValue::StrRef(StrRef::invalid())),
225            GffType::UInt32
226        );
227    }
228
229    #[test]
230    fn gff_schema_trait_is_implementable() {
231        struct TestType;
232        impl GffSchema for TestType {
233            fn schema() -> &'static [FieldSchema] {
234                &[FieldSchema {
235                    label: "Tag",
236                    expected_type: GffType::String,
237                    required: false,
238                    children: None,
239                    constraint: None,
240                }]
241            }
242        }
243        assert_eq!(TestType::schema().len(), 1);
244        assert_eq!(TestType::schema()[0].label, "Tag");
245    }
246}