Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

PropertyValue
Extension(s).utt
Magic SignatureUTT / V3.2
TypeTrigger Blueprint
Rust ReferenceView 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.

CategoryCoversRepresentative fields
Core Identity & GeometryWhat the trigger is and where it sits on the groundTag, Geometry
Interactive State & Sub-typesWhether the trigger acts as a loading zone, a trap, or a generic scripting boundaryType, Cursor, HighlightHeight
Trap MechanicsTrap visibility and the skill checks required to disarmTrapType, TrapOneShot
Transition & Behavioral HooksThe event scripts that fire on enter, click, leave, or disarm, plus the destination area when the trigger is a loading zoneScriptOnEnter, 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

FunctionSizeBehavior
LoadTrigger3381 BThe main constructor. It reads the trigger’s properties, scripts, and trap rules.
LoadTriggerGeometry743 BReads 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 RuleRuntime Behavior
Behavior Derived from TypeThe 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 BugThe 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 FallbackIf 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 ClampingThe 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 GeometryWhen 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-GatedUnlike 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 LoadingFields like LinkedTo, LinkedToModule, AutoRemoveKey, Tag, and Faction are only loaded into memory when the Trigger is processed from a .git area layout file.
Portrait ShadowingIf 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 AsymmetryTrapDisarmable 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 OrientationSetPosition 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 ValueLinkedTo, 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 LoadTriggerUTD’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 TypeExplanation
Legacy Engine ArtifactsAs with other templates, older asset revisions include TemplateResRef, Comment, PaletteID, and PartyRequired. The engine completely ignores these.
Superseded Legacy FieldsOlder 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.

  1. UTT-001 (Transition Enforcement): Warns when Type==1 (Transition) but no destination (LinkedTo, LinkedToModule, or TransitionDestin) is configured.
  2. UTT-002 (Trap Consistency): Informs when TrapDetectDC/DisarmDC are set (engine reads from traps.2da); also warns when TrapFlag=true but Type != 2.
  3. UTT-003 (Geometry Safety): Warns when the trigger’s geometry contains fewer than 3 vertices.
  4. UTT-004 (OnClick on Generic Trigger): Informs when OnClick is set on a Generic trigger (Type==0); the event only fires for Transition triggers.
  5. UTT-005 (Highlight Bounding): Informs when HighlightHeight <= 0.0; the engine falls back to a default of 0.1.
  6. UTT-006 (Portrait Shadowing): Warns when PortraitId < 0xFFFE and Portrait resref is set; the resref is ignored at runtime.
  7. UTT-007 (PartyRequired Dead Data): Informs when PartyRequired is set; the K1 engine never reads this field.

Phase 2 (range / 2DA / resref existence, requires LintContext)

Implemented under rakata_lint::rules::utt_range.

  1. UTT-008 (Portrait Bounds): Errors when PortraitId (when not the 0xFFFE “use string Portrait” sentinel) does not resolve to a row in portraits.2da.
  2. UTT-009 (Resref Existence): Warns when any of OnDisarm, OnTrapTriggered, OnClick, OnHeartbeat, OnEnter, OnExit, or OnUserDefined (.ncs), or Portrait (.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" OnTrapTriggered entries that silently invoke the traps.2da fallback.