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

UTC Format (Creature Blueprint)

Description: The Creature (.utc) blueprint format defines the attributes, stats, and behavior of all in-scene NPCs and monsters. It covers a creature’s identity, class/level, appearance, equipment, and event scripts. Because they hold so much state, Creatures are one of the most dynamic and memory-heavy templates processed by the Odyssey Engine.

At a Glance

PropertyValue
Extension(s).utc
Magic SignatureUTC / V3.2
TypeCreature Blueprint
Rust ReferenceView rakata_generics::Utc in Rustdocs

Data Model Structure

Rakata maps a Creature into the rakata_generics::Utc struct. The struct’s Rustdocs document every field’s binary schema and GFF mapping; the table below is the high-level anatomy.

CategoryCoversRepresentative fields
Core StatisticsThe base stats that define the creature’s physical capabilitiesStrength, Dexterity, HitPoints
Identity & GraphicsWho the creature is and which 3D model it usesTag, Appearance_Type, Conversation
Class & Skill ProgressionThe creature’s level, classes, and skillsClassList, SkillList
Combat CapabilitiesThe feats and Force powers the creature can useFeatList, SpellList
Inventory & EquipmentThe items the creature spawns with, both equipped gear and inventory dropsEquip_ItemList, ItemList
Event HooksThe behavior scripts that fire when the creature reacts to the world, such as taking damage or noticing an enemyOnNotice, OnDamaged

rakata-lint validates these fields against the engine constraints documented below.

Engine Audits & Decompilation

The following documents the engine’s exact load sequence and field requirements for .utc files mapped from swkotor.exe.

(Documented from Ghidra decompilation of swkotor.exe, pulling from CSWSCreatureStats::ReadStatsFromGff at 0x005afce0 and its save-side counterpart CSWSCreatureStats::SaveStats at 0x005b1b90.)

Structural Load Phasing

FunctionSizeBehavior
ReadStatsFromGff7835 BThe massive initial pass that parses 57 basic creature scalars including strength, dexterity, and physical appearance.
LoadCreatureSets up how the creature physically sits in the world, handling their stealth states, collision size, and idle animations.
CSWSCreature::ReadScriptsFromGffAttaches all the custom event scripts that fire when the creature notices an enemy, takes damage, dies, or simply stands around (heartbeat). A genuine member of CSWSCreature (confirmed by its decompiled __thiscall signature), not a free function – correcting the “unnamespaced free function” framing this page previously carried for this specific function; ReadItemsFromGff wasn’t re-checked in this pass.
ReadItemsFromGffPulls all loot into memory, structuring items into equipped slots or the backpack. The “dropped entirely if a creature spawns dead” framing doesn’t hold up against the decompile – no branch anywhere in this call graph inspects hit points or a dead/alive state. See “Item Lists” below for what actually causes an entry to be dropped.
ReadSpellsFromGffSpecifically extracts the list of any Force powers or combat feats the creature is allowed to use.

Note

Zeroed Data Elements Tail and Wings are stronger than “bypassed”: ReadStatsFromGff never looks them up in the GFF struct at all – there’s no ReadFieldBYTE call for either label anywhere in the function. Instead it performs a flat, unconditional assignment of 0 to both members, overwriting whatever the object already held, regardless of whether the file even contains the fields. This is identical on the .utc blueprint path (LoadFromTemplate) and the save-instance path (LoadCreature) – both call the same ReadStatsFromGff, with no branch anywhere in it that distinguishes the two callers. This isn’t merely inert legacy data, though: SaveStats still writes both fields out unconditionally on every save. Whatever values a creature’s tail or wings hold in a save file are silently discarded the moment it’s loaded back in, a genuine round-trip loss rather than a “never populated” field.

Core Structural Findings

The engine strictly validates parameters when loading a .utc file. Improper formatting will trigger some of KOTOR’s most notorious game crashes.

Warning

Understanding Fatal Crash Codes (0x5fX) When the game engine parses a file and hits an invalid stat, it completely aborts loading. Instead of recovering gracefully, the engine deliberately triggers a fatal crash to your desktop and returns a specific hexadecimal error code (e.g., 0x5f7 or 0x5f4). The rules below track the specific scenarios where the game will crash.

Engine RuleRuntime Behavior
Class LimitsThe engine expects a strict limit of 2 discrete class types. Providing duplicate class configuration completely crashes the game (Engine Error 0x5f7).
Race BoundsThe engine compares Race against the compiled row count of racialtypes.2da. Exceeding this boundary fatally crashes the map loader (Engine Error 0x5f4).
Saves CalculationPre-computed saving throws (SaveWill, SaveFortitude) in the .utc file are completely ignored dead data. The engine overrides them exclusively by reading willbonus and fortbonus.
Perception FaultsA non-PC PerceptionRange initiates a read against appearance.2da for PERCEPTIONDIST. Failing to resolve this distance fails the entire creature load (Engine Error 0x5f5).
Movement FallbacksIf a unique MovementRate isn’t declared, the engine logic falls back directly to default WalkRate parameters.
Hard ClampingThe engine strictly limits specific numeric bounds upon load: Gender is clamped structurally at a maximum of 4, and GoodEvil is fiercely clamped so that it cannot exceed 100.
Appearance ShiftingIf Appearance_Head is 0, the engine overrides it to 1 to prevent rendering bugs. This correction checks the resolved value only, never whether the field was actually present – so it fires identically whether the 0 came from an explicit GFF byte or from an absent field’s carried-over default. That matters in practice: Appearance_Head is present in only 1 of 1958 vanilla .utc files. The other 1957 carry over the constructor’s own default of 0, which the correction then bumps to 1 every time – there is no way for a creature to end up stored at 0, whether the omission is universal (as it is in practice) or the field is explicitly zeroed.

Save-Game Snapshot Fields

A creature serialized into a save game carries live runtime state that a static .utc blueprint does not usually populate – but “usually” is doing real work in that sentence. The save writer (CSWSCreatureStats::SaveStats, the counterpart to ReadStatsFromGff) emits these alongside the template fields. They appear on the creature structs inside a save’s module GIT (UseTemplates = 0); see the Save Game Deep Dive. Since ReadStatsFromGff has no branch anywhere that distinguishes a .utc blueprint struct from a save-instance struct, several of these “snapshot” fields are genuinely read on the blueprint path too, the same “runtime fields read on the blueprint path” pattern already documented for .ute encounters – it’s just that vanilla .utc files never happen to populate them. Others are confirmed write-only on every path, blueprint included. The table below distinguishes the two.

Field(s)MeaningRead on the .utc blueprint path too?
CurrentHitPointsLive current HP.Yes – unconditional single read site, no UseTemplates-style gate. Absent-field default is derived from HitPoints (see below), not carried over from any prior “current HP” state.
MaxHitPointsComputed HP ceiling.No. Confirmed exhaustively: the field-name string has exactly two cross-references in the whole binary, both writers (SaveStats, SaveCharGenCreature). Zero readers anywhere – genuinely write-only, matching the already-documented “recomputed, not restored” rule below.
PregameCurrentNominally a current-HP mirror.No. Same exhaustive check: exactly two references, both writers, zero readers, on any path.
ForcePointsLive Force-point pool.Yes – unconditional, carries over the object’s own constructed value (0) when absent.
CurrentForceLive current Force.Yes – unconditional, but sibling-derived from ForcePoints when absent (see below), not carried over independently.
MaxForcePointsComputed Force-point ceiling.No. Same exhaustive write-only check as MaxHitPoints.

The remaining snapshot fields weren’t individually re-verified for blueprint-path readability in this pass, so treat their status as the original framing (save-side emphasis, not a blueprint/save distinction):

Field(s)Meaning
RefSaveThrow, WillSaveThrow, FortSaveThrowComputed saving-throw totals (base + ability modifier + active effects). Distinct from the template’s ignored SaveWill / SaveFortitude dead fields.
ArmorClassComputed AC snapshot.
Experience, GoldRuntime progression. Experience is present in 0 of 1958 vanilla .utc files – every creature in the game omits it, and every one of them carries over the constructor’s default of 0. That carried-over 0 is then passed through SetExperience, which refuses to lower a creature’s XP (it compares the incoming value against the member’s current value and only stores it if the incoming value isn’t smaller); since both sides of that comparison are 0 at this point, the call is a harmless no-op and Experience settles at 0 for every vanilla creature. No sentinel range exists for this field – it’s a plain, uncapped counter.
AIState, NotReorientingRuntime behaviour and orientation state. Both are confirmed genuinely read on the blueprint path too (see “Identity, Appearance, and State Fields” below), unconditional carry-over for each.
MClassLevUpInMulticlass level-up bookkeeping (class_count - 1).

Combat state (the active combat round and equipped-weapon data) is written separately through CCombatInformation::SaveData (0x00550f30; read back by CCombatInformation::LoadData at 0x00552350). The class/skill/feat/power progression is written by SaveClassInfo (0x005aec90) and reflects the creature’s current leveled state, which for a played character diverges from the blueprint. SaveClassInfo is a genuine member of CSWSCreatureStats itself, confirmed against the binary’s own class layout; CCombatInformation is a separate class with no members in common with CSWSCreatureStats, reached through a nested object the creature owns rather than through inheritance.

Does CSWSCreatureStats name the whole shared block?

Short answer: no. ReadStatsFromGff reads the bulk of a creature’s identity, six abilities, HP/FP pools, appearance and portrait fields, faction, challenge rating, AI state, and perception range directly inline, plus ClassList/LvlStatList inline too – no separate “read class info” delegate exists on the load side, even though the save side modularizes the equivalent work into SaveClassInfo. The two structured exceptions on load are CombatRoundData, delegated to CSWSCombatRound::LoadCombatRound, and the nested CombatInfo struct, delegated to CCombatInformation::LoadData.

That accounts for a large, coherent chunk of the 86-field shared set, but not all of it. A comparable amount lives one level up, owned directly by CSWSCreature::SaveCreature / CSWSCreature::LoadCreature rather than by CSWSCreatureStats: DetectMode, StealthMode, CreatureSize, IsDestroyable, IsRaiseable, DeadSelectable, AmbientAnimState, Animation, CreatnScrptFird, PM_IsDisguised, PM_Appearance, Listening, the full set of Script* event-hook resrefs, position/orientation, AreaId, and JoiningXP are all read and written inline in those two functions, with no CSWSCreatureStats involvement at all. Scripts are handled by CSWSCreature::ReadScriptsFromGff, a genuine member of CSWSCreature rather than a free function (correcting an earlier pass’s claim otherwise); items are handled by ReadItemsFromGff, not re-checked in this later pass so its namespacing is unconfirmed. Both are called from LoadCreature, not from CSWSCreatureStats, regardless of which one turns out to be a member. FollowInfo goes further still, delegated from SaveCreature to a fourth class, CSWSCreaturePartyFollowInfo::Save.

PerceptionList is the one field confirmed to split across classes in different directions: ReadStatsFromGff reads it, but SaveCreature writes it, directly and without delegating back to CSWSCreatureStats.

So CSWSCreatureStats is a real class boundary, not an invented one, and it does own everything under “Save-Game Snapshot Fields” above plus the class/skill/feat/power progression – but the 86-field shared set as a whole spans at least four engine classes (CSWSCreatureStats, CSWSCreature, CCombatInformation, CSWSCombatRound, plus CSWSCreaturePartyFollowInfo for FollowInfo) and two unnamespaced free functions. No single engine name covers the whole set; a Rust type modelling the full shared block needs a rakata-invented name, not a borrowed one. One corner is left untraced: CSWSObject::SaveObjectState / LoadObjectState and SaveListenData / LoadListenData run at the tail of SaveCreature / LoadCreature and were not decompiled here, so there may be a further split at the base CSWSObject layer this pass didn’t reach.

Write-Only Fields

Seven of the fields above look like round-trip state but are strictly one-way: SaveStats writes them on every save, and ReadStatsFromGff never reads a single one back. (Tail and Wings are in the same club; see the note above.) They’re one-way for two very different reasons, though, and the difference matters if you’re editing saves.

MaxHitPoints, ArmorClass, and the three saving-throw totals are recomputed, not restored. Each is written from a live getter (the same getters that combat rolls and UI displays call at runtime), and on load the engine simply rebuilds the number from inputs that already round-tripped:

Snapshot totalRebuilt on load from
MaxHitPointsClass levels and the Constitution modifier (non-PC creatures: the template’s own HitPoints, confirmed to be the literal raw value this same field feeds into CurrentHitPoints’s own sibling-derived default, see “Save-Game Snapshot Fields” above)
ArmorClassPer-class armour-bonus tables, natural AC, the Dexterity modifier, feat bonuses, and the active effect list (reapplied as the last step of LoadCreature)
RefSaveThrow / WillSaveThrow / FortSaveThrowThe class/feat base, the ability modifier, the effect bonus, and a permanent-bonus byte that does round-trip – under the lowercase labels refbonus / willbonus / fortbonus, not the capitalized totals

So editing any of these five totals in a save changes nothing; the engine derives the real numbers from the inputs in the right-hand column. The totals exist as convenience snapshots for external tooling, and no state is actually lost.

A corpus scan turns up capitalized variants of these labels – FortBonus, RefBonus, WillBonus – in a handful of .utc files, always holding 0. These are dead by construction, not merely dead in practice: GFF field-label lookup is case-sensitive, and the only literal strings ReadStatsFromGff ever constructs to search for are the lowercase fortbonus/refbonus/willbonus already documented above. A capitalized variant simply never matches during the field-lookup pass; it isn’t found, read, discarded, or compared against anything. The same applies to SubRace versus the modeled Subrace – the engine only ever looks up Subrace, so a SubRace-spelled field is unreachable regardless of what value it holds.

MClassLevUpIn and PregameCurrent are the genuine dead writes: no reader exists anywhere in the binary, not even in the character-generation export path (SaveCharGenCreature) that also writes them. The class count MClassLevUpIn supposedly bookkeeps is derived from the restored ClassList’s length instead, and PregameCurrent – despite the name – behaves as a continuously-refreshed current-HP mirror that nothing ever reads back.

TemplateResRef Isn’t Read by ReadStatsFromGff at All

TemplateResRef never appears anywhere in ReadStatsFromGff – it’s read one level up, by CSWSArea::LoadCreatures, and only on the branch that resolves a GIT Creature List entry’s blueprint reference (never on the direct save-instance path, which has no use for it). The read itself defaults to an empty resref, but the loader explicitly checks the presence flag: if TemplateResRef is absent, it destroys the just-allocated creature object and drops the entry entirely – LoadFromTemplate, and therefore ReadStatsFromGff, never runs for that entry at all. This is a genuine presence-chain abort, and the identical pattern (same field, same behavior) governs blueprint resolution for triggers, placeables, items, doors, encounters, and sounds – a templated GIT entry missing its own TemplateResRef is dropped outright, not defaulted.

Six Core Abilities Default to 0, Not a D&D-Style 10

Str, Dex, Con, Int, Wis, and Cha all follow one identical mechanism: each is read with the object’s own current value as the fallback and stamped back unconditionally, a carry-over in effect even though the store itself always executes. The constructor initializes all six to a literal 0 before any read happens, so a .utc file missing an ability score doesn’t fall back to a sensible tabletop default – it resolves to 0, not 10.

Identity, Appearance, and State Fields

Most of the remaining identity and appearance fields follow that same carry-over mechanism – the read’s own fallback is the object’s current value, stamped back unconditionally, so an absent field is a practical no-op on a freshly constructed blueprint load: Tag, Conversation, Deity, Description, Age, StartingPackage, Subrace (the free-text name – see the naming note below), SubraceIndex (the numeric id), Color_Skin/Color_Hair/Color_Tattoo1/Color_Tattoo2, Phenotype, Appearance_Type, Gender’s own default (distinct from the already-documented clamp-to-4), DuplicatingHead, UseBackupHead, FactionID, AIState (read via the INT reader despite being a WORD field, truncated on store), GoodEvil’s own default (distinct from the already-documented clamp-to-100), ChallengeRating, NaturalAC, Min1HP, PartyInteract, Disarmable, Portrait (only reached conditionally, see PortraitId below), and WalkRate’s own default (the object’s current movement_rate, distinct from the already-documented MovementRate-falls-back-to-WalkRate rule). SoundSetFile is stored on the owning CSWSCreature, not CSWSCreatureStats – consistent with the class-boundary split already documented above. SkillPoints is genuinely read twice, at two different struct scopes: once flat on the top-level struct, and separately once per entry inside LvlStatList (same label, a nested struct) – a typed view needs both reads, not one.

Naming corrections worth recording precisely. The color fields are underscored (Color_Skin, not ColorSkin), and the type field is Phenotype, not PhenoType. SubraceName does not exist as a GFF label at all – the two real, distinct labels are Subrace (the free-text name) and SubraceIndex (the numeric id); rakata’s own struct already maps these correctly.

FirstName/LastName do NOT carry over – they’re an unconditional blank stamp, unlike the structurally identical Description field a few lines away. Description’s fallback is the object’s own current value (&this->description); FirstName and LastName are each read against a freshly default-constructed, empty localized string instead, with no reference to the object’s prior name at all. An absent FirstName/LastName unconditionally overwrites whatever name the object held with an empty one – it does not leave a prior name in place, the way Description (and nearly everything else on this page) does.

Plot has an undocumented legacy-label fallback, the same shape as MovementRate->WalkRate. The loader first tries a field literally named Invulnerable; only if that’s absent does it fall back to trying Plot by name. Either way the result lands in the same plot member, written unconditionally, with a final fallback (if neither label is present) of the object’s own prior plot value. Invulnerable is a real, distinct GFF label – the same one already documented as read by LoadDoor/LoadPlaceable for their own objects – and it takes priority over Plot for creatures specifically.

NotReorienting round-trips through a polarity inversion, not a bug. The GFF-visible field is the logical negation of the internal reorienting member on both read and write; the double negation cancels out algebraically, so this isn’t a defect, just worth knowing if you’re ever comparing the label’s sense to the internal state directly.

PortraitId uses a literal sentinel default of 0xFFFF, not a carry-over. Unlike most fields on this page, PortraitId’s fallback is a fresh literal, not the object’s current value. 0xFFFF fails the same < 0xFFFE check documented for the explicit 0xFFFE sentinel, so an absent PortraitId behaves identically to writing 0xFFFE explicitly – both route to the Portrait-resref path, which itself carries over the object’s current value when absent.

Comment and the Legacy-Field Batch: Fully Confirmed Dead, With Two Corrections

A binary-wide string-existence check – the same decisive test already used for TemplateList/CRAdjust/SaveReflex/MemorizedList0 – confirms Comment, Morale, MoraleRecovery, MoraleBreakpoint, PaletteID, BlindSpot, MultiplierSet, NoPermDeath, IgnoreCrePath, Hologram, WillNotRender, and LawfulChaotic don’t exist as field-name strings anywhere in swkotor.exe. Comment specifically settles as “not read at all,” not merely “read and ignored.” TextureVar and BodyVariation are a related but distinct case: both strings genuinely exist in the binary, but their only cross-references are the item (.uti) loader and saver – a real field name on a different format, never read by any creature-related function, functionally just as dead for UTC purposes.

Two fields need pulling back out of the “confirmed dead” framing: BodyBag and Interruptable are genuinely read. Both are read live by ReadStatsFromGff via an ordinary carry-over ReadFieldBYTE call and stored into real members (creature->body_bag, this->interruptable). Neither is in this page’s own UTC-007 lint list, so there’s no existing contradiction to fix – but don’t assume UTC’s Interruptable shares the fate of the identically-named, confirmed-dead Interruptable field already documented on UTD/UTP; it’s a different field on a different format, and this one is live. Whether the values these two store are ever consumed downstream (combat/AI logic) wasn’t traced in this pass.

The 14 Script Hooks Default to "default", Matching UTD/UTT

All 14 (ScriptHeartbeat, ScriptOnNotice, ScriptSpellAt, ScriptAttacked, ScriptDamaged, ScriptDisturbed, ScriptEndRound, ScriptDialogue, ScriptSpawn, ScriptRested, ScriptDeath, ScriptUserDefine, ScriptOnBlocked – not ScriptBlocked – and ScriptEndDialogue, truncated on-disk to ScriptEndDialogu by the 16-byte GFF label limit) follow the identical mechanism already confirmed for doors and triggers: CSWSCreature’s constructor pre-arms all 14 script slots to the literal string "default" before any GFF read happens, and each read’s own fallback is the slot’s current value – so an absent hook resolves to "default", not empty. Confirmed for both entry points: LoadFromTemplate (fresh .utc blueprint load) and LoadCreature (save-reload), with no re-arming between construction and either load path.

Skills, Classes, and Powers: Presence Chains Down to the Individual Entry

UtcSkills. SkillList isn’t 8 individually-labelled fields – it’s a GFF list of 8 positional entries, each carrying one Rank byte, matching skills.2da row order. If SkillList is entirely absent, the whole block is skipped and the object’s skill ranks stay at whatever they already held (all-zero on a fresh blueprint load). If SkillList is present at all – even as an empty list – every one of the 8 positions is first force-zeroed, then each position missing its own list entry gets a default computed live from the engine’s own skill-check function: ability modifier plus any already-applied feat bonus, not a flat 0 (the raw component is 0 at this point, so that’s what the derived default reduces to in practice, but the mechanism is genuinely sibling-derived, not a literal). Absent and present-but-empty look identical on a fresh template, but diverge on an object with pre-existing nonzero ranks – absent leaves them untouched, present-but-empty wipes them.

ClassList entries. Class carries over the slot’s existing class id, applied only if the field was present and doesn’t resolve to the -1/NONE sentinel – otherwise the slot’s existing id stands untouched. A resolved-but-out-of-range class id crashes with the same 0x5f7 error code already documented for duplicate classes, not a separate failure mode – worth folding into the existing crash-code table. ClassLevel also carries over, and its own read is gated: it’s only attempted if the slot already resolved to a valid (non-NONE) class earlier in the same pass. Freshly-constructed baseline values: slot 0 defaults to Soldier at level 1, slot 1 to NONE at level 0.

Per-class powers (KnownList0) are read by a separate function, not inside the ClassList loop. CSWSCreatureStats::ReadSpellsFromGff runs after ReadStatsFromGff returns, from both LoadFromTemplate and LoadCreature, and re-walks ClassList independently. It confirms directly (not just as an observed file convention) that the per-class known-power list label is always literally built as "KnownList" + 0, regardless of which class index is being processed. An absent or empty KnownList0 simply leaves that class with zero known powers – no abort. Within a present list, each power’s Spell field defaults to the sentinel 0xFFFF; when a power resolves to that sentinel (explicit or absent), the whole power entry is skipped and never appended – a presence-chain abort at the individual-power level, not a defaulted 0 power. This is a real divergence from rakata’s current code: parse_known_list in crates/rakata-generics/src/utc.rs currently defaults a missing Spell to 0 and still pushes that as a power, where the engine would have silently dropped the entry instead.

A related but distinct read lives inside the same ClassList loop (not ReadSpellsFromGff): a Jedi-only SpellsPerDayList/NumSpellsLeft block for uses-per-day bookkeeping. An absent list is a soft no-op, and only the first entry of that list is ever actually applied – any further entries are read but discarded.

SpecAbilityList entries. Spell, SpellFlags, and SpellCasterLevel each independently default to a literal 0, unconditional, no presence gate on any of the three. An entry is appended as soon as the list-element fetch itself succeeds – there’s no scenario where a kept entry gets dropped for missing scalar fields, matching and sharpening the page’s existing “unconditionally appends… no deduplication” description with the actual defaults.

FeatList entries. Feat defaults to 0, but the add is gated on presence: AddFeat is only called if the field was actually present, so an absent Feat contributes nothing at all for that list position – a presence-chain abort scoped to the single entry, confirmed unambiguously via an identical read idiom used elsewhere in the same function (the per-level FeatList inside the PC LvlStatList loop), even though the top-level call site’s own presence-flag register couldn’t be resolved with full certainty from the decompiler output alone.

Item Lists: EquippedRes/InventoryRes Absence Drops the Whole Entry

Equip_ItemList is one flat GFF list, not per-slot numbered fields (Equip_ItemList0/1/… don’t exist) – the equip slot itself is the list element’s own struct-id, read structurally off the GFF element header rather than any field, so it has no “absent” state to document; rakata’s existing slot_id modeling is already correct.

Both EquippedRes (on Equip_ItemList) and InventoryRes (on ItemList) share the same fate on absence: presence-gated, and if the field is missing, or present but doesn’t resolve to a real .uti blueprint, the freshly-allocated item object is destroyed on the spot and the loop moves on – a genuine presence-chain abort. A resref-less entry is dropped entirely, not kept with an empty resref. Dropable is unconditional on both lists: a literal 0 (not droppable) stamped regardless of presence, no gating.

One thing that looks like an absent-field question but isn’t: if an equipped item fails the CanEquipItem slot check after loading successfully, it isn’t discarded – it’s rerouted into the creature’s backpack instead. That’s a post-load routing decision, not a defaults question.

Repos_PosX/Repos_PosY are never read anywhere in the creature item-loading call graph – confirmed by decompiling every function that touches Equip_ItemList/ItemList entries for a creature, and by a binary-wide string search. The only code that reads either label at all is a different function entirely (ReadContainerItemsFromGff, serving placeable/store containers, i.e. UTP/UTM), not anything reachable from a creature load. So on the creature path specifically, these two fields are structurally inert – stronger than “usually absent,” genuinely unread regardless of what a hand-authored file supplies. Worth knowing too: the only casing that exists as a string anywhere in the binary is Repos_PosX and lowercase Repos_Posy – there is no Repos_PosY (uppercase Y) string in swkotor.exe at all, on any object type.

What actually can drop an item entry entirely, on the save-reload path only (LoadCreature, unreachable from a standalone .utc blueprint load): each entry can carry an ObjectId pointing at an already-instantiated item object, and the loader silently skips the entry if that item’s live possessor doesn’t match the creature currently being loaded – no equip, no backpack add, nothing. This plausibly the origin of the “spawns dead” framing (items reassigned to a corpse/loot container after death would trigger exactly this mismatch), but the actual mechanism is narrower and different: a stale-possessor check specific to save-reloads, not a hit-points-driven rule, and it never fires on a fresh .utc blueprint spawn at all.

Gold: Party Members Don’t Round-Trip It

Gold is written for every creature, but on load the engine skips the read for anyone currently in the party: a party member’s wealth lives in the shared PT_GOLD pool in PARTYTABLE.res, and the per-member snapshots are frozen copies the loader deliberately ignores. Editing a party member’s Gold in a save does nothing – edit PT_GOLD instead. Ordinary NPCs, merchants, and corpses round-trip Gold normally. The full routing design, including how the writer freezes those per-member copies, is covered in Gold and the party pool.

DetectMode: A Genuine Round-Trip Bug

Unlike the write-only fields above, DetectMode looks like it should round-trip and doesn’t, and this one reads like an honest bug rather than a design choice. SaveCreature writes the live detect-mode value faithfully. On load, LoadCreature reads a DetectMode byte from the save struct just to advance past it; the value is never assigned to anything, and construction-time logic resets every restored creature to detect mode 1 regardless of what the save contained. This is an engine quirk to be aware of, not something rakata should “correct” on read; the on-disk value is real and byte-accurate, the engine’s own loader simply never consumes it.

JoiningXP: Only Restored on Fresh Spawns

JoiningXP shares the same shape as the DetectMode bug. SaveCreature writes it on every save, but only LoadFromTemplate (the fresh-spawn path used for template-based creatures) reads it back. LoadCreature, the loader used for ordinary save-game continuation, never reads JoiningXP at all, so it silently resets to 0 every time a save is reloaded.

Structural Fields Written Only When Live

FollowInfo (party-follow state) and ExpressionList (listen/expression data) are only written by SaveCreature when the corresponding live pointer or list is actually populated. Their absence from a save isn’t a defaults question so much as a statement that there was no runtime state to save in the first place; on load, an absent struct just means that piece of party-follow or listen-data state stays unallocated.

A few more fields depend on runtime conditions at save time rather than always being present:

Field(s)Emitted whenAbsent on load resolves to
PM_AppearancePM_IsDisguised == 10; the loader only attempts the read at all if PM_IsDisguised decoded true
CombatRoundData contentsCombat was mid-round at save timeThe struct header is always present, but SaveStats itself has no writer counterpart for this data at all; it’s the outer SaveCreature that writes the struct shell, and whether the roughly two dozen combat-round scalars inside it were actually populated depends entirely on whether the game happened to be captured mid-round
EffectList, VarTable, SWVarTable, ActionListList/struct headers are always written; contents reflect however many entries currently existEmpty containers simply restore no effects, script variables, or queued actions

As a minor aside: the per-level and per-class known-spell lists are always labelled KnownList0 / KnownRemoveList0 in the GFF, literally suffixed with the digit zero rather than substituting the level or class index. Reader and writer agree on this, so it’s internally consistent rather than a bug, just an oddity worth knowing if you’re ever diffing raw GFF structs by hand.

Legacy & Ignored Data

Finding TypeExplanation
Legacy Engine ArtifactsA staggering 17 .utc fields (such as Morale, SaveWill, BlindSpot, PaletteID) present in older files are actually Neverwinter Nights superset metrics that the K1 engine natively ignores.
Confirmed-Dead by String AbsenceTemplateList (a List, present but empty in every .utc in a full install – the single highest-prevalence unmodeled label found in the corpus), CRAdjust, SaveReflex, and MemorizedList0 don’t exist as field-name strings anywhere in swkotor.exe at all, the same decisive test already used to confirm several dead DLG fields. No code path can branch on TemplateList’s presence or read its contents; there’s no reason for a canonical writer to start emitting an empty one where rakata currently omits it, since nothing reads it either way. SaveReflex follows the same dead pattern already documented for SaveWill/SaveFortitude – none of the three raw saving-throw fields exist as strings to trace an override from, though the mechanism itself (a live refbonus-style computation superseding all three) is inferred by analogy rather than directly confirmed for SaveReflex.

Vanilla Data Anomalies

Corpus surveys of the K1 GOG .utc set surface two anomalies in the SpecAbilityList field. Both are vanilla data quirks rather than decoder bugs; the structural reader represents them faithfully.

Stacked SpecAbilityList entries on the Bastila variants

Six Bastila .utc templates (bastila00c, p_bastilla, p_bastilla001, p_bastilla003, p_bastilla005, p_bastilla006) each carry 99 identical entries of Spell = 52 (SPECIAL_ABILITY_BODY_FUEL) in their SpecAbilityList. The engine’s loader (the SpecAbilityList block of CSWSCreatureStats::ReadStatsFromGff at 0x005afce0) walks each list element and unconditionally appends (Spell, SpellFlags, SpellCasterLevel) to the in-memory special_abilities_ array; there is no deduplication step. Each of the 99 entries occupies its own array slot with independent SpellFlags and SpellCasterLevel, so the stacking is faithfully preserved at runtime.

The loop iteration count itself is taken from CResGFF::GetListCount masked down to a single byte, so any UTC with more than 255 SpecAbilityList entries would have its tail silently truncated at load. Bastila’s 99 sits comfortably below that cap.

Out-of-range Spell id on the partymember template

The partymember.utc template references Spell = 299. Vanilla K1 spells.2da has 132 rows (0131), so 299 does not resolve to any row. The SpecAbilityList loader does not validate Spell against spells.2da at load time; the value is stored verbatim in the in-memory entry. spells.2da is itself read into a per-row struct array sized exactly to row_count (CSWClass::LoadSpellsTable at 0x005be4c0), so a use-time lookup of Spell = 299 indexes past the end of that array. The realised behaviour depends on heap layout at runtime and is not deterministic from the load path alone.

Both anomalies are candidate targets for future Phase 2 / Phase 3 UTC lint rules (e.g., “SpecAbilityList[].Spell must resolve to a row in spells.2da”; optionally “warn on stacked-duplicate SpecAbilityList entries unless explicitly whitelisted as a known vanilla pattern”).


Implemented Linter Rules (Rakata-Lint)

Phase 1 (intra-resource, no context)

Implemented under rakata_lint::rules::utc.

  1. UTC-001 (Appearance Correction): Warns when Appearance_Head == 0; the engine forces this to 1 at runtime.
  2. UTC-002 (Class Limit): Warns when more than 2 entries appear in ClassList; the engine ignores classes beyond the second.
  3. UTC-003 (Class Duplications): Errors when duplicate class IDs exist in ClassList; causes a fatal engine crash (0x5f7) on load.
  4. UTC-004 (Dead Save Fields): Informs when SaveWill or SaveFortitude are populated; the engine reads willbonus/fortbonus instead.
  5. UTC-005 (Gender Clamp): Warns when Gender > 4; the engine clamps to a maximum of 4.
  6. UTC-006 (GoodEvil Clamp): Warns when GoodEvil > 100; the engine clamps to a maximum of 100.
  7. UTC-007 (Toolset / Legacy Fields): Informs when any of Comment, Morale*, PaletteID, BodyVariation, TextureVar, BlindSpot, MultiplierSet, NoPermDeath, IgnoreCrePath, Hologram, WillNotRender, or LawfulChaotic are set; never read by the K1 engine.

Phase 2 (range / 2DA / resref existence, requires LintContext)

Implemented under rakata_lint::rules::utc_range.

  1. UTC-008 (Race Bounds): Errors when Race does not resolve to a row in racialtypes.2da; engine crash 0x5f4 on load.
  2. UTC-009 (Class Bounds): Errors when any ClassList[].Class does not resolve to a row in classes.2da (or is negative); engine load failure.
  3. UTC-010 (Appearance Bounds): Errors when Appearance does not resolve to a row in appearance.2da; engine renders missing model.
  4. UTC-011 (Portrait Bounds): Errors when PortraitId (when not the 0xFFFE “use string Portrait” sentinel) does not resolve to a row in portraits.2da.
  5. UTC-012 (Resref Existence): Warns when Conversation (.dlg), Portrait (.tga), any of the 14 Script* hooks (.ncs), Equip_ItemList[i].EquippedRes (.uti), or ItemList[i].InventoryRes (.uti) does not resolve in the configured resource sources.