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}