Skip to main content

rakata_generics/git/
item.rs

1//! Item placements in a GIT.
2
3use rakata_core::ResRef;
4use rakata_formats::{
5    gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffType},
6    GffLocalizedString, GffStruct, GffValue,
7};
8
9use super::object_id_or_invalid;
10use crate::gff_helpers::{
11    get_bool, get_f32, get_i32, get_locstring, get_resref, get_string, get_u16, get_u32, get_u8,
12    upsert_field,
13};
14use crate::shared::ObjectId;
15use crate::uti::UtiProperty;
16
17/// An item instance placed in the area (struct type 0).
18#[derive(Debug, Clone, PartialEq, Default)]
19pub struct GitItem {
20    /// Template resref (`TemplateResRef`).
21    pub template_resref: ResRef,
22    /// X position (`XPosition`).
23    pub x_position: f32,
24    /// Y position (`YPosition`).
25    pub y_position: f32,
26    /// Z position (`ZPosition`).
27    pub z_position: f32,
28    /// X orientation (`XOrientation`).
29    pub x_orientation: f32,
30    /// Y orientation (`YOrientation`).
31    pub y_orientation: f32,
32    /// Z orientation (`ZOrientation`).
33    pub z_orientation: f32,
34    /// Runtime object ID (`ObjectId`). Save-game only.
35    pub object_id: ObjectId,
36}
37
38impl GitItem {
39    pub(crate) fn from_gff_struct(s: &GffStruct) -> Self {
40        Self {
41            template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
42            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
43            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
44            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
45            x_orientation: get_f32(s, "XOrientation").unwrap_or(0.0),
46            y_orientation: get_f32(s, "YOrientation").unwrap_or(0.0),
47            z_orientation: get_f32(s, "ZOrientation").unwrap_or(0.0),
48            object_id: object_id_or_invalid(s),
49        }
50    }
51
52    pub(crate) fn to_gff_struct(&self) -> GffStruct {
53        let mut s = GffStruct::new(0);
54        upsert_field(
55            &mut s,
56            "TemplateResRef",
57            GffValue::ResRef(self.template_resref),
58        );
59        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
60        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
61        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
62        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
63        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
64        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
65        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
66        s
67    }
68}
69
70/// GIT `List` (item) entry child schema (struct type 0).
71pub(crate) static ITEM_LIST_CHILDREN: &[FieldSchema] = &[
72    FieldSchema {
73        label: "ObjectId",
74        expected_type: GffType::UInt32,
75        life: FieldLife::Live,
76        required: false,
77        absent: AbsentDefault::Unverified,
78        children: None,
79        constraint: None,
80    },
81    FieldSchema {
82        label: "TemplateResRef",
83        expected_type: GffType::ResRef,
84        life: FieldLife::Live,
85        required: false,
86        absent: AbsentDefault::Unverified,
87        children: None,
88        constraint: None,
89    },
90    FieldSchema {
91        label: "XPosition",
92        expected_type: GffType::Single,
93        life: FieldLife::Live,
94        required: false,
95        absent: AbsentDefault::Unverified,
96        children: None,
97        constraint: None,
98    },
99    FieldSchema {
100        label: "YPosition",
101        expected_type: GffType::Single,
102        life: FieldLife::Live,
103        required: false,
104        absent: AbsentDefault::Unverified,
105        children: None,
106        constraint: None,
107    },
108    FieldSchema {
109        label: "ZPosition",
110        expected_type: GffType::Single,
111        life: FieldLife::Live,
112        required: false,
113        absent: AbsentDefault::Unverified,
114        children: None,
115        constraint: None,
116    },
117    FieldSchema {
118        label: "XOrientation",
119        expected_type: GffType::Single,
120        life: FieldLife::Live,
121        required: false,
122        absent: AbsentDefault::Unverified,
123        children: None,
124        constraint: None,
125    },
126    FieldSchema {
127        label: "YOrientation",
128        expected_type: GffType::Single,
129        life: FieldLife::Live,
130        required: false,
131        absent: AbsentDefault::Unverified,
132        children: None,
133        constraint: None,
134    },
135    FieldSchema {
136        label: "ZOrientation",
137        expected_type: GffType::Single,
138        life: FieldLife::Live,
139        required: false,
140        absent: AbsentDefault::Unverified,
141        children: None,
142        constraint: None,
143    },
144];
145
146// =========================================================================
147// The saved form
148// =========================================================================
149
150// Items as stored inside a save game.
151//
152// A saved creature's `ItemList` and `Equip_ItemList` do not reference items
153// the way a blueprint does. A `.utc` stores an `InventoryRes` or
154// `EquippedRes` resref and lets the loader fetch the `.uti`; a snapshot
155// stores the whole item inline, properties included. That is the same
156// `UseTemplates = 0` rule the containing `GIT` follows, one level further
157// down.
158//
159// ## Saved items live in more than one list
160//
161// The same struct shape turns up in a creature's `ItemList` and
162// `Equip_ItemList`, in a placeable's and a store's `ItemList`, and in the
163// area's own loose-item `List`. Which optional fields appear depends on the
164// list, not on the item:
165//
166// - `BodyVariation` and `TextureVar` appear on equipped creature items, and
167//   on some placeable and store items. No carried creature item in the
168//   fixture saves has them.
169// - `Infinite` appears on every store item and on nothing else.
170//
171// All three are modelled as [`Option`] so an item read out of one list
172// writes back into the same shape rather than gaining fields the engine
173// would not have put there.
174//
175// ## What a blueprint has that a snapshot does not
176//
177// `TemplateResRef`, `PaletteID` and `Comment` are authoring metadata a
178// runtime item has no use for, and `UpgradeLevel` is not carried either.
179// Going the other way, a snapshot adds `ObjectId` and a full position and
180// orientation, because a saved item exists somewhere in the world rather than
181// in a template library.
182
183/// One item as stored in a save.
184#[derive(Debug, Clone, PartialEq, Default)]
185pub struct SavedItem {
186    /// Object tag (`Tag`).
187    pub tag: String,
188    /// Runtime object id (`ObjectId`).
189    pub object_id: ObjectId,
190    /// Base item row (`BaseItem`).
191    pub base_item: i32,
192    /// Localized name (`LocalizedName`).
193    pub localized_name: GffLocalizedString,
194    /// Unidentified description (`Description`).
195    pub description: GffLocalizedString,
196    /// Identified description (`DescIdentified`).
197    pub description_identified: GffLocalizedString,
198
199    /// Stack size (`StackSize`).
200    pub stack_size: u16,
201    /// Remaining charges (`Charges`).
202    pub charges: u8,
203    /// Maximum charges (`MaxCharges`).
204    pub max_charges: u8,
205    /// Base cost (`Cost`).
206    pub cost: u32,
207    /// Additional cost (`AddCost`).
208    pub add_cost: u32,
209    /// Upgrade bitfield (`Upgrades`).
210    pub upgrades: u32,
211    /// Model variation (`ModelVariation`).
212    pub model_variation: u8,
213
214    /// Identified flag (`Identified`).
215    pub identified: bool,
216    /// Droppable flag (`Dropable`).
217    pub droppable: bool,
218    /// Plot-item flag (`Plot`).
219    pub plot: bool,
220    /// Stolen flag (`Stolen`).
221    pub stolen: bool,
222    /// Pickpocketable flag (`Pickpocketable`).
223    pub pickpocketable: bool,
224    /// Non-equippable flag (`NonEquippable`).
225    pub non_equippable: bool,
226    /// Newly-acquired flag (`NewItem`).
227    pub new_item: bool,
228    /// Pending-deletion flag (`DELETING`).
229    pub deleting: bool,
230
231    /// Body variation (`BodyVariation`). Not written for every list.
232    pub body_variation: Option<u8>,
233    /// Texture variation (`TextureVar`). Not written for every list.
234    pub texture_var: Option<u8>,
235    /// Restocks without limit (`Infinite`). Store inventories only.
236    pub infinite: Option<bool>,
237
238    /// World X position (`XPosition`).
239    pub x_position: f32,
240    /// World Y position (`YPosition`).
241    pub y_position: f32,
242    /// World Z position (`ZPosition`).
243    pub z_position: f32,
244    /// Facing X component (`XOrientation`).
245    pub x_orientation: f32,
246    /// Facing Y component (`YOrientation`).
247    pub y_orientation: f32,
248    /// Facing Z component (`ZOrientation`).
249    pub z_orientation: f32,
250
251    /// Item properties (`PropertiesList`). The element shape matches a
252    /// blueprint's field for field, so this reuses [`UtiProperty`] and its
253    /// conversions rather than re-deriving the same reads.
254    pub properties: Vec<UtiProperty>,
255}
256
257impl SavedItem {
258    /// Creates an empty saved item.
259    pub fn new() -> Self {
260        Self::default()
261    }
262
263    /// Reads one `ItemList` or `Equip_ItemList` element.
264    pub fn from_struct(structure: &GffStruct) -> Self {
265        Self {
266            tag: get_string(structure, "Tag").unwrap_or_default(),
267            object_id: object_id_or_invalid(structure),
268            base_item: get_i32(structure, "BaseItem").unwrap_or(0),
269            localized_name: get_locstring(structure, "LocalizedName")
270                .cloned()
271                .unwrap_or_default(),
272            description: get_locstring(structure, "Description")
273                .cloned()
274                .unwrap_or_default(),
275            description_identified: get_locstring(structure, "DescIdentified")
276                .cloned()
277                .unwrap_or_default(),
278
279            stack_size: get_u16(structure, "StackSize").unwrap_or(0),
280            charges: get_u8(structure, "Charges").unwrap_or(0),
281            max_charges: get_u8(structure, "MaxCharges").unwrap_or(0),
282            cost: get_u32(structure, "Cost").unwrap_or(0),
283            add_cost: get_u32(structure, "AddCost").unwrap_or(0),
284            upgrades: get_u32(structure, "Upgrades").unwrap_or(0),
285            model_variation: get_u8(structure, "ModelVariation").unwrap_or(0),
286
287            identified: get_bool(structure, "Identified").unwrap_or(false),
288            droppable: get_bool(structure, "Dropable").unwrap_or(false),
289            plot: get_bool(structure, "Plot").unwrap_or(false),
290            stolen: get_bool(structure, "Stolen").unwrap_or(false),
291            pickpocketable: get_bool(structure, "Pickpocketable").unwrap_or(false),
292            non_equippable: get_bool(structure, "NonEquippable").unwrap_or(false),
293            new_item: get_bool(structure, "NewItem").unwrap_or(false),
294            deleting: get_bool(structure, "DELETING").unwrap_or(false),
295
296            // Absent rather than zero: the engine writes these only in the
297            // lists that use them, and filling them in would add fields it
298            // never put there.
299            body_variation: get_u8(structure, "BodyVariation"),
300            texture_var: get_u8(structure, "TextureVar"),
301            infinite: get_bool(structure, "Infinite"),
302
303            x_position: get_f32(structure, "XPosition").unwrap_or(0.0),
304            y_position: get_f32(structure, "YPosition").unwrap_or(0.0),
305            z_position: get_f32(structure, "ZPosition").unwrap_or(0.0),
306            x_orientation: get_f32(structure, "XOrientation").unwrap_or(0.0),
307            y_orientation: get_f32(structure, "YOrientation").unwrap_or(0.0),
308            z_orientation: get_f32(structure, "ZOrientation").unwrap_or(0.0),
309
310            properties: match structure.field("PropertiesList") {
311                Some(GffValue::List(entries)) => entries
312                    .iter()
313                    .map(UtiProperty::from_struct)
314                    .collect::<Vec<_>>(),
315                _ => Vec::new(),
316            },
317        }
318    }
319
320    /// Writes this item back into a list element.
321    pub fn to_struct(&self, index: usize) -> GffStruct {
322        let mut s = GffStruct::new(i32::try_from(index).expect("item index fits i32"));
323
324        upsert_field(&mut s, "Tag", GffValue::String(self.tag.clone()));
325        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
326        upsert_field(&mut s, "BaseItem", GffValue::Int32(self.base_item));
327        upsert_field(
328            &mut s,
329            "LocalizedName",
330            GffValue::LocalizedString(self.localized_name.clone()),
331        );
332        upsert_field(
333            &mut s,
334            "Description",
335            GffValue::LocalizedString(self.description.clone()),
336        );
337        upsert_field(
338            &mut s,
339            "DescIdentified",
340            GffValue::LocalizedString(self.description_identified.clone()),
341        );
342
343        upsert_field(&mut s, "StackSize", GffValue::UInt16(self.stack_size));
344        upsert_field(&mut s, "Charges", GffValue::UInt8(self.charges));
345        upsert_field(&mut s, "MaxCharges", GffValue::UInt8(self.max_charges));
346        upsert_field(&mut s, "Cost", GffValue::UInt32(self.cost));
347        upsert_field(&mut s, "AddCost", GffValue::UInt32(self.add_cost));
348        upsert_field(&mut s, "Upgrades", GffValue::UInt32(self.upgrades));
349        upsert_field(
350            &mut s,
351            "ModelVariation",
352            GffValue::UInt8(self.model_variation),
353        );
354
355        upsert_field(
356            &mut s,
357            "Identified",
358            GffValue::UInt8(self.identified.into()),
359        );
360        upsert_field(&mut s, "Dropable", GffValue::UInt8(self.droppable.into()));
361        upsert_field(&mut s, "Plot", GffValue::UInt8(self.plot.into()));
362        upsert_field(&mut s, "Stolen", GffValue::UInt8(self.stolen.into()));
363        upsert_field(
364            &mut s,
365            "Pickpocketable",
366            GffValue::UInt8(self.pickpocketable.into()),
367        );
368        upsert_field(
369            &mut s,
370            "NonEquippable",
371            GffValue::UInt8(self.non_equippable.into()),
372        );
373        upsert_field(&mut s, "NewItem", GffValue::UInt8(self.new_item.into()));
374        upsert_field(&mut s, "DELETING", GffValue::UInt8(self.deleting.into()));
375
376        if let Some(value) = self.body_variation {
377            upsert_field(&mut s, "BodyVariation", GffValue::UInt8(value));
378        }
379        if let Some(value) = self.texture_var {
380            upsert_field(&mut s, "TextureVar", GffValue::UInt8(value));
381        }
382        if let Some(value) = self.infinite {
383            upsert_field(&mut s, "Infinite", GffValue::UInt8(value.into()));
384        }
385
386        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
387        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
388        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
389        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
390        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
391        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
392
393        upsert_field(
394            &mut s,
395            "PropertiesList",
396            GffValue::List(self.properties.iter().map(UtiProperty::to_struct).collect()),
397        );
398
399        s
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406
407    fn sample() -> SavedItem {
408        SavedItem {
409            tag: "g_w_blstrpstl001".to_string(),
410            object_id: ObjectId::new(0x8000_0042),
411            base_item: 4,
412            stack_size: 1,
413            charges: 3,
414            cost: 250,
415            identified: true,
416            droppable: true,
417            x_position: 12.5,
418            properties: vec![UtiProperty {
419                cost_table: 2,
420                cost_value: 7,
421                property_name: 45,
422                subtype: 3,
423                chance_appear: 100,
424                useable: Some(true),
425                uses_per_day: Some(5),
426                ..UtiProperty::default()
427            }],
428            ..SavedItem::default()
429        }
430    }
431
432    #[test]
433    fn round_trips_through_a_list_element() {
434        let item = sample();
435
436        let parsed = SavedItem::from_struct(&item.to_struct(0));
437
438        assert_eq!(parsed, item);
439    }
440
441    #[test]
442    fn an_item_does_not_gain_list_specific_fields() {
443        // A carried creature item has none of the three; writing zeros back
444        // would add fields the engine never wrote there.
445        let item = sample();
446        assert_eq!(item.body_variation, None);
447
448        let written = item.to_struct(0);
449
450        assert!(written.field("BodyVariation").is_none());
451        assert!(written.field("TextureVar").is_none());
452        assert!(written.field("Infinite").is_none());
453    }
454
455    #[test]
456    fn a_store_item_keeps_its_infinite_flag() {
457        let item = SavedItem {
458            infinite: Some(true),
459            ..sample()
460        };
461
462        let parsed = SavedItem::from_struct(&item.to_struct(0));
463
464        assert_eq!(parsed.infinite, Some(true));
465    }
466
467    #[test]
468    fn an_equipped_item_keeps_its_equip_only_fields() {
469        let item = SavedItem {
470            body_variation: Some(2),
471            texture_var: Some(1),
472            ..sample()
473        };
474
475        let parsed = SavedItem::from_struct(&item.to_struct(0));
476
477        assert_eq!(parsed.body_variation, Some(2));
478        assert_eq!(parsed.texture_var, Some(1));
479    }
480
481    #[test]
482    fn optional_property_fields_stay_absent_when_unset() {
483        let item = SavedItem {
484            properties: vec![UtiProperty::default()],
485            ..SavedItem::default()
486        };
487
488        let written = item.to_struct(0);
489        let GffValue::List(entries) = written.field("PropertiesList").expect("written") else {
490            panic!("PropertiesList should be a list");
491        };
492
493        assert!(entries[0].field("Useable").is_none());
494        assert!(entries[0].field("UsesPerDay").is_none());
495        assert!(entries[0].field("UpgradeType").is_none());
496    }
497}