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)

A .utc file is a creature. Every NPC, enemy, droid and companion in the game starts life as one of these blueprints: a template that says who the creature is, what it looks like, how strong it is, what it carries, and which scripts fire when it notices you or takes a hit.

UTC is one of the formats built on GFF (Generic File Format), the engine’s labelled key/value tree. If you have not read the GFF page yet, start there. This page assumes you know what a field label and a struct are.

You will touch a .utc when you want to change a creature’s stats, swap its appearance, give it different gear, or hook a new script to it. You will touch the saved form of one when you edit a character mid-playthrough.

This page documents UTC’s field defaults, the class-boundary split between CSWSCreature and CSWSCreatureStats, and the save-versus-template load paths. Evidence is drawn from Ghidra decompilation of swkotor.exe (K1 GOG build), cross-checked against a full K1 install’s vanilla .utc corpus and real save files. The tables below are lookup surfaces, meant to be searched rather than read start to end.

At a Glance

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

Why this page is the long one

Two design decisions in the engine account for nearly everything surprising here, and if you hold them in mind the rest of the page stops looking like a list of exceptions.

One loader serves both a blueprint and a save. ReadStatsFromGff has no branch distinguishing “fresh creature from a template” from “restore this character”. So a field documented as save-only is often read on the blueprint path too, and a bug on one path is a bug on both. The differences that do exist are usually one level up, in the caller, not in the field handling.

A creature is constructed first and the file is overlaid onto it. Nothing here is built from the file. An absent field almost always means “keep what the constructor put there”, which is why so much of this page is about values you never see written down anywhere.

Creatures also hold more state than any other template, so there is simply more surface.

Blueprints ship inside module archives and the BIF files a chitin.key points at. The Aurora toolset produces them, and every mod tool since has followed its lead.

The same field set appears again inside a save game. When the engine stores a module it writes each live creature into that module’s GIT as a full snapshot, not as a reference back to the blueprint. Those snapshots carry runtime state a static blueprint never populates. See the Save Game Deep Dive for how the pieces fit together.

Field Schema

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

FamilyCoversRepresentative fields
Core statisticsBase stats defining physical capabilityStrength, Dexterity, HitPoints
Identity and graphicsWho the creature is, which model it usesTag, Appearance_Type, Conversation
Class and skill progressionLevel, classes, skillsClassList, SkillList
Combat capabilitiesFeats and Force powersFeatList, SpellList
Inventory and equipmentSpawn gear, both equipped and carriedEquip_ItemList, ItemList
Event hooksScripts that fire on world eventsOnNotice, OnDamaged

Engine Audits & Decompilation

The two functions that matter most are CSWSCreatureStats::ReadStatsFromGff at 0x005afce0 and its save-side counterpart CSWSCreatureStats::SaveStats at 0x005b1b90.

(Provenance: derived, not attested. These rows sit on the reverse-engineering queue. Where a specific claim is weaker than the rest, we say so inline.)

How the engine loads a creature

These functions do the work. ReadStatsFromGff is the big one.

FunctionSizeWhat it does
ReadStatsFromGff7835 BParses the basic creature scalars: strength, dexterity, physical appearance and the rest.
LoadCreatureSets up how the creature sits in the world: stealth state, collision size, idle animations.
CSWSCreature::ReadScriptsFromGffAttaches the event scripts that fire on notice, damage, death and heartbeat. A genuine member of CSWSCreature, confirmed by its decompiled __thiscall signature, not a free function.
ReadItemsFromGffPulls loot into memory, sorting items into equipped slots or the backpack.
ReadSpellsFromGffExtracts the Force powers and combat feats the creature may use.

You may have read elsewhere that ReadItemsFromGff drops everything if a creature spawns dead. That framing does not survive the decompile. No branch anywhere in this call graph inspects hit points or a dead/alive state. See what actually drops an item entry.

Which class owns which field

A creature’s fields split across several engine classes. That matters because the split decides which function reads a given label, and each function brings its own defaulting rules. Two fields sitting next to each other in the file can behave differently on absence purely because different classes own them.

ReadStatsFromGff reads the bulk of a creature’s identity inline: the abilities, HP and FP pools, appearance and portrait fields, faction, challenge rating, AI state, and perception range. It reads ClassList and 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.

Two structured exceptions delegate out on load: CombatRoundData goes to CSWSCombatRound::LoadCombatRound, and the nested CombatInfo struct goes to CCombatInformation::LoadData.

That is a large, coherent chunk of the shared field set. It is not all of it. A comparable amount lives one level up, read and written inline by CSWSCreature::SaveCreature and LoadCreature with no CSWSCreatureStats involvement:

DetectMode, StealthMode, CreatureSize, IsDestroyable, IsRaiseable, DeadSelectable, AmbientAnimState, Animation, CreatnScrptFird, PM_IsDisguised, PM_Appearance, Listening, the full set of Script* event hooks, position and orientation, AreaId, and JoiningXP.

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, owning everything under Save-game snapshot fields plus the class, skill, feat and power progression. But no single class owns the shared field set: it spans CSWSCreatureStats, CSWSCreature, CCombatInformation, CSWSCombatRound and CSWSCreaturePartyFollowInfo, plus two unnamespaced free functions. That is why a Rust type modelling the whole block needs a rakata-invented name rather than a borrowed one.

Constructed defaults on the CSWSCreature side

Most CSWSCreature-owned fields construct to a plain literal and stay there. StealthMode, CreatureSize, AmbientAnimState, CreatnScrptFird, PM_IsDisguised, PM_Appearance and JoiningXP all zero-initialize in CSWSCreature’s own constructor.

Two are overridden inside that same constructor and do not end where they start. Animation takes 10000 from the base CSWSObject constructor and is immediately re-set through SetAnimation, landing at 10001. DetectMode zero-initializes and is then bumped to 1 by an internal SetDetectMode call.

The base class CSWSObject, not CSWSCreature, owns several more:

FieldConstructed value
IsDestroyable1
IsRaiseable, DeadSelectable, Listening0
positionthe origin, (0.0, 0.0, 0.0)
orientation(1.0, 0.0, 0.0), three components rather than two
AreaIdthe object-reference sentinel 0x7F000000, not a plain 0

Important

LoadCreature does not use those constructed values as its absent-field fallback. Its defaults are hardcoded literals baked into each read call site rather than live reads of the object’s current field. Several disagree with what the constructor sets:

FieldRead fallbackConstructed
CreatureSize30
IsRaiseable10
DeadSelectable10
Animation1000010001
AreaId00x7F000000

For a save missing one of those, the creature ends up at the read literal, not at what a fresh CSWSCreature would hold. The read default is the correct absent-field answer there.

DetectMode’s read default is a mismatched 0 as well. It reconciles only because the constructor’s SetDetectMode(1) runs after LoadCreature returns, not because the read carries the true value.

For the rest of the group (IsDestroyable, StealthMode, AmbientAnimState, CreatnScrptFird, PM_IsDisguised, PM_Appearance, Listening) the literal and the constructed value coincide, so the split is invisible. It is still a distinct mechanism, not a coincidence that generalises.

Position, orientation and JoiningXP are not read by LoadCreature at all. They come through CSWSArea::LoadCreatures and LoadFromTemplate instead. Both use their own literal 0.0 position default, matching the constructed origin, and both apply orientation only when the read value is nonzero. That is a presence-adjacent gate, not a plain carry-over. A template-spawned creature with no orientation in the file keeps the constructor’s (1.0, 0.0, 0.0) facing, but by that separate mechanism rather than by LoadCreature’s own defaulting.

Rules that crash the game

Most of a creature’s numbers are not values. They are row indices into 2DA tables: Race indexes racialtypes.2da, Appearance indexes appearance.2da, a resolved movement rate indexes creaturespeed.2da. The file supplies the index and the table supplies the meaning.

That is why the failures here are crashes rather than warnings. A row index that does not name a row has no fallback meaning to reach for, so the engine notices and gives up instead of continuing with a creature it cannot describe.

Warning

Fatal crash codes (0x5fX) When the engine parses a file and hits an invalid stat, it aborts loading entirely. Instead of recovering, it triggers a fatal crash to desktop and returns a hexadecimal error code such as 0x5f7 or 0x5f4. The rules below track the scenarios that produce one.

RuleRuntime behaviour
Class identityA duplicate class id in ClassList crashes the game (0x5f7). A resolved-but-out-of-range class id crashes with the same code, not a separate failure mode.
Race boundsThe engine compares Race against the compiled row count of racialtypes.2da. Exceeding it fatally crashes the map loader (0x5f4).
Saves calculationPre-computed saving throws (SaveWill, SaveFortitude) in the file are ignored dead data. The engine reads willbonus and fortbonus instead.
Perception faultsA non-PC PerceptionRange triggers a read against appearance.2da for PERCEPTIONDIST. Failing to resolve that distance fails the whole creature load (0x5f5).
Hard clampingGender clamps structurally at a maximum of 4. GoodEvil clamps so it cannot exceed 100.
Appearance shiftingAn Appearance_Head of 0 is overridden to 1.

PerceptionRange takes the fault-prone path by default. The field is never read at all for a PC. For a non-PC, an absent PerceptionRange defaults to a hardcoded literal 11, not a carried-over value. That 11 is itself the sentinel routing into the appearance.2da lookup above. So an absent field lands on the failure-prone branch by construction, not by coincidence. Any other resolved value is used directly as a ranges.2da row index instead.

Appearance_Head fires on the resolved value. The check never asks whether the field was present, so it behaves the same for an explicit 0 and for an absent field’s carried-over default. Almost no vanilla .utc carries the field, so nearly every creature takes the carried-over 0 and is bumped to 1. No creature can end up stored at 0.

WalkRate is read only where MovementRate was absent. Where MovementRate is present, the WalkRate read is skipped outright rather than performed as a no-op.

Both movement fields absent lands on creaturespeed.2da row zero

Every shipped creature omits both MovementRate and WalkRate. The constructor’s zero-initialized value survives both reads and reaches the movement-rate setter as a literal 0.

That 0 is not a speed. It is a row index into creaturespeed.2da, whose WALKRATE and RUNRATE columns hold the actual floats.

The setter carries one sentinel. A resolved value of exactly 7, which the both-absent case cannot reach, redirects to an appearance.2da and creaturespeed.2da name lookup instead of indexing directly.

Absent-field defaults

The engine does not build a creature from the file. It builds a creature first, then overlays the file onto it.

That is the single idea this whole section rests on. ReadStatsFromGff runs against an object whose constructor has already set every member to something. Most reads pass the member’s own current value in as the fallback and then stamp the result back unconditionally, so an absent field resolves to whatever construction left there. We call that carry-over.

Two consequences follow:

  • Absent does not mean zero. It means “whatever the constructor chose”, and the constructor does not always choose zero. HitPoints starts at 1, the script hooks start at "default", AreaId starts at a sentinel.
  • The same file loads differently onto a used object. On a fresh blueprint load carry-over is a no-op, because the construction value is all that was there. On a save reload onto an object that already holds state, an absent field silently preserves that state.

These apply on any load, blueprint or save alike, because ReadStatsFromGff does not branch on its caller. The fields that break the pattern each get a heading below.

The core abilities default to 0, not a tabletop 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. The constructor initializes every one of them to a literal 0 before any read happens.

So a .utc missing an ability score does not fall back to a sensible tabletop default. It resolves to 0, not 10.

Identity, appearance and state

These all use plain carry-over:

Tag, Conversation, Deity, Description, Age, StartingPackage, Subrace, SubraceIndex, Color_Skin, Color_Hair, Color_Tattoo1, Color_Tattoo2, Phenotype, Appearance_Type, DuplicatingHead, UseBackupHead, FactionID, ChallengeRating, NaturalAC, Min1HP, PartyInteract, Disarmable.

Some need a qualifier:

FieldQualifier
GenderThis is its own absent default, separate from the clamp to 4 above.
GoodEvilLikewise separate from the clamp to 100 above.
AIStateRead through the INT reader despite being a WORD field, and truncated on store.
WalkRateIts own default is the object’s current movement_rate, separate from the MovementRate falls back to WalkRate rule above.
PortraitReached only conditionally. See PortraitId below.

Naming corrections

The colour fields are underscored: Color_Skin, not ColorSkin. 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).

TemplateResRef is not read by ReadStatsFromGff at all

TemplateResRef never appears anywhere in ReadStatsFromGff. It is read one level up, by CSWSArea::LoadCreatures, and only on the branch resolving a GIT Creature List entry’s blueprint reference. The direct save-instance path 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.

This is a genuine presence-chain abort. The identical pattern, same field and same behaviour, governs blueprint resolution for triggers, placeables, items, doors, encounters and sounds. A templated GIT entry missing its own TemplateResRef is dropped outright, not defaulted.

FirstName and LastName

Neither carries over. Both are an unconditional blank stamp, unlike the structurally identical Description 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.

An absent FirstName or LastName 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.

SoundSetFile

It is not a resref. It reads with ReadFieldWORD as a soundset.2da row index, and it lives on the owning CSWSCreature rather than CSWSCreatureStats, matching the class boundary above.

It constructs to -1 (0xFFFF as a WORD), the same “nothing assigned” sentinel as the WORD-indexed PortraitId and FactionID. That is not the 0x7F000000 object-reference sentinel used elsewhere on this page.

PortraitId

Its default is a literal sentinel 0xFFFF, not a carry-over. Unlike most fields here, PortraitId’s fallback is a fresh literal rather than 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.

SkillPoints is read twice, at two struct scopes

A typed view needs both reads: one flat on the top-level struct, and one per entry inside LvlStatList under the same label.

Both construct to 0, by different mechanisms. The flat read is the ordinary idiom, defaulting to the top-level member’s own constructed value. The per-entry read borrows that same top-level member rather than the freshly-built level-up record’s own field, because the loop runs before the flat read while the top-level member still holds its constructed value.

The level-up record’s field independently constructs to 0 too, so nothing diverges in practice. The mechanism is still unique on this page: nowhere else does a sibling’s value stand in for a field’s own.

Plot and Invulnerable

Plot has an undocumented legacy-label fallback, the same shape as MovementRate to WalkRate. The loader first tries a field literally named Invulnerable. Only if that is 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 of the object’s own prior plot value if neither label is present.

Invulnerable is a real, distinct GFF label, the same one already documented as read by LoadDoor and LoadPlaceable for their own objects. It takes priority over Plot for creatures specifically.

NotReorienting

It round-trips through a polarity inversion, which is 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.

Worth knowing only if you are comparing the label’s sense to the internal state directly.

The script hooks default to "default"

They follow the mechanism already confirmed for doors and triggers. CSWSCreature’s constructor pre-arms every script slot 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.

The hooks are ScriptHeartbeat, ScriptOnNotice, ScriptSpellAt, ScriptAttacked, ScriptDamaged, ScriptDisturbed, ScriptEndRound, ScriptDialogue, ScriptSpawn, ScriptRested, ScriptDeath, ScriptUserDefine, ScriptOnBlocked (not ScriptBlocked), and ScriptEndDialogue, which truncates on disk to ScriptEndDialogu under the 16-byte GFF label limit.

Confirmed for both entry points: LoadFromTemplate for a fresh .utc blueprint load, and LoadCreature for a save reload, with no re-arming between construction and either path.

Skills, classes and powers

SkillList

SkillList is not eight labelled fields. It is a GFF list of eight positional entries, one Rank byte each, in skills.2da row order. rakata models it as UtcSkills.

Absent and present-but-empty are different, and the difference only shows on an object that already holds ranks:

SkillListEffect
AbsentThe block is skipped and existing ranks stay untouched.
Present, including emptyAll eight positions are force-zeroed first.

Where the list is present, a position with no entry of its own takes a default computed live from the engine’s skill-check function: ability modifier plus any feat bonus already applied, not a flat 0. The raw component is 0 at that point so it reduces to zero in practice, but the mechanism is sibling-derived rather than literal.

ClassList

Class carries over the slot’s existing class id, applied only if the field was present and does not resolve to the -1/NONE sentinel. Otherwise the slot’s existing id stands untouched.

ClassLevel also carries over, and its own read is gated: it is attempted only 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.

A creature has two class slots, and that is a property of the object rather than of the file. A ClassList carrying more entries than that has nowhere to put the extras. Note that this is a different situation from the two 0x5f7 crashes above: a duplicate id and an out-of-range id are both fatal, while a third valid, distinct entry is not. See Open questions for how firmly we have established that.

LvlStatList

The per-level ledger: one entry per level the character has taken, extended one entry at a time by CSWSCreatureStats::LevelUp during ordinary play. For the player character it is also what the hit point and force pools are summed from, rather than from ClassLevel. See Write-only fields.

Every field in an entry reads with the entry’s own current value as its fallback, so an absent field leaves whatever the entry already held, and a short or missing list does not abort the load.

LabelWhat an absent field leaves behind
LvlStatAbilitycarries over. Constructs to 6
LvlStatHitDiecarries over. Constructs to 0
LvlStatForcecarries over. Constructs to 0
LvlStatClasscarries over. Constructs to 0
SkillPointsthe creature’s top-level SkillPoints, not the entry’s own
SkillList[].Rankcarries over, per skill
FeatListuntouched. Presence-gated per entry
KnownList0untouched. Append-only, never cleared
KnownRemoveList0untouched. Append-only, never cleared

LvlStatAbility’s constructed 6 is a sentinel rather than a value. The valid ability range is 0 through 5, so a 6 matches no case and applies no bump. SaveClassInfo agrees from the other side: it emits the label only when the stored value is not 6. Absent and an explicit 6 are the same state at both ends.

The per-entry SkillPoints borrows its default from outside the entry. It falls back to the creature’s top-level SkillPoints rather than to the entry’s own prior value, because that loop runs ahead of the flat read later in the same pass. Both resolve to 0 today only because the top-level member constructs to zero, so a file that sets the top-level field while omitting it on an entry would diverge.

A ledger entry’s nested lists are inert on load. FeatList, KnownList0 and KnownRemoveList0 inside an entry are separate storage from their same-named counterparts at the top level, and nothing re-applies them to the creature. They become live only when a live level-up consumes that entry.

Struct ids, for anyone writing this list. SaveClassInfo uses a flat literal per list rather than a running counter. Every LvlStatList element carries 0, as does every element of a ledger entry’s own FeatList, KnownList0 and KnownRemoveList0. At the top level a ClassList element carries 2 and a FeatList element 1, and a KnownList0 nested inside ClassList carries 3. So KnownList0 means two different ids depending on which depth wrote it.

KnownList0 and SpellsPerDayList

Per-class powers 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 built literally as "KnownList" + 0 regardless of which class index is being processed.

An absent or empty KnownList0 leaves that class with zero known powers, with no abort. Within a present list, each power’s Spell field defaults to the sentinel 0xFFFF. When a power resolves to that sentinel, explicitly or by absence, the whole power entry is skipped and never appended. That is a presence-chain abort at the individual-power level, not a defaulted 0 power.

A related but distinct read lives inside the same ClassList loop, not in ReadSpellsFromGff: a Jedi-only SpellsPerDayList and 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 applied. Further entries are read and discarded.

SpecAbilityList

Spell, SpellFlags and SpellCasterLevel each independently default to a literal 0, unconditional, with no presence gate on any of the three.

An entry is appended as soon as the list-element fetch succeeds. There is no scenario where a kept entry gets dropped for missing scalar fields.

FeatList

Feat defaults to 0, but AddFeat is called only where the field was present, so an absent Feat contributes nothing for that list position. That is a presence-chain abort scoped to the single entry.

(We could not resolve the top-level call site’s own presence-flag register with certainty from the decompiler output. The reading comes from the identical idiom in the per-level FeatList inside the PC LvlStatList loop.)

At the list level, FeatList does not share SkillList’s absent-versus-empty asymmetry. The list is iterated only once the field is found and its element count is nonzero, both checked together. So an absent FeatList and a present-but-empty one hit the same skip and leave existing feats untouched.

There is no equivalent of SkillList’s force-zero-then-refill anywhere in this function. That step exists for skills because the eight ranks are a fixed-size array needing a defined baseline. Feats have no fixed slots to reset. The two diverge for that structural reason, so the skill behaviour does not transfer.

Item lists

Equip_ItemList is one flat GFF list, not per-slot numbered fields. Equip_ItemList0, Equip_ItemList1 and so on do not exist. The equip slot is the list element’s own struct id, read structurally off the GFF element header through CResGFF::GetElementType rather than from any field. It has no “absent” state to document.

ItemList does the opposite in the same loader. It is walked purely by position and its element struct ids are never read. So the sequential values vanilla writes there carry nothing, and a writer is free with them in a way it is not with Equip_ItemList’s.

EquippedRes and InventoryRes

Both share the same fate on absence. Each is presence-gated, and if the field is missing, or present but does not resolve to a real .uti blueprint, the freshly-allocated item object is destroyed on the spot and the loop moves on. That is 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, meaning not droppable, is stamped regardless of presence, with no gating.

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

ObjectId, and the one thing that drops an entry

An entry can be dropped outright, and only on the save-reload path (LoadCreature, which is unreachable from a standalone .utc blueprint load).

Each entry can carry an ObjectId pointing at an already-instantiated item. The loader silently skips the entry where that item’s live possessor is not the creature being loaded: no equip, no backpack add, nothing.

That is plausibly the origin of the “spawns dead” framing, since items reassigned to a corpse or loot container after death would trigger exactly this mismatch. The real mechanism is narrower: a stale-possessor check specific to save reloads, not a hit-points rule, and it never fires on a fresh blueprint spawn.

Both Equip_ItemList[].ObjectId and ItemList[].ObjectId use the same object-reference sentinel 0x7F000000, not plain 0. The read runs only on a save reload. On a fresh blueprint load the loop’s local is pre-initialized to that same sentinel, so both paths land identically.

Repos_PosX and Repos_Posy are inert here

Both are unread on the creature path, regardless of what a hand-authored file supplies. The only code reading either label is ReadContainerItemsFromGff, which serves placeable and store containers, and nothing reachable from a creature load calls it.

We established this by decompiling every function that touches a creature’s Equip_ItemList or ItemList entries, and by a binary-wide string search.

That search also settles the casing. The strings in swkotor.exe are Repos_PosX and lowercase Repos_Posy. There is no Repos_PosY with an uppercase Y anywhere in the binary, on any object type.

ItemList[].Infinite is a store field

It is not a creature field at all, unlike its identically-named counterpart on UTM’s own ItemList.

The "Infinite" label string has exactly two cross-references in the whole binary, both inside CSWSStore::LoadStore and SaveStore. No function anywhere in a creature’s item-loading call graph reads it.

There is no absent-field default to give here, the same way there is none for a field that is never looked up. This is a store-exclusive field that happens to share a label and a parent-list name with UTC’s own ItemList.

Save-game snapshot fields

A creature serialized into a save carries live runtime state a static .utc blueprint does not usually populate. CSWSCreatureStats::SaveStats emits these alongside the template fields, and they appear on the creature structs inside a save’s module GIT.

“Usually” is doing real work in that sentence. ReadStatsFromGff has no branch distinguishing a blueprint struct from a save-instance struct, so several of these snapshot fields are genuinely read on the blueprint path too, the same pattern .ute encounters show. Vanilla .utc files simply never populate them. Others are write-only on every path, blueprint included.

FieldMeaningRead on the blueprint path too?
CurrentHitPointsLive current HP.Yes. Unconditional single read site, no UseTemplates-style gate. The absent-field default derives from HitPoints, not from any prior “current HP” state.
HitPointsThe HP pool CurrentHitPoints derives from.Yes. Unconditional, carrying over the object’s own hit points.
MaxHitPointsComputed HP ceiling.No. The field-name string has exactly two cross-references in the whole binary, both writers (SaveStats, SaveCharGenCreature). Zero readers anywhere.
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, not carried over independently.
MaxForcePointsComputed Force-point ceiling.No. Same exhaustive write-only check as MaxHitPoints.

The HitPoints carry-over bottoms out at 1. The object constructor sets hit points to 1 before any GFF field is read. So a blueprint omitting HitPoints produces a creature on one hit point, and one omitting both fields produces a creature on one current hit point too.

The remaining snapshot fields:

FieldMeaning
RefSaveThrow, WillSaveThrow, FortSaveThrowComputed saving-throw totals: base plus ability modifier plus active effects. Distinct from the template’s ignored SaveWill and SaveFortitude dead fields. Confirmed write-only by the same exhaustive check as MaxHitPoints. SaveStats computes each fresh at save time, so there is no backing field for an absent-field default to apply to.
ArmorClassComputed AC snapshot, via CSWSCreature::GetArmorClass() at save time. Confirmed write-only by the same exhaustive check.
ExperienceRuntime progression, omitted by every vanilla .utc, so every creature carries over the constructor’s 0. That 0 passes through SetExperience, which refuses to lower a creature’s XP by comparing against the current value and storing only where the incoming one is larger. Both sides are 0 here, so the call is a no-op. A plain uncapped counter with no sentinel range.
GoldNot a CSWSCreatureStats field. The backing storage is CSWSCreature::gold, proxied through GetGold and SetGold from ReadStatsFromGff and SaveStats. Unlike the fields above, this one genuinely follows the self-referencing carry-over idiom, constructing to 0.
AIStateConstructs to 0. Stored as a WORD member but read through the INT reader, the truncation quirk already documented. Unconditional carry-over, confirmed genuinely read on the blueprint path too.
NotReorientingNot its own field either. The backing storage is CSWSObject::reorienting, and NotReorienting is written and read as its logical inverse. reorienting constructs to 1 (true), so NotReorienting constructs to 0 (false). Confirmed genuinely read on the blueprint path too, unconditional carry-over.
MClassLevUpInMulticlass level-up bookkeeping. Confirmed write-only: computed fresh at save time as class_count - 1 from the live class count, with no backing field to default when the file omits it. A freshly built object would only ever export MClassLevUpIn = 0, because class_count itself constructs to 1.

Combat state, meaning 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 and power progression is written by SaveClassInfo (0x005aec90) and reflects the creature’s current levelled 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.

Tail and Wings are a round-trip loss, not inert legacy data

ReadStatsFromGff never looks either label up. There is no ReadFieldBYTE call for them anywhere in the function. It assigns 0 to both members flat and unconditionally, overwriting whatever the object held and whatever the file contains.

That is identical on the blueprint path and the save-instance path, since both call the same ReadStatsFromGff and nothing in it distinguishes the callers.

SaveStats still writes both out on every save. So whatever a creature’s tail or wings hold in a save file is discarded the moment it loads back in.

For what a writer should do with them, see fields the engine never reads. Dropping them from a blueprint costs nothing. A save writer matching the engine’s own output writes them as 0.

Write-only fields

Several of the fields above look like round-trip state but are strictly one-way. SaveStats writes them on every save, and ReadStatsFromGff never reads one back. (Tail and Wings are in the same club.)

They are one-way for two very different reasons, and the difference decides whether editing one is pointless or actively misleading.

MaxHitPoints, ArmorClass and the three saving-throw totals are recomputed, not restored. Each is written from a live getter, the same getters combat rolls and UI displays call at runtime. 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. For non-PC creatures, the template’s own HitPoints, confirmed to be the literal raw value this same field feeds into CurrentHitPoints’s sibling-derived default.
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 and 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 those totals in a save changes nothing: the engine derives the real numbers from the inputs in the right-hand column. They are snapshots for external tooling, and no state is lost by ignoring them.

The player character’s pools are rebuilt from somewhere else entirely, and that is the part an editor has to know. The class-levels-and-ability-modifier rule above is the companion and NPC path. For the actively-controlled player, MaxHitPoints and MaxForcePoints ignore ClassList’s ClassLevel completely and sum the per-level LvlStatHitDie and LvlStatForce values out of LvlStatList, the same ledger CSWSCreatureStats::LevelUp extends one entry at a time during ordinary play. A LvlStatList shorter than the character’s level does not abort the load. It pads with zeroed entries, flooring at one hit point per level.

So raising a player’s ClassLevel without extending LvlStatList to match gives a character whose pools are wrong from the first load, silently and with nothing reporting it. The same edit on a companion is harmless, because nothing on that path consults the ledger. This is the one place where the two creature kinds diverge on what an input actually is.

The permanent-bonus bytes. refbonus, willbonus and fortbonus are the raw bytes that do round-trip. Each constructs to 0 on CSWSCreatureStats, and an absent one on a template load carries that 0 over unconditionally, the same idiom as ForcePoints and Experience above.

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 and willbonus documented above. A capitalized variant is never found, read, discarded, or compared against anything.

The same applies to SubRace versus the modelled 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. PregameCurrent, despite the name, behaves as a continuously-refreshed current-HP mirror that nothing ever reads back.

Gold: party members do not 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 does not. 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 and expression data) are written by SaveCreature only when the corresponding live pointer or list is actually populated.

Their absence from a save is less a defaults question than a statement that there was no runtime state to save. On load, an absent struct 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:

FieldEmitted whenAbsent on load resolves to
PM_AppearancePM_IsDisguised == 10. The loader only attempts the read if PM_IsDisguised decoded true.
CombatRoundData contentsCombat was mid-round at save timeThe struct header is always present, but SaveStats has no writer counterpart for this data at all. The outer SaveCreature writes the struct shell, and whether the roughly two dozen combat-round scalars inside it were populated depends entirely on whether the game happened to be captured mid-round.
EffectList, VarTable, SWVarTable, ActionListList and struct headers are always written; contents reflect however many entries existEmpty containers 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 and 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 is internally consistent rather than a bug. Worth knowing if you are ever diffing raw GFF structs by hand.

Fields the engine never reads

A field the engine never reads is not automatically a field you may leave out. The engine is one consumer among several: the toolset, save managers and other mod tools read these files too, and a blueprint that round-trips through one of them can lose a field the game itself would have ignored.

So “dead” here means “the K1 engine does not read it”, and what a writer should do about that is a separate question. See “the engine ignores this” is not “you may leave it out”.

Finding typeExplanation
Legacy engine artifactsFields present in older files are Neverwinter Nights superset metrics the K1 engine does not read. Morale, SaveWill, BlindSpot and PaletteID have no label string in swkotor.exe, so those four are dead by the same test as the row below rather than merely untraced.
Confirmed dead by string absenceTemplateList, CRAdjust, SaveReflex and MemorizedList0.

Some labels have no string in the binary at all

TemplateList, CRAdjust, SaveReflex and MemorizedList0 do not exist as field-name strings anywhere in swkotor.exe.

That test is worth understanding, because it is the strongest evidence available on this page. GFF lookup works by matching a label against a literal string the reader constructs. If the string is not in the binary, no code path can branch on the field’s presence or read its contents, whatever the file holds. It is not “we did not find a reader”; it is “a reader cannot exist”. The same test settled several dead DLG fields.

TemplateList is worth naming separately. It is a List, present but empty in every .utc in a full install, and the highest-prevalence unmodelled label in the corpus.

SaveReflex follows the dead pattern already documented for SaveWill and SaveFortitude. None of the three raw saving-throw fields exists as a string to trace an override from, so the live refbonus-style computation superseding them is (provenance: inferred) for SaveReflex specifically, by analogy with its two siblings.

Comment and the legacy batch

A binary-wide string-existence check, the same decisive test used for TemplateList, CRAdjust, SaveReflex and MemorizedList0, confirms that Comment, Morale, MoraleRecovery, MoraleBreakpoint, PaletteID, BlindSpot, MultiplierSet, NoPermDeath, IgnoreCrePath, Hologram, WillNotRender and LawfulChaotic do not 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. They are real field names on a different format, never read by any creature-related function, and functionally just as dead for UTC purposes.

BodyBag and Interruptable are genuinely read

Two fields need pulling back out of the “confirmed dead” framing. 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 UTC-007 lint list, so there is no existing contradiction to fix. But do not assume UTC’s Interruptable shares the fate of the identically-named, confirmed-dead Interruptable field documented on UTD and UTP. It is a different field on a different format, and this one is live.

Whether the values these two store are ever consumed downstream, in combat or AI logic, was not traced in this pass.

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, and the structural reader represents them faithfully.

Stacked SpecAbilityList entries on the Bastila variants

The Bastila 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 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 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 (0 to 131), 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 UTC lint rules: for example, “SpecAbilityList[].Spell must resolve to a row in spells.2da”, and optionally “warn on stacked-duplicate SpecAbilityList entries unless explicitly whitelisted as a known vanilla pattern”.


Implemented Linter Rules (Rakata-Lint)

rakata-lint encodes the rules above so you can check a file before the engine does. The split below is about what a rule needs to run: the first set reads only the file in front of it, the second needs the install’s 2DA tables and resource sources to resolve references against.

Where a rule’s wording and this page’s engine notes seem to disagree, the engine notes win: several rules were written before the decompilation work that now backs this page. The two class rules are the case worth knowing, because they look like one rule and are not. UTC-002 counts entries. UTC-003 compares ids. A ClassList can trip either, both, or neither.

Intra-resource rules needing no context, under rakata_lint::rules::utc:

RuleLevelFires when
UTC-001 Appearance correctionWarnAppearance_Head == 0; the engine forces this to 1 at runtime.
UTC-002 Class limitWarnMore than two entries in ClassList, which is more than a creature has slots for. Counts entries, and does not care whether they are distinct.
UTC-003 Class duplicationsErrorThe same class id appears twice in ClassList; fatal engine crash (0x5f7) on load. Fires once per repeat, so a list of three identical ids reports twice.
UTC-004 Dead save fieldsInfoSaveWill or SaveFortitude populated; the engine reads willbonus and fortbonus instead.
UTC-005 Gender clampWarnGender > 4; the engine clamps to a maximum of 4.
UTC-006 GoodEvil clampWarnGoodEvil > 100; the engine clamps to a maximum of 100.
UTC-007 Toolset and legacy fieldsInfoAny of Comment, Morale*, PaletteID, BodyVariation, TextureVar, BlindSpot, MultiplierSet, NoPermDeath, IgnoreCrePath, Hologram, WillNotRender or LawfulChaotic is set; never read by the K1 engine.

Range, 2DA and resref-existence rules requiring a LintContext, under rakata_lint::rules::utc_range:

RuleLevelFires when
UTC-008 Race boundsErrorRace does not resolve to a row in racialtypes.2da; engine crash 0x5f4 on load.
UTC-009 Class boundsErrorAny ClassList[].Class does not resolve to a row in classes.2da, or is negative; engine load failure.
UTC-010 Appearance boundsErrorAppearance does not resolve to a row in appearance.2da; engine renders a missing model.
UTC-011 Portrait boundsErrorPortraitId, when not the 0xFFFE “use string Portrait” sentinel, does not resolve to a row in portraits.2da.
UTC-012 Resref existenceWarnConversation (.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.

Open questions

  • Is a third class entry ignored, or fatal? The two-slot structure under ClassList is established. What the loader does with a third valid, distinct entry is not: no pass recorded here has walked ReadStatsFromGff’s ClassList loop to find out. The 0x5f7 crashes we have confirmed are for duplicate and out-of-range ids, neither of which is a plain count problem. Tracked on #106.
  • The base CSWSObject layer is untraced. CSWSObject::SaveObjectState and LoadObjectState, and SaveListenData and LoadListenData, run at the tail of SaveCreature and LoadCreature. We did not decompile them in this pass, so there may be a further class split at the base layer we have not reached.
  • ReadItemsFromGff’s namespacing. We confirmed CSWSCreature::ReadScriptsFromGff is a genuine class member. We did not re-check whether ReadItemsFromGff is one or a free function.
  • FeatList’s top-level presence flag. We could not resolve the top-level call site’s own presence-flag register with certainty from the decompiler output. The documented reading comes by analogy from the identical idiom in the per-level FeatList inside the PC LvlStatList loop.
  • SaveReflex’s override path is (provenance: inferred), by analogy with SaveWill and SaveFortitude. No string exists in the binary to trace it from directly.
  • Downstream use of BodyBag and Interruptable. Both are genuinely read and stored. Whether combat or AI logic ever consumes the stored values was not traced.
  • Runtime behaviour of an out-of-range Spell id. Spell = 299 on partymember.utc indexes past the end of the spells.2da struct array. What actually happens depends on heap layout and cannot be determined from the load path alone.
  • LoadCreature’s size, and the sizes of ReadScriptsFromGff, ReadItemsFromGff and ReadSpellsFromGff, are not recorded. Only ReadStatsFromGff’s 7835 B is.

Every label the schema declares

Generated from the schema, so no label can be quietly left out. How to read these tables.

What the engine does with each field

FieldTypeEngineWhen absent
MaxHitPointsSHORTwrites it, never reads it back: the field-name string has exactly two cross-references in the binary, both writers, and zero readers anywhere on any pathnot one constant; we substitute 0
HitPointsSHORTreads itkeeps 1
CurrentHitPointsSHORTreads itnot one constant; we substitute 0
ItemList[].Repos_PosXWORDnever reads it: never read anywhere in the creature item-loading call graph; the only reader of either label is ReadContainerItemsFromGff, serving placeable and store containers, which no creature load reachesnot one constant; the field holds the absence
ItemList[].Repos_PosyWORDnever reads it: never read anywhere in the creature item-loading call graph; the only reader of either label is ReadContainerItemsFromGff, serving placeable and store containers, which no creature load reachesnot one constant; the field holds the absence
ItemList[].InfiniteBYTEnever reads it: the Infinite label has exactly two cross-references in the binary, both inside CSWSStore::LoadStore and SaveStore, so no function in this type’s item-loading call graph reads itnot one constant; we substitute 0
ItemList[].Repos_PosYWORDnever reads it: never read anywhere in the creature item-loading call graph; the only reader of either label is ReadContainerItemsFromGff, serving placeable and store containers, which no creature load reachesnot one constant; we substitute 0
CommentCExoStringnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute ""
PaletteIDBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0
SaveWillBYTEnever reads it: the template’s own save-throw fields are ignored; the engine computes saving throws from base plus ability modifier plus active effects and reads these nevernot one constant; we substitute 0
SaveFortitudeBYTEnever reads it: the template’s own save-throw fields are ignored; the engine computes saving throws from base plus ability modifier plus active effects and reads these nevernot one constant; we substitute 0
BodyVariationBYTEnever reads it: the string exists in the binary but its only cross-references are the item loader and saver, so it is a real field name on a different format and no creature function reads itnot one constant; we substitute 0
TextureVarBYTEnever reads it: the string exists in the binary but its only cross-references are the item loader and saver, so it is a real field name on a different format and no creature function reads itnot one constant; we substitute 0
MoraleBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0
MoraleRecoveryBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0
MoraleBreakpointBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0
BlindSpotFLOATnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0.0
MultiplierSetBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0
NoPermDeathBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0
IgnoreCrePathBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0
HologramBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0
WillNotRenderBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0
LawfulChaoticBYTEnever reads it: the field-name string does not exist anywhere in swkotor.exe, so no creature function can look it up; a binary-wide string-existence check settles this as never read rather than read and ignorednot one constant; we substitute 0

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
FirstNameCExoLocStringstamps empty
LastNameCExoLocStringstamps empty
DescriptionCExoLocStringkeeps empty
IsPCBYTENOT EXAMINED; we substitute 0
TagCExoStringkeeps ""
ConversationCResRefkeeps ""
InterruptableBYTEkeeps 0
AgeINTkeeps 0
GenderBYTEkeeps 0
StartingPackageBYTEkeeps 0
RaceBYTENOT EXAMINED; we substitute 0
SubraceCExoStringkeeps ""
SubraceIndexBYTEkeeps 0
DeityCExoStringkeeps ""
StrBYTEkeeps 0
DexBYTEkeeps 0
IntBYTEkeeps 0
WisBYTEkeeps 0
ConBYTEkeeps 0
ChaBYTEkeeps 0
NaturalACBYTEkeeps 0
SoundSetFileWORDstamps 65535
GoldDWORDkeeps 0
InvulnerableBYTEkeeps 0
PlotBYTEkeeps 0
Min1HPBYTEkeeps 0
PartyInteractBYTEkeeps 0
NotReorientingBYTEkeeps 0
DisarmableBYTEkeeps 0
ExperienceDWORDkeeps 0
PortraitIdWORDstamps 65535
PortraitCResRefkeeps ""
GoodEvilBYTEkeeps 0
Color_SkinBYTEkeeps 0
Color_HairBYTEkeeps 0
Color_Tattoo1BYTEkeeps 0
Color_Tattoo2BYTEkeeps 0
PhenotypeINTkeeps 0
Appearance_TypeWORDkeeps 0
Appearance_HeadBYTEkeeps 0
DuplicatingHeadBYTEkeeps 0
UseBackupHeadBYTEkeeps 0
FactionIDWORDkeeps 0
ChallengeRatingFLOATkeeps 0.0
AIStateINTkeeps 0
BodyBagBYTEkeeps 0
PerceptionRangeBYTEstamps 11
willbonusSHORTkeeps 0
fortbonusSHORTkeeps 0
refbonusSHORTkeeps 0
ForcePointsSHORTkeeps 0
CurrentForceSHORTnot one constant; we substitute 0
SkillPointsWORDkeeps 0
MovementRateBYTEnot one constant; our reader works it out from other fields
WalkRateINTnot one constant; we substitute 0
ScriptHeartbeatCResRefkeeps "default"
ScriptOnNoticeCResRefkeeps "default"
ScriptSpellAtCResRefkeeps "default"
ScriptAttackedCResRefkeeps "default"
ScriptDamagedCResRefkeeps "default"
ScriptDisturbedCResRefkeeps "default"
ScriptEndRoundCResRefkeeps "default"
ScriptDialogueCResRefkeeps "default"
ScriptSpawnCResRefkeeps "default"
ScriptRestedCResRefkeeps "default"
ScriptDeathCResRefkeeps "default"
ScriptUserDefineCResRefkeeps "default"
ScriptOnBlockedCResRefkeeps "default"
ScriptEndDialoguCResRefkeeps "default"
ClassListListNOT EXAMINED; we substitute container
ClassList[].KnownList0Listnot one constant; we substitute container
ClassList[].KnownList0[].SpellWORDNOT EXAMINED; we substitute 0
ClassList[].KnownList0[].SpellFlagsBYTENOT EXAMINED; we substitute 0
ClassList[].KnownList0[].SpellMetaMagicBYTENOT EXAMINED; we substitute 0
ClassList[].ClassINTNOT EXAMINED; we substitute 0
ClassList[].ClassLevelSHORTNOT EXAMINED; we substitute 0
ClassList[].SpellsPerDayListListnot one constant; we substitute container
FeatListListNOT EXAMINED; we substitute container
FeatList[].Feat (required)WORDNOT EXAMINED; we substitute 0
SkillListListnot one constant; we substitute container
SkillList[].Rank (required)BYTENOT EXAMINED; we substitute 0
Equip_ItemListListNOT EXAMINED; we substitute container
Equip_ItemList[].EquippedRes (required)CResRefnot one constant; we substitute ""
Equip_ItemList[].DropableBYTEstamps 0
Equip_ItemList[].ObjectIdDWORDstamps 2130706432
ItemListListnot one constant; we substitute container
ItemList[].InventoryRes (required)CResRefnot one constant; we substitute ""
ItemList[].DropableBYTEstamps 0
ItemList[].ObjectIdDWORDstamps 2130706432
SpecAbilityListListnot one constant; we substitute container
SpecAbilityList[].Spell (required)WORDNOT EXAMINED; we substitute 0
SpecAbilityList[].SpellFlagsBYTENOT EXAMINED; we substitute 0
SpecAbilityList[].SpellCasterLevelBYTENOT EXAMINED; we substitute 0
TemplateResRefCResRefnot one constant; we substitute ""
CreatureSizeINTstamps 3
IsDestroyableBYTEstamps 1
IsRaiseableBYTEstamps 1
DeadSelectableBYTEstamps 1
AmbientAnimStateBYTEstamps 0
AnimationINTstamps 10000
CreatnScrptFirdBYTEstamps 0
PM_IsDisguisedBYTEstamps 0
PM_AppearanceWORDstamps 0
ListeningBYTEstamps 0
AreaIdDWORDstamps 0
DetectModeBYTEstamps 0
StealthModeBYTEstamps 0
LvlStatListListnot one constant; we substitute container