UTT Format (Trigger Blueprint)
Description: The Trigger (.utt) blueprint defines invisible zones placed across level maps. While encounters spawn creatures, triggers operate as tripwires – firing scripts, acting as loading zones to new areas, or springing mechanical traps when a character crosses them.
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .utt |
| Magic Signature | UTT / V3.2 |
| Type | Trigger Blueprint |
| Rust Reference | View rakata_generics::Utt in Rustdocs |
Data Model Structure
Rakata maps a Trigger into the rakata_generics::Utt 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 & Geometry | What the trigger is and where it sits on the ground | Tag, Geometry |
| Interactive State & Sub-types | Whether the trigger acts as a loading zone, a trap, or a generic scripting boundary | Type, Cursor, HighlightHeight |
| Trap Mechanics | Trap visibility and the skill checks required to disarm | TrapType, TrapOneShot |
| Transition & Behavioral Hooks | The event scripts that fire on enter, click, leave, or disarm, plus the destination area when the trigger is a loading zone | ScriptOnEnter, LinkedTo |
rakata-lint validates these fields against the engine constraints documented below.
Engine Audits & Decompilation
(Decompilation logic for this section was entirely audited and verified via native Ghidra pipeline against swkotor.exe, explicitly pulling from CSWSTrigger::LoadTrigger at 0x0058da80.)
Structural Load Phasing
| Function | Size | Behavior |
|---|---|---|
LoadTrigger | 3381 B | The main constructor. It reads the trigger’s properties, scripts, and trap rules. |
LoadTriggerGeometry | 743 B | Reads the PointX / PointY / PointZ vertices that draw the trigger’s boundary on the floor. The vertices are stored relative to the trigger position (each PointX is vertex.x - XPosition), so world-space geometry is recovered by adding the trigger position back. |
LoadFromTemplate (0x0058ee06) | – | The blueprint-loading entry point: opens the .utt resource named by the placed instance’s TemplateResRef and calls LoadTrigger against the blueprint’s own top-level struct. |
CSWSArea::LoadTriggers (0x0050a350) | – | The area-level dispatcher that decides, per placed trigger, whether to call LoadTrigger directly on the .git instance struct (no template) or LoadFromTemplate (templated) – and, in the templated case, re-applies several instance-only fields afterward. See “Geometry and the Instance Overlay” below. |
Core Structural Findings
| Engine Rule | Runtime Behavior |
|---|---|
| Behavior Derived from Type | The engine determines the trigger’s behavior and UI cursor based on the Type field. Type 1 makes it a map transition zone. Type 2 makes it a trap. |
| OnClick Duplication Bug | The engine has a known bug where it copies the ScriptOnEnter value and uses it to overwrite the OnClick listener by default, unless explicitly overridden. The copy happens after ScriptOnEnter has already resolved its own absent-value carry-over, so OnClick’s fallback is whatever ScriptOnEnter ended up as (present value or ScriptOnEnter’s own carried-over default, see below) – not the raw constructed value directly. |
| Trap Hook Fallback | If the OnTrapTriggered script is left empty, set to null, or named "default", the engine ignores it and pulls the default script from traps.2da based on the TrapType. TrapType’s own absent default is the sentinel 0xFF (255, see the trap-flag row above); a trigger missing both TrapType and OnTrapTriggered looks up row 255 of traps.2da, which almost certainly doesn’t exist – an out-of-range lookup, not a clean “no trap” fallback. |
| Highlight Clamping | The trigger’s HighlightHeight is ignored by the engine unless it is greater than 0.0. If it is exactly zero or negative, the engine falls back to a default rendering height of 0.1. |
| Orientation Drives Geometry | When a trigger instance supplies an orientation, the engine re-rotates its geometry vertices about the trigger position by the yaw difference between the geometry’s prior frame and the new orientation (new_vertex = new_pos + R(yaw_new) * inverse(R(yaw_old)) * (vertex - old_pos)). Only the yaw is used; pitch and roll are forced to zero, and the orientation vector is normalized if not unit length. If the instance also carries an explicit Geometry list, that list is applied directly instead. Net: a trigger’s shape is position + yaw + position-relative geometry, and rewriting orientation without re-baking geometry desyncs the two. |
Geometry Is Presence-Gated, Not Context-Gated | Unlike LinkedTo/Tag/Faction below, the Geometry read inside LoadTrigger is gated only on whether the struct it was handed contains a Geometry list at all – there is no check for whether that struct came from a .utt blueprint or a .git instance. A hand-authored .utt blueprint that carried a Geometry field would have it read on the ordinary template-load path, the same way a placed instance’s would. See “Geometry and the Instance Overlay” below for how a placed instance’s own geometry, when present, ends up taking precedence anyway. |
| Contextual Loading | Fields like LinkedTo, LinkedToModule, AutoRemoveKey, Tag, and Faction are only loaded into memory when the Trigger is processed from a .git area layout file. |
| Portrait Shadowing | If PortraitId is < 0xFFFE, the engine completely ignores the Portrait string ResRef and relies entirely on the ID. Any value in the Portrait ResRef field is treated as dead data. |
| Trap Flag Fresh-Object Asymmetry | TrapDisarmable and TrapDetectable both default to 1 (disarmable, detectable) on a freshly constructed trigger, but the GFF reader’s own missing-field fallback for each is a hardcoded literal 0, ignoring the constructed value entirely. A hand-edited or third-party file that simply omits these fields loads as non-disarmable and non-detectable, the opposite of what a “fresh” trigger would suggest. TrapOneShot doesn’t share this asymmetry: its read genuinely carries over the object’s current value, and the constructor sets that value to 1 (true) before the read runs, so an absent TrapOneShot correctly resolves to 1 – the odd one out is TrapDisarmable/TrapDetectable’s literal-0 override, not a general rule about trap flags. TrapType also carries over rather than using a literal, and the constructor’s value there is the sentinel 0xFF (255), which feeds directly into the Trap Hook Fallback row below. |
| Presence-Gated Position and Orientation | SetPosition and SetOrientation are only actually applied if the corresponding position and orientation fields were present in the file at all. Every vanilla writer always emits both, so this only matters for hand-edited files that omit them entirely – the trigger then keeps whatever position or orientation it already held instead of resetting to the origin. |
| Found-Flag Preserves Prior Value | LinkedTo, LinkedToModule, AutoRemoveKey, Tag, and Faction each fall back to a genuine constant (empty string or 0) while reading, but that constant is only used to populate the read itself. If the field is absent from the file, nothing is committed to the trigger at all, so it keeps whatever value it already held rather than being reset to the constant. LinkedToFlags does not follow this pattern, despite sitting in the same “instance overlay” family documented below – it’s read with a hardcoded literal 0 default and stamped unconditionally, with no found-flag gate at all. Confirmed as a deliberate, consistent choice rather than an oversight: UTD’s LoadDoor reads its own LinkedToFlags the exact same unconditional-literal way. |
Remaining Absent-Field Defaults
All 7 script slots default to the literal string "default", not an empty resref – shared with UTD. OnDisarm, ScriptHeartbeat, ScriptOnEnter, ScriptOnExit, ScriptUserDefine, OnTrapTriggered, and OnClick are each read as a carry-over of the object’s own current script-slot value, and the CSWSTrigger constructor pre-arms every one of those slots to the literal string "default", not empty. This is the identical mechanism already confirmed for doors – the OnTrapTriggered fallback rule above (“empty, null, or literally "default"”) exists because an absent field naturally becomes "default" through this carry-over, not because the engine independently special-cases that spelling. The other six hooks have no equivalent secondary lookup: an absent one simply keeps the literal resref "default", which won’t resolve to a real script unless a module happens to ship one named exactly that.
TransitionDestin reads unconditionally regardless of source struct, matching UTD’s mechanism exactly. The field carries over the object’s own current value (an empty localized string from construction) with no found-flag gate at all – confirmed at the mechanism level, not just the observed outcome, to be the identical pattern already documented for UTD’s LoadDoor. Whatever “instance wins” behavior a templated trigger shows for this field comes entirely from CSWSArea::LoadTriggers’s own overlay step (see below), which re-reads it unconditionally off the .git struct after the template load – there’s no found-flag check at that overlay site either.
PortraitId defaults to the sentinel 0xFFFF, and Portrait is read twice. PortraitId is an unconditional literal 0xFFFF on absence, which lands squarely in the already-documented >= 0xFFFE “use string Portrait” range – so an absent PortraitId behaves identically to an explicit 0xFFFE. Portrait itself defaults to an empty resref, unconditional – but LoadTrigger reads it twice: once conditionally (only when PortraitId resolved >= 0xFFFE), and again unconditionally right after the Cursor read. The second call always runs and is what actually determines the final value, so the practical absent-field answer is simply an empty resref regardless of what PortraitId did. This double-read is specific to LoadTrigger – UTD’s LoadDoor reads Portrait only once.
The remaining fields are unremarkable unconditional literals: LocalizedName to an empty localized string, KeyName to an empty string, Cursor and Type to 0 (an absent Type satisfies neither the Transition nor Trap branch, so the Cursor override those branches can apply never fires either), SetByPlayerParty to 0, and LoadScreenID to 0.
Geometry and the Instance Overlay
Geometry’s vanilla absence from every .utt blueprint in a full install is an authoring-tool habit, not an engine restriction. CSWSArea::LoadTriggers (0x0050a350), the area-level dispatcher that walks a .git area’s placed triggers, is where the real fork happens: for a template-backed trigger it calls LoadFromTemplate (0x0058ee06), which opens the referenced .utt and calls LoadTrigger against the blueprint’s own top-level struct – exactly the same call LoadTrigger receives for a non-templated, fully-inline .git trigger. Nothing inside LoadTrigger itself treats a blueprint struct differently from an instance struct when it comes to Geometry.
What actually produces the documented “instance geometry wins” behavior is a second, explicit step: after LoadFromTemplate returns, LoadTriggers re-reads LinkedToModule, TransitionDestin (see UTD’s equivalent field for the same truncated-label pattern), LinkedTo, LinkedToFlags, position, and Geometry a second time, straight off the .git instance struct, and calls LoadTriggerGeometry directly a second time if the instance struct supplies its own Geometry list. So a template-backed trigger’s final geometry is an overlay, not a rejection: the blueprint’s geometry is read and would take effect if nothing overrode it, but a placed instance’s own Geometry – when present – is applied afterward and wins.
Legacy & Ignored Data
| Finding Type | Explanation |
|---|---|
| Legacy Engine Artifacts | As with other templates, older asset revisions include TemplateResRef, Comment, PaletteID, and PartyRequired. The engine completely ignores these. |
| Superseded Legacy Fields | Older asset revisions typically map TrapDetectDC and DisarmDC in the .utt file itself, but the engine ignores them – it calculates DCs dynamically using the rules in the .2da files instead. |
Implemented Linter Rules (Rakata-Lint)
Phase 1 (intra-resource, no context)
Implemented under rakata_lint::rules::utt.
- UTT-001 (Transition Enforcement): Warns when
Type==1(Transition) but no destination (LinkedTo,LinkedToModule, orTransitionDestin) is configured. - UTT-002 (Trap Consistency): Informs when
TrapDetectDC/DisarmDCare set (engine reads fromtraps.2da); also warns whenTrapFlag=truebutType != 2. - UTT-003 (Geometry Safety): Warns when the trigger’s geometry contains fewer than 3 vertices.
- UTT-004 (OnClick on Generic Trigger): Informs when
OnClickis set on a Generic trigger (Type==0); the event only fires for Transition triggers. - UTT-005 (Highlight Bounding): Informs when
HighlightHeight <= 0.0; the engine falls back to a default of0.1. - UTT-006 (Portrait Shadowing): Warns when
PortraitId < 0xFFFEandPortraitresref is set; the resref is ignored at runtime. - UTT-007 (PartyRequired Dead Data): Informs when
PartyRequiredis set; the K1 engine never reads this field.
Phase 2 (range / 2DA / resref existence, requires LintContext)
Implemented under rakata_lint::rules::utt_range.
- UTT-008 (Portrait Bounds): Errors when
PortraitId(when not the0xFFFE“use string Portrait” sentinel) does not resolve to a row inportraits.2da. - UTT-009 (Resref Existence): Warns when any of
OnDisarm,OnTrapTriggered,OnClick,OnHeartbeat,OnEnter,OnExit, orOnUserDefined(.ncs), orPortrait(.tga), does not resolve in the configured resource sources.LinkedToModule(area transition) is deferred to Phase 3.
Pending
- Default Script Identification: Identifies empty / null / literally-named
"default"OnTrapTriggeredentries that silently invoke thetraps.2dafallback.