Skip to main content

rakata_generics/git/
trigger.rs

1//! Trigger placements in a GIT.
2
3use rakata_core::ResRef;
4use rakata_formats::{GffLocalizedString, GffModel, GffStruct};
5
6use crate::git::blocks::{SavedPortrait, SavedPortraitLabels};
7use crate::shared::ObjectId;
8use crate::utt::UttGeometryPoint;
9
10/// The part of a `trigger` entry that is what every trigger 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 GitTriggerCommon {
16    /// Linked-to tag (`LinkedTo`). Waypoint tag in the target module.
17    #[gff(LinkedTo, not_a_constant)]
18    pub linked_to: String,
19    /// Linked-to flags (`LinkedToFlags`).
20    #[gff(LinkedToFlags, not_a_constant)]
21    pub linked_to_flags: u8,
22    /// Linked module resref (`LinkedToModule`). Area transition target.
23    #[gff(LinkedToModule, not_a_constant)]
24    pub linked_to_module: ResRef,
25    /// Runtime object ID (`ObjectId`). Save-game only.
26    /// Runtime object id (`ObjectId`), which only a saved form carries.
27    ///
28    /// The area-level dispatcher reads it once per element ahead of the
29    /// static-versus-saved branch, so the engine reads it on both forms and
30    /// the liveness below is right. What it does not do is author it: every
31    /// element the engine saves carries one and no element the toolset shipped
32    /// does. Held as the absence so writing a static placement back does not
33    /// invent a runtime id for it.
34    #[gff(ObjectId, stamped = ObjectId::INVALID, optional = ObjectId)]
35    pub object_id: Option<ObjectId>,
36    /// Transition destination text (`TransitionDestin`, truncated from
37    /// `TransitionDestination` by the 16-byte GFF label limit).
38    #[gff(TransitionDestin, constructed)]
39    pub transition_destination: GffLocalizedString,
40    /// X position (`XPosition`).
41    #[gff(XPosition, unexamined)]
42    pub x_position: f32,
43    /// Y position (`YPosition`).
44    #[gff(YPosition, unexamined)]
45    pub y_position: f32,
46    /// Z position (`ZPosition`).
47    #[gff(ZPosition, unexamined)]
48    pub z_position: f32,
49    /// X orientation (`XOrientation`).
50    ///
51    /// Every trigger in a retail install leaves the three orientation fields
52    /// at zero, which is why they went unnoticed while the other object types
53    /// got theirs. A trigger's shape comes from its `Geometry` polygon, so
54    /// rotating one is not something vanilla content ever needed. That says
55    /// nothing about a mod that does.
56    #[gff(XOrientation, unexamined)]
57    pub x_orientation: f32,
58    /// Y orientation (`YOrientation`).
59    #[gff(YOrientation, unexamined)]
60    pub y_orientation: f32,
61    /// Z orientation (`ZOrientation`).
62    #[gff(ZOrientation, unexamined)]
63    pub z_orientation: f32,
64}
65
66/// A trigger instance placed in the area (struct type 1).
67///
68/// Triggers carry geometry (vertex polygon) and optional area-transition
69/// fields (`LinkedToModule`, `LinkedTo`, `LinkedToFlags`,
70/// `TransitionDestination`).
71///
72/// ## `Tag` is dead here, and deliberately unmodelled
73///
74/// Every trigger placement that carries a `Tag` carries a real value, and the
75/// engine does not read it at this path: a templated trigger takes its tag
76/// from the `.utt` blueprint, with no overlay from the placement. Same
77/// situation as [`GitDoor`](super::GitDoor), and the same trap. Setting it on
78/// a placement looks like renaming that trigger and changes nothing.
79#[derive(Debug, Clone, PartialEq, GffModel)]
80#[gff_manual_element]
81pub struct GitTrigger {
82    /// The trigger's boundary (`Geometry`).
83    #[gff(Geometry, not_a_constant, list = UttGeometryPoint, element_id = 3)]
84    pub geometry: Vec<UttGeometryPoint>,
85    /// The block both forms carry, which the list declares as its element.
86    ///
87    /// No `#[gff]`: the arm contributes its parts to the split, and the
88    /// element is this block, so declaring it here too would make one label
89    /// two declarations. The arm's own codec reads and writes it.
90    pub common: GitTriggerCommon,
91    /// Template resref (`TemplateResRef`).
92    #[gff(TemplateResRef, unexamined)]
93    pub template_resref: ResRef,
94}
95
96// =========================================================================
97// The saved form
98// =========================================================================
99
100// Triggers as stored inside a save game.
101//
102// A savegame trigger carries the whole object inline rather than referencing a
103// `.utt`, including the polygon that defines where it fires. See
104// `docs/src/formats/save/index.md` for the template-versus-snapshot rule.
105//
106// ## Trap fields are a subset of a door's
107//
108// A saved trigger writes `TrapDetectable`, `TrapDisarmable`, `TrapOneShot`
109// and `TrapType`, and none of `TrapDetectDC`, `DisarmDC` or `TrapFlag`. A
110// saved door writes all seven. Declaring the four here rather than reaching
111// for a door's block keeps a trigger from writing back three fields the
112// engine never puts on one.
113//
114// ## What is not modelled
115//
116// `ActionList`, `VarTable` and `SWVarTable` are live runtime state whose
117// layouts are only partly audited, and they are skipped for the same reason
118// [`SavedCreature`](crate::git::creature::SavedCreature) skips them.
119
120/// A trigger as stored inside a save game's module `GIT`.
121#[derive(Debug, Clone, PartialEq, GffModel)]
122#[gff_manual_element]
123pub struct SavedTrigger {
124    /// Portrait, as whichever of the two labels the trigger carries.
125    ///
126    /// Declared per owner: the audit parts both labels from the door's and
127    /// the placeable's on the absent axis.
128    #[gff(flatten = SavedPortraitLabels, manual_read, manual_write)]
129    pub portrait: SavedPortrait,
130    /// Whether the trap can be spotted (`TrapDetectable`).
131    #[gff(TrapDetectable, stamped = false)]
132    pub trap_detectable: bool,
133    /// Whether the trap can be disarmed (`TrapDisarmable`).
134    #[gff(TrapDisarmable, stamped = false)]
135    pub trap_disarmable: bool,
136    /// Whether the trap fires once and clears (`TrapOneShot`).
137    #[gff(TrapOneShot, constructed = true)]
138    pub trap_one_shot: bool,
139    /// Trap row in `traps.2da` (`TrapType`).
140    #[gff(TrapType, constructed = 255)]
141    pub trap_type: u8,
142    /// The block both forms carry, which the list declares as its element.
143    ///
144    /// No `#[gff]`: the arm contributes its parts to the split, and the
145    /// element is this block, so declaring it here too would make one label
146    /// two declarations. The arm's own codec reads and writes it.
147    pub common: GitTriggerCommon,
148    /// Object tag (`Tag`).
149    #[gff(
150        Tag,
151        read_only_dead = "a templated placement takes its tag from the blueprint, which the placement cannot override",
152        unexamined
153    )]
154    pub tag: String,
155    /// Polygon defining the trigger area (`Geometry`).
156    #[gff(Geometry, not_a_constant, list = UttGeometryPoint, element_id = 3)]
157    pub geometry: Vec<UttGeometryPoint>,
158    /// Consume the key on use (`AutoRemoveKey`).
159    #[gff(AutoRemoveKey, unexamined)]
160    pub auto_remove_key: bool,
161    /// Accepts scripted commands (`Commandable`).
162    #[gff(Commandable, unexamined)]
163    pub commandable: bool,
164    /// Object that created this trigger at runtime (`CreatorId`).
165    #[gff(CreatorId, unexamined)]
166    pub creator_id: u32,
167    /// Cursor shown over the trigger (`Cursor`).
168    #[gff(Cursor, unexamined)]
169    pub cursor: u8,
170    /// Faction id (`Faction`).
171    #[gff(Faction, unexamined)]
172    pub faction: u32,
173    /// Highlight rendering height (`HighlightHeight`).
174    #[gff(HighlightHeight, unexamined)]
175    pub highlight_height: f32,
176    /// Key tag that disarms this trigger (`KeyName`).
177    #[gff(KeyName, unexamined)]
178    pub key_name: String,
179    /// Load screen shown across the transition (`LoadScreenID`).
180    #[gff(LoadScreenID, unexamined)]
181    pub load_screen_id: u16,
182    /// Displayed name (`LocalizedName`).
183    #[gff(LocalizedName, unexamined)]
184    pub localized_name: GffLocalizedString,
185    /// `OnClick`.
186    #[gff(OnClick, constructed = ResRef::const_new("default").expect("a literal short enough for a resref"))]
187    pub on_click: ResRef,
188    /// `OnDisarm`.
189    #[gff(OnDisarm, constructed = ResRef::const_new("default").expect("a literal short enough for a resref"))]
190    pub on_disarm: ResRef,
191    /// `OnTrapTriggered`.
192    #[gff(OnTrapTriggered, constructed = ResRef::const_new("default").expect("a literal short enough for a resref"))]
193    pub on_trap_triggered: ResRef,
194    /// `ScriptHeartbeat`.
195    #[gff(ScriptHeartbeat, constructed = ResRef::const_new("default").expect("a literal short enough for a resref"))]
196    pub script_heartbeat: ResRef,
197    /// `ScriptOnEnter`.
198    #[gff(ScriptOnEnter, constructed = ResRef::const_new("default").expect("a literal short enough for a resref"))]
199    pub script_on_enter: ResRef,
200    /// `ScriptOnExit`.
201    #[gff(ScriptOnExit, constructed = ResRef::const_new("default").expect("a literal short enough for a resref"))]
202    pub script_on_exit: ResRef,
203    /// `ScriptUserDefine`.
204    #[gff(ScriptUserDefine, constructed = ResRef::const_new("default").expect("a literal short enough for a resref"))]
205    pub script_user_define: ResRef,
206    /// Placed by the player party at runtime (`SetByPlayerParty`).
207    #[gff(SetByPlayerParty, unexamined)]
208    pub set_by_player_party: bool,
209    /// Trigger kind (`Type`): generic, area transition, or trap.
210    #[gff(Type, unexamined)]
211    pub trigger_type: i32,
212}
213
214impl SavedTrigger {
215    /// Reads one element, resolving the portrait's two labels into one value.
216    pub fn read_element(structure: &GffStruct) -> Self {
217        Self {
218            common: GitTriggerCommon::read_declared(structure),
219            portrait: SavedPortrait::read(structure),
220            ..Self::read_declared(structure)
221        }
222    }
223
224    /// Writes one element, emitting whichever portrait label the value uses.
225    pub fn write_element(&self, structure: &mut GffStruct) {
226        self.write_declared(structure);
227        self.common.write_declared(structure);
228        self.portrait.write(structure);
229    }
230}
231
232impl GitTrigger {
233    /// Reads one element, with the block the list declares as its element.
234    pub fn read_element(structure: &GffStruct) -> Self {
235        Self {
236            common: GitTriggerCommon::read_declared(structure),
237            ..Self::read_declared(structure)
238        }
239    }
240
241    /// Writes one element, the common block included.
242    pub fn write_element(&self, structure: &mut GffStruct) {
243        self.write_declared(structure);
244        self.common.write_declared(structure);
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    /// One value written into an element struct carrying the list's own id.
253    macro_rules! written {
254        ($value:expr, $id:expr) => {{
255            let mut element = GffStruct::new($id);
256            $value.write_element(&mut element);
257            element
258        }};
259    }
260
261    fn sample() -> SavedTrigger {
262        SavedTrigger {
263            tag: "tr_transition".to_string(),
264            common: GitTriggerCommon {
265                linked_to: "wp_arrive".to_string(),
266                object_id: Some(ObjectId::new(0x8000_0012)),
267                ..GitTriggerCommon::default()
268            },
269            trigger_type: 1,
270            portrait: SavedPortrait::Id(0),
271            geometry: vec![
272                UttGeometryPoint {
273                    point_x: 0.0,
274                    point_y: 0.0,
275                    point_z: 0.0,
276                },
277                UttGeometryPoint {
278                    point_x: 5.0,
279                    point_y: 0.0,
280                    point_z: 0.0,
281                },
282            ],
283            load_screen_id: 3,
284            trap_detectable: true,
285            script_on_enter: ResRef::new("k_trg_enter").expect("valid resref"),
286            ..SavedTrigger::default()
287        }
288    }
289
290    #[test]
291    fn round_trips_through_a_list_element() {
292        let trigger = sample();
293
294        let parsed = SavedTrigger::read_element(&written!(trigger, 0));
295
296        assert_eq!(parsed, trigger);
297    }
298
299    #[test]
300    fn does_not_gain_the_trap_dcs_a_door_carries() {
301        // A saved trigger has no TrapDetectDC, DisarmDC or TrapFlag. Reusing
302        // the shared trap block would write all three.
303        let written = written!(sample(), 0);
304
305        assert!(written.field("TrapDetectDC").is_none());
306        assert!(written.field("DisarmDC").is_none());
307        assert!(written.field("TrapFlag").is_none());
308        assert!(written.field("TrapType").is_some());
309    }
310}