Skip to main content

rakata_generics/git/
waypoint.rs

1//! Waypoint placements in a GIT, and the map note one can carry.
2
3use rakata_formats::{
4    gff_schema::{AbsentDefault, FieldLife, FieldSchema, GffType},
5    GffLocalizedString, GffStruct, GffValue,
6};
7
8use super::object_id_or_invalid;
9use crate::gff_helpers::{get_bool, get_f32, get_locstring, get_string, upsert_field};
10use crate::shared::ObjectId;
11
12/// A waypoint placed in the area (struct type 5).
13///
14/// Waypoints are the one object list with a single form. `LoadWaypoints`
15/// ignores `UseTemplates` and always reads inline data, so there is no
16/// static / saved split here and [`Git::waypoints`](super::Git::waypoints) is a plain [`Vec`].
17///
18/// ## `TemplateResRef` is dead here, and deliberately unmodelled
19///
20/// Every waypoint in a real install carries a `TemplateResRef`, and nothing
21/// reads it. No waypoint load path looks the label up under any name,
22/// including the script-spawn fallback, where the resref comes from the
23/// script's own `CreateObject` argument rather than from the file. Unlike
24/// [`GitDoor`](super::GitDoor), a waypoint placement never resolves a blueprint.
25///
26/// Present in every file is not a reason to model it. The projection rule has
27/// no prevalence threshold: a field the engine ignores is one the typed view
28/// can drop however common it is, and callers needing byte fidelity work with
29/// the raw [`Gff`](rakata_formats::Gff) tree. Modelling it would also invite a resref-existence
30/// check on a name nothing opens, which is the concrete cost of treating
31/// prevalence as significance.
32///
33/// ## `LinkedTo` is dead here too
34///
35/// The transition group belongs to doors and triggers. A waypoint cannot
36/// carry the player anywhere, so a destination tag on one names a target for
37/// a transition that will never fire. Every waypoint in a real install
38/// carries the label and every one of them leaves it empty, which is what a
39/// field the toolset writes and nothing consumes looks like.
40///
41/// `Tag`, in contrast, is live and modelled. A waypoint is always inline with
42/// no blueprint behind it, so unlike [`GitDoor`](super::GitDoor) there is no second copy for
43/// the placement's to lose to.
44///
45/// A map note is one value rather than a flag plus two optionals: across
46/// every waypoint in the fixture saves, `HasMapNote` is set exactly when
47/// `MapNote` is present, so the states where they disagree cannot occur and
48/// are not worth being able to build.
49#[derive(Debug, Clone, PartialEq, Default)]
50pub struct GitWaypoint {
51    /// Object tag (`Tag`).
52    pub tag: String,
53    /// Runtime object ID (`ObjectId`).
54    pub object_id: ObjectId,
55    /// Displayed name (`LocalizedName`).
56    pub localized_name: GffLocalizedString,
57    /// Accepts scripted commands (`Commandable`).
58    pub commandable: bool,
59    /// Map note, when this waypoint carries one.
60    pub map_note: Option<GitMapNote>,
61
62    /// X position (`XPosition`).
63    pub x_position: f32,
64    /// Y position (`YPosition`).
65    pub y_position: f32,
66    /// Z position (`ZPosition`).
67    pub z_position: f32,
68    /// X orientation (`XOrientation`).
69    pub x_orientation: f32,
70    /// Y orientation (`YOrientation`).
71    pub y_orientation: f32,
72    /// Z orientation (`ZOrientation`).
73    pub z_orientation: f32,
74}
75
76/// The map note a waypoint carries, if it carries one.
77#[derive(Debug, Clone, PartialEq, Default)]
78pub struct GitMapNote {
79    /// Note text (`MapNote`).
80    pub text: GffLocalizedString,
81    /// Whether the note shows on the map right now (`MapNoteEnabled`).
82    pub enabled: bool,
83}
84
85impl GitWaypoint {
86    /// Reads one `WaypointList` element.
87    pub fn from_gff_struct(s: &GffStruct) -> Self {
88        Self {
89            tag: get_string(s, "Tag").unwrap_or_default(),
90            object_id: object_id_or_invalid(s),
91            localized_name: get_locstring(s, "LocalizedName")
92                .cloned()
93                .unwrap_or_default(),
94            commandable: get_bool(s, "Commandable").unwrap_or(false),
95            // Keyed off the text, not the flag: a waypoint with no MapNote
96            // has nothing to show whatever HasMapNote claims.
97            map_note: get_locstring(s, "MapNote").cloned().map(|text| GitMapNote {
98                text,
99                enabled: get_bool(s, "MapNoteEnabled").unwrap_or(false),
100            }),
101
102            x_position: get_f32(s, "XPosition").unwrap_or(0.0),
103            y_position: get_f32(s, "YPosition").unwrap_or(0.0),
104            z_position: get_f32(s, "ZPosition").unwrap_or(0.0),
105            x_orientation: get_f32(s, "XOrientation").unwrap_or(0.0),
106            y_orientation: get_f32(s, "YOrientation").unwrap_or(0.0),
107            z_orientation: get_f32(s, "ZOrientation").unwrap_or(0.0),
108        }
109    }
110
111    /// Writes this waypoint back into a `WaypointList` element.
112    pub fn to_gff_struct(&self) -> GffStruct {
113        let mut s = GffStruct::new(5);
114        upsert_field(&mut s, "Tag", GffValue::String(self.tag.clone()));
115        upsert_field(&mut s, "ObjectId", GffValue::UInt32(self.object_id.get()));
116        upsert_field(
117            &mut s,
118            "LocalizedName",
119            GffValue::LocalizedString(self.localized_name.clone()),
120        );
121        upsert_field(
122            &mut s,
123            "Commandable",
124            GffValue::UInt8(self.commandable.into()),
125        );
126
127        upsert_field(
128            &mut s,
129            "HasMapNote",
130            GffValue::UInt8(self.map_note.is_some().into()),
131        );
132        if let Some(note) = &self.map_note {
133            upsert_field(
134                &mut s,
135                "MapNote",
136                GffValue::LocalizedString(note.text.clone()),
137            );
138            upsert_field(
139                &mut s,
140                "MapNoteEnabled",
141                GffValue::UInt8(note.enabled.into()),
142            );
143        }
144
145        upsert_field(&mut s, "XPosition", GffValue::Single(self.x_position));
146        upsert_field(&mut s, "YPosition", GffValue::Single(self.y_position));
147        upsert_field(&mut s, "ZPosition", GffValue::Single(self.z_position));
148        upsert_field(&mut s, "XOrientation", GffValue::Single(self.x_orientation));
149        upsert_field(&mut s, "YOrientation", GffValue::Single(self.y_orientation));
150        upsert_field(&mut s, "ZOrientation", GffValue::Single(self.z_orientation));
151        s
152    }
153}
154
155/// GIT `WaypointList` entry child schema (struct type 5).
156pub(crate) static WAYPOINT_LIST_CHILDREN: &[FieldSchema] = &[
157    // Dead: `LoadWaypoint`'s field list has no room for it, and a waypoint
158    // has no rendered model to select in the first place. Unrelated to the
159    // `Appearance` on a door or placeable despite the shared label.
160    FieldSchema {
161        label: "Appearance",
162        expected_type: GffType::UInt8,
163        life: FieldLife::ReadOnlyDead(
164            "a waypoint has no rendered model to select, and no waypoint load path reads the field",
165        ),
166        required: false,
167        absent: AbsentDefault::Unverified,
168        children: None,
169        constraint: None,
170    },
171    // Dead, and not for the reason a door's or placeable's is. Those have a
172    // blueprint that carries the real value and an instance copy that goes
173    // unread. A waypoint resolves no `TemplateResRef` at all, so this value
174    // has no source anywhere rather than a source that loses.
175    FieldSchema {
176        label: "Description",
177        expected_type: GffType::LocalizedString,
178        life: FieldLife::ReadOnlyDead(
179            "a waypoint resolves no template, so this value has no source anywhere rather than one that goes unread",
180        ),
181        required: false,
182        absent: AbsentDefault::Unverified,
183        children: None,
184        constraint: None,
185    },
186    FieldSchema {
187        label: "Tag",
188        expected_type: GffType::String,
189        life: FieldLife::Live,
190        required: false,
191        absent: AbsentDefault::Unverified,
192        children: None,
193        constraint: None,
194    },
195    FieldSchema {
196        label: "LocalizedName",
197        expected_type: GffType::LocalizedString,
198        life: FieldLife::Live,
199        required: false,
200        absent: AbsentDefault::Unverified,
201        children: None,
202        constraint: None,
203    },
204    FieldSchema {
205        label: "Commandable",
206        expected_type: GffType::UInt8,
207        life: FieldLife::Live,
208        required: false,
209        absent: AbsentDefault::Unverified,
210        children: None,
211        constraint: None,
212    },
213    FieldSchema {
214        label: "HasMapNote",
215        expected_type: GffType::UInt8,
216        life: FieldLife::Live,
217        required: false,
218        absent: AbsentDefault::Unverified,
219        children: None,
220        constraint: None,
221    },
222    FieldSchema {
223        label: "MapNote",
224        expected_type: GffType::LocalizedString,
225        life: FieldLife::Live,
226        required: false,
227        absent: AbsentDefault::Unverified,
228        children: None,
229        constraint: None,
230    },
231    FieldSchema {
232        label: "MapNoteEnabled",
233        expected_type: GffType::UInt8,
234        life: FieldLife::Live,
235        required: false,
236        absent: AbsentDefault::Unverified,
237        children: None,
238        constraint: None,
239    },
240    FieldSchema {
241        label: "XOrientation",
242        expected_type: GffType::Single,
243        life: FieldLife::Live,
244        required: false,
245        absent: AbsentDefault::Unverified,
246        children: None,
247        constraint: None,
248    },
249    FieldSchema {
250        label: "YOrientation",
251        expected_type: GffType::Single,
252        life: FieldLife::Live,
253        required: false,
254        absent: AbsentDefault::Unverified,
255        children: None,
256        constraint: None,
257    },
258    FieldSchema {
259        label: "ZOrientation",
260        expected_type: GffType::Single,
261        life: FieldLife::Live,
262        required: false,
263        absent: AbsentDefault::Unverified,
264        children: None,
265        constraint: None,
266    },
267    // Legitimate content, and meaningless here: waypoints have no transition
268    // capability, so a destination tag on one names a target for a transition
269    // that cannot fire. Empty in every vanilla waypoint.
270    FieldSchema {
271        label: "LinkedTo",
272        expected_type: GffType::String,
273        life: FieldLife::ReadOnlyDead(
274            "a waypoint has no transition capability, so there is nothing for a destination tag to name",
275        ),
276        required: false,
277        absent: AbsentDefault::Unverified,
278        children: None,
279        constraint: None,
280    },
281    // Present in every vanilla waypoint and read by no load path, including
282    // the script-spawn fallback, where the resref comes from the script's own
283    // CreateObject argument rather than from the file.
284    FieldSchema {
285        label: "TemplateResRef",
286        expected_type: GffType::ResRef,
287        life: FieldLife::ReadOnlyDead(
288            "no waypoint load path reads it, including the script-spawn fallback, where the resref comes from the script call",
289        ),
290        required: false,
291        absent: AbsentDefault::Unverified,
292        children: None,
293        constraint: None,
294    },
295    FieldSchema {
296        label: "ObjectId",
297        expected_type: GffType::UInt32,
298        life: FieldLife::Live,
299        required: false,
300        absent: AbsentDefault::Unverified,
301        children: None,
302        constraint: None,
303    },
304    FieldSchema {
305        label: "XPosition",
306        expected_type: GffType::Single,
307        life: FieldLife::Live,
308        required: false,
309        absent: AbsentDefault::Unverified,
310        children: None,
311        constraint: None,
312    },
313    FieldSchema {
314        label: "YPosition",
315        expected_type: GffType::Single,
316        life: FieldLife::Live,
317        required: false,
318        absent: AbsentDefault::Unverified,
319        children: None,
320        constraint: None,
321    },
322    FieldSchema {
323        label: "ZPosition",
324        expected_type: GffType::Single,
325        life: FieldLife::Live,
326        required: false,
327        absent: AbsentDefault::Unverified,
328        children: None,
329        constraint: None,
330    },
331];