1use 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#[derive(Debug, Clone, PartialEq)]
30pub struct GitTrigger {
31 pub template_resref: ResRef,
33 pub linked_to_module: ResRef,
35 pub transition_destination: GffLocalizedString,
38 pub linked_to: String,
40 pub linked_to_flags: u8,
42 pub x_position: f32,
44 pub y_position: f32,
46 pub z_position: f32,
48 pub x_orientation: f32,
56 pub y_orientation: f32,
58 pub z_orientation: f32,
60 pub geometry: Vec<GitTriggerPoint>,
62 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
152pub(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
183pub(crate) static TRIGGER_LIST_CHILDREN: &[FieldSchema] = &[
185 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#[derive(Debug, Clone, PartialEq, Default)]
344pub struct SavedTrigger {
345 pub tag: String,
347 pub object_id: ObjectId,
349 pub creator_id: u32,
351
352 pub trigger_type: i32,
354 pub localized_name: GffLocalizedString,
356 pub portrait: SavedPortrait,
358 pub faction: u32,
360 pub cursor: u8,
362 pub highlight_height: f32,
364 pub commandable: bool,
366 pub set_by_player_party: bool,
368
369 pub x_position: f32,
371 pub y_position: f32,
373 pub z_position: f32,
375 pub x_orientation: f32,
377 pub y_orientation: f32,
379 pub z_orientation: f32,
381 pub geometry: Vec<GitTriggerPoint>,
383
384 pub linked_to: String,
386 pub linked_to_flags: u8,
388 pub linked_to_module: ResRef,
390 pub transition_destin: GffLocalizedString,
392 pub load_screen_id: u16,
394
395 pub key_name: String,
397 pub auto_remove_key: bool,
399
400 pub trap_detectable: bool,
402 pub trap_disarmable: bool,
404 pub trap_one_shot: bool,
406 pub trap_type: u8,
408
409 pub on_click: ResRef,
411 pub on_disarm: ResRef,
413 pub on_trap_triggered: ResRef,
415 pub script_heartbeat: ResRef,
417 pub script_on_enter: ResRef,
419 pub script_on_exit: ResRef,
421 pub script_user_define: ResRef,
423}
424
425impl SavedTrigger {
426 pub fn new() -> Self {
428 Self::default()
429 }
430
431 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 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 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}