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_i16, get_locstring, get_resref, get_string, get_u16, get_u32, get_u8,
12 upsert_field,
13};
14use crate::shared::{ObjectId, SavedPortrait, TrapDefaults, TrapSettings};
15
16#[derive(Debug, Clone, PartialEq, Default)]
46pub struct GitDoor {
47 pub template_resref: ResRef,
49 pub bearing: f32,
51 pub x: f32,
53 pub y: f32,
55 pub z: f32,
57 pub linked_to_module: ResRef,
59 pub linked_to: String,
61 pub linked_to_flags: u8,
63 pub transition_destination: GffLocalizedString,
65 pub appearance: u32,
74 pub object_id: ObjectId,
76}
77
78impl GitDoor {
79 pub(crate) fn from_gff_struct(s: &GffStruct) -> Self {
80 Self {
81 template_resref: get_resref(s, "TemplateResRef").unwrap_or_default(),
82 bearing: get_f32(s, "Bearing").unwrap_or(0.0),
83 x: get_f32(s, "X").unwrap_or(0.0),
84 y: get_f32(s, "Y").unwrap_or(0.0),
85 z: get_f32(s, "Z").unwrap_or(0.0),
86 linked_to_module: get_resref(s, "LinkedToModule").unwrap_or_default(),
87 linked_to: get_string(s, "LinkedTo").unwrap_or_default(),
88 linked_to_flags: get_u8(s, "LinkedToFlags").unwrap_or(0),
89 transition_destination: get_locstring(s, "TransitionDestin")
90 .cloned()
91 .unwrap_or_else(|| GffLocalizedString::new(StrRef::invalid())),
92 appearance: get_u32(s, "Appearance").unwrap_or(0),
93 object_id: object_id_or_invalid(s),
94 }
95 }
96
97 pub(crate) fn to_gff_struct(&self) -> GffStruct {
98 let mut s = GffStruct::new(8);
99 upsert_field(
100 &mut s,
101 "TemplateResRef",
102 GffValue::ResRef(self.template_resref),
103 );
104 upsert_field(&mut s, "Bearing", GffValue::Single(self.bearing));
105 upsert_field(&mut s, "X", GffValue::Single(self.x));
106 upsert_field(&mut s, "Y", GffValue::Single(self.y));
107 upsert_field(&mut s, "Z", GffValue::Single(self.z));
108 upsert_field(
109 &mut s,
110 "LinkedToModule",
111 GffValue::ResRef(self.linked_to_module),
112 );
113 upsert_field(&mut s, "LinkedTo", GffValue::String(self.linked_to.clone()));
114 upsert_field(
115 &mut s,
116 "LinkedToFlags",
117 GffValue::UInt8(self.linked_to_flags),
118 );
119 upsert_field(
120 &mut s,
121 "TransitionDestin",
122 GffValue::LocalizedString(self.transition_destination.clone()),
123 );
124 upsert_field(&mut s, "Appearance", GffValue::UInt32(self.appearance));
125 upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
126 s
127 }
128}
129
130pub(crate) static DOOR_LIST_CHILDREN: &[FieldSchema] = &[
132 FieldSchema {
133 label: "Appearance",
134 expected_type: GffType::UInt32,
135 life: FieldLife::Live,
136 required: false,
137 absent: AbsentDefault::Unverified,
138 children: None,
139 constraint: None,
140 },
141 FieldSchema {
142 label: "ObjectId",
143 expected_type: GffType::UInt32,
144 life: FieldLife::Live,
145 required: false,
146 absent: AbsentDefault::Unverified,
147 children: None,
148 constraint: None,
149 },
150 FieldSchema {
151 label: "TemplateResRef",
152 expected_type: GffType::ResRef,
153 life: FieldLife::Live,
154 required: false,
155 absent: AbsentDefault::Unverified,
156 children: None,
157 constraint: None,
158 },
159 FieldSchema {
162 label: "Tag",
163 expected_type: GffType::String,
164 life: FieldLife::ReadOnlyDead(
165 "a templated placement takes its tag from the blueprint, which the placement cannot override",
166 ),
167 required: false,
168 absent: AbsentDefault::Unverified,
169 children: None,
170 constraint: None,
171 },
172 FieldSchema {
173 label: "LinkedToModule",
174 expected_type: GffType::ResRef,
175 life: FieldLife::Live,
176 required: false,
177 absent: AbsentDefault::Unverified,
178 children: None,
179 constraint: None,
180 },
181 FieldSchema {
182 label: "LinkedTo",
183 expected_type: GffType::String,
184 life: FieldLife::Live,
185 required: false,
186 absent: AbsentDefault::Unverified,
187 children: None,
188 constraint: None,
189 },
190 FieldSchema {
191 label: "LinkedToFlags",
192 expected_type: GffType::UInt8,
193 life: FieldLife::Live,
194 required: false,
195 absent: AbsentDefault::Unverified,
196 children: None,
197 constraint: None,
198 },
199 FieldSchema {
200 label: "TransitionDestin",
201 expected_type: GffType::LocalizedString,
202 life: FieldLife::Live,
203 required: false,
204 absent: AbsentDefault::Unverified,
205 children: None,
206 constraint: None,
207 },
208 FieldSchema {
209 label: "Bearing",
210 expected_type: GffType::Single,
211 life: FieldLife::Live,
212 required: false,
213 absent: AbsentDefault::Unverified,
214 children: None,
215 constraint: None,
216 },
217 FieldSchema {
218 label: "X",
219 expected_type: GffType::Single,
220 life: FieldLife::Live,
221 required: false,
222 absent: AbsentDefault::Unverified,
223 children: None,
224 constraint: None,
225 },
226 FieldSchema {
227 label: "Y",
228 expected_type: GffType::Single,
229 life: FieldLife::Live,
230 required: false,
231 absent: AbsentDefault::Unverified,
232 children: None,
233 constraint: None,
234 },
235 FieldSchema {
236 label: "Z",
237 expected_type: GffType::Single,
238 life: FieldLife::Live,
239 required: false,
240 absent: AbsentDefault::Unverified,
241 children: None,
242 constraint: None,
243 },
244];
245
246#[derive(Debug, Clone, PartialEq, Default)]
277pub struct SavedDoor {
278 pub tag: String,
280 pub object_id: ObjectId,
282
283 pub appearance: u32,
285 pub generic_type: u8,
287 pub loc_name: GffLocalizedString,
289 pub description: GffLocalizedString,
291 pub portrait: SavedPortrait,
293
294 pub x: f32,
296 pub y: f32,
298 pub z: f32,
300 pub bearing: f32,
302
303 pub current_hp: i16,
305 pub hp: i16,
307 pub hardness: u8,
309 pub fortitude: u8,
311 pub reflex: u8,
313 pub will: u8,
315 pub min1_hp: bool,
317 pub plot: bool,
319 pub is_static: bool,
321 pub commandable: bool,
323 pub faction: u32,
325
326 pub open_state: u8,
328 pub lockable: bool,
330 pub locked: bool,
332 pub open_lock_dc: u8,
334 pub close_lock_dc: u8,
336 pub secret_door_dc: u8,
338 pub key_name: String,
340 pub key_required: bool,
342 pub auto_remove_key: bool,
344
345 pub trap: TrapSettings,
347
348 pub linked_to: String,
350 pub linked_to_flags: u8,
352 pub linked_to_module: ResRef,
354 pub transition_destin: GffLocalizedString,
356 pub load_screen_id: u16,
358
359 pub conversation: ResRef,
361
362 pub on_click: ResRef,
364 pub on_closed: ResRef,
366 pub on_damaged: ResRef,
368 pub on_death: ResRef,
370 pub on_dialog: ResRef,
372 pub on_disarm: ResRef,
374 pub on_fail_to_open: ResRef,
376 pub on_heartbeat: ResRef,
378 pub on_lock: ResRef,
380 pub on_melee_attacked: ResRef,
382 pub on_open: ResRef,
384 pub on_spell_cast_at: ResRef,
386 pub on_trap_triggered: ResRef,
388 pub on_unlock: ResRef,
390 pub on_user_defined: ResRef,
392}
393
394impl SavedDoor {
395 pub fn new() -> Self {
397 Self::default()
398 }
399
400 pub fn from_struct(structure: &GffStruct) -> Self {
402 Self {
403 tag: get_string(structure, "Tag").unwrap_or_default(),
404 object_id: object_id_or_invalid(structure),
405
406 appearance: get_u32(structure, "Appearance").unwrap_or(0),
407 generic_type: get_u8(structure, "GenericType").unwrap_or(0),
408 loc_name: get_locstring(structure, "LocName")
409 .cloned()
410 .unwrap_or_default(),
411 description: get_locstring(structure, "Description")
412 .cloned()
413 .unwrap_or_default(),
414 portrait: SavedPortrait::read(
415 |label| get_resref(structure, label),
416 |label| get_u16(structure, label),
417 ),
418
419 x: get_f32(structure, "X").unwrap_or(0.0),
420 y: get_f32(structure, "Y").unwrap_or(0.0),
421 z: get_f32(structure, "Z").unwrap_or(0.0),
422 bearing: get_f32(structure, "Bearing").unwrap_or(0.0),
423
424 current_hp: get_i16(structure, "CurrentHP").unwrap_or(0),
425 hp: get_i16(structure, "HP").unwrap_or(0),
426 hardness: get_u8(structure, "Hardness").unwrap_or(0),
427 fortitude: get_u8(structure, "Fort").unwrap_or(0),
428 reflex: get_u8(structure, "Ref").unwrap_or(0),
429 will: get_u8(structure, "Will").unwrap_or(0),
430 min1_hp: get_bool(structure, "Min1HP").unwrap_or(false),
431 plot: get_bool(structure, "Plot").unwrap_or(false),
432 is_static: get_bool(structure, "Static").unwrap_or(false),
433 commandable: get_bool(structure, "Commandable").unwrap_or(false),
434 faction: get_u32(structure, "Faction").unwrap_or(0),
435
436 open_state: get_u8(structure, "OpenState").unwrap_or(0),
437 lockable: get_bool(structure, "Lockable").unwrap_or(false),
438 locked: get_bool(structure, "Locked").unwrap_or(false),
439 open_lock_dc: get_u8(structure, "OpenLockDC").unwrap_or(0),
440 close_lock_dc: get_u8(structure, "CloseLockDC").unwrap_or(0),
441 secret_door_dc: get_u8(structure, "SecretDoorDC").unwrap_or(0),
442 key_name: get_string(structure, "KeyName").unwrap_or_default(),
443 key_required: get_bool(structure, "KeyRequired").unwrap_or(false),
444 auto_remove_key: get_bool(structure, "AutoRemoveKey").unwrap_or(false),
445
446 trap: TrapSettings::read(
447 TrapDefaults::DOOR,
448 |label| get_bool(structure, label),
449 |label| get_u8(structure, label),
450 ),
451
452 linked_to: get_string(structure, "LinkedTo").unwrap_or_default(),
453 linked_to_flags: get_u8(structure, "LinkedToFlags").unwrap_or(0),
454 linked_to_module: get_resref(structure, "LinkedToModule").unwrap_or_default(),
455 transition_destin: get_locstring(structure, "TransitionDestin")
456 .cloned()
457 .unwrap_or_default(),
458 load_screen_id: get_u16(structure, "LoadScreenID").unwrap_or(0),
459
460 conversation: get_resref(structure, "Conversation").unwrap_or_default(),
461
462 on_click: get_resref(structure, "OnClick").unwrap_or_default(),
463 on_closed: get_resref(structure, "OnClosed").unwrap_or_default(),
464 on_damaged: get_resref(structure, "OnDamaged").unwrap_or_default(),
465 on_death: get_resref(structure, "OnDeath").unwrap_or_default(),
466 on_dialog: get_resref(structure, "OnDialog").unwrap_or_default(),
467 on_disarm: get_resref(structure, "OnDisarm").unwrap_or_default(),
468 on_fail_to_open: get_resref(structure, "OnFailToOpen").unwrap_or_default(),
469 on_heartbeat: get_resref(structure, "OnHeartbeat").unwrap_or_default(),
470 on_lock: get_resref(structure, "OnLock").unwrap_or_default(),
471 on_melee_attacked: get_resref(structure, "OnMeleeAttacked").unwrap_or_default(),
472 on_open: get_resref(structure, "OnOpen").unwrap_or_default(),
473 on_spell_cast_at: get_resref(structure, "OnSpellCastAt").unwrap_or_default(),
474 on_trap_triggered: get_resref(structure, "OnTrapTriggered").unwrap_or_default(),
475 on_unlock: get_resref(structure, "OnUnlock").unwrap_or_default(),
476 on_user_defined: get_resref(structure, "OnUserDefined").unwrap_or_default(),
477 }
478 }
479
480 pub fn to_struct(&self, index: usize) -> GffStruct {
482 let mut s = GffStruct::new(i32::try_from(index).expect("door index fits i32"));
483
484 upsert_field(&mut s, "Tag", GffValue::String(self.tag.clone()));
485 upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
486
487 upsert_field(&mut s, "Appearance", GffValue::UInt32(self.appearance));
488 upsert_field(&mut s, "GenericType", GffValue::UInt8(self.generic_type));
489 upsert_field(
490 &mut s,
491 "LocName",
492 GffValue::LocalizedString(self.loc_name.clone()),
493 );
494 upsert_field(
495 &mut s,
496 "Description",
497 GffValue::LocalizedString(self.description.clone()),
498 );
499 self.portrait
500 .write(|label, value| upsert_field(&mut s, label, value));
501
502 upsert_field(&mut s, "X", GffValue::Single(self.x));
503 upsert_field(&mut s, "Y", GffValue::Single(self.y));
504 upsert_field(&mut s, "Z", GffValue::Single(self.z));
505 upsert_field(&mut s, "Bearing", GffValue::Single(self.bearing));
506
507 upsert_field(&mut s, "CurrentHP", GffValue::Int16(self.current_hp));
508 upsert_field(&mut s, "HP", GffValue::Int16(self.hp));
509 upsert_field(&mut s, "Hardness", GffValue::UInt8(self.hardness));
510 upsert_field(&mut s, "Fort", GffValue::UInt8(self.fortitude));
511 upsert_field(&mut s, "Ref", GffValue::UInt8(self.reflex));
512 upsert_field(&mut s, "Will", GffValue::UInt8(self.will));
513 upsert_field(&mut s, "Min1HP", GffValue::UInt8(self.min1_hp.into()));
514 upsert_field(&mut s, "Plot", GffValue::UInt8(self.plot.into()));
515 upsert_field(&mut s, "Static", GffValue::UInt8(self.is_static.into()));
516 upsert_field(
517 &mut s,
518 "Commandable",
519 GffValue::UInt8(self.commandable.into()),
520 );
521 upsert_field(&mut s, "Faction", GffValue::UInt32(self.faction));
522
523 upsert_field(&mut s, "OpenState", GffValue::UInt8(self.open_state));
524 upsert_field(&mut s, "Lockable", GffValue::UInt8(self.lockable.into()));
525 upsert_field(&mut s, "Locked", GffValue::UInt8(self.locked.into()));
526 upsert_field(&mut s, "OpenLockDC", GffValue::UInt8(self.open_lock_dc));
527 upsert_field(&mut s, "CloseLockDC", GffValue::UInt8(self.close_lock_dc));
528 upsert_field(&mut s, "SecretDoorDC", GffValue::UInt8(self.secret_door_dc));
529 upsert_field(&mut s, "KeyName", GffValue::String(self.key_name.clone()));
530 upsert_field(
531 &mut s,
532 "KeyRequired",
533 GffValue::UInt8(self.key_required.into()),
534 );
535 upsert_field(
536 &mut s,
537 "AutoRemoveKey",
538 GffValue::UInt8(self.auto_remove_key.into()),
539 );
540
541 self.trap
542 .write(|label, value| upsert_field(&mut s, label, value));
543
544 upsert_field(&mut s, "LinkedTo", GffValue::String(self.linked_to.clone()));
545 upsert_field(
546 &mut s,
547 "LinkedToFlags",
548 GffValue::UInt8(self.linked_to_flags),
549 );
550 upsert_field(
551 &mut s,
552 "LinkedToModule",
553 GffValue::ResRef(self.linked_to_module),
554 );
555 upsert_field(
556 &mut s,
557 "TransitionDestin",
558 GffValue::LocalizedString(self.transition_destin.clone()),
559 );
560 upsert_field(
561 &mut s,
562 "LoadScreenID",
563 GffValue::UInt16(self.load_screen_id),
564 );
565
566 upsert_field(&mut s, "Conversation", GffValue::ResRef(self.conversation));
567
568 upsert_field(&mut s, "OnClick", GffValue::ResRef(self.on_click));
569 upsert_field(&mut s, "OnClosed", GffValue::ResRef(self.on_closed));
570 upsert_field(&mut s, "OnDamaged", GffValue::ResRef(self.on_damaged));
571 upsert_field(&mut s, "OnDeath", GffValue::ResRef(self.on_death));
572 upsert_field(&mut s, "OnDialog", GffValue::ResRef(self.on_dialog));
573 upsert_field(&mut s, "OnDisarm", GffValue::ResRef(self.on_disarm));
574 upsert_field(
575 &mut s,
576 "OnFailToOpen",
577 GffValue::ResRef(self.on_fail_to_open),
578 );
579 upsert_field(&mut s, "OnHeartbeat", GffValue::ResRef(self.on_heartbeat));
580 upsert_field(&mut s, "OnLock", GffValue::ResRef(self.on_lock));
581 upsert_field(
582 &mut s,
583 "OnMeleeAttacked",
584 GffValue::ResRef(self.on_melee_attacked),
585 );
586 upsert_field(&mut s, "OnOpen", GffValue::ResRef(self.on_open));
587 upsert_field(
588 &mut s,
589 "OnSpellCastAt",
590 GffValue::ResRef(self.on_spell_cast_at),
591 );
592 upsert_field(
593 &mut s,
594 "OnTrapTriggered",
595 GffValue::ResRef(self.on_trap_triggered),
596 );
597 upsert_field(&mut s, "OnUnlock", GffValue::ResRef(self.on_unlock));
598 upsert_field(
599 &mut s,
600 "OnUserDefined",
601 GffValue::ResRef(self.on_user_defined),
602 );
603
604 s
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611
612 fn sample() -> SavedDoor {
613 SavedDoor {
614 tag: "sec_door_01".to_string(),
615 object_id: ObjectId::new(0x8000_0031),
616 appearance: 12,
617 generic_type: 3,
618 portrait: SavedPortrait::Id(42),
619 x: 12.5,
620 y: -3.0,
621 bearing: 1.57,
622 current_hp: 40,
623 hp: 60,
624 locked: true,
625 open_lock_dc: 25,
626 key_name: "sec_key".to_string(),
627 trap: TrapSettings {
628 detectable: true,
629 detect_dc: 15,
630 trap_type: 4,
631 ..TrapSettings::default()
632 },
633 linked_to: "wp_exit".to_string(),
634 load_screen_id: 7,
635 on_open: ResRef::new("k_door_open").expect("valid resref"),
636 ..SavedDoor::default()
637 }
638 }
639
640 #[test]
641 fn round_trips_through_a_list_element() {
642 let door = sample();
643
644 let parsed = SavedDoor::from_struct(&door.to_struct(0));
645
646 assert_eq!(parsed, door);
647 }
648
649 #[test]
650 fn a_portrait_id_does_not_gain_a_portrait_resref() {
651 let written = sample().to_struct(0);
654
655 assert!(written.field("PortraitId").is_some());
656 assert!(written.field("Portrait").is_none());
657 }
658
659 #[test]
660 fn a_portrait_resref_survives_the_round_trip() {
661 let door = SavedDoor {
662 portrait: SavedPortrait::ResRef(ResRef::new("po_pdoor").expect("valid resref")),
663 ..sample()
664 };
665
666 let written = door.to_struct(0);
667 assert!(written.field("PortraitId").is_none());
668 assert_eq!(SavedDoor::from_struct(&written).portrait, door.portrait);
669 }
670
671 #[test]
672 fn an_absent_portrait_reads_as_the_engine_default() {
673 let parsed = SavedDoor::from_struct(&GffStruct::new(0));
675
676 assert_eq!(parsed.portrait, SavedPortrait::Id(0));
677 }
678}