UTE Format (Encounter Blueprint)
Description: The Encounter (.ute) blueprint defines interactive spawn points and boundary triggers across a level map. Instead of acting merely as a spatial zone, encounters handle complex difficulty scaling, bubble-sort creature limits, and explicit coordinate vertices to dynamically deploy combatants when a player crosses their geometry bounds.
At a Glance
| Property | Value |
|---|---|
| Extension(s) | .ute |
| Magic Signature | UTE / V3.2 |
| Type | Encounter Blueprint |
| Rust Reference | View rakata_generics::Ute in Rustdocs |
Data Model Structure
Rakata maps an Encounter into the rakata_generics::Ute 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 |
|---|---|---|
| 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 |
| Behavioral Hooks | The scripts that run when a player enters or exits the trigger, or when the spawn pool runs dry | OnEntered, OnExhausted |
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 .ute files mapped from swkotor.exe.
(Decompilation logic for this section was audited and verified via native Ghidra pipeline against swkotor.exe, explicitly pulling from the primary dispatcher CSWSEncounter::LoadEncounter at 0x00593830.)
Structural Load Phasing
The engine processes an Encounter structurally across several chunked subroutines, each responsible for unique spatial and logic bindings.
| Function | Size | Behavior |
|---|---|---|
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 actual blueprint-versus-instance fork lives two calls up, in CSWSArea::LoadEncounters (0x00505060): with no template, it calls CSWSEncounter::LoadEncounter (0x00593830) directly on the GIT instance struct; with a template, it opens the .ute named by the instance’s TemplateResRef and calls CSWSEncounter::LoadFromTemplate (0x00593a90), which runs ReadEncounterFromGff against the blueprint’s own struct. Afterward, LoadEncounters re-reads position, Geometry, and SpawnPointList a second time from the GIT instance as overrides – but the runtime-tracking scalars, AreaList, and SpawnList are not among the re-read fields, so whatever a template supplies for those would stand as read, with no instance-level override mechanism 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, not 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 |
PlayerOnly | false |
Faction | 1 |
OnEntered, OnExit, OnHeartbeat, OnExhausted, OnUserDefined | Empty resref/script, all five read in that order with the identical mechanism |
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’s 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.)
Core Structural Findings
The engine rigorously evaluates geometric and spatial boundaries. Improper definitions break the spawn mapping algorithm.
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 Behavior |
|---|---|
| Tag Overrides | The engine forcefully converts any Tag to all-lowercase via CSWSObject::SetTag. Any static casing is lost immediately upon load. |
| 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 completely omitted from the blueprint, the engine falls back and safely synthesizes a default 4-vertex spatial 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 | The engine prioritizes using DifficultyIndex to look up the difficulty in encdifficulty.2da. The static Difficulty field is read back only if the 2DA table itself fails to load altogether, a rare installation-level failure rather than a per-encounter fallback; a valid table always wins. |
| Bubble Sorting | Upon loading the CreatureList, the engine runs a Bubble Sort algorithm to firmly re-order the encounter’s spawn pool by ascending CR (Challenge Rating), completely overriding any custom static display order. |
| 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. |
Legacy & Ignored Data
| Finding Type | Explanation |
|---|---|
| Passive Legacy Artifacts | Unused fields left over from older tools or Odyssey branches (e.g., TemplateResRef, Comment, PaletteID) are completely dark. The engine inherently ignores them. |
Toolset-Only Appearance | Nearly every .ute file carries an Appearance INT, but ReadEncounterFromGff – the one function that parses every encounter field, blueprint or GIT instance alike – 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 is a completely inactive legacy metric 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 crashes the engine on load.
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.