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)

A .utt file is a trigger: an invisible polygon on the floor that does something when a character walks into it. That something is one of three things, running a script, moving the party to another area, or springing a trap.

At a Glance

PropertyValue
Extension(s).utt
Magic SignatureUTT / V3.2
TypeTrigger Blueprint
Rust ReferenceView rakata_generics::Utt in Rustdocs

Field Schema

The format’s field families, as an orientation before the full list.

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 & Behavioural HooksThe event scripts that fire on enter, click, leave, or disarm, plus the destination area when the trigger is a loading zoneScriptOnEnter, LinkedTo

Engine Audits & Decompilation

Read from CSWSTrigger::LoadTrigger at 0x0058da80 in swkotor.exe. Provenance: derived, not attested. The rows below have not been separately re-derived, so they sit on the reverse-engineering queue.

The load path

FunctionSizeBehaviour
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)n/aThe 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)n/aThe area-level dispatcher. Per placed trigger it calls LoadTrigger directly on the .git instance struct, or LoadFromTemplate where one applies, re-applying several instance-only fields afterward. See “Geometry and the Instance Overlay” below.

Both load paths start from the same constructed object. Every placed trigger is allocated and run through CSWSTrigger’s real constructor, which seeds all seven script slots to "default" and TrapType to 0xFF, before the dispatcher looks at whether a template applies. LoadTrigger takes no caller-context parameter, so the save-instance call and the templated call invoke it identically.

So an absent script hook or trap field on a save-restored trigger resolves to the same constructed default as on the blueprint path, with no divergence between them.

Rules the engine enforces

Engine RuleRuntime Behaviour
Behaviour Derived from TypeThe Type field decides the trigger’s behaviour and UI cursor. Type 1 makes it a map transition zone. Type 2 makes it a trap.
OnClick Duplication BugThe engine copies the ScriptOnEnter value over the OnClick listener by default, unless OnClick is 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, a present value or ScriptOnEnter’s own carried-over default (see below), rather than the raw constructed value.
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), so a trigger missing both TrapType and OnTrapTriggered looks up row 255 of traps.2da, which almost certainly doesn’t exist. That is 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 GeometryAn instance orientation re-rotates the geometry vertices about the trigger position. See below.
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, with 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 ID decides and the Portrait string ResRef is dead data.
Presence-Gated Position and OrientationSetPosition and SetOrientation are applied only where the corresponding fields were present. Every vanilla writer emits both, so this matters for hand-edited files: the trigger keeps whatever it already held rather than resetting to the origin.

Orientation and geometry are two halves of one shape

An instance orientation re-rotates the 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 normalised where it is not unit length. Where the instance also carries an explicit Geometry list, that list is applied directly instead.

So a trigger’s shape is position plus yaw plus position-relative geometry, and rewriting the orientation without re-baking the geometry desyncs the two.

Two trap flags ignore their constructed value; one does not

TrapDisarmable and TrapDetectable are both 1 on a freshly constructed trigger, but each read’s missing-field fallback is a hardcoded literal 0 that ignores the constructed value. A file omitting them loads as non-disarmable and non-detectable, the opposite of what a fresh trigger suggests.

TrapOneShot does not share that. Its read carries over the object’s current value, which the constructor set to 1, so an absent TrapOneShot resolves to 1.

TrapType also carries over, from a constructed 0xFF, which is what feeds the trap-hook fallback above. So the literal-0 override is specific to TrapDisarmable and TrapDetectable, not a rule about trap flags.

An absent field usually commits nothing, and LinkedToFlags is the exception

LinkedTo, LinkedToModule, AutoRemoveKey, Tag and Faction each name a constant fallback while reading, but that constant only populates the read. Where the field is absent nothing is committed, so the trigger keeps whatever it already held.

LinkedToFlags does not follow that, despite sitting in the same instance-overlay family below: it reads with a hardcoded literal 0 and is stamped unconditionally, with no found-flag gate. That is deliberate rather than an oversight: UTD’s LoadDoor reads its own LinkedToFlags the same unconditional way.

Remaining Absent-Field Defaults

Note

Which traps.2da columns, since it is not one column and it differs by object type A trigger reads its default script from TrapScript; a door or a placeable reads MineScript. The two are separate columns and the wrong one silently supplies the wrong script.

The DC columns split the same way. Only triggers consult DisarmDCMod and DetectDCMod; doors and placeables take their disarm and detect DCs from their own GFF fields and never touch those columns at all.

So all three blueprint types route to this table and none of them routes to the same place.

Every script slot defaults to the literal string "default" rather than 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 rest 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

It 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 rather than from the observed outcome, to be the identical pattern already documented for UTD’s LoadDoor. Whatever “instance wins” behaviour 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, with no found-flag check at that overlay site either.

PortraitId and Portrait

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, where UTD’s LoadDoor reads Portrait only once.

The remaining literals

These are unconditional:

FieldAbsent value
LocalizedNameempty localized string
KeyNameempty string
Cursor, Type0
SetByPlayerParty, LoadScreenID0
CreatorId0x7F000000

An absent Type satisfies neither the transition nor the trap branch, so the Cursor override those branches can apply never fires either.

CreatorId is an object reference, not a plain integer, defaulting to the same 0x7F000000 sentinel as AreaId and GIT’s LastEntered/LastLeft. CSWSTrigger’s constructor sets that same literal independently of the read. Unlike AreaId, where the constructor and the read-call literal disagree, a trigger’s CreatorId lands on the sentinel either way.

Geometry and the Instance Overlay

Geometry’s absence from every vanilla .utt blueprint is an authoring-tool habit, not an engine restriction. LoadTrigger gets the same call whether its struct came from a blueprint or from a fully inline .git trigger, and reads Geometry identically either way.

The fork is one level up, in CSWSArea::LoadTriggers (0x0050a350). For a template-backed trigger it calls LoadFromTemplate (0x0058ee06), which opens the .utt and runs LoadTrigger against the blueprint. On return it re-reads a set of fields straight off the .git instance struct: LinkedToModule, TransitionDestin (see UTD’s equivalent field for the same truncated-label pattern), LinkedTo, LinkedToFlags, position and Geometry, calling LoadTriggerGeometry a second time where the instance carries its own list.

So instance geometry is an overlay, not a rejection. The blueprint’s geometry is read and would stand if nothing replaced it.

Fields the engine never reads

What a writer should do with each is a separate question, and it has four possible answers: see the engine ignores this is not you may leave it out.

Finding TypeExplanation
Legacy Engine ArtifactsAs with other templates, older asset revisions include TemplateResRef, Comment, PaletteID and PartyRequired. LoadTrigger reads none of them.
Superseded Legacy FieldsOlder asset revisions typically map TrapDetectDC and DisarmDC in the .utt file itself, but LoadTrigger never reads them. The DCs come from the traps.2da columns above 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.

Every label the schema declares

Generated from the schema, so no label can be quietly left out. How to read these tables.

What the engine does with each field

FieldTypeEngineWhen absent
LinkedToCExoStringnever reads it: LoadTriggers re-reads this straight off the .git instance struct after the template load, with no found-flag check at the overlay site unlike Geometry beside it, and a .utt blueprint only ever reaches LoadTrigger through that branchNOT EXAMINED; we substitute ""
LinkedToFlagsBYTEnever reads it: LoadTriggers re-reads this straight off the .git instance struct after the template load, with no found-flag check at the overlay site unlike Geometry beside it, and a .utt blueprint only ever reaches LoadTrigger through that branchNOT EXAMINED; we substitute 0
LinkedToModuleCResRefnever reads it: LoadTriggers re-reads this straight off the .git instance struct after the template load, with no found-flag check at the overlay site unlike Geometry beside it, and a .utt blueprint only ever reaches LoadTrigger through that branchNOT EXAMINED; we substitute ""
TransitionDestinCExoLocStringnever reads it: LoadTriggers re-reads this straight off the .git instance struct after the template load, with no found-flag check at the overlay site unlike Geometry beside it, and a .utt blueprint only ever reaches LoadTrigger through that branchNOT EXAMINED; we substitute empty
PartyRequiredBYTEnever reads it: read by nothing on the trigger pathnot one constant; we substitute 0

Fields nobody has examined

Whether the engine reads these has not been established, which is not the same as establishing that it does not. Where When absent carries an answer, that half is settled.

FieldTypeWhen absent
TagCExoStringkeeps ""
LocalizedNameCExoLocStringstamps empty
FactionDWORDkeeps 0
CursorBYTEstamps 0
KeyNameCExoStringstamps ""
PortraitIdWORDstamps 65535
PortraitCResRefstamps ""
ScriptHeartbeatCResRefkeeps "default"
ScriptOnEnterCResRefkeeps "default"
ScriptOnExitCResRefkeeps "default"
ScriptUserDefineCResRefkeeps "default"
OnTrapTriggeredCResRefkeeps "default"
OnDisarmCResRefkeeps "default"
OnClickCResRefkeeps "default"
TrapTypeBYTEkeeps 255
TrapOneShotBYTEkeeps 1
TrapDisarmableBYTEstamps 0
TrapDetectableBYTEstamps 0
AutoRemoveKeyBYTEkeeps 0
TypeINTstamps 0
HighlightHeightFLOATNOT EXAMINED; we substitute 0.0
LoadScreenIDWORDstamps 0
SetByPlayerPartyBYTEstamps 0
GeometryListnot one constant; we substitute container
Geometry[].PointXFLOATNOT EXAMINED; we substitute 0.0
Geometry[].PointYFLOATNOT EXAMINED; we substitute 0.0
Geometry[].PointZFLOATNOT EXAMINED; we substitute 0.0
TemplateResRefCResRefNOT EXAMINED; we substitute ""
CommentCExoStringNOT EXAMINED; we substitute ""
PaletteIDBYTENOT EXAMINED; we substitute 0
TrapDetectDCBYTEstamps 0
DisarmDCBYTEstamps 0
TrapFlagBYTEstamps 0
CreatorIdDWORDstamps 2130706432
XPositionFLOATnot one constant; we substitute 0.0
YPositionFLOATnot one constant; we substitute 0.0
ZPositionFLOATnot one constant; we substitute 0.0
XOrientationFLOATnot one constant; we substitute 0.0
YOrientationFLOATnot one constant; we substitute 0.0
ZOrientationFLOATnot one constant; we substitute 0.0