Skip to main content

rakata_generics/git/
area_effect.rs

1//! Area-of-effect objects, which savegame GITs carry and module GITs do not.
2
3use rakata_core::ResRef;
4use rakata_formats::{
5    gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffType},
6    GffStruct, GffValue,
7};
8
9use super::object_id_or_invalid;
10use crate::gff_helpers::{get_f32, get_i32, get_resref, get_string, get_u32, get_u8, upsert_field};
11use crate::shared::ObjectId;
12
13/// The dimensions an area-of-effect object covers.
14///
15/// `Shape` decides which dimension fields the loader reads at all: `0` reads
16/// `Radius` and nothing else, `1` reads `Length` and `Width` and nothing
17/// else, and any other value reads neither, leaving the effect with no
18/// geometry. Modelling that as one value keeps a circle from carrying a width
19/// and a rectangle from carrying a radius.
20#[derive(Debug, Clone, PartialEq)]
21pub enum GitAreaEffectShape {
22    /// `Shape = 0`: a circle of `Radius`.
23    Circle {
24        /// Effect radius (`Radius`).
25        radius: f32,
26    },
27    /// `Shape = 1`: a rectangle of `Length` by `Width`.
28    Rectangle {
29        /// Effect length (`Length`).
30        length: f32,
31        /// Effect width (`Width`).
32        width: f32,
33    },
34    /// Any other `Shape` byte, which the loader treats as no geometry.
35    None(u8),
36}
37
38impl Default for GitAreaEffectShape {
39    fn default() -> Self {
40        Self::Circle { radius: 0.0 }
41    }
42}
43
44/// An area-of-effect object placed in the area (struct type 13).
45///
46/// These are runtime spell and ability effects. Unlike every other object
47/// list, they have no blueprint: no loader in the binary ever opens a
48/// template for one, so `UseTemplates` has nothing to select between and
49/// [`Git::area_effects`](super::Git::area_effects) is a plain [`Vec`] rather than a [`GitObjects`](super::GitObjects).
50///
51/// # Not exercised by the fixture corpus
52///
53/// Every save in `fixtures/saves/` has an empty `AreaEffectList`, so this
54/// field set comes from the engine's own load and save paths rather than from
55/// real data. A test asserts the lists are still empty, so a corpus that
56/// grows one fails loudly instead of quietly going unchecked.
57#[derive(Debug, Clone, PartialEq, Default)]
58pub struct GitAreaEffect {
59    /// Object tag (`Tag`).
60    pub tag: String,
61    /// Runtime object id (`ObjectId`).
62    ///
63    /// Defaults to the engine-wide `OBJECT_INVALID` placeholder rather than
64    /// zero, which is what a freshly constructed object carries.
65    pub object_id: ObjectId,
66    /// Effect row (`AreaEffectId`), into `vfx_persistent.2da`.
67    pub area_effect_id: i32,
68    /// Originating spell (`SpellId`).
69    pub spell_id: u32,
70    /// Save DC of the originating spell (`SpellSaveDC`).
71    pub spell_save_dc: i32,
72    /// Level of the originating spell (`SpellLevel`).
73    pub spell_level: i32,
74    /// Metamagic applied to the originating spell (`MetaMagicType`).
75    pub meta_magic_type: u8,
76
77    /// The ground the effect covers.
78    pub shape: GitAreaEffectShape,
79
80    /// Object that created the effect (`CreatorId`).
81    pub creator_id: u32,
82    /// Object the effect is attached to (`LinkedToObject`).
83    pub linked_to_object: u32,
84    /// Last object to enter the effect (`LastEntered`).
85    pub last_entered: u32,
86    /// Last object to leave the effect (`LastLeft`).
87    pub last_left: u32,
88
89    /// Remaining duration (`Duration`).
90    pub duration: u32,
91    /// How the duration is counted (`DurationType`).
92    pub duration_type: u8,
93    /// Day of the last heartbeat (`LastHrtbtDay`).
94    pub last_heartbeat_day: u32,
95    /// Time of the last heartbeat (`LastHrtbtTime`).
96    pub last_heartbeat_time: u32,
97
98    /// X position (`PositionX`).
99    pub position_x: f32,
100    /// Y position (`PositionY`).
101    pub position_y: f32,
102    /// Z position (`PositionZ`).
103    pub position_z: f32,
104    /// X orientation (`OrientationX`).
105    pub orientation_x: f32,
106    /// Y orientation (`OrientationY`).
107    pub orientation_y: f32,
108    /// Z orientation (`OrientationZ`).
109    pub orientation_z: f32,
110
111    /// `OnHeartbeat`. Written by the engine, never read back.
112    pub on_heartbeat: ResRef,
113    /// `OnUserDefined`. Written by the engine, never read back.
114    pub on_user_defined: ResRef,
115    /// `OnObjEnter`. Written by the engine, never read back.
116    pub on_obj_enter: ResRef,
117    /// `OnObjExit`. Written by the engine, never read back.
118    pub on_obj_exit: ResRef,
119}
120
121impl GitAreaEffect {
122    pub(crate) fn from_gff_struct(s: &GffStruct) -> Self {
123        let shape = match get_u8(s, "Shape").unwrap_or(0) {
124            0 => GitAreaEffectShape::Circle {
125                radius: get_f32(s, "Radius").unwrap_or(0.0),
126            },
127            1 => GitAreaEffectShape::Rectangle {
128                length: get_f32(s, "Length").unwrap_or(0.0),
129                width: get_f32(s, "Width").unwrap_or(0.0),
130            },
131            other => GitAreaEffectShape::None(other),
132        };
133
134        Self {
135            tag: get_string(s, "Tag").unwrap_or_default(),
136            object_id: object_id_or_invalid(s),
137            area_effect_id: get_i32(s, "AreaEffectId").unwrap_or(0),
138            spell_id: get_u32(s, "SpellId").unwrap_or(0),
139            spell_save_dc: get_i32(s, "SpellSaveDC").unwrap_or(0),
140            spell_level: get_i32(s, "SpellLevel").unwrap_or(0),
141            meta_magic_type: get_u8(s, "MetaMagicType").unwrap_or(0),
142
143            shape,
144
145            creator_id: get_u32(s, "CreatorId").unwrap_or(0),
146            linked_to_object: get_u32(s, "LinkedToObject").unwrap_or(0),
147            last_entered: get_u32(s, "LastEntered").unwrap_or(0),
148            last_left: get_u32(s, "LastLeft").unwrap_or(0),
149
150            duration: get_u32(s, "Duration").unwrap_or(0),
151            duration_type: get_u8(s, "DurationType").unwrap_or(0),
152            last_heartbeat_day: get_u32(s, "LastHrtbtDay").unwrap_or(0),
153            last_heartbeat_time: get_u32(s, "LastHrtbtTime").unwrap_or(0),
154
155            position_x: get_f32(s, "PositionX").unwrap_or(0.0),
156            position_y: get_f32(s, "PositionY").unwrap_or(0.0),
157            position_z: get_f32(s, "PositionZ").unwrap_or(0.0),
158            orientation_x: get_f32(s, "OrientationX").unwrap_or(0.0),
159            orientation_y: get_f32(s, "OrientationY").unwrap_or(0.0),
160            orientation_z: get_f32(s, "OrientationZ").unwrap_or(0.0),
161
162            on_heartbeat: get_resref(s, "OnHeartbeat").unwrap_or_default(),
163            on_user_defined: get_resref(s, "OnUserDefined").unwrap_or_default(),
164            on_obj_enter: get_resref(s, "OnObjEnter").unwrap_or_default(),
165            on_obj_exit: get_resref(s, "OnObjExit").unwrap_or_default(),
166        }
167    }
168
169    pub(crate) fn to_gff_struct(&self) -> GffStruct {
170        let mut s = GffStruct::new(13);
171
172        upsert_field(&mut s, "Tag", GffValue::String(self.tag.clone()));
173        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
174        upsert_field(&mut s, "AreaEffectId", GffValue::Int32(self.area_effect_id));
175        upsert_field(&mut s, "SpellId", GffValue::UInt32(self.spell_id));
176        upsert_field(&mut s, "SpellSaveDC", GffValue::Int32(self.spell_save_dc));
177        upsert_field(&mut s, "SpellLevel", GffValue::Int32(self.spell_level));
178        upsert_field(
179            &mut s,
180            "MetaMagicType",
181            GffValue::UInt8(self.meta_magic_type),
182        );
183
184        // Only the fields the loader would read back for this shape. A
185        // circle with a Width would be a field the engine never wrote.
186        match &self.shape {
187            GitAreaEffectShape::Circle { radius } => {
188                upsert_field(&mut s, "Shape", GffValue::UInt8(0));
189                upsert_field(&mut s, "Radius", GffValue::Single(*radius));
190            }
191            GitAreaEffectShape::Rectangle { length, width } => {
192                upsert_field(&mut s, "Shape", GffValue::UInt8(1));
193                upsert_field(&mut s, "Length", GffValue::Single(*length));
194                upsert_field(&mut s, "Width", GffValue::Single(*width));
195            }
196            GitAreaEffectShape::None(raw) => {
197                upsert_field(&mut s, "Shape", GffValue::UInt8(*raw));
198            }
199        }
200
201        upsert_field(&mut s, "CreatorId", GffValue::UInt32(self.creator_id));
202        upsert_field(
203            &mut s,
204            "LinkedToObject",
205            GffValue::UInt32(self.linked_to_object),
206        );
207        upsert_field(&mut s, "LastEntered", GffValue::UInt32(self.last_entered));
208        upsert_field(&mut s, "LastLeft", GffValue::UInt32(self.last_left));
209
210        upsert_field(&mut s, "Duration", GffValue::UInt32(self.duration));
211        upsert_field(&mut s, "DurationType", GffValue::UInt8(self.duration_type));
212        upsert_field(
213            &mut s,
214            "LastHrtbtDay",
215            GffValue::UInt32(self.last_heartbeat_day),
216        );
217        upsert_field(
218            &mut s,
219            "LastHrtbtTime",
220            GffValue::UInt32(self.last_heartbeat_time),
221        );
222
223        upsert_field(&mut s, "PositionX", GffValue::Single(self.position_x));
224        upsert_field(&mut s, "PositionY", GffValue::Single(self.position_y));
225        upsert_field(&mut s, "PositionZ", GffValue::Single(self.position_z));
226        upsert_field(&mut s, "OrientationX", GffValue::Single(self.orientation_x));
227        upsert_field(&mut s, "OrientationY", GffValue::Single(self.orientation_y));
228        upsert_field(&mut s, "OrientationZ", GffValue::Single(self.orientation_z));
229
230        // The engine writes these four and no loader reads them back, so a
231        // restored effect never fires them again. They are written anyway to
232        // match what the engine's own writer produces.
233        upsert_field(&mut s, "OnHeartbeat", GffValue::ResRef(self.on_heartbeat));
234        upsert_field(
235            &mut s,
236            "OnUserDefined",
237            GffValue::ResRef(self.on_user_defined),
238        );
239        upsert_field(&mut s, "OnObjEnter", GffValue::ResRef(self.on_obj_enter));
240        upsert_field(&mut s, "OnObjExit", GffValue::ResRef(self.on_obj_exit));
241
242        s
243    }
244}
245
246/// GIT `AreaEffectList` entry child schema (struct type 0xd).
247pub(crate) static AREA_EFFECT_LIST_CHILDREN: &[FieldSchema] = &[
248    FieldSchema {
249        label: "Tag",
250        expected_type: GffType::String,
251        life: FieldLife::Live,
252        required: false,
253        absent: AbsentDefault::Unverified,
254        children: None,
255        constraint: None,
256    },
257    FieldSchema {
258        label: "AreaEffectId",
259        expected_type: GffType::Int32,
260        life: FieldLife::Live,
261        required: false,
262        absent: AbsentDefault::Unverified,
263        children: None,
264        constraint: None,
265    },
266    FieldSchema {
267        label: "SpellId",
268        expected_type: GffType::UInt32,
269        life: FieldLife::Live,
270        required: false,
271        absent: AbsentDefault::Unverified,
272        children: None,
273        constraint: None,
274    },
275    FieldSchema {
276        label: "SpellSaveDC",
277        expected_type: GffType::Int32,
278        life: FieldLife::Live,
279        required: false,
280        absent: AbsentDefault::Unverified,
281        children: None,
282        constraint: None,
283    },
284    FieldSchema {
285        label: "SpellLevel",
286        expected_type: GffType::Int32,
287        life: FieldLife::Live,
288        required: false,
289        absent: AbsentDefault::Unverified,
290        children: None,
291        constraint: None,
292    },
293    FieldSchema {
294        label: "MetaMagicType",
295        expected_type: GffType::UInt8,
296        life: FieldLife::Live,
297        required: false,
298        absent: AbsentDefault::Unverified,
299        children: None,
300        constraint: None,
301    },
302    FieldSchema {
303        label: "Shape",
304        expected_type: GffType::UInt8,
305        life: FieldLife::Live,
306        required: false,
307        absent: AbsentDefault::Unverified,
308        children: None,
309        constraint: None,
310    },
311    FieldSchema {
312        label: "Radius",
313        expected_type: GffType::Single,
314        life: FieldLife::Live,
315        required: false,
316        absent: AbsentDefault::Unverified,
317        children: None,
318        constraint: None,
319    },
320    FieldSchema {
321        label: "Length",
322        expected_type: GffType::Single,
323        life: FieldLife::Live,
324        required: false,
325        absent: AbsentDefault::Unverified,
326        children: None,
327        constraint: None,
328    },
329    FieldSchema {
330        label: "Width",
331        expected_type: GffType::Single,
332        life: FieldLife::Live,
333        required: false,
334        absent: AbsentDefault::Unverified,
335        children: None,
336        constraint: None,
337    },
338    FieldSchema {
339        label: "CreatorId",
340        expected_type: GffType::UInt32,
341        life: FieldLife::Live,
342        required: false,
343        absent: AbsentDefault::Unverified,
344        children: None,
345        constraint: None,
346    },
347    FieldSchema {
348        label: "LinkedToObject",
349        expected_type: GffType::UInt32,
350        life: FieldLife::Live,
351        required: false,
352        absent: AbsentDefault::Unverified,
353        children: None,
354        constraint: None,
355    },
356    FieldSchema {
357        label: "LastEntered",
358        expected_type: GffType::UInt32,
359        life: FieldLife::Live,
360        required: false,
361        absent: AbsentDefault::Unverified,
362        children: None,
363        constraint: None,
364    },
365    FieldSchema {
366        label: "LastLeft",
367        expected_type: GffType::UInt32,
368        life: FieldLife::Live,
369        required: false,
370        absent: AbsentDefault::Unverified,
371        children: None,
372        constraint: None,
373    },
374    FieldSchema {
375        label: "Duration",
376        expected_type: GffType::UInt32,
377        life: FieldLife::Live,
378        required: false,
379        absent: AbsentDefault::Unverified,
380        children: None,
381        constraint: None,
382    },
383    FieldSchema {
384        label: "DurationType",
385        expected_type: GffType::UInt8,
386        life: FieldLife::Live,
387        required: false,
388        absent: AbsentDefault::Unverified,
389        children: None,
390        constraint: None,
391    },
392    FieldSchema {
393        label: "LastHrtbtDay",
394        expected_type: GffType::UInt32,
395        life: FieldLife::Live,
396        required: false,
397        absent: AbsentDefault::Unverified,
398        children: None,
399        constraint: None,
400    },
401    FieldSchema {
402        label: "LastHrtbtTime",
403        expected_type: GffType::UInt32,
404        life: FieldLife::Live,
405        required: false,
406        absent: AbsentDefault::Unverified,
407        children: None,
408        constraint: None,
409    },
410    FieldSchema {
411        label: "OnHeartbeat",
412        expected_type: GffType::ResRef,
413        life: FieldLife::Live,
414        required: false,
415        absent: AbsentDefault::Unverified,
416        children: None,
417        constraint: None,
418    },
419    FieldSchema {
420        label: "OnUserDefined",
421        expected_type: GffType::ResRef,
422        life: FieldLife::Live,
423        required: false,
424        absent: AbsentDefault::Unverified,
425        children: None,
426        constraint: None,
427    },
428    FieldSchema {
429        label: "OnObjEnter",
430        expected_type: GffType::ResRef,
431        life: FieldLife::Live,
432        required: false,
433        absent: AbsentDefault::Unverified,
434        children: None,
435        constraint: None,
436    },
437    FieldSchema {
438        label: "OnObjExit",
439        expected_type: GffType::ResRef,
440        life: FieldLife::Live,
441        required: false,
442        absent: AbsentDefault::Unverified,
443        children: None,
444        constraint: None,
445    },
446    FieldSchema {
447        label: "ObjectId",
448        expected_type: GffType::UInt32,
449        life: FieldLife::Live,
450        required: false,
451        absent: AbsentDefault::Unverified,
452        children: None,
453        constraint: None,
454    },
455    FieldSchema {
456        label: "OrientationX",
457        expected_type: GffType::Single,
458        life: FieldLife::Live,
459        required: false,
460        absent: AbsentDefault::Unverified,
461        children: None,
462        constraint: None,
463    },
464    FieldSchema {
465        label: "OrientationY",
466        expected_type: GffType::Single,
467        life: FieldLife::Live,
468        required: false,
469        absent: AbsentDefault::Unverified,
470        children: None,
471        constraint: None,
472    },
473    FieldSchema {
474        label: "OrientationZ",
475        expected_type: GffType::Single,
476        life: FieldLife::Live,
477        required: false,
478        absent: AbsentDefault::Unverified,
479        children: None,
480        constraint: None,
481    },
482    FieldSchema {
483        label: "PositionX",
484        expected_type: GffType::Single,
485        life: FieldLife::Live,
486        required: false,
487        absent: AbsentDefault::Unverified,
488        children: None,
489        constraint: None,
490    },
491    FieldSchema {
492        label: "PositionY",
493        expected_type: GffType::Single,
494        life: FieldLife::Live,
495        required: false,
496        absent: AbsentDefault::Unverified,
497        children: None,
498        constraint: None,
499    },
500    FieldSchema {
501        label: "PositionZ",
502        expected_type: GffType::Single,
503        life: FieldLife::Live,
504        required: false,
505        absent: AbsentDefault::Unverified,
506        children: None,
507        constraint: None,
508    },
509];