UTE Format (Encounter Blueprint)
A .ute file is an encounter: a boundary on the floor plus the pool of creatures to spawn when the party crosses it. It carries where they appear, how many arrive at once, and how the engine scales them against the player.
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .ute |
| Magic Signature | UTE / V3.2 |
| Type | Encounter Blueprint |
| Rust Reference | View rakata_generics::Ute in Rustdocs |
Field Schema
The format’s field families, as an orientation before the full list.
| Category | Covers | Representative fields |
|---|---|---|
| Spawn Population | The creature blueprints the encounter can spawn | CreatureList |
| Difficulty & Limits | How many creatures spawn at once and how hard they are relative to the player | MaxCreatures, DifficultyIndex |
| Trigger Boundaries | The coordinates that trace the tripwire that fires the spawn | Geometry |
| Behavioural Hooks | The scripts that run when a player enters or exits the trigger, or when the spawn pool runs dry | OnEntered, OnExhausted |
Engine Audits & Decompilation
Read from the primary dispatcher CSWSEncounter::LoadEncounter at 0x00593830 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
Loading an encounter is split across four subroutines.
| Function | Size | Behaviour |
|---|---|---|
ReadEncounterFromGff (0x00592430) | 3445 B | The initial pass that sets up the encounter’s identity, difficulty limits, and the spawn list. It also reads the runtime spawn-tracking fields, AreaList and SpawnList inline; see “Runtime Fields Are Read on the Blueprint Path Too” below. |
ReadEncounterScriptsFromGff | 567 B | Attaches scripts that trigger when players enter, exit, or exhaust the spawn pool. |
LoadEncounterSpawnPoints (0x00590410) | 364 B | Reads the coordinates so the engine knows exactly where to spawn the creatures. Called from inside ReadEncounterFromGff itself, gated only on whether the source struct has a non-empty SpawnPointList. |
LoadEncounterGeometry | 651 B | Reads the coordinates that trace the trigger’s boundaries on the floor. |
ReadEncounterFromGff and ReadEncounterScriptsFromGff are shared verbatim between two callers: the save-game path reads them straight off the area’s GIT struct, while the blueprint/template path reads the same fields off the .ute file’s own top-level struct. There is no UseTemplates branch inside the field readers themselves; the fork only decides which file supplies the struct.
Runtime Fields Are Read on the Blueprint Path Too
NumberSpawned, HeartbeatDay, HeartbeatTime, LastSpawnDay, LastSpawnTime, LastEntered, LastLeft, Started, Exhausted, CurrentSpawns, CustomScriptId, AreaListMaxSize, SpawnPoolActive, AreaPoints, plus the SpawnPointList, AreaList, and SpawnList lists, all read as one contiguous, unconditional block inside ReadEncounterFromGff. None of them sit behind a source-type check; the function reads whatever the struct it was handed contains, blueprint or GIT instance alike.
The blueprint-versus-instance fork lives two calls up, in CSWSArea::LoadEncounters (0x00505060). With no template it calls CSWSEncounter::LoadEncounter (0x00593830) on the GIT instance struct directly. With one it opens the .ute named by TemplateResRef and calls CSWSEncounter::LoadFromTemplate (0x00593a90), which runs ReadEncounterFromGff against the blueprint.
LoadEncounters then re-reads position, Geometry and SpawnPointList off the GIT instance as overrides. The runtime-tracking scalars, AreaList and SpawnList are not among them, so whatever a template supplies for those stands, with no instance-level override at all.
Practically: the corpus-observed absence of these fields from every vanilla .ute is an authoring-tool habit, not an engine restriction. A hand-authored .ute carrying them would have every one read and applied on the ordinary template-load path. One authoring hazard worth flagging: AreaList’s read allocates its buffer using AreaListMaxSize, itself one of the fields read from the same struct just above it. A file that supplies AreaList entries without a large enough AreaListMaxSize would size the destination buffer too small, with nothing in the loader stopping the read.
Absent-Field Defaults
Every scalar field ReadEncounterFromGff/ReadEncounterScriptsFromGff reads shares one idiom: the read call’s own “default if absent” argument is the field’s already-constructed value on the object, and the result is stored back with no visible branch on presence. Functionally this is a carry-over rather than a fresh literal stamp, even though the assignment itself always executes: an absent field always resolves to whatever a freshly constructed CSWSEncounter already held for that member.
| Field | Constructed default carried over on absence |
|---|---|
LocalizedName | Empty localized string |
Active | true, the one boolean on this struct that constructs to nonzero; Reset/PlayerOnly/Started/Exhausted all construct to false |
Reset | false |
ResetTime | 60 |
Respawns | 0 |
SpawnOption | 0 |
MaxCreatures | 8 |
RecCreatures | 2 |
NumberSpawned | 0 |
HeartbeatDay | 0 |
HeartbeatTime | 0 |
LastSpawnDay | 0 |
LastSpawnTime | 0 |
CurrentSpawns | 0 |
AreaListMaxSize | 16 |
SpawnPoolActive | 0.0, a float rather than the integer its neighbours might suggest, confirmed against both the struct layout and the ReadFieldFLOAT call |
LastEntered | 0x7F000000 (OBJECT_INVALID) |
LastLeft | 0x7F000000 (OBJECT_INVALID) |
PlayerOnly | false |
Faction | 1 |
OnEntered, OnExit, OnHeartbeat, OnExhausted, OnUserDefined | Empty resref/script, all five read in that order with the identical mechanism |
LastEntered and LastLeft are object references, not calendar integers, despite sitting beside HeartbeatDay and LastSpawnDay in both the struct and the field list. The constructor writes 0x7F000000 rather than 0.
GFF has no distinct object-reference field type, so both come through the same ReadFieldDWORD path as every plain DWORD on this struct. The sentinel value is what marks them, not the read.
Every field traced in this pass is absent from every vanilla .ute in a full install, so these constructed defaults are what every shipped encounter runs on.
XPosition/YPosition/ZPosition follow the same self-default read, but with an extra step: the constructor sets the encounter’s position to the origin via SetPosition, each coordinate is individually read against its own current value, and the assembled vector is then unconditionally passed through SetPosition again. So an absent position doesn’t just leave the origin untouched in isolation. It is carried over and then re-stamped, landing on the same origin either way. (This is the read LoadEncounters’ own instance-level position override, mentioned above, layers on top of.)
CustomScriptId and AreaPoints Default to Uninitialized Memory, Not a Constructed Value
Every other field here carries over a reproducible constructor default. CSWSEncounter’s constructor never assigns these two at all, confirmed in the decompiled constructor and by walking the raw disassembly for a write to either struct offset: neither appears, while every neighbouring offset does. The object is allocated unzeroed with no memset anywhere in construction.
So an absent CustomScriptId or AreaPoints carries over whatever was already in that heap memory, not a stable value. Same hazard as UTI’s gated PropertiesList scalars, for the same reason.
The two land on opposite sides of whether it matters.
CustomScriptId is consumed. NWScript’s GetUserDefinedEventNumber() reads this member directly off an encounter. CSWSEncounter::EventHandler refreshes it with a real value immediately before running OnUserDefined, so it is always correct inside that handler.
Nothing stops a script calling GetUserDefinedEventNumber() on an encounter outside such a handler, before any event has fired since the object was constructed or loaded. The script then reads whatever sits there, GFF-loaded or heap garbage, and that value can flow into script comparisons, switches or indexing. That is a live correctness hazard.
AreaPoints is scratch space. CSWSEncounter::TallyEnemyRadiusPoints and CalculateSpawnPool, both part of the spawn-pool difficulty math, fully overwrite it before reading it back on every path traced, so garbage on absence is inert.
Note
One condition left open: whether
CalculateSpawnPoolcan run before the encounter has an assigned area, which is whatTallyEnemyRadiusPoints’ own zeroing write is gated on. Static analysis could not settle it. If that path exists,AreaPointsreads as garbage there too.
Rules the engine enforces
Warning
Understanding Fatal Log Drops While minor coordinate math errors usually just cause creatures to spawn inside walls, failing strict geometry constraints causes KOTOR to abruptly abort parsing the Encounter. Specifically, if a
.utefile declares it has geometry boundaries but fails to provide the actual coordinate vertices, the engine dumps a fatal error to its trace log and refuses to spawn the encounter at all.
| Engine Rule | Runtime Behaviour |
|---|---|
| Tag Overrides | CSWSObject::SetTag lowercases any Tag on load, so the casing in the file is lost. |
| Geometry Integrity | If Geometry is explicitly defined but has 0 vertices, the engine logs a “has geometry, but no vertices” error and aborts loading the encounter entirely. |
| Geometry Synthesis | If the Geometry list is omitted entirely, the engine synthesizes a default 4-vertex box. In practice this branch is dead code: it only fires under a spawn-position-override mode that the encounter loader’s sole caller never enables, so no real load path can reach it. Every encounter that actually spawns needs genuine, non-empty geometry. |
| Difficulty Resolution | Provenance: traced. DifficultyIndex selects a row of encdifficulty.2da, and the engine reads one column from it, VALUE, into the encounter’s runtime difficulty. The lookup lives in CSWSEncounter::ReadEncounterFromGff (0x00592430); the table is pre-cached at startup. The static Difficulty field is read only where that cache is null, meaning the table failed to load at all, which is installation-level rather than a per-encounter fallback. |
| Bubble Sorting | On loading the CreatureList, the engine bubble-sorts the spawn pool by ascending CR (Challenge Rating). A display order set in the file does not survive. |
| Area Instantiation | AreaList buffer allocation size is strictly dictated by AreaListMaxSize. If the real list exceeds this size, the buffer will silently overrun. |
| Structural List Omission | CreatureList, SpawnPointList, AreaList, and SpawnList (a pending/scheduled spawn pool distinct from CreatureList) are each only reloaded if present and non-empty. Omitting any of them leaves that list exactly as it already was, empty on a freshly built encounter, rather than raising an error. |
| Spawn Point Orientation | Each SpawnPointList entry stores its facing as a single raw heading float, not a direction vector. The value is stored and reloaded unmodified with no normalization step, unlike vector-based orientation elsewhere in the engine’s placement schema. |
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 Type | Explanation |
|---|---|
| Passive Legacy Artifacts | Fields left over from older tools or earlier BioWare engines (TemplateResRef, Comment, PaletteID). ReadEncounterFromGff reads none of them. |
Toolset-Only Appearance | Nearly every .ute file carries an Appearance INT, but ReadEncounterFromGff, the one function that parses every encounter field whether blueprint or GIT instance, never queries a field by that name, and no other function in the binary does either. It’s the level editor’s own icon/model pick for the encounter’s spawn marker in the 3D view, invisible to swkotor.exe. Compare GIT’s Appearance, which is the same label but a genuinely engine-read field on a different object type. |
| Superseded Legacy Fields | The static Difficulty field goes unread as long as DifficultyIndex maps to a valid row inside encdifficulty.2da. |
Implemented Linter Rules (Rakata-Lint)
Phase 1 (intra-resource, no context)
Implemented under rakata_lint::rules::ute.
- UTE-001 (Dead Difficulty Traces): Warns when
Difficulty > 0whileDifficultyIndex >= 0; the engine ignores the staticDifficultyin favor of the 2DA lookup. - UTE-002 (Deficient Spawn Loops): Warns when an encounter is marked
Active=truebutCreatureListis empty. - UTE-003 (Dead Field Evaluation): Informs when
TemplateResRef,Comment, orPaletteIDare populated; never read by the K1 engine. - UTE-004 (Geometry Integrity Risk): Warns when
Geometryhas 0 vertices; an explicitly defined empty geometry array makes the engine log an error and abandon the encounter, which then never spawns.
Phase 2 (resource existence, requires LintContext)
Implemented under rakata_lint::rules::ute_range.
- UTE-005 (Resref Existence): Warns when any of
OnEntered,OnExit,OnHeartbeat,OnExhausted, orOnUserDefined(.ncs) does not resolve, or when anyCreatureList[i].ResRef(.utc) does not resolve in the configured resource sources.
Every label the schema declares
Generated from the schema, so no label can be quietly left out. How to read these tables.
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.
| Field | Type | When absent |
|---|---|---|
TemplateResRef | CResRef | NOT EXAMINED; we substitute "" |
Tag | CExoString | the page does not say; we substitute "" |
LocalizedName | CExoLocString | keeps empty |
Comment | CExoString | NOT EXAMINED; we substitute "" |
PaletteID | BYTE | NOT EXAMINED; we substitute 0 |
Active | BYTE | keeps 1 |
Reset | BYTE | keeps 0 |
ResetTime | INT | keeps 60 |
Respawns | INT | keeps 0 |
SpawnOption | INT | keeps 0 |
MaxCreatures | INT | keeps 8 |
RecCreatures | INT | keeps 2 |
PlayerOnly | BYTE | keeps 0 |
Faction | DWORD | keeps 1 |
DifficultyIndex | INT | the page does not say; we substitute 0 |
Difficulty | INT | the page does not say; we substitute 0 |
XPosition | FLOAT | keeps 0.0 |
YPosition | FLOAT | keeps 0.0 |
ZPosition | FLOAT | keeps 0.0 |
OnEntered | CResRef | keeps "" |
OnExit | CResRef | keeps "" |
OnHeartbeat | CResRef | keeps "" |
OnExhausted | CResRef | keeps "" |
OnUserDefined | CResRef | keeps "" |
NumberSpawned | INT | keeps 0 |
HeartbeatDay | DWORD | keeps 0 |
HeartbeatTime | DWORD | keeps 0 |
LastSpawnDay | DWORD | keeps 0 |
LastSpawnTime | DWORD | keeps 0 |
LastEntered | DWORD | keeps 2130706432 |
LastLeft | DWORD | keeps 2130706432 |
Started | BYTE | keeps 0 |
Exhausted | BYTE | keeps 0 |
CurrentSpawns | INT | keeps 0 |
CustomScriptId | INT | whatever the memory held; we substitute 0 |
AreaListMaxSize | INT | keeps 16 |
SpawnPoolActive | FLOAT | keeps 0.0 |
AreaPoints | FLOAT | whatever the memory held; we substitute 0.0 |
CreatureList | List | not one constant; we substitute container |
CreatureList[].ResRef | CResRef | NOT EXAMINED; we substitute "" |
CreatureList[].CR | FLOAT | NOT EXAMINED; we substitute 0.0 |
CreatureList[].SingleSpawn | BYTE | NOT EXAMINED; we substitute 0 |
Geometry | List | not one constant; we substitute container |
Geometry[].X | FLOAT | NOT EXAMINED; we substitute 0.0 |
Geometry[].Y | FLOAT | NOT EXAMINED; we substitute 0.0 |
Geometry[].Z | FLOAT | NOT EXAMINED; we substitute 0.0 |
SpawnPointList | List | not one constant; we substitute container |
SpawnPointList[].X | FLOAT | NOT EXAMINED; we substitute 0.0 |
SpawnPointList[].Y | FLOAT | NOT EXAMINED; we substitute 0.0 |
SpawnPointList[].Z | FLOAT | NOT EXAMINED; we substitute 0.0 |
SpawnPointList[].Orientation | FLOAT | NOT EXAMINED; we substitute 0.0 |
AreaList | List | not one constant; we substitute container |
AreaList[].AreaObject | DWORD | NOT EXAMINED; we substitute 0 |
SpawnList | List | not one constant; we substitute container |
SpawnList[].SpawnResRef | CResRef | NOT EXAMINED; we substitute "" |
SpawnList[].SpawnCR | FLOAT | NOT EXAMINED; we substitute 0.0 |