Skip to main content

rakata_generics/git/
placeable.rs

1//! Placeable 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::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/// A placeable instance placed in the area (struct type 9).
18#[derive(Debug, Clone, PartialEq, Default)]
19pub struct GitPlaceable {
20    /// Template resref (`TemplateResRef`).
21    pub template_resref: ResRef,
22    /// Facing angle in radians (`Bearing`).
23    pub bearing: f32,
24    /// X position (`X`).
25    pub x: f32,
26    /// Y position (`Y`).
27    pub y: f32,
28    /// Z position (`Z`).
29    pub z: f32,
30    /// Visual model override (`Appearance`).
31    ///
32    /// Read on a sparse placement as well as a saved one: `LoadDoor` and
33    /// `LoadPlaceable` are the same field readers whichever path calls them,
34    /// so a template-referencing placement carrying its own `Appearance` has
35    /// that value read as an override on top of the blueprint's. Read as a
36    /// DWORD and truncated to a byte, which for a door keys `doortypes.2da`'s
37    /// model columns. Defaults to `0` when absent.
38    pub appearance: u32,
39    /// Runtime object ID (`ObjectId`). Save-game only.
40    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
73/// GIT `Placeable List` entry child schema (struct type 9).
74pub(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// =========================================================================
141// The saved form
142// =========================================================================
143
144// Placeables as stored inside a save game.
145//
146// A static `.git` placeable is a placement referencing a `.utp`. A savegame
147// placeable carries the whole object inline, container contents included.
148//
149// ## `ItemList` tracks contents, not capacity
150//
151// The engine writes a placeable's `ItemList` only when the container
152// actually holds something. Across the fixture saves no `ItemList` is
153// present-but-empty, and plenty of placeables flag `HasInventory` while
154// carrying no `ItemList` at all, so presence follows the contents rather
155// than the flag. An empty [`Vec`] therefore writes no field, which is what
156// keeps an empty footlocker from gaining one it never had.
157//
158// ## What is not modelled
159//
160// `ActionList`, `EffectList`, `VarTable` and `SWVarTable` are live runtime
161// state whose layouts are only partly audited, and they are skipped for the
162// same reason [`SavedCreature`](crate::saved_creature::SavedCreature) skips
163// them.
164
165/// A placeable as stored inside a save game's module `GIT`.
166#[derive(Debug, Clone, PartialEq, Default)]
167pub struct SavedPlaceable {
168    /// Object tag (`Tag`).
169    pub tag: String,
170    /// Runtime object id (`ObjectId`).
171    pub object_id: ObjectId,
172
173    /// Appearance row (`Appearance`), into `placeables.2da`.
174    pub appearance: u32,
175    /// Current animation state (`Animation`).
176    pub animation: i32,
177    /// Displayed name (`LocName`).
178    pub loc_name: GffLocalizedString,
179    /// Description (`Description`).
180    pub description: GffLocalizedString,
181    /// Portrait, in whichever of its two forms the object uses.
182    pub portrait: SavedPortrait,
183
184    /// World X position (`X`).
185    pub x: f32,
186    /// World Y position (`Y`).
187    pub y: f32,
188    /// World Z position (`Z`).
189    pub z: f32,
190    /// Facing, in radians (`Bearing`).
191    pub bearing: f32,
192
193    /// Current hit points (`CurrentHP`).
194    pub current_hp: i16,
195    /// Maximum hit points (`HP`).
196    pub hp: i16,
197    /// Damage reduction (`Hardness`).
198    pub hardness: u8,
199    /// Fortitude save (`Fort`).
200    pub fortitude: u8,
201    /// Reflex save (`Ref`).
202    pub reflex: u8,
203    /// Will save (`Will`).
204    pub will: u8,
205    /// Cannot be reduced below 1 HP (`Min1HP`).
206    pub min1_hp: bool,
207    /// Plot flag (`Plot`).
208    pub plot: bool,
209    /// Static, non-interactive geometry (`Static`).
210    pub is_static: bool,
211    /// Accepts scripted commands (`Commandable`).
212    pub commandable: bool,
213    /// Faction id (`Faction`).
214    pub faction: u32,
215
216    /// Can be interacted with at all (`Useable`).
217    pub useable: bool,
218    /// Party members may use it directly (`PartyInteract`).
219    pub party_interact: bool,
220    /// Currently open (`Open`).
221    pub open: bool,
222    /// Light state (`LightState`).
223    pub light_state: u8,
224
225    /// Holds an inventory (`HasInventory`).
226    pub has_inventory: bool,
227    /// Destroy the object once emptied (`DieWhenEmpty`).
228    pub die_when_empty: bool,
229    /// Renders as a loose pile rather than a container (`GroundPile`).
230    pub ground_pile: bool,
231    /// Body-bag row (`BodyBag`).
232    pub body_bag: u8,
233    /// Is a body bag (`IsBodyBag`).
234    pub is_body_bag: bool,
235    /// Body bag is visible (`IsBodyBagVisible`).
236    pub is_body_bag_visible: bool,
237    /// Is a corpse (`IsCorpse`).
238    pub is_corpse: bool,
239    /// Container contents (`ItemList`). Empty writes no field at all.
240    pub items: Vec<SavedItem>,
241
242    /// Can be locked at all (`Lockable`).
243    pub lockable: bool,
244    /// Currently locked (`Locked`).
245    pub locked: bool,
246    /// DC to pick the lock (`OpenLockDC`).
247    pub open_lock_dc: u8,
248    /// DC to re-lock (`CloseLockDC`).
249    pub close_lock_dc: u8,
250    /// Key tag that opens this placeable (`KeyName`).
251    pub key_name: String,
252    /// A key is required regardless of lock DC (`KeyRequired`).
253    pub key_required: bool,
254    /// Consume the key on use (`AutoRemoveKey`).
255    pub auto_remove_key: bool,
256
257    /// Trap configuration (`TrapDetectable`, `TrapDetectDC`, ...).
258    pub trap: TrapSettings,
259
260    /// Attached conversation (`Conversation`).
261    pub conversation: ResRef,
262
263    /// `OnClosed`.
264    pub on_closed: ResRef,
265    /// `OnDamaged`.
266    pub on_damaged: ResRef,
267    /// `OnDeath`.
268    pub on_death: ResRef,
269    /// `OnDialog`.
270    pub on_dialog: ResRef,
271    /// `OnDisarm`.
272    pub on_disarm: ResRef,
273    /// `OnEndDialogue`.
274    pub on_end_dialogue: ResRef,
275    /// `OnHeartbeat`.
276    pub on_heartbeat: ResRef,
277    /// `OnInvDisturbed`.
278    pub on_inv_disturbed: ResRef,
279    /// `OnLock`.
280    pub on_lock: ResRef,
281    /// `OnMeleeAttacked`.
282    pub on_melee_attacked: ResRef,
283    /// `OnOpen`.
284    pub on_open: ResRef,
285    /// `OnSpellCastAt`.
286    pub on_spell_cast_at: ResRef,
287    /// `OnTrapTriggered`.
288    pub on_trap_triggered: ResRef,
289    /// `OnUnlock`.
290    pub on_unlock: ResRef,
291    /// `OnUsed`.
292    pub on_used: ResRef,
293    /// `OnUserDefined`.
294    pub on_user_defined: ResRef,
295}
296
297impl SavedPlaceable {
298    /// Creates an empty saved placeable.
299    pub fn new() -> Self {
300        Self::default()
301    }
302
303    /// Reads one saved `Placeable List` element.
304    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    /// Writes this placeable back into a `Placeable List` element.
394    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        // Presence tracks contents: an empty container has no ItemList at all.
473        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        // The engine writes ItemList only when the container holds something,
593        // so a flag with no contents must not conjure an empty list.
594        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}