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}