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}