Skip to main content

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}