1use rakata_core::ResRef;
4use rakata_formats::{
5 gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffType},
6 GffLocalizedString, GffStruct, GffValue,
7};
8
9use super::item::SavedItem;
10use super::object_id_or_invalid;
11use crate::gff_helpers::{
12 get_bool, get_f32, get_i16, get_i32, get_locstring, get_resref, get_string, get_u16, get_u32,
13 get_u8, upsert_field,
14};
15use crate::shared::{ObjectId, SavedPortrait, TrapDefaults, TrapSettings};
16
17#[derive(Debug, Clone, PartialEq, Default)]
19pub struct GitPlaceable {
20 pub template_resref: ResRef,
22 pub bearing: f32,
24 pub x: f32,
26 pub y: f32,
28 pub z: f32,
30 pub appearance: u32,
39 pub object_id: ObjectId,
41}
42
43impl GitPlaceable {
44 pub(crate) fn from_gff_struct(s: &GffStruct) -> Self {
45 Self {
46 template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
47 bearing: get_f32(s, "Bearing").unwrap_or(0.0),
48 x: get_f32(s, "X").unwrap_or(0.0),
49 y: get_f32(s, "Y").unwrap_or(0.0),
50 z: get_f32(s, "Z").unwrap_or(0.0),
51 appearance: get_u32(s, "Appearance").unwrap_or(0),
52 object_id: object_id_or_invalid(s),
53 }
54 }
55
56 pub(crate) fn to_gff_struct(&self) -> GffStruct {
57 let mut s = GffStruct::new(9);
58 upsert_field(
59 &mut s,
60 "TemplateResRef",
61 GffValue::ResRef(self.template_resref),
62 );
63 upsert_field(&mut s, "Bearing", GffValue::Single(self.bearing));
64 upsert_field(&mut s, "X", GffValue::Single(self.x));
65 upsert_field(&mut s, "Y", GffValue::Single(self.y));
66 upsert_field(&mut s, "Z", GffValue::Single(self.z));
67 upsert_field(&mut s, "Appearance", GffValue::UInt32(self.appearance));
68 upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
69 s
70 }
71}
72
73pub(crate) static PLACEABLE_LIST_CHILDREN: &[FieldSchema] = &[
75 FieldSchema {
76 label: "Appearance",
77 expected_type: GffType::UInt32,
78 life: FieldLife::Live,
79 required: false,
80 absent: AbsentDefault::Unverified,
81 children: None,
82 constraint: None,
83 },
84 FieldSchema {
85 label: "ObjectId",
86 expected_type: GffType::UInt32,
87 life: FieldLife::Live,
88 required: false,
89 absent: AbsentDefault::Unverified,
90 children: None,
91 constraint: None,
92 },
93 FieldSchema {
94 label: "TemplateResRef",
95 expected_type: GffType::ResRef,
96 life: FieldLife::Live,
97 required: false,
98 absent: AbsentDefault::Unverified,
99 children: None,
100 constraint: None,
101 },
102 FieldSchema {
103 label: "Bearing",
104 expected_type: GffType::Single,
105 life: FieldLife::Live,
106 required: false,
107 absent: AbsentDefault::Unverified,
108 children: None,
109 constraint: None,
110 },
111 FieldSchema {
112 label: "X",
113 expected_type: GffType::Single,
114 life: FieldLife::Live,
115 required: false,
116 absent: AbsentDefault::Unverified,
117 children: None,
118 constraint: None,
119 },
120 FieldSchema {
121 label: "Y",
122 expected_type: GffType::Single,
123 life: FieldLife::Live,
124 required: false,
125 absent: AbsentDefault::Unverified,
126 children: None,
127 constraint: None,
128 },
129 FieldSchema {
130 label: "Z",
131 expected_type: GffType::Single,
132 life: FieldLife::Live,
133 required: false,
134 absent: AbsentDefault::Unverified,
135 children: None,
136 constraint: None,
137 },
138];
139
140#[derive(Debug, Clone, PartialEq, Default)]
167pub struct SavedPlaceable {
168 pub tag: String,
170 pub object_id: ObjectId,
172
173 pub appearance: u32,
175 pub animation: i32,
177 pub loc_name: GffLocalizedString,
179 pub description: GffLocalizedString,
181 pub portrait: SavedPortrait,
183
184 pub x: f32,
186 pub y: f32,
188 pub z: f32,
190 pub bearing: f32,
192
193 pub current_hp: i16,
195 pub hp: i16,
197 pub hardness: u8,
199 pub fortitude: u8,
201 pub reflex: u8,
203 pub will: u8,
205 pub min1_hp: bool,
207 pub plot: bool,
209 pub is_static: bool,
211 pub commandable: bool,
213 pub faction: u32,
215
216 pub useable: bool,
218 pub party_interact: bool,
220 pub open: bool,
222 pub light_state: u8,
224
225 pub has_inventory: bool,
227 pub die_when_empty: bool,
229 pub ground_pile: bool,
231 pub body_bag: u8,
233 pub is_body_bag: bool,
235 pub is_body_bag_visible: bool,
237 pub is_corpse: bool,
239 pub items: Vec<SavedItem>,
241
242 pub lockable: bool,
244 pub locked: bool,
246 pub open_lock_dc: u8,
248 pub close_lock_dc: u8,
250 pub key_name: String,
252 pub key_required: bool,
254 pub auto_remove_key: bool,
256
257 pub trap: TrapSettings,
259
260 pub conversation: ResRef,
262
263 pub on_closed: ResRef,
265 pub on_damaged: ResRef,
267 pub on_death: ResRef,
269 pub on_dialog: ResRef,
271 pub on_disarm: ResRef,
273 pub on_end_dialogue: ResRef,
275 pub on_heartbeat: ResRef,
277 pub on_inv_disturbed: ResRef,
279 pub on_lock: ResRef,
281 pub on_melee_attacked: ResRef,
283 pub on_open: ResRef,
285 pub on_spell_cast_at: ResRef,
287 pub on_trap_triggered: ResRef,
289 pub on_unlock: ResRef,
291 pub on_used: ResRef,
293 pub on_user_defined: ResRef,
295}
296
297impl SavedPlaceable {
298 pub fn new() -> Self {
300 Self::default()
301 }
302
303 pub fn from_struct(structure: &GffStruct) -> Self {
305 Self {
306 tag: get_string(structure, "Tag").unwrap_or_default(),
307 object_id: object_id_or_invalid(structure),
308
309 appearance: get_u32(structure, "Appearance").unwrap_or(0),
310 animation: get_i32(structure, "Animation").unwrap_or(0),
311 loc_name: get_locstring(structure, "LocName")
312 .cloned()
313 .unwrap_or_default(),
314 description: get_locstring(structure, "Description")
315 .cloned()
316 .unwrap_or_default(),
317 portrait: SavedPortrait::read(
318 |label| get_resref(structure, label),
319 |label| get_u16(structure, label),
320 ),
321
322 x: get_f32(structure, "X").unwrap_or(0.0),
323 y: get_f32(structure, "Y").unwrap_or(0.0),
324 z: get_f32(structure, "Z").unwrap_or(0.0),
325 bearing: get_f32(structure, "Bearing").unwrap_or(0.0),
326
327 current_hp: get_i16(structure, "CurrentHP").unwrap_or(0),
328 hp: get_i16(structure, "HP").unwrap_or(0),
329 hardness: get_u8(structure, "Hardness").unwrap_or(0),
330 fortitude: get_u8(structure, "Fort").unwrap_or(0),
331 reflex: get_u8(structure, "Ref").unwrap_or(0),
332 will: get_u8(structure, "Will").unwrap_or(0),
333 min1_hp: get_bool(structure, "Min1HP").unwrap_or(false),
334 plot: get_bool(structure, "Plot").unwrap_or(false),
335 is_static: get_bool(structure, "Static").unwrap_or(false),
336 commandable: get_bool(structure, "Commandable").unwrap_or(false),
337 faction: get_u32(structure, "Faction").unwrap_or(0),
338
339 useable: get_bool(structure, "Useable").unwrap_or(false),
340 party_interact: get_bool(structure, "PartyInteract").unwrap_or(false),
341 open: get_bool(structure, "Open").unwrap_or(false),
342 light_state: get_u8(structure, "LightState").unwrap_or(0),
343
344 has_inventory: get_bool(structure, "HasInventory").unwrap_or(false),
345 die_when_empty: get_bool(structure, "DieWhenEmpty").unwrap_or(false),
346 ground_pile: get_bool(structure, "GroundPile").unwrap_or(false),
347 body_bag: get_u8(structure, "BodyBag").unwrap_or(0),
348 is_body_bag: get_bool(structure, "IsBodyBag").unwrap_or(false),
349 is_body_bag_visible: get_bool(structure, "IsBodyBagVisible").unwrap_or(false),
350 is_corpse: get_bool(structure, "IsCorpse").unwrap_or(false),
351 items: match structure.field("ItemList") {
352 Some(GffValue::List(entries)) => {
353 entries.iter().map(SavedItem::from_struct).collect()
354 }
355 _ => Vec::new(),
356 },
357
358 lockable: get_bool(structure, "Lockable").unwrap_or(false),
359 locked: get_bool(structure, "Locked").unwrap_or(false),
360 open_lock_dc: get_u8(structure, "OpenLockDC").unwrap_or(0),
361 close_lock_dc: get_u8(structure, "CloseLockDC").unwrap_or(0),
362 key_name: get_string(structure, "KeyName").unwrap_or_default(),
363 key_required: get_bool(structure, "KeyRequired").unwrap_or(false),
364 auto_remove_key: get_bool(structure, "AutoRemoveKey").unwrap_or(false),
365
366 trap: TrapSettings::read(
367 TrapDefaults::PLACEABLE,
368 |label| get_bool(structure, label),
369 |label| get_u8(structure, label),
370 ),
371
372 conversation: get_resref(structure, "Conversation").unwrap_or_default(),
373
374 on_closed: get_resref(structure, "OnClosed").unwrap_or_default(),
375 on_damaged: get_resref(structure, "OnDamaged").unwrap_or_default(),
376 on_death: get_resref(structure, "OnDeath").unwrap_or_default(),
377 on_dialog: get_resref(structure, "OnDialog").unwrap_or_default(),
378 on_disarm: get_resref(structure, "OnDisarm").unwrap_or_default(),
379 on_end_dialogue: get_resref(structure, "OnEndDialogue").unwrap_or_default(),
380 on_heartbeat: get_resref(structure, "OnHeartbeat").unwrap_or_default(),
381 on_inv_disturbed: get_resref(structure, "OnInvDisturbed").unwrap_or_default(),
382 on_lock: get_resref(structure, "OnLock").unwrap_or_default(),
383 on_melee_attacked: get_resref(structure, "OnMeleeAttacked").unwrap_or_default(),
384 on_open: get_resref(structure, "OnOpen").unwrap_or_default(),
385 on_spell_cast_at: get_resref(structure, "OnSpellCastAt").unwrap_or_default(),
386 on_trap_triggered: get_resref(structure, "OnTrapTriggered").unwrap_or_default(),
387 on_unlock: get_resref(structure, "OnUnlock").unwrap_or_default(),
388 on_used: get_resref(structure, "OnUsed").unwrap_or_default(),
389 on_user_defined: get_resref(structure, "OnUserDefined").unwrap_or_default(),
390 }
391 }
392
393 pub fn to_struct(&self, index: usize) -> GffStruct {
395 let mut s = GffStruct::new(i32::try_from(index).expect("placeable index fits i32"));
396
397 upsert_field(&mut s, "Tag", GffValue::String(self.tag.clone()));
398 upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
399
400 upsert_field(&mut s, "Appearance", GffValue::UInt32(self.appearance));
401 upsert_field(&mut s, "Animation", GffValue::Int32(self.animation));
402 upsert_field(
403 &mut s,
404 "LocName",
405 GffValue::LocalizedString(self.loc_name.clone()),
406 );
407 upsert_field(
408 &mut s,
409 "Description",
410 GffValue::LocalizedString(self.description.clone()),
411 );
412 self.portrait
413 .write(|label, value| upsert_field(&mut s, label, value));
414
415 upsert_field(&mut s, "X", GffValue::Single(self.x));
416 upsert_field(&mut s, "Y", GffValue::Single(self.y));
417 upsert_field(&mut s, "Z", GffValue::Single(self.z));
418 upsert_field(&mut s, "Bearing", GffValue::Single(self.bearing));
419
420 upsert_field(&mut s, "CurrentHP", GffValue::Int16(self.current_hp));
421 upsert_field(&mut s, "HP", GffValue::Int16(self.hp));
422 upsert_field(&mut s, "Hardness", GffValue::UInt8(self.hardness));
423 upsert_field(&mut s, "Fort", GffValue::UInt8(self.fortitude));
424 upsert_field(&mut s, "Ref", GffValue::UInt8(self.reflex));
425 upsert_field(&mut s, "Will", GffValue::UInt8(self.will));
426 upsert_field(&mut s, "Min1HP", GffValue::UInt8(self.min1_hp.into()));
427 upsert_field(&mut s, "Plot", GffValue::UInt8(self.plot.into()));
428 upsert_field(&mut s, "Static", GffValue::UInt8(self.is_static.into()));
429 upsert_field(
430 &mut s,
431 "Commandable",
432 GffValue::UInt8(self.commandable.into()),
433 );
434 upsert_field(&mut s, "Faction", GffValue::UInt32(self.faction));
435
436 upsert_field(&mut s, "Useable", GffValue::UInt8(self.useable.into()));
437 upsert_field(
438 &mut s,
439 "PartyInteract",
440 GffValue::UInt8(self.party_interact.into()),
441 );
442 upsert_field(&mut s, "Open", GffValue::UInt8(self.open.into()));
443 upsert_field(&mut s, "LightState", GffValue::UInt8(self.light_state));
444
445 upsert_field(
446 &mut s,
447 "HasInventory",
448 GffValue::UInt8(self.has_inventory.into()),
449 );
450 upsert_field(
451 &mut s,
452 "DieWhenEmpty",
453 GffValue::UInt8(self.die_when_empty.into()),
454 );
455 upsert_field(
456 &mut s,
457 "GroundPile",
458 GffValue::UInt8(self.ground_pile.into()),
459 );
460 upsert_field(&mut s, "BodyBag", GffValue::UInt8(self.body_bag));
461 upsert_field(
462 &mut s,
463 "IsBodyBag",
464 GffValue::UInt8(self.is_body_bag.into()),
465 );
466 upsert_field(
467 &mut s,
468 "IsBodyBagVisible",
469 GffValue::UInt8(self.is_body_bag_visible.into()),
470 );
471 upsert_field(&mut s, "IsCorpse", GffValue::UInt8(self.is_corpse.into()));
472 if !self.items.is_empty() {
474 upsert_field(
475 &mut s,
476 "ItemList",
477 GffValue::List(
478 self.items
479 .iter()
480 .enumerate()
481 .map(|(index, item)| item.to_struct(index))
482 .collect(),
483 ),
484 );
485 }
486
487 upsert_field(&mut s, "Lockable", GffValue::UInt8(self.lockable.into()));
488 upsert_field(&mut s, "Locked", GffValue::UInt8(self.locked.into()));
489 upsert_field(&mut s, "OpenLockDC", GffValue::UInt8(self.open_lock_dc));
490 upsert_field(&mut s, "CloseLockDC", GffValue::UInt8(self.close_lock_dc));
491 upsert_field(&mut s, "KeyName", GffValue::String(self.key_name.clone()));
492 upsert_field(
493 &mut s,
494 "KeyRequired",
495 GffValue::UInt8(self.key_required.into()),
496 );
497 upsert_field(
498 &mut s,
499 "AutoRemoveKey",
500 GffValue::UInt8(self.auto_remove_key.into()),
501 );
502
503 self.trap
504 .write(|label, value| upsert_field(&mut s, label, value));
505
506 upsert_field(&mut s, "Conversation", GffValue::ResRef(self.conversation));
507
508 upsert_field(&mut s, "OnClosed", GffValue::ResRef(self.on_closed));
509 upsert_field(&mut s, "OnDamaged", GffValue::ResRef(self.on_damaged));
510 upsert_field(&mut s, "OnDeath", GffValue::ResRef(self.on_death));
511 upsert_field(&mut s, "OnDialog", GffValue::ResRef(self.on_dialog));
512 upsert_field(&mut s, "OnDisarm", GffValue::ResRef(self.on_disarm));
513 upsert_field(
514 &mut s,
515 "OnEndDialogue",
516 GffValue::ResRef(self.on_end_dialogue),
517 );
518 upsert_field(&mut s, "OnHeartbeat", GffValue::ResRef(self.on_heartbeat));
519 upsert_field(
520 &mut s,
521 "OnInvDisturbed",
522 GffValue::ResRef(self.on_inv_disturbed),
523 );
524 upsert_field(&mut s, "OnLock", GffValue::ResRef(self.on_lock));
525 upsert_field(
526 &mut s,
527 "OnMeleeAttacked",
528 GffValue::ResRef(self.on_melee_attacked),
529 );
530 upsert_field(&mut s, "OnOpen", GffValue::ResRef(self.on_open));
531 upsert_field(
532 &mut s,
533 "OnSpellCastAt",
534 GffValue::ResRef(self.on_spell_cast_at),
535 );
536 upsert_field(
537 &mut s,
538 "OnTrapTriggered",
539 GffValue::ResRef(self.on_trap_triggered),
540 );
541 upsert_field(&mut s, "OnUnlock", GffValue::ResRef(self.on_unlock));
542 upsert_field(&mut s, "OnUsed", GffValue::ResRef(self.on_used));
543 upsert_field(
544 &mut s,
545 "OnUserDefined",
546 GffValue::ResRef(self.on_user_defined),
547 );
548
549 s
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 fn sample() -> SavedPlaceable {
558 SavedPlaceable {
559 tag: "footlocker01".to_string(),
560 object_id: ObjectId::new(0x8000_0077),
561 appearance: 68,
562 portrait: SavedPortrait::Id(0),
563 x: 4.25,
564 bearing: 2.5,
565 current_hp: 15,
566 hp: 15,
567 useable: true,
568 has_inventory: true,
569 locked: true,
570 open_lock_dc: 20,
571 trap: TrapSettings {
572 detectable: true,
573 trap_type: 2,
574 ..TrapSettings::default()
575 },
576 on_open: ResRef::new("k_plc_open").expect("valid resref"),
577 ..SavedPlaceable::default()
578 }
579 }
580
581 #[test]
582 fn round_trips_through_a_list_element() {
583 let placeable = sample();
584
585 let parsed = SavedPlaceable::from_struct(&placeable.to_struct(0));
586
587 assert_eq!(parsed, placeable);
588 }
589
590 #[test]
591 fn an_empty_container_writes_no_item_list() {
592 let placeable = sample();
595 assert!(placeable.has_inventory);
596 assert!(placeable.items.is_empty());
597
598 let written = placeable.to_struct(0);
599
600 assert!(written.field("ItemList").is_none());
601 }
602
603 #[test]
604 fn contents_survive_the_round_trip() {
605 let placeable = SavedPlaceable {
606 items: vec![SavedItem {
607 tag: "g_i_credits001".to_string(),
608 stack_size: 250,
609 ..SavedItem::default()
610 }],
611 ..sample()
612 };
613
614 let parsed = SavedPlaceable::from_struct(&placeable.to_struct(0));
615
616 assert_eq!(parsed.items, placeable.items);
617 }
618}