Skip to main content

rakata_generics/git/
trigger.rs

1//! Trigger placements in a GIT.
2
3use rakata_core::{ResRef, StrRef};
4use rakata_formats::{
5    gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffType},
6    GffLocalizedString, GffStruct, GffValue,
7};
8
9use super::object_id_or_invalid;
10use crate::gff_helpers::{
11    get_bool, get_f32, get_i32, get_locstring, get_resref, get_string, get_u16, get_u32, get_u8,
12    upsert_field,
13};
14use crate::shared::{GitTriggerPoint, ObjectId, SavedPortrait};
15
16/// A trigger instance placed in the area (struct type 1).
17///
18/// Triggers carry geometry (vertex polygon) and optional area-transition
19/// fields (`LinkedToModule`, `LinkedTo`, `LinkedToFlags`,
20/// `TransitionDestination`).
21///
22/// ## `Tag` is dead here, and deliberately unmodelled
23///
24/// Every trigger placement that carries a `Tag` carries a real value, and the
25/// engine does not read it at this path: a templated trigger takes its tag
26/// from the `.utt` blueprint, with no overlay from the placement. Same
27/// situation as [`GitDoor`](super::GitDoor), and the same trap. Setting it on a placement
28/// looks like renaming that trigger and changes nothing.
29#[derive(Debug, Clone, PartialEq)]
30pub struct GitTrigger {
31    /// Template resref (`TemplateResRef`).
32    pub template_resref: ResRef,
33    /// Linked module resref (`LinkedToModule`). Area transition target.
34    pub linked_to_module: ResRef,
35    /// Transition destination text (`TransitionDestin`, truncated from
36    /// `TransitionDestination` by the 16-byte GFF label limit).
37    pub transition_destination: GffLocalizedString,
38    /// Linked-to tag (`LinkedTo`). Waypoint tag in the target module.
39    pub linked_to: String,
40    /// Linked-to flags (`LinkedToFlags`).
41    pub linked_to_flags: u8,
42    /// X position (`XPosition`).
43    pub x_position: f32,
44    /// Y position (`YPosition`).
45    pub y_position: f32,
46    /// Z position (`ZPosition`).
47    pub z_position: f32,
48    /// X orientation (`XOrientation`).
49    ///
50    /// Every trigger in a retail install leaves the three orientation fields
51    /// at zero, which is why they went unnoticed while the other object types
52    /// got theirs. A trigger's shape comes from [`Self::geometry`], so
53    /// rotating one is not something vanilla content ever needed. That says
54    /// nothing about a mod that does.
55    pub x_orientation: f32,
56    /// Y orientation (`YOrientation`).
57    pub y_orientation: f32,
58    /// Z orientation (`ZOrientation`).
59    pub z_orientation: f32,
60    /// Geometry polygon vertices (`Geometry`).
61    pub geometry: Vec<GitTriggerPoint>,
62    /// Runtime object ID (`ObjectId`). Save-game only.
63    pub object_id: ObjectId,
64}
65
66impl Default for GitTrigger {
67    fn default() -> Self {
68        Self {
69            template_resref: ResRef::blank(),
70            linked_to_module: ResRef::blank(),
71            transition_destination: GffLocalizedString::new(StrRef::invalid()),
72            linked_to: String::new(),
73            linked_to_flags: 0,
74            x_position: 0.0,
75            y_position: 0.0,
76            z_position: 0.0,
77            x_orientation: 0.0,
78            y_orientation: 0.0,
79            z_orientation: 0.0,
80            geometry: Vec::new(),
81            object_id: ObjectId::new(0),
82        }
83    }
84}
85
86impl GitTrigger {
87    pub(crate) fn from_gff_struct(s: &GffStruct) -> Self {
88        let geometry = match s.field("Geometry") {
89            Some(GffValue::List(elements)) => elements
90                .iter()
91                .map(GitTriggerPoint::from_gff_struct)
92                .collect(),
93            _ => Vec::new(),
94        };
95
96        Self {
97            template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
98            linked_to_module: get_resref(s, "LinkedToModule").unwrap_or_default(),
99            transition_destination: get_locstring(s, "TransitionDestin")
100                .cloned()
101                .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
102            linked_to: get_string(s, "LinkedTo").unwrap_or_default(),
103            linked_to_flags: get_u8(s, "LinkedToFlags").unwrap_or(0),
104            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
105            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
106            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
107            x_orientation: get_f32(s, "XOrientation").unwrap_or(0.0),
108            y_orientation: get_f32(s, "YOrientation").unwrap_or(0.0),
109            z_orientation: get_f32(s, "ZOrientation").unwrap_or(0.0),
110            geometry,
111            object_id: object_id_or_invalid(s),
112        }
113    }
114
115    pub(crate) fn to_gff_struct(&self) -> GffStruct {
116        let mut s = GffStruct::new(1);
117        upsert_field(
118            &mut s,
119            "TemplateResRef",
120            GffValue::ResRef(self.template_resref),
121        );
122        upsert_field(
123            &mut s,
124            "LinkedToModule",
125            GffValue::ResRef(self.linked_to_module),
126        );
127        upsert_field(
128            &mut s,
129            "TransitionDestin",
130            GffValue::LocalizedString(self.transition_destination.clone()),
131        );
132        upsert_field(&mut s, "LinkedTo", GffValue::String(self.linked_to.clone()));
133        upsert_field(
134            &mut s,
135            "LinkedToFlags",
136            GffValue::UInt8(self.linked_to_flags),
137        );
138        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
139        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
140        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
141        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
142        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
143        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
144        let geom_structs: Vec<GffStruct> =
145            self.geometry.iter().map(|p| p.to_gff_struct()).collect();
146        upsert_field(&mut s, "Geometry", GffValue::List(geom_structs));
147        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
148        s
149    }
150}
151
152/// GIT `TriggerList` geometry entry child schema (PointX/PointY/PointZ).
153pub(crate) static TRIGGER_GEOMETRY_CHILDREN: &[FieldSchema] = &[
154    FieldSchema {
155        label: "PointX",
156        expected_type: GffType::Single,
157        life: FieldLife::Live,
158        required: false,
159        absent: AbsentDefault::Unverified,
160        children: None,
161        constraint: None,
162    },
163    FieldSchema {
164        label: "PointY",
165        expected_type: GffType::Single,
166        life: FieldLife::Live,
167        required: false,
168        absent: AbsentDefault::Unverified,
169        children: None,
170        constraint: None,
171    },
172    FieldSchema {
173        label: "PointZ",
174        expected_type: GffType::Single,
175        life: FieldLife::Live,
176        required: false,
177        absent: AbsentDefault::Unverified,
178        children: None,
179        constraint: None,
180    },
181];
182
183/// GIT `TriggerList` entry child schema (struct type 1).
184pub(crate) static TRIGGER_LIST_CHILDREN: &[FieldSchema] = &[
185    // Legitimate content, and read by nothing: a templated trigger takes its
186    // tag from the `.utt` blueprint with no overlay, same as a door.
187    FieldSchema {
188        label: "Tag",
189        expected_type: GffType::String,
190        life: FieldLife::ReadOnlyDead(
191            "a templated placement takes its tag from the blueprint, which the placement cannot override",
192        ),
193        required: false,
194        absent: AbsentDefault::Unverified,
195        children: None,
196        constraint: None,
197    },
198    FieldSchema {
199        label: "XOrientation",
200        expected_type: GffType::Single,
201        life: FieldLife::Live,
202        required: false,
203        absent: AbsentDefault::Unverified,
204        children: None,
205        constraint: None,
206    },
207    FieldSchema {
208        label: "YOrientation",
209        expected_type: GffType::Single,
210        life: FieldLife::Live,
211        required: false,
212        absent: AbsentDefault::Unverified,
213        children: None,
214        constraint: None,
215    },
216    FieldSchema {
217        label: "ZOrientation",
218        expected_type: GffType::Single,
219        life: FieldLife::Live,
220        required: false,
221        absent: AbsentDefault::Unverified,
222        children: None,
223        constraint: None,
224    },
225    FieldSchema {
226        label: "ObjectId",
227        expected_type: GffType::UInt32,
228        life: FieldLife::Live,
229        required: false,
230        absent: AbsentDefault::Unverified,
231        children: None,
232        constraint: None,
233    },
234    FieldSchema {
235        label: "TemplateResRef",
236        expected_type: GffType::ResRef,
237        life: FieldLife::Live,
238        required: false,
239        absent: AbsentDefault::Unverified,
240        children: None,
241        constraint: None,
242    },
243    FieldSchema {
244        label: "LinkedToModule",
245        expected_type: GffType::ResRef,
246        life: FieldLife::Live,
247        required: false,
248        absent: AbsentDefault::Unverified,
249        children: None,
250        constraint: None,
251    },
252    FieldSchema {
253        label: "TransitionDestin",
254        expected_type: GffType::LocalizedString,
255        life: FieldLife::Live,
256        required: false,
257        absent: AbsentDefault::Unverified,
258        children: None,
259        constraint: None,
260    },
261    FieldSchema {
262        label: "LinkedTo",
263        expected_type: GffType::String,
264        life: FieldLife::Live,
265        required: false,
266        absent: AbsentDefault::Unverified,
267        children: None,
268        constraint: None,
269    },
270    FieldSchema {
271        label: "LinkedToFlags",
272        expected_type: GffType::UInt8,
273        life: FieldLife::Live,
274        required: false,
275        absent: AbsentDefault::Unverified,
276        children: None,
277        constraint: None,
278    },
279    FieldSchema {
280        label: "XPosition",
281        expected_type: GffType::Single,
282        life: FieldLife::Live,
283        required: false,
284        absent: AbsentDefault::Unverified,
285        children: None,
286        constraint: None,
287    },
288    FieldSchema {
289        label: "YPosition",
290        expected_type: GffType::Single,
291        life: FieldLife::Live,
292        required: false,
293        absent: AbsentDefault::Unverified,
294        children: None,
295        constraint: None,
296    },
297    FieldSchema {
298        label: "ZPosition",
299        expected_type: GffType::Single,
300        life: FieldLife::Live,
301        required: false,
302        absent: AbsentDefault::Unverified,
303        children: None,
304        constraint: None,
305    },
306    FieldSchema {
307        label: "Geometry",
308        expected_type: GffType::List,
309        life: FieldLife::Live,
310        required: false,
311        absent: AbsentDefault::Unverified,
312        children: Some(TRIGGER_GEOMETRY_CHILDREN),
313        constraint: None,
314    },
315];
316
317// =========================================================================
318// The saved form
319// =========================================================================
320
321// Triggers as stored inside a save game.
322//
323// A static `.git` trigger is a placement referencing a `.utt`. A savegame
324// trigger carries the whole object inline, including the polygon that
325// defines where it fires.
326//
327// ## Trap fields are a subset of a door's
328//
329// A saved trigger writes `TrapDetectable`, `TrapDisarmable`, `TrapOneShot`
330// and `TrapType`, and none of `TrapDetectDC`, `DisarmDC` or `TrapFlag`. That
331// is why the four live here as plain fields rather than reusing the shared
332// [`TrapSettings`](crate::shared::TrapSettings) block doors and placeables
333// share: writing that block back would put three fields on a trigger the
334// engine never puts there.
335//
336// ## What is not modelled
337//
338// `ActionList`, `VarTable` and `SWVarTable` are live runtime state whose
339// layouts are only partly audited, and they are skipped for the same reason
340// [`SavedCreature`](crate::saved_creature::SavedCreature) skips them.
341
342/// A trigger as stored inside a save game's module `GIT`.
343#[derive(Debug, Clone, PartialEq, Default)]
344pub struct SavedTrigger {
345    /// Object tag (`Tag`).
346    pub tag: String,
347    /// Runtime object id (`ObjectId`).
348    pub object_id: ObjectId,
349    /// Object that created this trigger at runtime (`CreatorId`).
350    pub creator_id: u32,
351
352    /// Trigger kind (`Type`): generic, area transition, or trap.
353    pub trigger_type: i32,
354    /// Displayed name (`LocalizedName`).
355    pub localized_name: GffLocalizedString,
356    /// Portrait, in whichever of its two forms the object uses.
357    pub portrait: SavedPortrait,
358    /// Faction id (`Faction`).
359    pub faction: u32,
360    /// Cursor shown over the trigger (`Cursor`).
361    pub cursor: u8,
362    /// Highlight rendering height (`HighlightHeight`).
363    pub highlight_height: f32,
364    /// Accepts scripted commands (`Commandable`).
365    pub commandable: bool,
366    /// Placed by the player party at runtime (`SetByPlayerParty`).
367    pub set_by_player_party: bool,
368
369    /// World X position (`XPosition`).
370    pub x_position: f32,
371    /// World Y position (`YPosition`).
372    pub y_position: f32,
373    /// World Z position (`ZPosition`).
374    pub z_position: f32,
375    /// Facing X component (`XOrientation`).
376    pub x_orientation: f32,
377    /// Facing Y component (`YOrientation`).
378    pub y_orientation: f32,
379    /// Facing Z component (`ZOrientation`).
380    pub z_orientation: f32,
381    /// Polygon defining the trigger area (`Geometry`).
382    pub geometry: Vec<GitTriggerPoint>,
383
384    /// Area transition target tag (`LinkedTo`).
385    pub linked_to: String,
386    /// Transition kind (`LinkedToFlags`).
387    pub linked_to_flags: u8,
388    /// Target module for a cross-module transition (`LinkedToModule`).
389    pub linked_to_module: ResRef,
390    /// Transition tooltip text (`TransitionDestin`).
391    pub transition_destin: GffLocalizedString,
392    /// Load screen shown across the transition (`LoadScreenID`).
393    pub load_screen_id: u16,
394
395    /// Key tag that disarms this trigger (`KeyName`).
396    pub key_name: String,
397    /// Consume the key on use (`AutoRemoveKey`).
398    pub auto_remove_key: bool,
399
400    /// Trap can be spotted (`TrapDetectable`).
401    pub trap_detectable: bool,
402    /// Trap can be disarmed (`TrapDisarmable`).
403    pub trap_disarmable: bool,
404    /// Trap fires once and is spent (`TrapOneShot`).
405    pub trap_one_shot: bool,
406    /// Trap row (`TrapType`).
407    pub trap_type: u8,
408
409    /// `OnClick`.
410    pub on_click: ResRef,
411    /// `OnDisarm`.
412    pub on_disarm: ResRef,
413    /// `OnTrapTriggered`.
414    pub on_trap_triggered: ResRef,
415    /// `ScriptHeartbeat`.
416    pub script_heartbeat: ResRef,
417    /// `ScriptOnEnter`.
418    pub script_on_enter: ResRef,
419    /// `ScriptOnExit`.
420    pub script_on_exit: ResRef,
421    /// `ScriptUserDefine`.
422    pub script_user_define: ResRef,
423}
424
425impl SavedTrigger {
426    /// Creates an empty saved trigger.
427    pub fn new() -> Self {
428        Self::default()
429    }
430
431    /// Reads one saved `TriggerList` element.
432    pub fn from_struct(structure: &GffStruct) -> Self {
433        Self {
434            tag: get_string(structure, "Tag").unwrap_or_default(),
435            object_id: object_id_or_invalid(structure),
436            creator_id: get_u32(structure, "CreatorId").unwrap_or(0),
437
438            trigger_type: get_i32(structure, "Type").unwrap_or(0),
439            localized_name: get_locstring(structure, "LocalizedName")
440                .cloned()
441                .unwrap_or_default(),
442            portrait: SavedPortrait::read(
443                |label| get_resref(structure, label),
444                |label| get_u16(structure, label),
445            ),
446            faction: get_u32(structure, "Faction").unwrap_or(0),
447            cursor: get_u8(structure, "Cursor").unwrap_or(0),
448            highlight_height: get_f32(structure, "HighlightHeight").unwrap_or(0.0),
449            commandable: get_bool(structure, "Commandable").unwrap_or(false),
450            set_by_player_party: get_bool(structure, "SetByPlayerParty").unwrap_or(false),
451
452            x_position: get_f32(structure, "XPosition").unwrap_or(0.0),
453            y_position: get_f32(structure, "YPosition").unwrap_or(0.0),
454            z_position: get_f32(structure, "ZPosition").unwrap_or(0.0),
455            x_orientation: get_f32(structure, "XOrientation").unwrap_or(0.0),
456            y_orientation: get_f32(structure, "YOrientation").unwrap_or(0.0),
457            z_orientation: get_f32(structure, "ZOrientation").unwrap_or(0.0),
458            geometry: match structure.field("Geometry") {
459                Some(GffValue::List(points)) => points
460                    .iter()
461                    .map(GitTriggerPoint::from_gff_struct)
462                    .collect(),
463                _ => Vec::new(),
464            },
465
466            linked_to: get_string(structure, "LinkedTo").unwrap_or_default(),
467            linked_to_flags: get_u8(structure, "LinkedToFlags").unwrap_or(0),
468            linked_to_module: get_resref(structure, "LinkedToModule").unwrap_or_default(),
469            transition_destin: get_locstring(structure, "TransitionDestin")
470                .cloned()
471                .unwrap_or_default(),
472            load_screen_id: get_u16(structure, "LoadScreenID").unwrap_or(0),
473
474            key_name: get_string(structure, "KeyName").unwrap_or_default(),
475            auto_remove_key: get_bool(structure, "AutoRemoveKey").unwrap_or(false),
476
477            trap_detectable: get_bool(structure, "TrapDetectable").unwrap_or(false),
478            trap_disarmable: get_bool(structure, "TrapDisarmable").unwrap_or(false),
479            trap_one_shot: get_bool(structure, "TrapOneShot").unwrap_or(false),
480            trap_type: get_u8(structure, "TrapType").unwrap_or(0),
481
482            on_click: get_resref(structure, "OnClick").unwrap_or_default(),
483            on_disarm: get_resref(structure, "OnDisarm").unwrap_or_default(),
484            on_trap_triggered: get_resref(structure, "OnTrapTriggered").unwrap_or_default(),
485            script_heartbeat: get_resref(structure, "ScriptHeartbeat").unwrap_or_default(),
486            script_on_enter: get_resref(structure, "ScriptOnEnter").unwrap_or_default(),
487            script_on_exit: get_resref(structure, "ScriptOnExit").unwrap_or_default(),
488            script_user_define: get_resref(structure, "ScriptUserDefine").unwrap_or_default(),
489        }
490    }
491
492    /// Writes this trigger back into a `TriggerList` element.
493    pub fn to_struct(&self, index: usize) -> GffStruct {
494        let mut s = GffStruct::new(i32::try_from(index).expect("trigger index fits i32"));
495
496        upsert_field(&mut s, "Tag", GffValue::String(self.tag.clone()));
497        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
498        upsert_field(&mut s, "CreatorId", GffValue::UInt32(self.creator_id));
499
500        upsert_field(&mut s, "Type", GffValue::Int32(self.trigger_type));
501        upsert_field(
502            &mut s,
503            "LocalizedName",
504            GffValue::LocalizedString(self.localized_name.clone()),
505        );
506        self.portrait
507            .write(|label, value| upsert_field(&mut s, label, value));
508        upsert_field(&mut s, "Faction", GffValue::UInt32(self.faction));
509        upsert_field(&mut s, "Cursor", GffValue::UInt8(self.cursor));
510        upsert_field(
511            &mut s,
512            "HighlightHeight",
513            GffValue::Single(self.highlight_height),
514        );
515        upsert_field(
516            &mut s,
517            "Commandable",
518            GffValue::UInt8(self.commandable.into()),
519        );
520        upsert_field(
521            &mut s,
522            "SetByPlayerParty",
523            GffValue::UInt8(self.set_by_player_party.into()),
524        );
525
526        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
527        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
528        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
529        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
530        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
531        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
532        upsert_field(
533            &mut s,
534            "Geometry",
535            GffValue::List(
536                self.geometry
537                    .iter()
538                    .map(GitTriggerPoint::to_gff_struct)
539                    .collect(),
540            ),
541        );
542
543        upsert_field(&mut s, "LinkedTo", GffValue::String(self.linked_to.clone()));
544        upsert_field(
545            &mut s,
546            "LinkedToFlags",
547            GffValue::UInt8(self.linked_to_flags),
548        );
549        upsert_field(
550            &mut s,
551            "LinkedToModule",
552            GffValue::ResRef(self.linked_to_module),
553        );
554        upsert_field(
555            &mut s,
556            "TransitionDestin",
557            GffValue::LocalizedString(self.transition_destin.clone()),
558        );
559        upsert_field(
560            &mut s,
561            "LoadScreenID",
562            GffValue::UInt16(self.load_screen_id),
563        );
564
565        upsert_field(&mut s, "KeyName", GffValue::String(self.key_name.clone()));
566        upsert_field(
567            &mut s,
568            "AutoRemoveKey",
569            GffValue::UInt8(self.auto_remove_key.into()),
570        );
571
572        upsert_field(
573            &mut s,
574            "TrapDetectable",
575            GffValue::UInt8(self.trap_detectable.into()),
576        );
577        upsert_field(
578            &mut s,
579            "TrapDisarmable",
580            GffValue::UInt8(self.trap_disarmable.into()),
581        );
582        upsert_field(
583            &mut s,
584            "TrapOneShot",
585            GffValue::UInt8(self.trap_one_shot.into()),
586        );
587        upsert_field(&mut s, "TrapType", GffValue::UInt8(self.trap_type));
588
589        upsert_field(&mut s, "OnClick", GffValue::ResRef(self.on_click));
590        upsert_field(&mut s, "OnDisarm", GffValue::ResRef(self.on_disarm));
591        upsert_field(
592            &mut s,
593            "OnTrapTriggered",
594            GffValue::ResRef(self.on_trap_triggered),
595        );
596        upsert_field(
597            &mut s,
598            "ScriptHeartbeat",
599            GffValue::ResRef(self.script_heartbeat),
600        );
601        upsert_field(
602            &mut s,
603            "ScriptOnEnter",
604            GffValue::ResRef(self.script_on_enter),
605        );
606        upsert_field(
607            &mut s,
608            "ScriptOnExit",
609            GffValue::ResRef(self.script_on_exit),
610        );
611        upsert_field(
612            &mut s,
613            "ScriptUserDefine",
614            GffValue::ResRef(self.script_user_define),
615        );
616
617        s
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    fn sample() -> SavedTrigger {
626        SavedTrigger {
627            tag: "tr_transition".to_string(),
628            object_id: ObjectId::new(0x8000_0012),
629            trigger_type: 1,
630            portrait: SavedPortrait::Id(0),
631            geometry: vec![
632                GitTriggerPoint {
633                    point_x: 0.0,
634                    point_y: 0.0,
635                    point_z: 0.0,
636                },
637                GitTriggerPoint {
638                    point_x: 5.0,
639                    point_y: 0.0,
640                    point_z: 0.0,
641                },
642            ],
643            linked_to: "wp_arrive".to_string(),
644            load_screen_id: 3,
645            trap_detectable: true,
646            trap_type: 9,
647            script_on_enter: ResRef::new("k_trg_enter").expect("valid resref"),
648            ..SavedTrigger::default()
649        }
650    }
651
652    #[test]
653    fn round_trips_through_a_list_element() {
654        let trigger = sample();
655
656        let parsed = SavedTrigger::from_struct(&trigger.to_struct(0));
657
658        assert_eq!(parsed, trigger);
659    }
660
661    #[test]
662    fn does_not_gain_the_trap_dcs_a_door_carries() {
663        // A saved trigger has no TrapDetectDC, DisarmDC or TrapFlag. Reusing
664        // the shared trap block would write all three.
665        let written = sample().to_struct(0);
666
667        assert!(written.field("TrapDetectDC").is_none());
668        assert!(written.field("DisarmDC").is_none());
669        assert!(written.field("TrapFlag").is_none());
670        assert!(written.field("TrapType").is_some());
671    }
672}