rakata_generics/shared.rs
1//! Shared typed building blocks used across multiple GFF generic wrappers.
2//!
3//! These types reduce drift between format modules by centralizing repeated
4//! field groups (for example trap settings and common trap-related scripts).
5
6use rakata_core::ResRef;
7use rakata_formats::GffValue;
8
9/// A runtime object handle, as the engine hands them out.
10///
11/// A newtype rather than a bare `u32` because the two are not
12/// interchangeable: zero is a perfectly ordinary object id, so a plain
13/// integer field cannot tell "object number zero" from "no object". Every
14/// type carrying one of these had that bug, defaulting to zero and thereby
15/// claiming an unplaced object referred to whichever object the engine
16/// numbered first.
17///
18/// [`Default`] is [`Self::INVALID`], which is what makes the fix structural.
19/// A type holding this can keep `#[derive(Default)]` and get the right value,
20/// where a bare `u32` needs every author to remember.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct ObjectId(u32);
23
24impl ObjectId {
25 /// The engine-wide `OBJECT_INVALID` placeholder, carried by anything that
26 /// has not been assigned a runtime id.
27 pub const INVALID: Self = Self(0x7F00_0000);
28
29 /// Wraps a raw handle.
30 pub const fn new(raw: u32) -> Self {
31 Self(raw)
32 }
33
34 /// The raw handle, for writing back into a GFF field.
35 pub const fn get(self) -> u32 {
36 self.0
37 }
38
39 /// Whether this names an actual object rather than the placeholder.
40 pub const fn is_valid(self) -> bool {
41 self.0 != Self::INVALID.0
42 }
43}
44
45impl Default for ObjectId {
46 fn default() -> Self {
47 Self::INVALID
48 }
49}
50
51impl From<u32> for ObjectId {
52 fn from(raw: u32) -> Self {
53 Self(raw)
54 }
55}
56
57impl From<ObjectId> for u32 {
58 fn from(id: ObjectId) -> Self {
59 id.0
60 }
61}
62
63/// Shared trap configuration block used by door/placeable/trap-like templates.
64#[derive(Debug, Clone, PartialEq, Eq, Default)]
65pub struct TrapSettings {
66 /// Trap-detectable flag (`TrapDetectable`).
67 pub detectable: bool,
68 /// Trap detect DC (`TrapDetectDC`).
69 pub detect_dc: u8,
70 /// Trap-disarmable flag (`TrapDisarmable`).
71 pub disarmable: bool,
72 /// Trap disarm DC (`DisarmDC`).
73 pub disarm_dc: u8,
74 /// Trap flag (`TrapFlag`).
75 pub flag: u8,
76 /// Trap one-shot flag (`TrapOneShot`).
77 pub one_shot: bool,
78 /// Trap type (`TrapType`).
79 pub trap_type: u8,
80}
81
82/// The `PortraitId` value that hands control back to the `Portrait` resref.
83///
84/// A trigger's read stamps this when the file carries no `PortraitId`, which
85/// lands inside the documented `>= 0xFFFE` range where the engine consults
86/// the string field instead of the id. Defaulting to `0` would name portrait
87/// row zero and shadow the resref, which is the opposite behaviour.
88pub const PORTRAIT_ID_USE_RESREF: u16 = 0xFFFF;
89
90/// `TrapType`'s value when a file does not carry one.
91///
92/// Not a `traps.2da` row. Row `255` does not exist in vanilla data, so a file
93/// missing both this and `OnTrapTriggered` sends the engine to an
94/// out-of-range lookup rather than to a clean "no trap" state. Reading an
95/// absent `TrapType` as `0` instead would name a row that does exist, turning
96/// "no trap configured" into "trap type 0".
97pub const TRAP_TYPE_ABSENT: u8 = 0xFF;
98
99/// What one loader holds for the trap fields a file leaves out.
100///
101/// Three consumers share `TrapSettings` and none of them agree. A door
102/// carries its constructor's armed flags over, a placeable's read stamps `0`
103/// across all three, and a trigger stamps `0` over two of them while
104/// `TrapOneShot` carries `1` over like a door's. So the defaults cannot live
105/// in the shared reader; they belong to whoever is calling it.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub(crate) struct TrapDefaults {
108 detectable: bool,
109 disarmable: bool,
110 one_shot: bool,
111}
112
113impl TrapDefaults {
114 /// `CSWSDoor`: the constructor arms all three and every read carries over.
115 pub(crate) const DOOR: Self = Self {
116 detectable: true,
117 disarmable: true,
118 one_shot: true,
119 };
120
121 /// `CSWSTrigger`: `TrapDetectable` and `TrapDisarmable` are constructed
122 /// armed and then stamped `0` by their own reads, so a trigger that omits
123 /// them loads undetectable and undisarmable. `TrapOneShot` is the one that
124 /// genuinely carries its constructed `1`.
125 pub(crate) const TRIGGER: Self = Self {
126 detectable: false,
127 disarmable: false,
128 one_shot: true,
129 };
130
131 /// `CSWSPlaceable`: all three constructed armed and all three stamped `0`,
132 /// so an absent trap flag set loads as a non-functional trap.
133 pub(crate) const PLACEABLE: Self = Self {
134 detectable: false,
135 disarmable: false,
136 one_shot: false,
137 };
138}
139
140impl TrapSettings {
141 /// Builds trap settings from caller-provided field readers.
142 ///
143 /// `defaults` is the calling loader's own absent-field behaviour; see
144 /// [`TrapDefaults`] for why it cannot be one shared set.
145 pub(crate) fn read<FBool, FU8>(
146 defaults: TrapDefaults,
147 mut get_bool: FBool,
148 mut get_u8: FU8,
149 ) -> Self
150 where
151 FBool: FnMut(&str) -> Option<bool>,
152 FU8: FnMut(&str) -> Option<u8>,
153 {
154 Self {
155 detectable: get_bool("TrapDetectable").unwrap_or(defaults.detectable),
156 detect_dc: get_u8("TrapDetectDC").unwrap_or(0),
157 disarmable: get_bool("TrapDisarmable").unwrap_or(defaults.disarmable),
158 disarm_dc: get_u8("DisarmDC").unwrap_or(0),
159 flag: get_u8("TrapFlag").unwrap_or(0),
160 one_shot: get_bool("TrapOneShot").unwrap_or(defaults.one_shot),
161 trap_type: get_u8("TrapType").unwrap_or(TRAP_TYPE_ABSENT),
162 }
163 }
164
165 /// Writes trap settings via caller-provided upsert callback.
166 pub(crate) fn write<FUpsert>(&self, mut upsert: FUpsert)
167 where
168 FUpsert: FnMut(&str, GffValue),
169 {
170 upsert("TrapDetectable", GffValue::UInt8(u8::from(self.detectable)));
171 upsert("TrapDetectDC", GffValue::UInt8(self.detect_dc));
172 upsert("TrapDisarmable", GffValue::UInt8(u8::from(self.disarmable)));
173 upsert("DisarmDC", GffValue::UInt8(self.disarm_dc));
174 upsert("TrapFlag", GffValue::UInt8(self.flag));
175 upsert("TrapOneShot", GffValue::UInt8(u8::from(self.one_shot)));
176 upsert("TrapType", GffValue::UInt8(self.trap_type));
177 }
178}
179
180/// Shared script hook bundle common to placeables and doors.
181#[derive(Debug, Clone, PartialEq, Eq, Default)]
182pub struct CommonTrapScripts {
183 /// On-closed script (`OnClosed`).
184 pub on_closed: ResRef,
185 /// On-damaged script (`OnDamaged`).
186 pub on_damaged: ResRef,
187 /// On-death script (`OnDeath`).
188 pub on_death: ResRef,
189 /// On-disarm script (`OnDisarm`).
190 pub on_disarm: ResRef,
191 /// On-heartbeat script (`OnHeartbeat`).
192 pub on_heartbeat: ResRef,
193 /// On-lock script (`OnLock`).
194 pub on_lock: ResRef,
195 /// On-melee-attacked script (`OnMeleeAttacked`).
196 pub on_melee_attacked: ResRef,
197 /// On-open script (`OnOpen`).
198 pub on_open: ResRef,
199 /// On-spell-cast-at script (`OnSpellCastAt`).
200 pub on_spell_cast_at: ResRef,
201 /// On-trap-triggered script (`OnTrapTriggered`).
202 pub on_trap_triggered: ResRef,
203 /// On-unlock script (`OnUnlock`).
204 pub on_unlock: ResRef,
205 /// On-user-defined script (`OnUserDefined`).
206 pub on_user_defined: ResRef,
207}
208
209/// The resref a script slot holds when a file does not name one.
210///
211/// A door's constructor pre-arms all fifteen slots with the literal string
212/// `"default"`, and a trigger's does the same for its seven, so an absent hook
213/// on either resolves to that name rather than to nothing. This is the
214/// mechanism behind the documented `traps.2da` fallback: `OnTrapTriggered`
215/// becomes `"default"` on its own, which is one of the three spellings that
216/// routes to the table. A placeable's constructor arms nothing, so its hooks
217/// really are empty when absent.
218pub const SCRIPT_SLOT_SEED: ResRef = match ResRef::const_new("default") {
219 Ok(seed) => seed,
220 // Unreachable: a seven-character ASCII literal is inside every ResRef
221 // limit. `expect` is not const, so the match spells out the same thing.
222 Err(_) => panic!("\"default\" is a valid resref"),
223};
224
225impl CommonTrapScripts {
226 /// Builds the shared script bundle from caller-provided field readers.
227 ///
228 /// `seed` is what the calling loader's constructor left in each slot:
229 /// [`SCRIPT_SLOT_SEED`] for a door, empty for a placeable.
230 pub(crate) fn read<FResRef>(seed: ResRef, mut get_resref: FResRef) -> Self
231 where
232 FResRef: FnMut(&str) -> Option<ResRef>,
233 {
234 Self {
235 on_closed: get_resref("OnClosed").unwrap_or(seed),
236 on_damaged: get_resref("OnDamaged").unwrap_or(seed),
237 on_death: get_resref("OnDeath").unwrap_or(seed),
238 on_disarm: get_resref("OnDisarm").unwrap_or(seed),
239 on_heartbeat: get_resref("OnHeartbeat").unwrap_or(seed),
240 on_lock: get_resref("OnLock").unwrap_or(seed),
241 on_melee_attacked: get_resref("OnMeleeAttacked").unwrap_or(seed),
242 on_open: get_resref("OnOpen").unwrap_or(seed),
243 on_spell_cast_at: get_resref("OnSpellCastAt").unwrap_or(seed),
244 on_trap_triggered: get_resref("OnTrapTriggered").unwrap_or(seed),
245 on_unlock: get_resref("OnUnlock").unwrap_or(seed),
246 on_user_defined: get_resref("OnUserDefined").unwrap_or(seed),
247 }
248 }
249
250 /// Writes the shared script bundle via caller-provided upsert callback.
251 pub(crate) fn write<FUpsert>(&self, mut upsert: FUpsert)
252 where
253 FUpsert: FnMut(&str, GffValue),
254 {
255 upsert("OnClosed", GffValue::ResRef(self.on_closed));
256 upsert("OnDamaged", GffValue::ResRef(self.on_damaged));
257 upsert("OnDeath", GffValue::ResRef(self.on_death));
258 upsert("OnDisarm", GffValue::ResRef(self.on_disarm));
259 upsert("OnHeartbeat", GffValue::ResRef(self.on_heartbeat));
260 upsert("OnLock", GffValue::ResRef(self.on_lock));
261 upsert("OnMeleeAttacked", GffValue::ResRef(self.on_melee_attacked));
262 upsert("OnOpen", GffValue::ResRef(self.on_open));
263 upsert("OnSpellCastAt", GffValue::ResRef(self.on_spell_cast_at));
264 upsert("OnTrapTriggered", GffValue::ResRef(self.on_trap_triggered));
265 upsert("OnUnlock", GffValue::ResRef(self.on_unlock));
266 upsert("OnUserDefined", GffValue::ResRef(self.on_user_defined));
267 }
268}
269
270/// How a saved object names its portrait.
271///
272/// A saved object carries either `Portrait`, a resref naming the image
273/// directly, or `PortraitId`, a row index into `portraits.2da`. Across every
274/// placeable and trigger in the fixture saves the two are exactly
275/// complementary: never both on one object, and never neither. Modelling them
276/// as one value rather than two [`Option`]s means the state where an object
277/// claims both, or claims neither, cannot be built.
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum SavedPortrait {
280 /// `Portrait`: a resref naming the portrait image.
281 ResRef(ResRef),
282 /// `PortraitId`: a row index into `portraits.2da`.
283 Id(u16),
284}
285
286impl Default for SavedPortrait {
287 /// Row 0 of `portraits.2da`, which is what an object with no portrait
288 /// field at all resolves to.
289 fn default() -> Self {
290 Self::Id(0)
291 }
292}
293
294impl SavedPortrait {
295 /// Reads whichever of the two fields the object carries.
296 ///
297 /// Prefers `Portrait` when both somehow appear, since a resref names an
298 /// image outright and a row index still has to be looked up.
299 pub(crate) fn read<FRes, FU16>(mut get_resref: FRes, mut get_u16: FU16) -> Self
300 where
301 FRes: FnMut(&str) -> Option<ResRef>,
302 FU16: FnMut(&str) -> Option<u16>,
303 {
304 match (get_resref("Portrait"), get_u16("PortraitId")) {
305 (Some(resref), _) => Self::ResRef(resref),
306 (None, Some(id)) => Self::Id(id),
307 (None, None) => Self::default(),
308 }
309 }
310
311 /// Writes back the one field this portrait uses, and only that one.
312 pub(crate) fn write<F>(&self, mut upsert: F)
313 where
314 F: FnMut(&str, GffValue),
315 {
316 match self {
317 Self::ResRef(resref) => upsert("Portrait", GffValue::ResRef(*resref)),
318 Self::Id(id) => upsert("PortraitId", GffValue::UInt16(*id)),
319 }
320 }
321}
322
323/// Shared grid/repository position used by inventory list entries.
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
325pub struct InventoryGridPosition {
326 /// Repository/grid X coordinate.
327 pub x: u16,
328 /// Repository/grid Y coordinate.
329 pub y: u16,
330}
331
332/// A vertex in a trigger geometry polygon (`PointX`/`PointY`/`PointZ`).
333#[derive(Debug, Clone, PartialEq, Default)]
334pub struct GitTriggerPoint {
335 /// X coordinate (`PointX`).
336 pub point_x: f32,
337 /// Y coordinate (`PointY`).
338 pub point_y: f32,
339 /// Z coordinate (`PointZ`).
340 pub point_z: f32,
341}
342
343impl GitTriggerPoint {
344 /// Builds a trigger point from a raw GFF struct.
345 pub fn from_gff_struct(s: &rakata_formats::GffStruct) -> Self {
346 use crate::gff_helpers::get_f32;
347 Self {
348 point_x: get_f32(s, "PointX").unwrap_or(0.0),
349 point_y: get_f32(s, "PointY").unwrap_or(0.0),
350 point_z: get_f32(s, "PointZ").unwrap_or(0.0),
351 }
352 }
353
354 /// Converts this trigger point to a raw GFF struct.
355 pub fn to_gff_struct(&self) -> rakata_formats::GffStruct {
356 use crate::gff_helpers::upsert_field;
357 let mut s = rakata_formats::GffStruct::new(0);
358 upsert_field(&mut s, "PointX", GffValue::Single(self.point_x));
359 upsert_field(&mut s, "PointY", GffValue::Single(self.point_y));
360 upsert_field(&mut s, "PointZ", GffValue::Single(self.point_z));
361 s
362 }
363}