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

UTD Format (Door Blueprint)

A .utd file is a door: what it looks like, whether it is locked and what opens it, any trap sitting on it, and where it leads when it doubles as an area transition. Its scripts cover the events a door has, being opened, forced, unlocked or destroyed.

At a Glance

PropertyValue
Extension(s).utd
Magic SignatureUTD / V3.2
TypeDoor Blueprint
Rust ReferenceView rakata_generics::Utd in Rustdocs

Field Schema

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

CategoryCoversRepresentative fields
Core Identity & GeometryWhat the door looks like, its faction, and the text displayed when targetedAppearance, TemplateResRef, LocName
Lock & Trap MechanicsWhether the door is locked, which key opens it, and the rules for attached trapsLocked, KeyName, TrapType, DisarmDC
Transition PathwaysThe linked destination used when the door acts as a loading zone to another areaLinkedTo, LinkedToFlags
Behavioural HooksThe scripts that run when a player opens, destroys, or fails to unlock the doorOnOpen, OnFailToOpen, OnMeleeAttacked

Engine Audits & Decompilation

Read from the shared field-reading routine CSWSDoor::LoadDoor at 0x0058a1f0; see “Save versus Template Load Paths” below for the actual top-level dispatcher 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

LoadDoor reads the door’s fields in four groups.

DomainSub-fields EvaluatedPurpose
Scales & State22Reads the physical health, visual appearance, and base traits determining whether the door is locked or indestructible.
Hooks15Attaches custom event scripts that fire when the door is opened, forced, unlocked, or trapped.
Mechanical9Configures the lock difficulty tiers and the specific skill hurdles required to detect and disarm any attached traps.
Transitions4Links the door strictly to another area (.are), turning it into a physical loading screen transition node.

Save versus Template Load Paths

A single UseTemplates flag, forwarded from the area loader, decides how a door’s fields are populated. The branch point is CSWSDoor::LoadDoorExternal (0x0058c5f0); LoadDoor (0x0058a1f0) is the shared field-reading routine both branches call.

UseTemplatesWhat happens
clearLoadDoorExternal calls LoadDoor on the door’s full instance snapshot out of the savegame. Every field is already in that struct.
setLoadDoorExternal calls CSWSDoor::LoadFromTemplate (0x0058b468), which reads TemplateResRef, opens the blueprint, and calls the same LoadDoor against it.

Four instance-only fields are overlaid back afterward on the template path, because a blueprint does not know which instance it belongs to: TransitionDestin, LinkedTo, LinkedToFlags and LinkedToModule. (TransitionDestin is the on-disk 16-byte-truncated label for what the engine’s own source calls TransitionDestination.)

The overlay is unconditional whenever the template branch is taken and the blueprint load succeeds. The only way to skip it is for the blueprint load to fail on an empty or unresolvable TemplateResRef, which aborts the whole door load rather than just the overlay.

LoadDoor makes no distinction between callers, so on a templated door TransitionDestin genuinely is read off the blueprint and then immediately overwritten. A hand-authored .utd carrying it is read and discarded the same way.

Both paths start from the same constructed object

CSWSArea::LoadDoors allocates every placed door and runs CSWSDoor’s real constructor before it inspects UseTemplates at all. There is no lighter-weight allocation for a save-restored door that skips the constructor’s seeding.

So an absent script hook on a save-restored door resolves to "default", and absent trap settings resolve to the constructed values documented below (TrapType 0xFF, TrapDetectable/TrapDisarmable/TrapOneShot 1, TrapFlag/TrapDetectDC/DisarmDC 0) identically on both paths.

Tag has instance-only stakes and is not overlaid

LoadDoor holds the only Tag read in the whole door-load call graph, so on a templated door Tag comes from the blueprint like Appearance or HP, and nothing re-reads it from the placed instance.

That is a real gap in the overlay mechanism. Several doors sharing one blueprint would share one Tag, breaking any script targeting a door by tag.

It does not bite in practice, because the convention works around it: every door instance in a full install carries a distinct TemplateResRef, one blueprint per placed door. The mechanism is templated; the authoring is not.

This isn’t a door-specific quirk: every templated GIT object type behaves identically. See GIT’s “The Blueprint’s Tag Always Wins” for the general pattern across Placeables, Triggers, Sounds, Stores, Encounters, Creatures, and Items.

Rules the engine enforces

Engine RuleRuntime Behaviour
Appearance TruncationThe engine reads Appearance as a 32-bit integer and keeps its low byte alone. An id above 255 therefore arrives as a different row of the appearance table, and the door renders as whatever model that row names.
Static EnforcementA door marked Static has plot forced to 1, so static level architecture cannot be destroyed.
Trap Hook FallbackIf the OnTrapTriggered script is left empty, set to null, or literally named "default", the engine pulls the default standard script from traps.2da instead.
HP SynchronizationCurrentHP is clamped against the door’s maximum HP, but only on the template load path. A direct savegame load takes the raw saved CurrentHP value with no clamp applied.
No Other OmissionsAside from the Portrait fork below, the save routine writes every door field unconditionally. No other door field is ever left out of a vanilla save.

The portrait fork, and why the fully-absent case still lands on 0x22E

PortraitId selects between two branches:

PortraitIdBehaviour
0Hardcoded to 0x22E.
< 0xFFFEThe ID is used and the Portrait resref is dead data.
>= 0xFFFEThe Portrait resref is consulted instead.

An absent PortraitId defaults to 0xFFFF, matching UTC, UTT and UTP. Doors have a single read site in LoadDoor, shared by both callers, so that is the only default rather than a save-path-specific one.

0xFFFF lands on the string branch. But an absent Portrait then defaults to an empty resref, and an empty resref routes into the same hardcoded-portrait call with the same 0x22E argument. So a door missing both fields reaches 0x22E anyway, by the string branch’s fallback rather than by the ID default diverging from its siblings.

Absent-field defaults

Every read goes through a CResGFF::ReadField* call taking a fallback argument. That fallback is either the object’s own current member, a true carry-over from whatever the constructor set, or a fresh literal at the read site that ignores the member. The two are indistinguishable from the resulting value, so each field is listed with which mechanism applies.

Note

traps.2da columns differ by object type A door or placeable reads its default trap script from MineScript, where a trigger reads TrapScript. Doors and placeables also take their disarm and detect DCs from their own GFF fields rather than from the table’s DisarmDCMod and DetectDCMod, which only triggers consult. See UTT for the trigger side.

Every script hook defaults to the literal string "default", not an empty resref. The CSWSDoor constructor loops over every script slot, OnClosed, OnDamaged, OnDeath, OnDisarm, OnHeartbeat, OnLock, OnMeleeAttacked, OnOpen, OnSpellCastAt, OnTrapTriggered, OnUnlock, OnUserDefined, OnClick, OnFailToOpen and OnDialog, assigning that literal, and every hook read carries it over.

That is the mechanism behind the OnTrapTriggered fallback above: an absent hook becomes "default", so the engine is not special-casing three spellings of empty. The rest have no secondary lookup and keep the resref "default", which resolves to nothing unless a module ships an .ncs by that exact name.

TrapType

Its absent default is the sentinel 0xFF (255), not 0, and it can chain into an out-of-range lookup. Combined with the OnTrapTriggered-absent-becomes-"default" carry-over above: a door missing both TrapType and OnTrapTriggered ends up looking up row 255 of traps.2da, which almost certainly doesn’t exist in vanilla data. That’s an out-of-range 2DA lookup, not a clean “no trap configured” state, and worth a lint rule of its own distinct from the already-flagged OnTrapTriggered fallback.

Lockable, HP, and the three fresh literals

Lockable defaults to 0, so a door is not lockable unless the file says so. Same carry-over mechanism as most fields here (constructor sets lockable = 0), against the more intuitive assumption that a door would be lockable by default.

HP (maximum) carries over a constructed default of 1, not 0. A freshly constructed door object nominally has 1 hit point before any GFF read touches it; an absent HP field leaves it there.

Hardness, Static, and LoadScreenID use a fresh literal 0 rather than carry-over: a different mechanism reaching the same value. Where most numeric fields on this page pass the object’s current member as the read’s fallback, these three pass a hardcoded 0 regardless of what the constructor set. That is also 0 for Hardness and Static, so the observable value is identical either way. Only the mechanism differs.

Plot and Invulnerable

They are entangled, and the gate is unresolved. LoadDoor reads a field named "Invulnerable" first, then conditionally reads "Plot", which can overwrite the first result. Both use the same carry-over fallback (this->object.plot, constructed 0).

The condition gating that second read traces to a stack value with no discoverable prior write in LoadDoor or either known caller. (Provenance: not traced. Looked and could not settle it.)

Whichever branch fires, both reads share a fallback, so an absent Plot and an absent Invulnerable both resolve to 0, unless Static is true, which forces 1 per the rule above. Note that Invulnerable is not part of the documented UTD schema and the toolset does not write it, but the engine reads it if a file supplies one.

OpenState

It carries an override that never fires in practice. The field carries over from a constructed 0, and the result then feeds a check that can force a hardcoded 3, gated on an internal flag the constructor sets to 1 and nothing in LoadDoor resets. Under the normal construct-then-load flow the override cannot fire, so an absent OpenState resolves to 0. It is recorded in case some unexamined path clears that flag first.

The ordinary carry-overs

Carry-over from the constructor:

Constructed valueFields
0Faction, GenericType, AutoRemoveKey, KeyRequired, OpenLockDC, CloseLockDC, SecretDoorDC, Fort, Ref, Will, DisarmDC, TrapDetectDC, TrapFlag, Min1HP, Locked (unlocked)
0.0Bearing
1 (true)TrapDetectable, TrapDisarmable, TrapOneShot

Fresh literal at the read site rather than carry-over, with the same practical value: LocName and Description to an empty localized string, Conversation to an empty resref, and Tag to an empty string, the one string field routed through SetTag.

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 ArtifactsAnimationState, NotBlastable, OpenLockDiff, OpenLockDiffMod, Comment, Interruptable and PaletteID are never read anywhere in LoadDoor, confirmed by a full-text search of its decompiled body. That is the complete list rather than a sample. Whatever storage the struct carries for these, if any, stays at whatever the constructor set regardless of on-disk GFF content.

Implemented Linter Rules (Rakata-Lint)

Phase 1 (intra-resource, no context)

Implemented under rakata_lint::rules::utd.

  1. UTD-001 (Static Parity): Warns when Static=true but Plot=false; the engine forces Plot to true at runtime.
  2. UTD-002 (HP Bounds): Errors when CurrentHP > HP; the engine clamps to HP on template load.
  3. UTD-003 (Portrait Shadowing): Warns when PortraitId < 0xFFFE and Portrait resref is set; the resref is ignored at runtime.

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

Implemented under rakata_lint::rules::utd_range.

  1. UTD-004 (Generic Door Type Bounds): Errors when GenericType does not resolve to a row in genericdoors.2da; engine renders missing model.
  2. UTD-005 (Portrait Bounds): Errors when PortraitId (when not the 0xFFFE “use string Portrait” sentinel) does not resolve to a row in portraits.2da.
  3. UTD-006 (Resref Existence): Warns when Conversation (.dlg), Portrait (.tga), or any On* script hook (.ncs) does not resolve in the configured resource sources. LinkedToModule (area transition) is deferred to Phase 3.

Pending

  • Appearance Truncation: Flags legacy Appearance (u32) values above 255 (engine truncates to a single byte).
  • Trap Hook Fallback Detection: Scans for empty / null / literally-named "default" OnTrapTriggered references that silently invoke the traps.2da fallback.
  • Portrait Zero Hardcode: Detects PortraitId == 0 mappings since the engine hardcodes lookup to 0x22E.

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
OpenLockDiffBYTEnever reads it: a full-text search of LoadDoor’s decompiled body lists exactly seven fields never read anywhere in the function, and this is one of themnot one constant; we substitute 0
OpenLockDiffModCHARnever reads it: a full-text search of LoadDoor’s decompiled body lists exactly seven fields never read anywhere in the function, and this is one of themnot one constant; we substitute 0
NotBlastableBYTEnever reads it: a full-text search of LoadDoor’s decompiled body lists exactly seven fields never read anywhere in the function, and this is one of themnot one constant; we substitute 0
LinkedToFlagsBYTEnever reads it: LoadDoorExternal overlays this from the save instance immediately after LoadDoor returns, unconditionally whenever the template branch is taken, and a .utd blueprint only ever reaches LoadDoor through that branchNOT EXAMINED; we substitute 0
LinkedToCExoStringnever reads it: LoadDoorExternal overlays this from the save instance immediately after LoadDoor returns, unconditionally whenever the template branch is taken, and a .utd blueprint only ever reaches LoadDoor through that branchNOT EXAMINED; we substitute ""
LinkedToModuleCResRefnever reads it: LoadDoorExternal overlays this from the save instance immediately after LoadDoor returns, unconditionally whenever the template branch is taken, and a .utd blueprint only ever reaches LoadDoor through that branchNOT EXAMINED; we substitute ""
TransitionDestinCExoLocStringnever reads it: LoadDoorExternal overlays this from the save instance immediately after LoadDoor returns, unconditionally whenever the template branch is taken, and a .utd blueprint only ever reaches LoadDoor through that branchNOT EXAMINED; we substitute empty

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
TemplateResRefCResRefNOT EXAMINED; we substitute ""
TagCExoStringkeeps ""
LocNameCExoLocStringstamps empty
DescriptionCExoLocStringstamps empty
CommentCExoStringNOT EXAMINED; we substitute ""
ConversationCResRefstamps ""
FactionDWORDkeeps 0
GenericTypeBYTEkeeps 0
AppearanceDWORDNOT EXAMINED; we substitute 0
OpenStateBYTEkeeps 0
AnimationStateBYTENOT EXAMINED; we substitute 0
BearingFLOATkeeps 0.0
LockableBYTENOT EXAMINED; we substitute 0
LockedBYTEkeeps 0
KeyRequiredBYTEkeeps 0
KeyNameCExoStringNOT EXAMINED; we substitute ""
AutoRemoveKeyBYTEkeeps 0
OpenLockDCBYTEkeeps 0
CloseLockDCBYTEkeeps 0
SecretDoorDCBYTEkeeps 0
CurrentHPSHORTNOT EXAMINED; we substitute 0
HPSHORTkeeps 1
HardnessBYTEstamps 0
FortBYTEkeeps 0
RefBYTEkeeps 0
WillBYTEkeeps 0
PlotBYTEkeeps 0
InvulnerableBYTEkeeps 0
Min1HPBYTEkeeps 0
StaticBYTEstamps 0
InterruptableBYTENOT EXAMINED; we substitute 0
PortraitIdWORDstamps 65535
PortraitCResRefstamps ""
PaletteIDBYTENOT EXAMINED; we substitute 0
TrapDetectableBYTEkeeps 1
TrapDetectDCBYTEkeeps 0
TrapDisarmableBYTEkeeps 1
DisarmDCBYTEkeeps 0
TrapFlagBYTEkeeps 0
TrapOneShotBYTEkeeps 1
TrapTypeBYTEkeeps 255
OnClosedCResRefkeeps "default"
OnDamagedCResRefkeeps "default"
OnDeathCResRefkeeps "default"
OnDisarmCResRefkeeps "default"
OnHeartbeatCResRefkeeps "default"
OnLockCResRefkeeps "default"
OnMeleeAttackedCResRefkeeps "default"
OnOpenCResRefkeeps "default"
OnSpellCastAtCResRefkeeps "default"
OnTrapTriggeredCResRefkeeps "default"
OnUnlockCResRefkeeps "default"
OnUserDefinedCResRefkeeps "default"
OnClickCResRefkeeps "default"
OnFailToOpenCResRefkeeps "default"
OnDialogCResRefkeeps "default"
LoadScreenIDWORDstamps 0