rakata_generics/git/waypoint.rs
1//! Waypoint placements in a GIT, and the map note one can carry.
2
3use rakata_core::ResRef;
4use rakata_formats::gff::upsert_field;
5use rakata_formats::gff_label;
6use rakata_formats::{GffLocalizedString, GffModel, GffStruct, GffValue};
7
8use crate::shared::ObjectId;
9/// A waypoint placed in the area (struct type 5).
10///
11/// Waypoints are the one object list with a single form. `LoadWaypoints`
12/// ignores `UseTemplates` and always reads inline data, so there is no
13/// static / saved split here and [`Git::waypoints`](super::Git::waypoints) is a plain [`Vec`].
14///
15/// ## Dead fields this type does not model
16///
17/// `TemplateResRef` and `LinkedTo` are universal in a real install and read by
18/// nothing; `docs/src/formats/gff/utw.md` carries both the engine side and the
19/// corpus figures. `docs/src/architecture.md`'s projection rule uses this
20/// type's `TemplateResRef` as its worked example of prevalence not being a
21/// criterion for modelling.
22///
23/// `Tag`, in contrast, is live and modelled. A waypoint is always inline with
24/// no blueprint behind it, so unlike [`GitDoor`](super::GitDoor) there is no second copy for
25/// the placement's to lose to.
26///
27/// ## The map note is one value, not a flag plus two optionals
28///
29/// The engine gates the note behind `HasMapNote` and then discards the whole
30/// trio unless `MapNote` itself was there, so neither half alone produces a
31/// note and there is no state to model between them.
32///
33/// Read the flag, not the text. Keying the note off `MapNote` alone passes
34/// against the fixture saves, where the two never disagree, and a larger
35/// corpus does not support it. `docs/src/testing.md` carries that as a worked
36/// example of a corpus too small to tell a relationship from a constant.
37#[derive(Debug, Clone, PartialEq, GffModel)]
38#[gff_entry(Appearance, wire = u8, read_only_dead = "a waypoint has no rendered model to select, and no waypoint load path reads the field", unexamined)]
39#[gff_entry(Description, wire = GffLocalizedString, read_only_dead = "a waypoint resolves no template, so this value has no source anywhere rather than one that goes unread", unexamined)]
40#[gff_entry(HasMapNote, wire = u8, constructed)]
41#[gff_entry(LinkedTo, wire = String, read_only_dead = "a waypoint has no transition capability, so there is nothing for a destination tag to name", unexamined)]
42#[gff_entry(TemplateResRef, wire = ResRef, read_only_dead = "no waypoint load path reads it, including the script-spawn fallback, where the resref comes from the script call", unexamined)]
43pub struct GitWaypoint {
44 /// Object tag (`Tag`).
45 #[gff(Tag, stamped)]
46 pub tag: String,
47 /// Displayed name (`LocalizedName`).
48 #[gff(LocalizedName, stamped)]
49 pub localized_name: GffLocalizedString,
50 /// Accepts scripted commands (`Commandable`).
51 /// Whether the object takes scripted commands (`Commandable`).
52 ///
53 /// One of the base-object labels `CSWSObject::LoadObjectState` reads, and
54 /// that read runs only on the save-instance branch, so a static placement
55 /// never reaches it. `SaveObjectState` writes it whenever anything is
56 /// saved, which is why every saved element carries it and no install
57 /// element does. A waypoint is the one owner this reaches, because
58 /// `WaypointList` has no static arm to keep it off.
59 #[gff(Commandable, unexamined, optional = bool)]
60 pub commandable: Option<bool>,
61 /// Map note, when this waypoint carries one.
62 ///
63 /// The two labels sit at this level rather than under one of their own,
64 /// so the member is flattened. Whether it is there at all is a rule no
65 /// attribute states: `git.md` has an absent `MapNote` discarding the
66 /// whole `HasMapNote`/`MapNoteEnabled`/`MapNote` trio, and the flag is
67 /// derived from this member on write so the two cannot disagree.
68 #[gff(flatten = GitMapNote, manual_read, manual_write)]
69 pub map_note: Option<GitMapNote>,
70 /// X orientation (`XOrientation`).
71 #[gff(XOrientation, not_a_constant)]
72 pub x_orientation: f32,
73 /// Y orientation (`YOrientation`).
74 #[gff(YOrientation, not_a_constant)]
75 pub y_orientation: f32,
76 /// Z orientation (`ZOrientation`).
77 /// Z orientation (`ZOrientation`), which only a saved form carries.
78 #[gff(ZOrientation, not_a_constant, optional = f32)]
79 pub z_orientation: Option<f32>,
80 /// Runtime object ID (`ObjectId`).
81 /// Runtime object id (`ObjectId`), which only a saved form carries.
82 ///
83 /// The area-level dispatcher reads it once per element ahead of the
84 /// static-versus-saved branch, so the engine reads it on both forms and
85 /// the liveness below is right. What it does not do is author it: every
86 /// element the engine saves carries one and no element the toolset shipped
87 /// does. Held as the absence so writing a static placement back does not
88 /// invent a runtime id for it.
89 #[gff(ObjectId, stamped = ObjectId::INVALID, optional = ObjectId)]
90 pub object_id: Option<ObjectId>,
91 /// X position (`XPosition`).
92 #[gff(XPosition, stamped)]
93 pub x_position: f32,
94 /// Y position (`YPosition`).
95 #[gff(YPosition, stamped)]
96 pub y_position: f32,
97 /// Z position (`ZPosition`).
98 #[gff(ZPosition, stamped)]
99 pub z_position: f32,
100}
101
102/// The two labels a waypoint's map note carries, at the waypoint's own level.
103#[derive(Debug, Clone, PartialEq, GffModel)]
104pub struct GitMapNote {
105 /// Note text (`MapNote`).
106 #[gff(MapNote, constructed)]
107 pub text: GffLocalizedString,
108 /// Whether the note shows on the map right now (`MapNoteEnabled`).
109 #[gff(MapNoteEnabled, stamped)]
110 pub enabled: bool,
111}
112
113impl GitWaypoint {
114 /// Reads one waypoint, resolving the map-note group.
115 pub fn read_element(structure: &GffStruct) -> Self {
116 let carried = ["MapNote", "MapNoteEnabled"]
117 .iter()
118 .all(|label| structure.field(label).is_some());
119 Self {
120 map_note: carried.then(|| GitMapNote::read_declared(structure)),
121 ..Self::read_declared(structure)
122 }
123 }
124
125 /// Writes one waypoint, deriving the flag from whether the note is there.
126 pub fn write_element(&self, structure: &mut GffStruct) {
127 self.write_declared(structure);
128 upsert_field(
129 structure,
130 gff_label!("HasMapNote"),
131 GffValue::UInt8(u8::from(self.map_note.is_some())),
132 );
133 if let Some(note) = &self.map_note {
134 note.write_declared(structure);
135 }
136 }
137}