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}