UTW Format (Waypoint Blueprint)
Description: The Waypoint (.utw) blueprint defines static reference coordinates within an area map. Unlike functional triggers or physical placeables, waypoints act exclusively as invisible logic markers. They provide coordinate anchors for creature patrol routes, spawn locations, camera focal points, or visible map pins in the player’s UI.
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .utw |
| Magic Signature | UTW / V3.2 |
| Type | Waypoint Blueprint |
| Rust Reference | View rakata_generics::Utw in Rustdocs |
Data Model Structure
Rakata maps a Waypoint into the rakata_generics::Utw struct. The struct’s Rustdocs document every field’s binary schema and GFF mapping; the table below is the high-level anatomy.
| Category | Covers | Representative fields |
|---|---|---|
| Core Identity | The waypoint’s name and the tag that scripts target | Tag, LocalizedName |
| Spatial Geometry | The map coordinates and facing that creatures or cameras reference | XPosition, XOrientation |
| Map Navigation Notes | Whether the waypoint draws a pin on the player’s mini-map, and the pin’s text | HasMapNote, MapNote |
rakata-lint validates these fields against the engine constraints documented below.
Engine Audits & Decompilation
The following documents the engine’s exact load sequence and field requirements for .utw files mapped from swkotor.exe.
(Decompilation logic for this section was audited and verified via native Ghidra pipeline against swkotor.exe, explicitly pulling from CSWSWaypoint::LoadWaypoint at 0x005c7f30.)
Structural Load Phasing
| Function | Size | Behavior |
|---|---|---|
LoadWaypoint | 682 B | The main constructor. It loads the waypoint’s identity, map geometry, and checks for mini-map pins. |
LoadFromTemplate (0x005c83b0) | 134 B | A fallback used when dynamically spawning a waypoint from a script. It is a thin wrapper: open the .utw file’s own GFF, fetch its top-level struct, and hand off to LoadWaypoint – there is no separate field-reading logic for the script-spawn path. |
Core Structural Findings
| Engine Rule | Runtime Behavior |
|---|---|
| Map Note Two-Gate Pattern | The skip covers more than the note text. If HasMapNote is 0 or missing, the engine never even attempts the read for MapNoteEnabled or MapNote – there’s no fallback fetch for either, they’re simply not asked for. If HasMapNote resolves to 1, both MapNoteEnabled and MapNote are read (each with its own default if individually absent), but none of the three values – not HasMapNote, not MapNoteEnabled, not MapNote – actually lands on the waypoint unless MapNote itself was genuinely present in the file. Absent MapNote on an otherwise-HasMapNote=1 file discards the whole trio silently, leaving the waypoint at its constructed defaults (HasMapNote=0, MapNoteEnabled=0, MapNote empty) exactly as if none of the three fields had been touched. |
| Orientation Normalization | The engine computes the true magnitude of the orientation vector (not just the squared value) and calls Vector::Normalize() whenever it isn’t exactly 1.0. That function has its own epsilon guard: below a magnitude of 1e-9 it doesn’t divide at all – it snaps straight to a sentinel facing of (1.0, 0.0, 0.0). So a waypoint with all three orientation fields absent (a zero vector) doesn’t produce garbage or a divide-by-zero; it lands on that exact sentinel. This is a different code path and a different fallback than the (0, 1, 0) sentinel documented elsewhere in this codebase for area-effect objects’ orientation – the two object types don’t share the normalization call, and their epsilon thresholds and fallback vectors both differ, so don’t assume one from the other. |
| Position Override | When a waypoint is loaded from a .git area layout via LoadWaypoints, the engine re-reads the X and Y coordinates directly from the .git file, completely overriding the .utw. It also forcefully calculates the Z height based on the terrain collision mesh via ComputeHeight. This is a second read layered on top of the one below, not a replacement for it: LoadWaypoint itself always reads position and orientation first, from whichever struct it’s handed, blueprint or GIT instance alike. |
| Dynamic Identification | Waypoints never pull an ObjectId from their own .utw file. It is always forcibly assigned by the .git list element (defaulting to 0x7f000000). |
| No Template Path for Placed Waypoints | The area’s UseTemplates flag – which switches triggers, stores, and sounds between a save-snapshot read and a template-blueprint read – is accepted by LoadWaypoints but never inspected. Every waypoint placed in a .git layout is always a full inline read of the waypoint struct. Confirmed by decompilation: LoadWaypoint never reads a field named TemplateResRef at all, not even a discarded read – unlike doors (see UTD), a placed waypoint genuinely never resolves a blueprint. LoadFromTemplate only matters for a waypoint a script spawns dynamically at runtime, and even there the resref it opens comes from the script’s own CreateObject() argument (its sole caller is ExecuteCommandCreateObject), not from a TemplateResRef GFF field – that field name is never looked up anywhere in the waypoint-loading code, under any circumstance. |
| Blueprint Placement Fields Are Real, Not GIT-Exclusive | LoadWaypoint unconditionally reads XPosition, YPosition, ZPosition, XOrientation, YOrientation, and a sixth field rakata does not currently model, ZOrientation – a full 3-component orientation vector, not the 2-component pair the typed struct exposes – and applies all of them via SetPosition/SetOrientation before it reads anything else. Each of the six falls back to a literal 0.0 if individually absent, with no presence check consulted afterward; see “Orientation Normalization” above for what a fully-absent orientation vector resolves to once normalization runs. This runs identically whether the source struct came from a .git instance or a .utw blueprint opened through LoadFromTemplate. A script-spawned waypoint (LoadFromTemplate) has no GIT instance to override it afterward, so a hand-authored .utw carrying these fields would have its placement taken from the blueprint directly and durably, not transiently. Only a waypoint placed in a .git area layout gets the X/Y/Z override described above; vanilla blueprints simply never author the fields because there’s no vanilla workflow that needs a script-spawned waypoint’s own position baked into the template. |
Tag and LocalizedName Are Unconditional Stamps | Both are read with an empty-value literal default (empty string for Tag, an empty LocalizedString for LocalizedName) and applied unconditionally – LoadWaypoint’s own presence flag for each read is captured and then never inspected. An absent Tag doesn’t leave a prior value in place; it overwrites with a literal empty string every time, the same for LocalizedName. |
Legacy & Ignored Data
| Finding Type | Explanation |
|---|---|
| Superseded Legacy Fields | Older asset revisions pad the file with fields like TemplateResRef, Appearance, PaletteID, Comment, LinkedTo, and Description. The KOTOR engine completely ignores these. |
LinkedToModule Shares a Label With UTD’s, Not Its Behaviour | A handful of .utw files carry a LinkedToModule CResRef, always empty. LoadWaypoint and LoadFromTemplate were both fully decompiled and read only Tag, LocalizedName, position, orientation, and the map-note fields – nothing resembling an area-transition field. The LinkedToModule string that does exist in the binary is referenced exclusively from Door and Trigger code (LoadDoor, LoadDoorExternal, SaveDoor, LoadTrigger, LoadTriggers, SaveTrigger), never from waypoint code. Waypoints have no area-transition capability in this engine at all; the two fields merely share interned label bytes, not a concept. |
A Placed Waypoint’s Appearance and Description Have No Source At All | Git.WaypointList[].Appearance (a BYTE) is present with a real value in every waypoint entry in a full install, and Description shows up in a handful too – but LoadWaypoint’s full field list, confirmed above, has no room for either. Compare Door and Placeable, where a placed instance’s Description is toolset residue but the value at least comes from somewhere (the referenced blueprint) – waypoints resolve no TemplateResRef at all, so there’s no blueprint to fall back to either. A placed waypoint’s Appearance and Description are read from nowhere, full stop: not the instance, not a template, because neither field name is ever looked up by this loader under any circumstance. |
Implemented Linter Rules (Rakata-Lint)
These diagnostics are implemented under rakata_lint::rules::utw.
- UTW-001 (Map Note Double-Gating): Warns when
MapNoteorMapNoteEnabledare populated butHasMapNote=false; this data is silently discarded by the engine. - UTW-002 (Orientation Warnings): Informs when the orientation vector magnitude is not within ~0.001 of 1.0; the engine forcibly normalizes at load.
Pending
- Tag Enforcement: Flags empty
Tagvalues since waypoints are primarily targeted by name from scripts.