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
| Property | Value |
|---|---|
| Extension(s) | .uti |
| Magic Signature | UTI / V3.2 |
| Type | Item Blueprint |
| Rust Reference | View 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:
- 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). - Economic & Charge Mechanics: The value of the item, and the number of charges left for consumable abilities (e.g.,
Cost,Charges). - Visual Geometry (Appearance): Setting what the item looks like when dropped on the floor or equipped (e.g.,
ModelVariation,TextureVar). - Combat & Upgrade Properties (
PropertiesList): The stat buffs, damage modifiers, and abilities bound to the item, alongside slots for workbench upgrades.
- Model Validation:
rakata-lintchecks 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.
| Function | Size | Behavior |
|---|---|---|
LoadDataFromGff | – | The main parser that sets what the item is, how many charges it holds, and its descriptions. |
LoadItemPropertiesFromGff | – | Reads the special properties (like energy damage or stat boosts), splitting them into ‘useable’ abilities versus permanent buffs. |
LoadItem | – | The constructor that decides whether to load the item onto a character or leave it idle in an inventory. |
LoadFromTemplate | – | A fallback used when spawning an item dynamically from a script instead of off a character. |
SaveItem / SaveItemProperties | – | The 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 Rule | Runtime Behavior |
|---|---|
| Description Cross-Swap | If 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 Truncation | If 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 Hooks | The 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 Fallback | The 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 Enforcement | During explicit serializing via SaveItem (when the player creates a save game), the engine actively forces and hardcodes Identified to 1 unconditionally. |
| Property Capabilities | Item 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 Kinds | The 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 Defaults | When 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 Application | The 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):
| Step | 2DA | Indexed By | Column Read | Purpose |
|---|---|---|---|---|
| 1 | itempropdef.2da | PropertyName | Name (INT) | TLK strref for the property’s display name (e.g. “Damage Bonus”). |
| 2 | itempropdef.2da | PropertyName | SubTypeResRef (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) | Subtype | Name (INT) | TLK strref for the subtype’s display name (e.g. “Acid”). |
Cost-table dispatch (resolved eagerly at startup inside LoadIPRPCostTables at 0x005c4730):
| Step | 2DA | Indexed By | Column Read | Purpose |
|---|---|---|---|---|
| 1 | iprp_costtable.2da | CostTable | Name (string) | Resref of the cost-specific 2DA (e.g. iprp_meleecost). Used as a resref despite the column name suggesting a label. |
| 2 | iprp_costtable.2da | CostTable | ClientLoad (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):
| Step | 2DA | Indexed By | Column Read | Purpose |
|---|---|---|---|---|
| 1 | iprp_paramtable.2da | Param1 | TableResRef (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.2daandiprp_paramtable.2darow counts are stored asbyte(u8) inCTwoDimArrays. 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/GetCExoStringEntrycompare verbatim). The exact spellings the engine uses areName,SubTypeResRef,TableResRef,Label, andClientLoad. - The subtype 2DA listed in
SubTypeResRefis loaded lazily on display viaGetPropertyStrings, not eagerly at startup. A missing subtype 2DA fails only the call that needs it, not the whole game load. - The
Namecolumn on every level of the dispatch is a TLK strref. TheLabelcolumn 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:
| Index | Name (resref of per-cost 2DA) | Label | ClientLoad |
|---|---|---|---|
| 0 | IPRP_BASE1 | Base1 | 0 |
| 1 | IPRP_BONUSCOST | Bonus | 0 |
| 2 | IPRP_MELEECOST | Melee | 1 |
| 3 | IPRP_CHARGECOST | SpellUse | 0 |
| 4 | IPRP_DAMAGECOST | Damage | 0 |
| 5 | IPRP_IMMUNCOST | Immune | 0 |
| 6 | IPRP_SOAKCOST | DamageSoak | 0 |
| 7 | IPRP_RESISTCOST | DamageResist | 0 |
| 8 | IPRP_BLADECOST | DancingScimitar | 0 |
| 9 | IPRP_SLOTSCOST | Slots | 0 |
| 10 | IPRP_WEIGHTCOST | Weight | 0 |
| 11 | IPRP_SRCOST | SpellResist | 0 |
| 12 | IPRP_STAMINACOST | Stamina | 0 |
| 13 | IPRP_SPELLLVCOST | SpellLevel | 0 |
| 14 | IPRP_AMMOCOST | Ammo | 0 |
| 15 | IPRP_REDCOST | WeightReduction | 0 |
| 16 | IPRP_SPELLCOST | Spells | 0 |
| 17 | IPRP_TRAPCOST | Traps | 0 |
| 18 | IPRP_LIGHTCOST | Light | 1 |
| 19 | IPRP_MONSTCOST | Monster_Cost | 0 |
| 20 | IPRP_NEG5COST | Negative_Modifiers | 0 |
| 21 | IPRP_NEG10COST | Negative_Modifiers | 0 |
| 22 | IPRP_DAMVULCOST | Damage_vulnerability | 0 |
| 23 | IPRP_SPELLLVLIMM | Spell_Level_Immunity | 0 |
| 24 | IPRP_ONHITCOST | OnHitCosts | 0 |
| 25 | IPRP_ONHITDC | OnHitDC_saves | 0 |
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.
| Handler | CostTable index | Per-cost 2DA | Column | Post-processing |
|---|---|---|---|---|
ApplyAbilityBonus | 1 | iprp_bonuscost | Value | – |
ApplyACBonus | 1 | iprp_bonuscost | Value | – |
ApplyImprovedSavingThrow | 1 | iprp_bonuscost | Value | – |
ApplyDamageReduction | 6 | iprp_soakcost | Amount | – |
ApplyDamageResistance | 7 | iprp_resistcost | Amount | – |
ApplyImprovedForceResistance | 11 (0xB) | iprp_srcost | Value | – |
ApplyAttackPenalty | 20 (0x14) | iprp_neg5cost | Value | negate |
ApplyDamagePenalty | 20 (0x14) | iprp_neg5cost | Value | negate |
ApplyReducedSavingThrows | 20 (0x14) | iprp_neg5cost | Value | none (table holds negatives) |
ApplyDecreasedAC | 20 (0x14) | iprp_neg5cost | Value | negate |
ApplyDecreasedAbilityScore | 21 (0x15) | iprp_neg10cost | Value | negate |
ApplyDecreasedSkillModifier | 21 (0x15) | iprp_neg10cost | Value | negate |
ApplyDamageVulnerability | 22 (0x16) | iprp_damvulcost | Value | – |
ApplyDamageImmunity | dynamic (property.cost_table) | per-property | Value | – |
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(coversPropertyName11Damage, 12DamageAlignmentGroup, and 13DamageRacialGroupin one switch) readsCostValuestraight as the damage amount. There is no per-cost 2DA lookup. Theiprp_damagecost.2datable is used for cost calculation (GetCost), not for damage-magnitude resolution.ApplyEnhancementBonusandApplyAttackBonusread(Rules->internal).all_2DAs->iprp_meleecostvia direct struct-field access (not throughGetIPRPCostTable), then read columnValue. Equivalent to a cost-table-index2(iprp_meleecost) dispatch, just inlined.ApplySkillBonusandApplyBonusFeatread the magnitude / feat id from the property struct directly.ApplyImmunityswitches on the subtype id and assigns one of ten hardcoded engine constants; no 2DA is consulted.ApplyRegenerationusesCostValueas the regen amount and a hardcoded6000ms tick interval; no 2DA.
Implications for decoded magnitude resolution. A decoder that resolves property magnitudes should:
- 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, columnValueorAmount, with the documented post-processing. - If the property kind is on the bypass list, the magnitude is
CostValuedirectly (or, forApplyImmunity, hardcoded per subtype). - For
ApplyDamageImmunity, the cost-table index is read from the property’s ownCostTablefield 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.
| Row | Label | Subtype 2DA | Notes |
|---|---|---|---|
| 0 | Ability | iprp_abilities | |
| 1 | Armor | – | AC base bonus |
| 2 | ArmorAlignmentGroup | iprp_aligngrp | |
| 3 | ArmorDamageType | iprp_combatdam | |
| 4 | ArmorRacialGroup | racialtypes | |
| 5 | Enhancement | – | Enhancement bonus to weapons |
| 6 | EnhancementAlignmentGroup | iprp_aligngrp | |
| 7 | EnhancementRacialGroup | racialtypes | |
| 8 | AttackPenalty | – | |
| 9 | BonusFeats | feat | |
| 10 | CastSpell | spells | active |
| 11 | Damage | iprp_damagetype | |
| 12 | DamageAlignmentGroup | iprp_aligngrp | |
| 13 | DamageRacialGroup | racialtypes | |
| 14 | DamageImmunity | iprp_damagetype | |
| 15 | DamagePenalty | – | |
| 16 | DamageReduced | iprp_protection | |
| 17 | DamageResist | iprp_damagetype | |
| 18 | Damage_Vulnerability | iprp_damagetype | |
| 19 | DecreaseAbilityScore | iprp_abilities | |
| 20 | DecreaseAC | iprp_acmodtype | |
| 21 | DecreasedSkill | skills | |
| 22 | DamageMelee | iprp_combatdam | |
| 23 | DamageRanged | iprp_combatdam | |
| 24 | Immunity | iprp_immunity | |
| 25 | ImprovedMagicResist | – | |
| 26 | ImprovedSavingThrows | iprp_saveelement | |
| 27 | ImprovedSavingThrowsSpecific | iprp_savingthrow | |
| 28 | Keen | – | |
| 29 | Light | – | |
| 30 | Mighty | – | |
| 31 | DamageNone | – | |
| 32 | OnHit | iprp_onhit | |
| 33 | ReducedSavingThrows | iprp_saveelement | |
| 34 | ReducedSpecificSavingThrow | iprp_savingthrow | |
| 35 | Regeneration | – | |
| 36 | Skill | skills | |
| 37 | ThievesTools | – | active |
| 38 | AttackBonus | – | |
| 39 | AttackBonusAlignmentGroup | iprp_aligngrp | |
| 40 | AttackBonusRacialGroup | racialtypes | |
| 41 | ToHitPenalty | – | |
| 42 | UnlimitedAmmo | iprp_ammotype | |
| 43 | UseLimitationAlignmentGroup | iprp_aligngrp | |
| 44 | UseLimitationClass | classes | |
| 45 | UseLimitationRacial | racialtypes | |
| 46 | Trap | traps | active |
| 47 | True_Seeing | – | |
| 48 | OnMonsterHit | iprp_monsterhit | |
| 49 | Massive_Criticals | – | |
| 50 | Freedom_of_Movement | – | |
| 51 | Monster_damage | – | |
| 52 | Special_Walk | iprp_walk | |
| 53 | Computer_Spike | – | active |
| 54 | Regeneration_Force_Points | – | |
| 55 | Blaster_Bolt_Deflect_Increase | – | |
| 56 | Blaster_Bolt_Defect_Decrease | – | Vanilla typo (Defect not Deflect); decoder must match the file spelling exactly. |
| 57 | Use_Limitation_Feat | feat | |
| 58 | Droid_Repair_Kit | – | |
| 59 | Disguise | appearance |
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 Type | Explanation |
|---|---|
| Superseded Legacy Fields | Directly 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 Artifacts | General 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.
- UTI-001 (Model Truncation Safety): Warns when
ModelVariation == 0; the engine forces this to 1 at runtime. - UTI-002 (Dead Cost Fields): Informs when
Costis set; the engine ignores this and computes item cost dynamically. - UTI-003 (Dead Body Overrides): Informs when
BodyVariationis set; the engine queriesbaseitems.2dainstead. - UTI-004 (Toolset-Only Fields): Informs when any of
TemplateResRef,Comment,PaletteID, orUpgradeLevelare set; never read by the K1 engine. - UTI-005 (Conditional TextureVar): Informs when
TextureVaris set; only evaluated if the base item’s 2DAmodel_typeis exactly 1.
Phase 2 (range / 2DA, requires LintContext)
Implemented under rakata_lint::rules::uti_range.
- UTI-006 (Base Item Bounds): Errors when
BaseItemdoes not resolve to a row inbaseitems.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. - UTI-007 (Valid Capability Bounds): Errors per
PropertiesListentry whenPropertyNamedoes not resolve to a row initempropdef.2da, or whenSubtypedoes not resolve to a row in the per-propertyiprp_*.2danamed byitempropdef[PropertyName].SubTypeResRef(skipped when the row has noSubTypeResRef, i.e. the property kind has no subtype dimension).UpgradeTypeandUsesPerDayuse the engine’s0xFF“not set” sentinel; both the absent-field and explicit-0xFFforms 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.