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)

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

PropertyValue
Extension(s).ute
Magic SignatureUTE / V3.2
TypeEncounter Blueprint
Rust ReferenceView 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.

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
Behavioral HooksThe scripts that run when a player enters or exits the trigger, or when the spawn pool runs dryOnEntered, 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.

FunctionSizeBehavior
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 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:

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
PlayerOnlyfalse
Faction1
OnEntered, OnExit, OnHeartbeat, OnExhausted, OnUserDefinedEmpty 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 .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 Behavior
Tag OverridesThe engine forcefully converts any Tag to all-lowercase via CSWSObject::SetTag. Any static casing is lost immediately upon load.
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 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 ResolutionThe 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 SortingUpon 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 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.

Legacy & Ignored Data

Finding TypeExplanation
Passive Legacy ArtifactsUnused fields left over from older tools or Odyssey branches (e.g., TemplateResRef, Comment, PaletteID) are completely dark. The engine inherently ignores them.
Toolset-Only AppearanceNearly 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 FieldsThe 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.

  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 crashes the engine on load.

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.