UTD Format (Door Blueprint)
Description: The Door (.utd) blueprint defines interactive pathways on a level map. Beyond acting as physical barriers or transitions between areas, doors house lock mechanics, trap configurations, script hooks, and basic visual states (open, destroyed, jammed).
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .utd |
| Magic Signature | UTD / V3.2 |
| Type | Door Blueprint |
| Rust Reference | View rakata_generics::Utd in Rustdocs |
Data Model Structure
Rakata maps a Door into the rakata_generics::Utd 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 door looks like, its faction, and the text displayed when targeted | Appearance, TemplateResRef, LocName |
| Lock & Trap Mechanics | Whether the door is locked, which key opens it, and the rules for attached traps | Locked, KeyName, TrapType, DisarmDC |
| Transition Pathways | The linked destination used when the door acts as a loading zone to another area | LinkedTo, LinkedToFlags |
| Behavioral Hooks | The scripts that run when a player opens, destroys, or fails to unlock the door | OnOpen, OnFailToOpen, OnMeleeAttacked |
rakata-lint validates these fields against the engine constraints documented below.
Engine Audits & Decompilation
The following information documents the engine’s exact load sequence and field requirements for .utd files mapped from swkotor.exe.
(Decompilation logic for this section was entirely audited and verified via native Ghidra pipeline against swkotor.exe, explicitly pulling from the shared field-reading routine CSWSDoor::LoadDoor at 0x0058a1f0; see “Save versus Template Load Paths” below for the actual top-level dispatcher.)
Structural Load Phasing
The engine processes a Door structurally by mapping its sub-fields into distinct operational constraints.
| Domain | Sub-fields Evaluated | Purpose |
|---|---|---|
| Scales & State | 22 | Reads the physical health, visual appearance, and base traits determining whether the door is locked or indestructible. |
| Hooks | 15 | Attaches custom event scripts that fire when the door is opened, forced, unlocked, or trapped. |
| Mechanical | 9 | Configures the lock difficulty tiers and the specific skill hurdles required to detect and disarm any attached traps. |
| Transitions | 4 | Links 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 down from the area loader, decides how a door’s fields get populated on load. The actual branch point is CSWSDoor::LoadDoorExternal (0x0058c7ca), the true top-level dispatcher; LoadDoor (0x0058a1f0) is the shared field-reading routine both branches ultimately call. When the flag is clear, LoadDoorExternal calls LoadDoor directly on the door’s full instance snapshot out of the savegame; every field is already sitting in that struct. When the flag is set, LoadDoorExternal calls CSWSDoor::LoadFromTemplate (0x0058b468), which reads TemplateResRef, opens the referenced .utd blueprint, and calls the same LoadDoor against the blueprint’s own struct. Because a blueprint has no idea which specific instance it belongs to, four instance-only fields – TransitionDestin (the on-disk, 16-byte-truncated label for what the engine’s own source calls TransitionDestination; both names refer to the same field, see the note below), LinkedTo, LinkedToFlags, and LinkedToModule – are overlaid back onto the freshly-loaded door from the original save instance immediately afterward, back in LoadDoorExternal. This 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 itself to fail (an empty or unresolvable TemplateResRef), which aborts the whole door load rather than merely skipping the overlay.
LoadDoor itself makes no distinction between the two callers: its read of TransitionDestin runs unconditionally regardless of whether the struct it was handed is the blueprint’s or the save instance’s. So on a template-backed door, the field genuinely is read off the blueprint – it just never gets a chance to matter, because LoadDoorExternal’s overlay overwrites it with the save instance’s value immediately after LoadDoor returns. A hand-authored .utd carrying TransitionDestin would have it read the same way, and then discarded the same way.
Tag is a fifth field with instance-only stakes, but it isn’t overlaid. LoadDoor contains the only Tag read in the entire door-load call graph, so on a templated door Tag comes from the blueprint, exactly like Appearance or HP – and unlike the four fields above, nothing in LoadDoorExternal re-reads Tag from the placed instance afterward. That’s a real gap in the instance-overlay mechanism, not a documentation omission: multiple doors sharing one .utd blueprint would share one Tag, which would break any script that targets a door by tag. In practice this doesn’t bite, because the vanilla toolset works around it at the content level rather than the engine level – real modules generally give each placed door its own dedicated .utd, one blueprint per instance, rather than truly sharing a single template across multiple doors. A corpus scan bears this out: every door instance in a full install carries a real, distinct TemplateResRef value. The mechanism is technically “templated,” but the authoring convention makes it behave like one blueprint per door in practice.
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.
Core Structural Findings
The CSWSDoor parser natively guarantees strict state adjustments upon parsing.
| Engine Rule | Runtime Behavior |
|---|---|
| Appearance Truncation | The engine reads Appearance as a 32-bit integer but forcefully truncates it to a single byte. Any ID above 255 automatically wraps to 0 and breaks the physical door model. |
| Static Enforcement | If the door is marked Static, the engine automatically forces plot = 1. This safely guarantees that static level architecture cannot be destroyed by players. |
| Portrait Shadowing | If PortraitId is 0, the engine hardcodes it to 0x22E. 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 Hook Fallback | If 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 Synchronization | CurrentHP 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 Omissions | Aside from the Portrait/PortraitId fork above, the save routine writes every door field unconditionally. No other door field is ever left out of a vanilla save. |
Absent-Field Defaults: Most Are Simple, A Few Are Not
The governing mechanism across LoadDoor is a CResGFF::ReadField* call that takes a fallback argument: sometimes that fallback is the object’s own current member value (a true carry-over from whatever the constructor set), sometimes it’s a fresh literal constructed at the read site that ignores the current member entirely. Both look identical in the decompiled store, so which one applies has to be checked per field, not assumed from the value alone.
All 15 script hooks default to the literal string "default", not an empty resref. This is the standout finding on this page: the CSWSDoor constructor explicitly loops over all 15 script-slot members (OnClosed, OnDamaged, OnDeath, OnDisarm, OnHeartbeat, OnLock, OnMeleeAttacked, OnOpen, OnSpellCastAt, OnTrapTriggered, OnUnlock, OnUserDefined, OnClick, OnFailToOpen, OnDialog) and assigns each the literal string "default", not an empty string. Every hook read is a carry-over of that constructed value: an absent hook resolves to whatever the member currently holds, which on a fresh load is "default". This is the actual mechanism behind the already-documented OnTrapTriggered fallback rule (“empty, null, or literally "default"” routes to traps.2da) – it exists specifically because an absent OnTrapTriggered naturally becomes "default" through this carry-over, not because the engine special-cases three different absent-value spellings. The other 14 hooks have no such secondary lookup: they simply keep the literal resref "default", which won’t resolve to a real .ncs unless a module happens to ship one named exactly that.
TrapType’s 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 – worth a lint rule of its own, distinct from the already-flagged OnTrapTriggered fallback.
Lockable defaults to 0 – a door is not lockable unless the file says so. Same carry-over mechanism as most fields here (constructor sets lockable = 0), but worth calling out explicitly since “lockable by default” is the more intuitive assumption for a door.
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, not carry-over – distinct mechanism, 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 (which also happens to be 0 for Hardness/Static, so the observable value is identical either way – only the mechanism differs, and it would matter if the constructed defaults ever diverged from 0).
Plot and Invulnerable are entangled, and the gating condition genuinely couldn’t be resolved. LoadDoor reads a field literally named "Invulnerable" first (carry-over fallback: this->object.plot, constructor default 0), then conditionally reads a field named "Plot" with the identical carry-over fallback, potentially overwriting the first result. The condition gating that second read traces to a stack value with no discoverable prior write inside LoadDoor or its two known callers – genuinely unresolved, not just unchecked. What’s certain regardless of which branch fires: both reads share the same fallback, so an absent Plot and an absent Invulnerable both resolve to 0 (not plot) – unless Static is present and true, which forces the final value to 1 regardless, per the already-documented Static-enforcement rule. Worth flagging on its own: Invulnerable is not part of the documented UTD schema and isn’t written by the vanilla toolset, but the engine genuinely reads it if a file supplies it – a live field with no prior mention on this page.
OpenState carries a dead-in-practice override. The field itself carries over normally (constructor default 0), but the read result then feeds a check that can force it to a hardcoded 3 – gated on an internal flag the constructor sets to 1 and that nothing observed inside LoadDoor ever resets to 0 before a load runs. Under the normal construct-then-load flow this override never fires, so OpenState absent resolves to the plain carried-over 0 in practice; flagged here in case some other, unexamined path resets that flag first.
The remaining fields all follow the ordinary carry-over pattern with unremarkable constructed defaults: Faction/GenericType/AutoRemoveKey to 0, Bearing to 0.0, KeyRequired/OpenLockDC/CloseLockDC/SecretDoorDC/Fort/Ref/Will/DisarmDC/TrapDetectDC/TrapFlag/Min1HP to 0, Locked to 0 (unlocked), TrapDetectable/TrapDisarmable/TrapOneShot to 1 (true) – and a handful default to a fresh literal rather than carrying over, with the same practical value: LocName/Description to an empty localized string (a throwaway default-constructed value at the read site, not this->name/this->description), Conversation to an empty resref, Tag to an empty string (the one string field routed through SetTag rather than a plain assignment, carry-over of the constructor’s own "").
Legacy & Ignored Data
| Finding Type | Explanation |
|---|---|
| Legacy Engine Artifacts | Confirmed by full-text search of LoadDoor’s decompiled body: exactly seven fields are never read anywhere in the function, and this is the complete list, not a sample – AnimationState, NotBlastable, OpenLockDiff, OpenLockDiffMod, Comment, Interruptable, PaletteID. Whatever storage (if any) the struct carries for these 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.
- UTD-001 (Static Parity): Warns when
Static=truebutPlot=false; the engine forces Plot to true at runtime. - UTD-002 (HP Bounds): Errors when
CurrentHP > HP; the engine clamps toHPon template load. - UTD-003 (Portrait Shadowing): Warns when
PortraitId < 0xFFFEandPortraitresref is set; the resref is ignored at runtime.
Phase 2 (range / 2DA / resref existence, requires LintContext)
Implemented under rakata_lint::rules::utd_range.
- UTD-004 (Generic Door Type Bounds): Errors when
GenericTypedoes not resolve to a row ingenericdoors.2da; engine renders missing model. - UTD-005 (Portrait Bounds): Errors when
PortraitId(when not the0xFFFE“use string Portrait” sentinel) does not resolve to a row inportraits.2da. - UTD-006 (Resref Existence): Warns when
Conversation(.dlg),Portrait(.tga), or any of the 15On*script hooks (.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"OnTrapTriggeredreferences that silently invoke thetraps.2dafallback. - Portrait Zero Hardcode: Detects
PortraitId == 0mappings since the engine hardcodes lookup to0x22E.