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

UTI Format (Item Blueprint)

The Item (.uti) blueprint serves as the central data model for all tangible loot, weapons, armor, and usable gear in the game. It defines how an item physically appears on characters, what custom properties or stat bonuses it applies through specific upgrade hierarchies, its intrinsic monetary cost, and exactly what its runtime state behaves like when dropped into the world map.

At a Glance

PropertyValue
Extension(s).uti
Magic SignatureUTI / V3.2
TypeItem Blueprint
Rust ReferenceView rakata_generics::Uti in Rustdocs

Data Model Structure

Rakata maps the Item definition directly into the rakata_generics::Uti struct. To view the exhaustive binary schema and strict GFF field mappings, please refer to the Rustdocs for this struct, where each field is explicitly documented.

An Item breaks down into four main categories:

  1. Core Identity: The basic text strings that provide the item’s name and description, including both identified and unidentified states (e.g., TemplateResRef, LocName, Description).
  2. Economic & Charge Mechanics: The value of the item, and the number of charges left for consumable abilities (e.g., Cost, Charges).
  3. Visual Geometry (Appearance): Setting what the item looks like when dropped on the floor or equipped (e.g., ModelVariation, TextureVar).
  4. Combat & Upgrade Properties (PropertiesList): The stat buffs, damage modifiers, and abilities bound to the item, alongside slots for workbench upgrades.
  • Model Validation: rakata-lint checks the data against engine constraints to prevent fatal runtime crashes.

Engine Audits & Decompilation

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

(Decompilation logic for this section was entirely audited and verified via native Ghidra pipeline against swkotor.exe, explicitly pulling from the primary dispatcher CSWSItem::LoadDataFromGff at 0x0055fcd0, the active-property predicate CSWSItem::IsFriendlyUsableItem at 0x00553900, the property-string resolver CSWSItem::GetPropertyStrings at 0x00554e00, and the IPRP table loaders CTwoDimArrays::LoadIPRPCostTables at 0x005c4730 / LoadIPRPParamTables at 0x005c49c0.)

Structural Load Phasing

The engine processes an Item structurally across multi-pass capabilities mappings.

FunctionSizeBehavior
LoadDataFromGffThe main parser that sets what the item is, how many charges it holds, and its descriptions.
LoadItemPropertiesFromGffReads the special properties (like energy damage or stat boosts), splitting them into ‘useable’ abilities versus permanent buffs.
LoadItemThe constructor that decides whether to load the item onto a character or leave it idle in an inventory.
LoadFromTemplateA fallback used when spawning an item dynamically from a script instead of off a character.
SaveItem / SaveItemPropertiesThe opposite pipeline that writes the item into a save game, which notoriously forces the item to always be flagged as “Identified”.

Core Structural Findings

The engine rigorously evaluates base-item mapping constraints from 2DA arrays and aggressively overrides improperly defined models.

Engine RuleRuntime Behavior
Description Cross-SwapIf either Description or DescIdentified is missing, the engine automatically duplicates the provided string into the missing field so item identification mechanics never crash the game.
Model TruncationIf an older tool incorrectly configures ModelVariation to 0, the engine forcefully bumps it to 1 upon load, ensuring the item always has visible geometry instead of rendering an invisible weapon or armor piece.
Model & Body Variation HooksThe engine completely ignores the .uti’s BodyVariation field, opting instead to enforce the exact body_var value predefined in baseitems.2da. Additionally, TextureVar is unconditionally bypassed unless the item’s base type is strictly configured as Model Type 1.
Cost Generation FallbackThe physical Cost integer provided in the file is dead data. The engine strictly computes economic value actively via GetCost() calculations based on its properties, completely ignoring your defined value.
Identifier EnforcementDuring explicit serializing via SaveItem (when the player creates a save game), the engine actively forces and hardcodes Identified to 1 unconditionally.
Property CapabilitiesItem properties are structurally split into Active and Passive memory tables at load. The engine evaluates every PropertyName index: any ID strictly mapping to 10, 37, 46, or 53 (e.g., Cast Power, Trap) is actively hooked as a usable player ability, while all other integers are silently applied as passive stat modifiers.
Data-Driven Property KindsThe engine does not hardcode a “PropertyName N -> semantic kind” table. Property-kind classification (Damage Bonus, Ability Bonus, Save Bonus, etc.) is resolved entirely by reading the Label column of itempropdef.2da at the row indexed by PropertyName. Mods that add new rows surface as new property kinds without engine changes.
Property Field DefaultsWhen a PropertiesList entry omits a field, the engine fills it from a fixed table: Useable=1 for active properties (PropertyName 10/37/46/53), Useable=0 for passive ones; UsesPerDay=0xFF and UpgradeType=0xFF – both 0xFF values function as “not set” sentinels rather than valid row indices.
Bit Flags ApplicationThe Dropable boolean explicitly sets bit 3 of the item’s internal memory flags, while Pickpocketable sets bit 4. Missing fields safely default to 0.

Property Table Dispatch

A UtiProperty carries three indices that point through three separate registry-of-registries chains. The engine holds no hardcoded mapping for any of them; every dispatch is a 2DA cell read, so mods that extend the underlying tables surface without engine modification.

Per-property subtype dispatch (resolved at display time inside GetPropertyStrings at 0x00554e00):

Step2DAIndexed ByColumn ReadPurpose
1itempropdef.2daPropertyNameName (INT)TLK strref for the property’s display name (e.g. “Damage Bonus”).
2itempropdef.2daPropertyNameSubTypeResRef (string)Resref of the per-property subtype 2DA (e.g. iprp_damagecost). Empty/missing means the property has no subtype dimension.
3(subtype 2DA from step 2)SubtypeName (INT)TLK strref for the subtype’s display name (e.g. “Acid”).

Cost-table dispatch (resolved eagerly at startup inside LoadIPRPCostTables at 0x005c4730):

Step2DAIndexed ByColumn ReadPurpose
1iprp_costtable.2daCostTableName (string)Resref of the cost-specific 2DA (e.g. iprp_meleecost). Used as a resref despite the column name suggesting a label.
2iprp_costtable.2daCostTableClientLoad (INT, optional)When set and the engine is running in client mode, the loader skips loading this row’s cost 2DA. Treated as server-only.
3(cost 2DA from step 1)CostValue(table-specific)The row at CostValue carries the cost effect for this property; column layout varies per cost table.

Param-table dispatch (resolved eagerly at startup inside LoadIPRPParamTables at 0x005c49c0):

Step2DAIndexed ByColumn ReadPurpose
1iprp_paramtable.2daParam1TableResRef (string)Resref of the param-specific 2DA.
2(param 2DA from step 1)Param1Value(table-specific)The row at Param1Value carries the parameter value for this property; column layout varies per param table.

Engine constraints:

  • Both iprp_costtable.2da and iprp_paramtable.2da row counts are stored as byte (u8) in CTwoDimArrays. Rows past index 255 are silently truncated by the loader and the affected per-property tables never get loaded into memory.
  • Column-name lookups in 2DAs are case-sensitive at the engine API (C2DA::GetINTEntry / GetCExoStringEntry compare verbatim). The exact spellings the engine uses are Name, SubTypeResRef, TableResRef, Label, and ClientLoad.
  • The subtype 2DA listed in SubTypeResRef is loaded lazily on display via GetPropertyStrings, not eagerly at startup. A missing subtype 2DA fails only the call that needs it, not the whole game load.
  • The Name column on every level of the dispatch is a TLK strref. The Label column on the same row holds a developer-readable identifier (e.g. Damage_Bonus) that does not require talktable resolution.

Cost-Table Magnitude Resolution

The cost-table dispatch chain documented above ends at “the row at CostValue carries the cost effect for this property; column layout varies per cost table.” This section pins down the column layout for vanilla K1’s iprp_costtable.2da entries and how each Apply<PropertyKind> handler reads from them, sourced from the CSWSItemPropertyHandler::Apply* family in swkotor.exe (handlers cluster around 0x004e5490-0x004e7e80 and 0x004e9230-0x004e9390).

iprp_costtable.2da (vanilla K1) — index to per-cost 2DA mapping:

IndexName (resref of per-cost 2DA)LabelClientLoad
0IPRP_BASE1Base10
1IPRP_BONUSCOSTBonus0
2IPRP_MELEECOSTMelee1
3IPRP_CHARGECOSTSpellUse0
4IPRP_DAMAGECOSTDamage0
5IPRP_IMMUNCOSTImmune0
6IPRP_SOAKCOSTDamageSoak0
7IPRP_RESISTCOSTDamageResist0
8IPRP_BLADECOSTDancingScimitar0
9IPRP_SLOTSCOSTSlots0
10IPRP_WEIGHTCOSTWeight0
11IPRP_SRCOSTSpellResist0
12IPRP_STAMINACOSTStamina0
13IPRP_SPELLLVCOSTSpellLevel0
14IPRP_AMMOCOSTAmmo0
15IPRP_REDCOSTWeightReduction0
16IPRP_SPELLCOSTSpells0
17IPRP_TRAPCOSTTraps0
18IPRP_LIGHTCOSTLight1
19IPRP_MONSTCOSTMonster_Cost0
20IPRP_NEG5COSTNegative_Modifiers0
21IPRP_NEG10COSTNegative_Modifiers0
22IPRP_DAMVULCOSTDamage_vulnerability0
23IPRP_SPELLLVLIMMSpell_Level_Immunity0
24IPRP_ONHITCOSTOnHitCosts0
25IPRP_ONHITDCOnHitDC_saves0

Per-handler magnitude resolution. Each Apply<Kind> handler that needs a cost-table magnitude calls CTwoDimArrays::GetIPRPCostTable(<index>) then C2DA::GetINTEntry(table, row=CostValue, column, out). The integer that comes back is the engine-side magnitude (in the units appropriate to the property kind: bonus number, damage soak amount, save delta, etc.). The column name is read case-sensitively; the only two columns the vanilla handlers consult are Value and Amount.

HandlerCostTable indexPer-cost 2DAColumnPost-processing
ApplyAbilityBonus1iprp_bonuscostValue
ApplyACBonus1iprp_bonuscostValue
ApplyImprovedSavingThrow1iprp_bonuscostValue
ApplyDamageReduction6iprp_soakcostAmount
ApplyDamageResistance7iprp_resistcostAmount
ApplyImprovedForceResistance11 (0xB)iprp_srcostValue
ApplyAttackPenalty20 (0x14)iprp_neg5costValuenegate
ApplyDamagePenalty20 (0x14)iprp_neg5costValuenegate
ApplyReducedSavingThrows20 (0x14)iprp_neg5costValuenone (table holds negatives)
ApplyDecreasedAC20 (0x14)iprp_neg5costValuenegate
ApplyDecreasedAbilityScore21 (0x15)iprp_neg10costValuenegate
ApplyDecreasedSkillModifier21 (0x15)iprp_neg10costValuenegate
ApplyDamageVulnerability22 (0x16)iprp_damvulcostValue
ApplyDamageImmunitydynamic (property.cost_table)per-propertyValue

Handlers that bypass the cost-table dispatch. A surprising number of vanilla handlers do not call GetIPRPCostTable at all and instead consume CostValue (or another property field) directly as the magnitude:

  • ApplyDamageBonus (covers PropertyName 11 Damage, 12 DamageAlignmentGroup, and 13 DamageRacialGroup in one switch) reads CostValue straight as the damage amount. There is no per-cost 2DA lookup. The iprp_damagecost.2da table is used for cost calculation (GetCost), not for damage-magnitude resolution.
  • ApplyEnhancementBonus and ApplyAttackBonus read (Rules->internal).all_2DAs->iprp_meleecost via direct struct-field access (not through GetIPRPCostTable), then read column Value. Equivalent to a cost-table-index 2 (iprp_meleecost) dispatch, just inlined.
  • ApplySkillBonus and ApplyBonusFeat read the magnitude / feat id from the property struct directly.
  • ApplyImmunity switches on the subtype id and assigns one of ten hardcoded engine constants; no 2DA is consulted.
  • ApplyRegeneration uses CostValue as the regen amount and a hardcoded 6000 ms tick interval; no 2DA.

Implications for decoded magnitude resolution. A decoder that resolves property magnitudes should:

  1. First check whether the property kind is on the cost-table list above; if yes, read the resolved magnitude from the listed cost 2DA at row CostValue, column Value or Amount, with the documented post-processing.
  2. If the property kind is on the bypass list, the magnitude is CostValue directly (or, for ApplyImmunity, hardcoded per subtype).
  3. For ApplyDamageImmunity, the cost-table index is read from the property’s own CostTable field rather than being hardcoded per handler; mod-extended cost tables resolve through the same path.

Vanilla itempropdef.2da Label Reference

The following table lists every label in the vanilla K1 itempropdef.2da. The decoder in rakata_generics::decoded matches on the Label column at the row indexed by UtiProperty::property_name. The Subtype 2DA column is the file’s SubTypeResRef cell verbatim (lowercased per the engine’s case-insensitive resref handling); an empty cell means the property has no subtype dimension.

The four rows the engine treats as active (loaded into the per-character usable-ability table per IsFriendlyUsableItem) are marked. Every other row is passive.

RowLabelSubtype 2DANotes
0Abilityiprp_abilities
1ArmorAC base bonus
2ArmorAlignmentGroupiprp_aligngrp
3ArmorDamageTypeiprp_combatdam
4ArmorRacialGroupracialtypes
5EnhancementEnhancement bonus to weapons
6EnhancementAlignmentGroupiprp_aligngrp
7EnhancementRacialGroupracialtypes
8AttackPenalty
9BonusFeatsfeat
10CastSpellspellsactive
11Damageiprp_damagetype
12DamageAlignmentGroupiprp_aligngrp
13DamageRacialGroupracialtypes
14DamageImmunityiprp_damagetype
15DamagePenalty
16DamageReducediprp_protection
17DamageResistiprp_damagetype
18Damage_Vulnerabilityiprp_damagetype
19DecreaseAbilityScoreiprp_abilities
20DecreaseACiprp_acmodtype
21DecreasedSkillskills
22DamageMeleeiprp_combatdam
23DamageRangediprp_combatdam
24Immunityiprp_immunity
25ImprovedMagicResist
26ImprovedSavingThrowsiprp_saveelement
27ImprovedSavingThrowsSpecificiprp_savingthrow
28Keen
29Light
30Mighty
31DamageNone
32OnHitiprp_onhit
33ReducedSavingThrowsiprp_saveelement
34ReducedSpecificSavingThrowiprp_savingthrow
35Regeneration
36Skillskills
37ThievesToolsactive
38AttackBonus
39AttackBonusAlignmentGroupiprp_aligngrp
40AttackBonusRacialGroupracialtypes
41ToHitPenalty
42UnlimitedAmmoiprp_ammotype
43UseLimitationAlignmentGroupiprp_aligngrp
44UseLimitationClassclasses
45UseLimitationRacialracialtypes
46Traptrapsactive
47True_Seeing
48OnMonsterHitiprp_monsterhit
49Massive_Criticals
50Freedom_of_Movement
51Monster_damage
52Special_Walkiprp_walk
53Computer_Spikeactive
54Regeneration_Force_Points
55Blaster_Bolt_Deflect_Increase
56Blaster_Bolt_Defect_DecreaseVanilla typo (Defect not Deflect); decoder must match the file spelling exactly.
57Use_Limitation_Featfeat
58Droid_Repair_Kit
59Disguiseappearance

Mod content extends this table with rows past index 59. The decoder’s typed-variant dispatch matches by Label, so a mod-added kind surfaces as DecodedProperty::Unknown { property_label: Some("ModLabel"), .. } instead of as a dispatch hole.

Legacy & Ignored Data

Finding TypeExplanation
Superseded Legacy FieldsDirectly supplying static Cost or BodyVariation values is a byproduct of older file versions; these remain inherently unused overhead compared to the physical runtime 2DA evaluation.
Passive Legacy ArtifactsGeneral nodes left over from older tools (like TemplateResRef, Comment, PaletteID, and explicitly UpgradeLevel) are bypassed on load entirely.

Implemented Linter Rules (Rakata-Lint)

Phase 1 (intra-resource, no context)

Implemented under rakata_lint::rules::uti.

  1. UTI-001 (Model Truncation Safety): Warns when ModelVariation == 0; the engine forces this to 1 at runtime.
  2. UTI-002 (Dead Cost Fields): Informs when Cost is set; the engine ignores this and computes item cost dynamically.
  3. UTI-003 (Dead Body Overrides): Informs when BodyVariation is set; the engine queries baseitems.2da instead.
  4. UTI-004 (Toolset-Only Fields): Informs when any of TemplateResRef, Comment, PaletteID, or UpgradeLevel are set; never read by the K1 engine.
  5. UTI-005 (Conditional TextureVar): Informs when TextureVar is set; only evaluated if the base item’s 2DA model_type is exactly 1.

Phase 2 (range / 2DA, requires LintContext)

Implemented under rakata_lint::rules::uti_range.

  1. UTI-006 (Base Item Bounds): Errors when BaseItem does not resolve to a row in baseitems.2da (or is negative); the engine indexes the table directly to look up model type, equip slot, and weapon class – an invalid id either crashes the load or produces a corrupt item.
  2. UTI-007 (Valid Capability Bounds): Errors per PropertiesList entry when PropertyName does not resolve to a row in itempropdef.2da, or when Subtype does not resolve to a row in the per-property iprp_*.2da named by itempropdef[PropertyName].SubTypeResRef (skipped when the row has no SubTypeResRef, i.e. the property kind has no subtype dimension). UpgradeType and UsesPerDay use the engine’s 0xFF “not set” sentinel; both the absent-field and explicit-0xFF forms decode as “not set” and the rule does not flag either.

Pending

  • Resref Existence: UTI’s only ResRef field is the toolset-only TemplateResRef (never read by the engine), so the per-format resref-existence rule from B6 was deliberately omitted.