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

UTM Format (Merchant Blueprint)

A .utm file is a merchant’s store: what it stocks, and what it charges. The format is small because a store is mostly a list of .uti items plus two markup percentages. The items carry their own stats, so the store only has to name them and price them.

At a Glance

PropertyValue
Extension(s).utm
Magic SignatureUTM / V3.2
TypeMerchant Blueprint
Rust ReferenceView rakata_generics::Utm in Rustdocs

Field Schema

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

CategoryCoversRepresentative fields
Core IdentityThe shop’s name and tagTag, LocName
Economic MetricsPrice scaling when buying or selling, plus basic shop rulesMarkUp, MarkDown, BuySellFlag
Store InventoryThe items in stock, including rules for infinite restockingItemList

Engine Audits & Decompilation

Read from CSWSStore::LoadStore at 0x005c7180 in swkotor.exe. Provenance: derived, not attested. The rows below have not been separately re-derived, so they sit on the reverse-engineering queue.

The load path

FunctionSizeBehaviour
LoadStore1341 BThe main parser. It reads the merchant’s identity, its MarkUp/MarkDown pricing, and its buy and sell permissions.
ItemList Readn/aWalks the store’s stock, taking either a saved item snapshot or a template named by InventoryRes.
AddItemToInventoryn/aAdds each item to the store’s inventory so the player can browse and buy it.

Rules the engine enforces

Engine RuleRuntime Behaviour
Cost SortingThe engine sorts the store’s stock by cost, cheapest first, as it builds the inventory. A display order set in the file does not survive.
Dynamic EconomicsMarkUp and MarkDown are percentages applied to an item’s base cost, one for what the merchant sells and one for what it buys.
Buy/Sell Bit FlagsBuySellFlag is two toggles: bit 0 lets the player sell to the merchant, bit 1 lets the merchant sell to the player.
BuySellFlag FallbackUnlike most of the merchant’s fields, BuySellFlag falls back to whatever value the store already holds when the field is missing, rather than resetting to a fixed literal. A freshly constructed store, never loaded from any file, starts at 3, with buy and sell both allowed.
Infinite StackingAn item flagged Infinite is never depleted. The player can buy it repeatedly and the stock does not fall.
Save vs. Template InventoryOn the savegame path every ItemList entry is a self-contained item snapshot. InventoryRes is consulted only on the template path, where it names a .uti the engine expands via CSWSItem::LoadFromTemplate; that resref is never written back into a save.
Absent ItemListA pure skip, not a clear. The whole block is gated on the list being found, and nothing in the function clears existing stock. LoadStore only ever adds items.

The remaining absent-field defaults

Tag and LocName are unconditional literal stamps, an empty string and an empty localized string, with no presence check afterward. MarkUp and MarkDown are unconditional literal 0, so a missing markup is price-neutral rather than an error. OnOpenStore defaults to an empty resref, the ordinary “no script” sentinel.

Comment and ID are dead outright. Neither field-name string exists anywhere in swkotor.exe, so LoadStore cannot read either under any circumstance. That is a stronger claim than the one made for Repos_PosX/Repos_Posy below. Those strings do exist, and CSWSItem::ReadContainerItemsFromGff reads them for items nested inside a container item. LoadStore never does, so a store’s grid coordinates go unread on this path while the same labels are live on another one.

Infinite belongs to the store entry; Dropable does not

Infinite is read off each ItemList entry with an unconditional literal 0 default, setting a bit the store loader owns outright. The underlying .uti has no say in it.

LoadStore’s ItemList loop never reads Dropable at all. It comes from the item load chain: a freshly constructed item is droppable, and CSWSItem::LoadDataFromGff then re-reads Dropable with an unconditional literal 0, overwriting that regardless of source.

Entries resolving a linked .uti through EquippedRes/InventoryRes get a second, narrower read off the store entry afterward, which can override the item’s value only if the store entry supplies it. Absent, whatever the item’s own load set stands, already false by that point rather than the constructor’s true. On the pure template path the entry is never asked for Dropable.

Pickpocketable has the same two-stage shape

CSWSItem::LoadItem reads Pickpocketable a second time off the store’s list entry, gated on presence, but only in the branch that resolved a linked .uti and only after that branch called LoadDataFromGff, which zeroes the bit unconditionally.

So by the time the gated re-read runs, the constructor’s true is gone. An absent Pickpocketable leaves the bit false. The presence-gated read can push it to true where the store entry supplies it; it cannot restore the constructed default the way a genuine carry-over would.

ItemList[].ObjectId never survives as the sentinel

LoadStore reads it per entry, defaulting to 0x7F000000. The id is checked against the running server’s live object table first: a matching live object means the entry is skipped rather than duplicated, which matters most when loading an in-progress session.

Where no live object matches, the loader allocates a new item and passes the resolved id into CSWSItem’s constructor and on to CSWSObject’s. That base constructor checks for 0x7F000000 specifically and, on a match, discards it and requests a fresh unique id from the global object table.

So an absent ObjectId never lands on the live item as the sentinel. The value exists transiently as the read’s return value, on its way to being replaced.

Fields the engine never reads

What a writer should do with each is a separate question, and it has four possible answers: see the engine ignores this is not you may leave it out.

Finding TypeExplanation
Legacy Interface ConfigurationsRepos_PosX and Repos_Posy are shop-grid coordinates inherited from other Odyssey games. LoadStore never reads either, and the shop UI is built when the player opens it, so a store’s coordinates go unused. They are not dead everywhere, though: the same two labels are live for items nested inside a container item, as above.

Note

Repos_PosY with a capital Y is a third label, and no string for it exists in swkotor.exe on any object type. The engine’s own string is the lowercase Repos_Posy, and label lookup is case-sensitive, so the two never resolve to each other. Rakata’s reader accepts either spelling and prefers the lowercase one.


Implemented Linter Rules (Rakata-Lint)

Phase 1 (intra-resource, no context)

Implemented under rakata_lint::rules::utm.

  1. UTM-001 (Legacy Grid Coordinates): Informs when inventory items carry non-zero shop-grid coordinates; LoadStore builds its UI on open and never reads them.
  2. UTM-002 (Unknown Buy/Sell Flags): Warns when BuySellFlag has bits set outside the canonical buy (bit 0) and sell (bit 1) toggles.
  3. UTM-003 (Legacy Store UI Fallback): Warns when BuySellFlag == 0 (missing or empty); the engine falls back to legacy UI behaviour and clamps MarkUp to 100.

Phase 2 (resource existence, requires LintContext)

Implemented under rakata_lint::rules::utm_range.

  1. UTM-004 (Resref Existence): Warns when OnOpenStore (.ncs) or any ItemList[i].InventoryRes (.uti) does not resolve in the configured resource sources. The toolset-only top-level ResRef (merchant template) is intentionally skipped, since the engine never reads it.

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
ItemList[].Repos_PosXWORDnever reads it: an older tool’s shop-grid coordinate; the engine builds its shop UI dynamically when opened and never reads thesenot one constant; we substitute 0
ItemList[].Repos_PosyWORDnever reads it: an older tool’s shop-grid coordinate; the engine builds its shop UI dynamically when opened and never reads thesenot one constant; we substitute 0
ItemList[].Repos_PosYWORDnever reads it: an older tool’s shop-grid coordinate; the engine builds its shop UI dynamically when opened and never reads thesenot 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
ResRefCResRefNOT EXAMINED; we substitute ""
TagCExoStringstamps ""
LocNameCExoLocStringstamps empty
MarkUpINTstamps 0
MarkDownINTstamps 0
OnOpenStoreCResRefstamps ""
CommentCExoStringNOT EXAMINED; we substitute ""
IDBYTENOT EXAMINED; we substitute 0
BuySellFlagBYTEkeeps 3
ItemListListnot one constant; we substitute container
ItemList[].InventoryResCResRefNOT EXAMINED; we substitute ""
ItemList[].DropableBYTEstamps 0
ItemList[].InfiniteBYTEstamps 0
ItemList[].ObjectIdDWORDstamps 2130706432