Skip to main content

rakata_generics/git/
placeable.rs

1//! Placeable placements in a GIT.
2
3use rakata_core::ResRef;
4use rakata_formats::{GffLocalizedString, GffModel, GffStruct};
5
6use super::item::SavedItem;
7use super::GitObjectPlacement;
8use crate::git::blocks::{LockState, Placement, SavedPortrait, SavedPortraitLabels, SavingThrows};
9use crate::shared::ObjectId;
10/// The part of a `placeable` entry that is what every placeable entry carries, whichever form it takes.
11///
12/// Both arms flatten it and the list's element is it, so the labels here
13/// are one declaration reached from both forms rather than two copies.
14#[derive(Debug, Clone, PartialEq, GffModel)]
15pub struct GitPlaceableCommon {
16    /// Visual model override (`Appearance`).
17    ///
18    /// Read on a sparse placement as well as a saved one: `LoadDoor` and
19    /// `LoadPlaceable` are the same field readers whichever path calls them,
20    /// so a template-referencing placement carrying its own `Appearance` has
21    /// that value read as an override on top of the blueprint's. Read as a
22    /// DWORD and truncated to a byte, which for a door keys `doortypes.2da`'s
23    /// model columns. Defaults to `0` when absent.
24    /// Read by `LoadPlaceable` and truncated to a byte, and the absent value
25    /// is a literal applied at the read site. Vanilla leaves the label out
26    /// when it would be zero.
27    #[gff(Appearance, live, stamped, omit = audited_constant(117))]
28    pub appearance: u32,
29    /// Runtime object ID (`ObjectId`). Save-game only.
30    /// Runtime object id (`ObjectId`), which only a saved form carries.
31    ///
32    /// The area-level dispatcher reads it once per element ahead of the
33    /// static-versus-saved branch, so the engine reads it on both forms and
34    /// the liveness below is right. What it does not do is author it: every
35    /// element the engine saves carries one and no element the toolset shipped
36    /// does. Held as the absence so writing a static placement back does not
37    /// invent a runtime id for it.
38    #[gff(ObjectId, stamped = ObjectId::INVALID, optional = ObjectId)]
39    pub object_id: Option<ObjectId>,
40}
41
42/// A placeable instance placed in the area (struct type 9).
43#[derive(Debug, Clone, PartialEq, GffModel)]
44#[gff_manual_element]
45pub struct GitPlaceable {
46    /// Placement, whose four labels sit at this object's own level.
47    #[gff(flatten = Placement)]
48    pub placement: Placement,
49    /// The block both forms carry, which the list declares as its element.
50    ///
51    /// No `#[gff]`: the arm contributes its parts to the split, and the
52    /// element is this block, so declaring it here too would make one label
53    /// two declarations. The arm's own codec reads and writes it.
54    pub common: GitPlaceableCommon,
55    /// Template resref (`TemplateResRef`).
56    #[gff(TemplateResRef, unexamined)]
57    pub template_resref: ResRef,
58}
59
60// =========================================================================
61// The saved form
62// =========================================================================
63
64// Placeables as stored inside a save game.
65//
66// A savegame placeable carries the whole object inline rather than referencing
67// a `.utp`, container contents included. See
68// `docs/src/formats/save/index.md` for the template-versus-snapshot rule.
69//
70// ## `ItemList` tracks contents, not capacity
71//
72// The engine writes a placeable's `ItemList` only when the container
73// actually holds something. Across the fixture saves no `ItemList` is
74// present-but-empty, and plenty of placeables flag `HasInventory` while
75// carrying no `ItemList` at all, so presence follows the contents rather
76// than the flag. An empty [`Vec`] therefore writes no field, which is what
77// keeps an empty footlocker from gaining one it never had.
78//
79// ## What is not modelled
80//
81// `ActionList`, `EffectList`, `VarTable` and `SWVarTable` are live runtime
82// state whose layouts are only partly audited, and they are skipped for the
83// same reason [`SavedCreature`](crate::git::creature::SavedCreature) skips
84// them.
85
86/// A placeable as stored inside a save game's module `GIT`.
87#[derive(Debug, Clone, PartialEq, GffModel)]
88#[gff_manual_element]
89pub struct SavedPlaceable {
90    /// Inventory the placeable holds (`ItemList`).
91    ///
92    /// Struct id `0` throughout, like a creature's own inventory and unlike a
93    /// store's, which really is positional. The id a list of items takes is
94    /// decided by the list that owns it rather than by the element type.
95    #[gff(
96        ItemList,
97        not_a_constant,
98        list = SavedItem,
99        element_extra = GitObjectPlacement,
100        element_id = 0,
101        omit = empty(50)
102    )]
103    pub items: Vec<SavedItem>,
104    /// Placement, whose four labels sit at this object's own level.
105    #[gff(flatten = Placement)]
106    pub placement: Placement,
107    /// Saving throws, likewise flat on the object.
108    #[gff(flatten = SavingThrows)]
109    pub saves: SavingThrows,
110    /// Lock state, likewise flat on the object.
111    #[gff(flatten = LockState)]
112    pub lock: LockState,
113    /// Portrait, as whichever of the two labels the object carries.
114    ///
115    /// Declared per owner rather than shared: the audit parts `Portrait` and
116    /// `PortraitId` across doors, placeables and triggers on the absent axis.
117    /// Both halves are this type's, because one member stands for two labels
118    /// and which one is written depends on the variant.
119    #[gff(flatten = SavedPortraitLabels, manual_read, manual_write)]
120    pub portrait: SavedPortrait,
121
122    // Trap settings, per owner: every label parts from the other
123    // owners on the absent axis, so no block spans them.
124    /// Whether the trap can be spotted (`TrapDetectable`).
125    #[gff(TrapDetectable, stamped)]
126    pub trap_detectable: bool,
127    /// Whether the trap can be disarmed (`TrapDisarmable`).
128    #[gff(TrapDisarmable, stamped)]
129    pub trap_disarmable: bool,
130    /// Whether the trap fires once and clears (`TrapOneShot`).
131    #[gff(TrapOneShot, stamped)]
132    pub trap_one_shot: bool,
133    /// Trap row in `traps.2da` (`TrapType`).
134    #[gff(TrapType, constructed = 255)]
135    pub trap_type: u8,
136    /// Difficulty of spotting the trap (`TrapDetectDC`).
137    #[gff(TrapDetectDC, stamped)]
138    pub trap_detect_dc: u8,
139    /// Difficulty of disarming it (`DisarmDC`).
140    #[gff(DisarmDC, stamped)]
141    pub trap_disarm_dc: u8,
142    /// Whether a trap is present at all (`TrapFlag`).
143    #[gff(TrapFlag, stamped)]
144    pub trap_flag: u8,
145    // Trap and lock scripts, per owner for the same reason.
146    /// Fired when the object closes (`OnClosed`).
147    #[gff(OnClosed, stamped)]
148    pub on_closed: ResRef,
149    /// Fired when it takes damage (`OnDamaged`).
150    #[gff(OnDamaged, stamped)]
151    pub on_damaged: ResRef,
152    /// Fired when it is destroyed (`OnDeath`).
153    #[gff(OnDeath, stamped)]
154    pub on_death: ResRef,
155    /// Fired when its trap is disarmed (`OnDisarm`).
156    #[gff(OnDisarm, stamped)]
157    pub on_disarm: ResRef,
158    /// Fired on the object heartbeat (`OnHeartbeat`).
159    #[gff(OnHeartbeat, stamped)]
160    pub on_heartbeat: ResRef,
161    /// Fired when it is locked (`OnLock`).
162    #[gff(OnLock, stamped)]
163    pub on_lock: ResRef,
164    /// Fired when it is struck in melee (`OnMeleeAttacked`).
165    #[gff(OnMeleeAttacked, stamped)]
166    pub on_melee_attacked: ResRef,
167    /// Fired when it opens (`OnOpen`).
168    #[gff(OnOpen, stamped)]
169    pub on_open: ResRef,
170    /// Fired when a spell targets it (`OnSpellCastAt`).
171    #[gff(OnSpellCastAt, stamped)]
172    pub on_spell_cast_at: ResRef,
173    /// Fired when its trap goes off (`OnTrapTriggered`).
174    #[gff(OnTrapTriggered, stamped)]
175    pub on_trap_triggered: ResRef,
176    /// Fired when it is unlocked (`OnUnlock`).
177    #[gff(OnUnlock, stamped)]
178    pub on_unlock: ResRef,
179    /// Fired on a user-defined event (`OnUserDefined`).
180    #[gff(OnUserDefined, stamped)]
181    pub on_user_defined: ResRef,
182    /// The block both forms carry, which the list declares as its element.
183    ///
184    /// No `#[gff]`: the arm contributes its parts to the split, and the
185    /// element is this block, so declaring it here too would make one label
186    /// two declarations. The arm's own codec reads and writes it.
187    pub common: GitPlaceableCommon,
188    /// Current animation state (`Animation`).
189    #[gff(Animation, unexamined)]
190    pub animation: i32,
191    /// Body-bag row (`BodyBag`).
192    #[gff(BodyBag, unexamined)]
193    pub body_bag: u8,
194    /// Accepts scripted commands (`Commandable`).
195    #[gff(Commandable, unexamined)]
196    pub commandable: bool,
197    /// Attached conversation (`Conversation`).
198    #[gff(Conversation, unexamined)]
199    pub conversation: ResRef,
200    /// Current hit points (`CurrentHP`).
201    #[gff(CurrentHP, unexamined)]
202    pub current_hp: i16,
203    /// Description (`Description`).
204    #[gff(Description, unexamined)]
205    pub description: GffLocalizedString,
206    /// Destroy the object once emptied (`DieWhenEmpty`).
207    #[gff(DieWhenEmpty, unexamined)]
208    pub die_when_empty: bool,
209    /// Faction id (`Faction`).
210    #[gff(Faction, unexamined)]
211    pub faction: u32,
212    /// Renders as a loose pile rather than a container (`GroundPile`).
213    #[gff(GroundPile, unexamined)]
214    pub ground_pile: bool,
215    /// Maximum hit points (`HP`).
216    #[gff(HP, unexamined)]
217    pub hp: i16,
218    /// Damage reduction (`Hardness`).
219    #[gff(Hardness, unexamined)]
220    pub hardness: u8,
221    /// Holds an inventory (`HasInventory`).
222    #[gff(HasInventory, unexamined)]
223    pub has_inventory: bool,
224    /// Is a body bag (`IsBodyBag`).
225    #[gff(IsBodyBag, unexamined)]
226    pub is_body_bag: bool,
227    /// Body bag is visible (`IsBodyBagVisible`).
228    #[gff(IsBodyBagVisible, unexamined)]
229    pub is_body_bag_visible: bool,
230    /// Is a corpse (`IsCorpse`).
231    #[gff(IsCorpse, unexamined)]
232    pub is_corpse: bool,
233    /// Light state (`LightState`).
234    #[gff(LightState, unexamined)]
235    pub light_state: u8,
236    /// Displayed name (`LocName`).
237    #[gff(LocName, unexamined)]
238    pub loc_name: GffLocalizedString,
239    /// Cannot be reduced below 1 HP (`Min1HP`).
240    #[gff(Min1HP, unexamined)]
241    pub min1_hp: bool,
242    /// `OnDialog`.
243    #[gff(OnDialog, stamped)]
244    pub on_dialog: ResRef,
245    /// `OnEndDialogue`.
246    #[gff(OnEndDialogue, stamped)]
247    pub on_end_dialogue: ResRef,
248    /// `OnInvDisturbed`.
249    #[gff(OnInvDisturbed, stamped)]
250    pub on_inv_disturbed: ResRef,
251    /// `OnUsed`.
252    #[gff(OnUsed, stamped)]
253    pub on_used: ResRef,
254    /// Currently open (`Open`).
255    #[gff(Open, unexamined)]
256    pub open: bool,
257    /// Party members may use it directly (`PartyInteract`).
258    #[gff(PartyInteract, unexamined)]
259    pub party_interact: bool,
260    /// Plot flag (`Plot`).
261    #[gff(Plot, unexamined)]
262    pub plot: bool,
263    /// Static, non-interactive geometry (`Static`).
264    #[gff(Static, unexamined)]
265    pub is_static: bool,
266    /// Object tag (`Tag`).
267    #[gff(Tag, unexamined)]
268    pub tag: String,
269    /// Can be interacted with at all (`Useable`).
270    #[gff(Useable, unexamined)]
271    pub useable: bool,
272}
273
274impl SavedPlaceable {
275    /// Reads one element, resolving the portrait's two labels into one value.
276    pub fn read_element(structure: &GffStruct) -> Self {
277        Self {
278            common: GitPlaceableCommon::read_declared(structure),
279            portrait: SavedPortrait::read(structure),
280            ..Self::read_declared(structure)
281        }
282    }
283
284    /// Writes one element, emitting whichever portrait label the value uses.
285    pub fn write_element(&self, structure: &mut GffStruct) {
286        self.write_declared(structure);
287        self.common.write_declared(structure);
288        self.portrait.write(structure);
289    }
290}
291
292impl GitPlaceable {
293    /// Reads one element, with the block the list declares as its element.
294    pub fn read_element(structure: &GffStruct) -> Self {
295        Self {
296            common: GitPlaceableCommon::read_declared(structure),
297            ..Self::read_declared(structure)
298        }
299    }
300
301    /// Writes one element, the common block included.
302    pub fn write_element(&self, structure: &mut GffStruct) {
303        self.write_declared(structure);
304        self.common.write_declared(structure);
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    /// One value written into an element struct carrying the list's own id.
313    macro_rules! written {
314        ($value:expr, $id:expr) => {{
315            let mut element = GffStruct::new($id);
316            $value.write_element(&mut element);
317            element
318        }};
319    }
320
321    fn sample() -> SavedPlaceable {
322        SavedPlaceable {
323            tag: "footlocker01".to_string(),
324            common: GitPlaceableCommon {
325                appearance: 68,
326                object_id: Some(ObjectId::new(0x8000_0077)),
327            },
328            portrait: SavedPortrait::Id(0),
329            placement: Placement {
330                x: 4.25,
331                bearing: 2.5,
332                ..Placement::default()
333            },
334            current_hp: 15,
335            hp: 15,
336            useable: true,
337            has_inventory: true,
338            lock: LockState {
339                locked: true,
340                open_lock_dc: 20,
341                ..LockState::default()
342            },
343            trap_detectable: true,
344            trap_type: 4,
345            on_open: ResRef::new("k_plc_open").expect("a legal resref"),
346            ..SavedPlaceable::default()
347        }
348    }
349
350    #[test]
351    fn round_trips_through_a_list_element() {
352        let placeable = sample();
353
354        let parsed = SavedPlaceable::read_element(&written!(placeable, 0));
355
356        assert_eq!(parsed, placeable);
357    }
358
359    #[test]
360    fn an_empty_container_writes_no_item_list() {
361        // The engine writes ItemList only when the container holds something,
362        // so a flag with no contents must not conjure an empty list.
363        let placeable = sample();
364        assert!(placeable.has_inventory);
365        assert!(placeable.items.is_empty());
366
367        let written = written!(placeable, 0);
368
369        assert!(written.field("ItemList").is_none());
370    }
371
372    #[test]
373    fn contents_survive_the_round_trip() {
374        let placeable = SavedPlaceable {
375            items: vec![SavedItem {
376                tag: "g_i_credits001".to_string(),
377                stack_size: 250,
378                ..SavedItem::default()
379            }],
380            ..sample()
381        };
382
383        let parsed = SavedPlaceable::read_element(&written!(placeable, 0));
384
385        assert_eq!(parsed.items, placeable.items);
386    }
387}