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

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

PropertyValue
Extension(s).ute
Magic SignatureUTE / V3.2
TypeEncounter Blueprint
Rust ReferenceView rakata_generics::Ute in Rustdocs

Field Schema

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

CategoryCoversRepresentative fields
Spawn PopulationThe creature blueprints the encounter can spawnCreatureList
Difficulty & LimitsHow many creatures spawn at once and how hard they are relative to the playerMaxCreatures, DifficultyIndex
Trigger BoundariesThe coordinates that trace the tripwire that fires the spawnGeometry
Behavioural HooksThe scripts that run when a player enters or exits the trigger, or when the spawn pool runs dryOnEntered, 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.

FunctionSizeBehaviour
ReadEncounterFromGff (0x00592430)3445 BThe 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.
ReadEncounterScriptsFromGff567 BAttaches scripts that trigger when players enter, exit, or exhaust the spawn pool.
LoadEncounterSpawnPoints (0x00590410)364 BReads 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.
LoadEncounterGeometry651 BReads 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.

FieldConstructed default carried over on absence
LocalizedNameEmpty localized string
Activetrue, the one boolean on this struct that constructs to nonzero; Reset/PlayerOnly/Started/Exhausted all construct to false
Resetfalse
ResetTime60
Respawns0
SpawnOption0
MaxCreatures8
RecCreatures2
NumberSpawned0
HeartbeatDay0
HeartbeatTime0
LastSpawnDay0
LastSpawnTime0
CurrentSpawns0
AreaListMaxSize16
SpawnPoolActive0.0, a float rather than the integer its neighbours might suggest, confirmed against both the struct layout and the ReadFieldFLOAT call
LastEntered0x7F000000 (OBJECT_INVALID)
LastLeft0x7F000000 (OBJECT_INVALID)
PlayerOnlyfalse
Faction1
OnEntered, OnExit, OnHeartbeat, OnExhausted, OnUserDefinedEmpty 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 CalculateSpawnPool can run before the encounter has an assigned area, which is what TallyEnemyRadiusPoints’ own zeroing write is gated on. Static analysis could not settle it. If that path exists, AreaPoints reads 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 .ute file 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 RuleRuntime Behaviour
Tag OverridesCSWSObject::SetTag lowercases any Tag on load, so the casing in the file is lost.
Geometry IntegrityIf 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 SynthesisIf 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 ResolutionProvenance: 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 SortingOn 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 InstantiationAreaList buffer allocation size is strictly dictated by AreaListMaxSize. If the real list exceeds this size, the buffer will silently overrun.
Structural List OmissionCreatureList, 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 OrientationEach 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 TypeExplanation
Passive Legacy ArtifactsFields left over from older tools or earlier BioWare engines (TemplateResRef, Comment, PaletteID). ReadEncounterFromGff reads none of them.
Toolset-Only AppearanceNearly 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 FieldsThe 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.

  1. UTE-001 (Dead Difficulty Traces): Warns when Difficulty > 0 while DifficultyIndex >= 0; the engine ignores the static Difficulty in favor of the 2DA lookup.
  2. UTE-002 (Deficient Spawn Loops): Warns when an encounter is marked Active=true but CreatureList is empty.
  3. UTE-003 (Dead Field Evaluation): Informs when TemplateResRef, Comment, or PaletteID are populated; never read by the K1 engine.
  4. UTE-004 (Geometry Integrity Risk): Warns when Geometry has 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.

  1. UTE-005 (Resref Existence): Warns when any of OnEntered, OnExit, OnHeartbeat, OnExhausted, or OnUserDefined (.ncs) does not resolve, or when any CreatureList[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.

FieldTypeWhen absent
TemplateResRefCResRefNOT EXAMINED; we substitute ""
TagCExoStringthe page does not say; we substitute ""
LocalizedNameCExoLocStringkeeps empty
CommentCExoStringNOT EXAMINED; we substitute ""
PaletteIDBYTENOT EXAMINED; we substitute 0
ActiveBYTEkeeps 1
ResetBYTEkeeps 0
ResetTimeINTkeeps 60
RespawnsINTkeeps 0
SpawnOptionINTkeeps 0
MaxCreaturesINTkeeps 8
RecCreaturesINTkeeps 2
PlayerOnlyBYTEkeeps 0
FactionDWORDkeeps 1
DifficultyIndexINTthe page does not say; we substitute 0
DifficultyINTthe page does not say; we substitute 0
XPositionFLOATkeeps 0.0
YPositionFLOATkeeps 0.0
ZPositionFLOATkeeps 0.0
OnEnteredCResRefkeeps ""
OnExitCResRefkeeps ""
OnHeartbeatCResRefkeeps ""
OnExhaustedCResRefkeeps ""
OnUserDefinedCResRefkeeps ""
NumberSpawnedINTkeeps 0
HeartbeatDayDWORDkeeps 0
HeartbeatTimeDWORDkeeps 0
LastSpawnDayDWORDkeeps 0
LastSpawnTimeDWORDkeeps 0
LastEnteredDWORDkeeps 2130706432
LastLeftDWORDkeeps 2130706432
StartedBYTEkeeps 0
ExhaustedBYTEkeeps 0
CurrentSpawnsINTkeeps 0
CustomScriptIdINTwhatever the memory held; we substitute 0
AreaListMaxSizeINTkeeps 16
SpawnPoolActiveFLOATkeeps 0.0
AreaPointsFLOATwhatever the memory held; we substitute 0.0
CreatureListListnot one constant; we substitute container
CreatureList[].ResRefCResRefNOT EXAMINED; we substitute ""
CreatureList[].CRFLOATNOT EXAMINED; we substitute 0.0
CreatureList[].SingleSpawnBYTENOT EXAMINED; we substitute 0
GeometryListnot one constant; we substitute container
Geometry[].XFLOATNOT EXAMINED; we substitute 0.0
Geometry[].YFLOATNOT EXAMINED; we substitute 0.0
Geometry[].ZFLOATNOT EXAMINED; we substitute 0.0
SpawnPointListListnot one constant; we substitute container
SpawnPointList[].XFLOATNOT EXAMINED; we substitute 0.0
SpawnPointList[].YFLOATNOT EXAMINED; we substitute 0.0
SpawnPointList[].ZFLOATNOT EXAMINED; we substitute 0.0
SpawnPointList[].OrientationFLOATNOT EXAMINED; we substitute 0.0
AreaListListnot one constant; we substitute container
AreaList[].AreaObjectDWORDNOT EXAMINED; we substitute 0
SpawnListListnot one constant; we substitute container
SpawnList[].SpawnResRefCResRefNOT EXAMINED; we substitute ""
SpawnList[].SpawnCRFLOATNOT EXAMINED; we substitute 0.0