Skip to main content

rakata_generics/git/
item.rs

1//! Item placements in a GIT.
2
3use rakata_core::ResRef;
4use rakata_formats::gff::upsert_field;
5use rakata_formats::gff_label;
6use rakata_formats::{GffLocalizedString, GffModel, GffStruct, GffValue};
7
8use super::GitObjectPlacement;
9
10/// An item instance placed in the area (struct type 0).
11#[derive(Debug, Clone, PartialEq, GffModel)]
12#[gff_manual_element]
13pub struct GitItem {
14    /// The block both forms carry, which the list declares as its element.
15    ///
16    /// No `#[gff]`: the arm contributes its parts to the split, and the
17    /// element is this block, so declaring it here too would make one label
18    /// two declarations. The arm's own codec reads and writes it.
19    pub common: GitObjectPlacement,
20    /// Template resref (`TemplateResRef`), the one label this arm adds.
21    #[gff(TemplateResRef, unexamined)]
22    pub template_resref: ResRef,
23}
24
25// =========================================================================
26// The saved form
27// =========================================================================
28
29// Items as stored inside a save game.
30//
31// A saved creature's `ItemList` and `Equip_ItemList` store the whole item
32// inline, properties included, where a `.utc` would store an `InventoryRes` or
33// `EquippedRes` resref and let the loader fetch the `.uti`. That is the same
34// `UseTemplates = 0` rule the containing `GIT` follows, one level further down;
35// see `docs/src/formats/save/index.md`.
36//
37// ## Saved items live in more than one list
38//
39// The same struct shape turns up in a creature's `ItemList` and
40// `Equip_ItemList`, in a placeable's and a store's `ItemList`, and in the
41// area's own loose-item `List`. Which optional fields appear depends on the
42// list, not on the item:
43//
44// - `BodyVariation` and `TextureVar` appear on equipped creature items, and
45//   on some placeable and store items. No carried creature item in the
46//   fixture saves has them.
47// - `Infinite` appears on every store item and on nothing else.
48//
49// All three are modelled as [`Option`] so an item read out of one list
50// writes back into the same shape rather than gaining fields the engine
51// would not have put there.
52//
53// ## What a blueprint has that a snapshot does not
54//
55// The authoring metadata a `.uti` carries is absent here, which costs nothing
56// because the engine bypasses it there too -- see the legacy-artifacts row on
57// `docs/src/formats/gff/uti.md`. Going the other way, a snapshot adds
58// `ObjectId` and a full position and orientation, because a saved item exists
59// somewhere in the world rather than in a template library.
60
61/// One item as stored in a save.
62#[derive(Debug, Clone, PartialEq, GffModel)]
63#[gff_manual_element]
64pub struct SavedItem {
65    /// Blueprint this item came from (`TemplateResRef`).
66    ///
67    /// On the saved type rather than the templated arm alone, because the
68    /// three nested item lists reach this type directly and the baseline
69    /// carries the label at all four paths.
70    ///
71    /// Written only where it holds something: no saved entry in the corpus
72    /// carries the label, so emitting a blank would put a field on every one
73    /// that never had it.
74    #[gff(TemplateResRef, unexamined, manual_write)]
75    pub template_resref: ResRef,
76    /// Body variation (`BodyVariation`), absent on most entries.
77    #[gff(BodyVariation, not_a_constant, optional = u8)]
78    pub body_variation: Option<u8>,
79    /// Texture variation (`TextureVar`), absent on most entries.
80    #[gff(TextureVar, not_a_constant, optional = u8)]
81    pub texture_var: Option<u8>,
82    /// Whether the stack never depletes (`Infinite`), absent on most entries.
83    #[gff(Infinite, not_a_constant, optional = bool)]
84    pub infinite: Option<bool>,
85    /// Equipment slot, when the list this came out of keys by slot.
86    ///
87    /// The element's own struct id rather than a label, and no field in the
88    /// file repeats it. Only `Equip_ItemList` reads it as data; the five other
89    /// lists carrying this type key by a constant or by position, and leave it
90    /// zero. Carried here because a writer that cannot reproduce it moves
91    /// every equipped item to slot zero.
92    pub slot_id: i32,
93    /// The block both forms carry, which the list declares as its element.
94    ///
95    /// No `#[gff]`: the arm contributes its parts to the split, and the
96    /// element is this block, so declaring it here too would make one label
97    /// two declarations. The arm's own codec reads and writes it.
98    pub common: GitObjectPlacement,
99    /// Additional cost (`AddCost`).
100    #[gff(AddCost, constructed)]
101    pub add_cost: u32,
102    /// Base item row (`BaseItem`).
103    #[gff(BaseItem, constructed = 30)]
104    pub base_item: i32,
105    /// Remaining charges (`Charges`).
106    #[gff(Charges, stamped = 50)]
107    pub charges: u8,
108    /// Base cost (`Cost`).
109    #[gff(Cost, unexamined)]
110    pub cost: u32,
111    /// Pending-deletion flag (`DELETING`).
112    #[gff(DELETING, constructed)]
113    pub deleting: bool,
114    /// Identified description (`DescIdentified`).
115    #[gff(DescIdentified, constructed)]
116    pub description_identified: GffLocalizedString,
117    /// Unidentified description (`Description`).
118    #[gff(Description, constructed)]
119    pub description: GffLocalizedString,
120    /// Droppable flag (`Dropable`).
121    #[gff(Dropable, stamped)]
122    pub droppable: bool,
123    /// Identified flag (`Identified`).
124    #[gff(Identified, stamped = true)]
125    pub identified: bool,
126    /// Localized name (`LocalizedName`).
127    #[gff(LocalizedName, constructed)]
128    pub localized_name: GffLocalizedString,
129    /// Maximum charges (`MaxCharges`).
130    #[gff(MaxCharges, not_a_constant)]
131    pub max_charges: u8,
132    /// Model variation (`ModelVariation`).
133    #[gff(ModelVariation, not_a_constant)]
134    pub model_variation: u8,
135    /// Newly-acquired flag (`NewItem`).
136    #[gff(NewItem, constructed)]
137    pub new_item: bool,
138    /// Non-equippable flag (`NonEquippable`).
139    #[gff(NonEquippable, constructed)]
140    pub non_equippable: bool,
141    /// Pickpocketable flag (`Pickpocketable`).
142    #[gff(Pickpocketable, stamped)]
143    pub pickpocketable: bool,
144    /// Plot-item flag (`Plot`).
145    #[gff(Plot, constructed)]
146    pub plot: bool,
147    /// Item properties (`PropertiesList`). The element shape matches a
148    /// blueprint's field for field, so this reuses [`UtiProperty`](crate::UtiProperty) and its
149    /// conversions rather than re-deriving the same reads.
150    /// Element ids are the index here, unlike a blueprint `.uti`'s own
151    /// properties list, which writes zero throughout. Which list owns it
152    /// decides, not the element type.
153    #[gff(PropertiesList, not_a_constant, list = GitItemProperty, element_id = positional)]
154    pub properties: Vec<GitItemProperty>,
155    /// Stack size (`StackSize`).
156    #[gff(StackSize, constructed = 1)]
157    pub stack_size: u16,
158    /// Stolen flag (`Stolen`).
159    #[gff(Stolen, constructed)]
160    pub stolen: bool,
161    /// Object tag (`Tag`).
162    #[gff(Tag, constructed)]
163    pub tag: String,
164    /// Upgrade bitfield (`Upgrades`).
165    #[gff(Upgrades, constructed)]
166    pub upgrades: u32,
167}
168
169impl GitItem {
170    /// Reads one element, with the block the list declares as its element.
171    pub fn read_element(structure: &GffStruct) -> Self {
172        Self {
173            common: GitObjectPlacement::read_declared(structure),
174            ..Self::read_declared(structure)
175        }
176    }
177
178    /// Writes one element, the common block included.
179    pub fn write_element(&self, structure: &mut GffStruct) {
180        self.write_declared(structure);
181        self.common.write_declared(structure);
182    }
183}
184
185impl SavedItem {
186    /// Reads one element, with the block the list declares as its element.
187    pub fn read_element(structure: &GffStruct) -> Self {
188        Self {
189            common: GitObjectPlacement::read_declared(structure),
190            ..Self::read_declared(structure)
191        }
192    }
193
194    /// Writes one element, the common block included.
195    pub fn write_element(&self, structure: &mut GffStruct) {
196        self.write_declared(structure);
197        if !self.template_resref.is_blank() {
198            upsert_field(
199                structure,
200                gff_label!("TemplateResRef"),
201                GffValue::ResRef(self.template_resref),
202            );
203        }
204        self.common.write_declared(structure);
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use rakata_formats::GffValue;
212
213    /// One value written into an element struct carrying the list's own id.
214    macro_rules! written {
215        ($value:expr, $id:expr) => {{
216            let mut element = GffStruct::new($id);
217            $value.write_element(&mut element);
218            element
219        }};
220    }
221
222    fn sample() -> SavedItem {
223        SavedItem {
224            tag: "g_w_blstrpstl001".to_string(),
225            common: GitObjectPlacement {
226                object_id: Some(crate::shared::ObjectId::new(0x8000_0042)),
227                x_position: 12.5,
228                ..GitObjectPlacement::default()
229            },
230            base_item: 4,
231            stack_size: 1,
232            charges: 3,
233            cost: 250,
234            identified: true,
235            droppable: true,
236            properties: vec![GitItemProperty {
237                cost_table: 2,
238                cost_value: 7,
239                property_name: 45,
240                subtype: 3,
241                chance_appear: 100,
242                useable: Some(true),
243                uses_per_day: 5,
244                ..GitItemProperty::default()
245            }],
246            ..SavedItem::default()
247        }
248    }
249
250    #[test]
251    fn round_trips_through_a_list_element() {
252        let item = sample();
253
254        let parsed = SavedItem::read_element(&written!(item, 0));
255
256        assert_eq!(parsed, item);
257    }
258
259    #[test]
260    fn an_item_does_not_gain_list_specific_fields() {
261        // A carried creature item has none of the three; writing zeros back
262        // would add fields the engine never wrote there.
263        let item = sample();
264        assert_eq!(item.body_variation, None);
265
266        let written = written!(item, 0);
267
268        assert!(written.field("BodyVariation").is_none());
269        assert!(written.field("TextureVar").is_none());
270        assert!(written.field("Infinite").is_none());
271    }
272
273    #[test]
274    fn a_store_item_keeps_its_infinite_flag() {
275        let item = SavedItem {
276            infinite: Some(true),
277            ..sample()
278        };
279
280        let parsed = SavedItem::read_element(&written!(item, 0));
281
282        assert_eq!(parsed.infinite, Some(true));
283    }
284
285    #[test]
286    fn an_equipped_item_keeps_its_equip_only_fields() {
287        let item = SavedItem {
288            body_variation: Some(2),
289            texture_var: Some(1),
290            ..sample()
291        };
292
293        let parsed = SavedItem::read_element(&written!(item, 0));
294
295        assert_eq!(parsed.body_variation, Some(2));
296        assert_eq!(parsed.texture_var, Some(1));
297    }
298
299    /// `Useable` and `UpgradeType` are `Option`s the model holds the absence
300    /// of, so an unset one writes nothing. `UsesPerDay` is not: the omission
301    /// that makes `Uti` leave it out was measured across 993 `.uti` files and
302    /// does not carry to a GIT path, so this type writes it and the schema
303    /// says so.
304    #[test]
305    fn optional_property_fields_stay_absent_when_unset() {
306        let item = SavedItem {
307            properties: vec![GitItemProperty::default()],
308            ..SavedItem::default()
309        };
310
311        let written = written!(item, 0);
312        let GffValue::List(entries) = written.field("PropertiesList").expect("written") else {
313            panic!("PropertiesList should be a list");
314        };
315
316        assert!(entries[0].field("Useable").is_none());
317        assert!(entries[0].field("UpgradeType").is_none());
318        assert!(entries[0].field("UsesPerDay").is_some());
319    }
320}
321
322/// One entry of a placed item's `PropertiesList`.
323///
324/// Not shared with `Uti`'s. The two agreed on every axis until an omission
325/// measured across 993 `.uti` files landed on that type; the population is
326/// not GIT's, so carrying it here would answer for paths nobody measured.
327#[derive(Debug, Clone, PartialEq, Eq, GffModel)]
328pub struct GitItemProperty {
329    /// Cost table index (`CostTable`).
330    #[gff(CostTable, required, undefined)]
331    pub cost_table: u8,
332    /// Cost table value (`CostValue`).
333    #[gff(CostValue, required, undefined)]
334    pub cost_value: u16,
335    /// Parameter table index (`Param1`).
336    #[gff(Param1, required, undefined)]
337    pub param1: u8,
338    /// Parameter value (`Param1Value`).
339    #[gff(Param1Value, required, undefined)]
340    pub param1_value: u8,
341    /// Property identifier (`PropertyName`).
342    #[gff(PropertyName, required, undefined)]
343    pub property_name: u16,
344    /// Property subtype (`Subtype`).
345    #[gff(Subtype, required, undefined)]
346    pub subtype: u16,
347    /// Chance the property appears (`ChanceAppear`).
348    #[gff(ChanceAppear, required, undefined = 100)]
349    pub chance_appear: u8,
350    /// Usable flag (`Useable`), when the entry carries one.
351    #[gff(Useable, not_a_constant, optional = bool)]
352    pub useable: Option<bool>,
353    /// Uses per day (`UsesPerDay`).
354    ///
355    /// No vanilla `.uti` carries this label on any property entry, across all
356    /// 993 of them, so the omission stands in for the absence and the model
357    /// does not have to.
358    #[gff(UsesPerDay, stamped)]
359    pub uses_per_day: u8,
360    /// Upgrade type (`UpgradeType`), when the entry carries one.
361    ///
362    /// Held optionally because absence and an explicit `0` are both real: of
363    /// 2040 property entries in the install, 1118 omit the label, 890 carry a
364    /// value and 32 carry a zero. An omission keyed on the engine's own
365    /// absent-value would drop those 32.
366    ///
367    /// The entry says the engine stamps `0` and our reader hands back `None`,
368    /// which the vocabulary cannot express in one place: `Absent::Resolves`
369    /// has no substitute slot, because it was shaped for the case where our
370    /// value is the engine's. Recorded here until that is settled.
371    #[gff(UpgradeType, stamped, optional = u8)]
372    pub upgrade_type: Option<u8>,
373}