1use 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#[derive(Debug, Clone, PartialEq, Default)]
19pub struct GitItem {
20 pub template_resref: ResRef,
22 pub x_position: f32,
24 pub y_position: f32,
26 pub z_position: f32,
28 pub x_orientation: f32,
30 pub y_orientation: f32,
32 pub z_orientation: f32,
34 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
70pub(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#[derive(Debug, Clone, PartialEq, Default)]
185pub struct SavedItem {
186 pub tag: String,
188 pub object_id: ObjectId,
190 pub base_item: i32,
192 pub localized_name: GffLocalizedString,
194 pub description: GffLocalizedString,
196 pub description_identified: GffLocalizedString,
198
199 pub stack_size: u16,
201 pub charges: u8,
203 pub max_charges: u8,
205 pub cost: u32,
207 pub add_cost: u32,
209 pub upgrades: u32,
211 pub model_variation: u8,
213
214 pub identified: bool,
216 pub droppable: bool,
218 pub plot: bool,
220 pub stolen: bool,
222 pub pickpocketable: bool,
224 pub non_equippable: bool,
226 pub new_item: bool,
228 pub deleting: bool,
230
231 pub body_variation: Option<u8>,
233 pub texture_var: Option<u8>,
235 pub infinite: Option<bool>,
237
238 pub x_position: f32,
240 pub y_position: f32,
242 pub z_position: f32,
244 pub x_orientation: f32,
246 pub y_orientation: f32,
248 pub z_orientation: f32,
250
251 pub properties: Vec<UtiProperty>,
255}
256
257impl SavedItem {
258 pub fn new() -> Self {
260 Self::default()
261 }
262
263 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 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 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 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}