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
| Property | Value |
|---|---|
| Extension(s) | .utm |
| Magic Signature | UTM / V3.2 |
| Type | Merchant Blueprint |
| Rust Reference | View rakata_generics::Utm in Rustdocs |
Field Schema
The format’s field families, as an orientation before the full list.
| Category | Covers | Representative fields |
|---|---|---|
| Core Identity | The shop’s name and tag | Tag, LocName |
| Economic Metrics | Price scaling when buying or selling, plus basic shop rules | MarkUp, MarkDown, BuySellFlag |
| Store Inventory | The items in stock, including rules for infinite restocking | ItemList |
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
| Function | Size | Behaviour |
|---|---|---|
LoadStore | 1341 B | The main parser. It reads the merchant’s identity, its MarkUp/MarkDown pricing, and its buy and sell permissions. |
ItemList Read | n/a | Walks the store’s stock, taking either a saved item snapshot or a template named by InventoryRes. |
AddItemToInventory | n/a | Adds each item to the store’s inventory so the player can browse and buy it. |
Rules the engine enforces
| Engine Rule | Runtime Behaviour |
|---|---|
| Cost Sorting | The 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 Economics | MarkUp 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 Flags | BuySellFlag is two toggles: bit 0 lets the player sell to the merchant, bit 1 lets the merchant sell to the player. |
| BuySellFlag Fallback | Unlike 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 Stacking | An item flagged Infinite is never depleted. The player can buy it repeatedly and the stock does not fall. |
| Save vs. Template Inventory | On 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 ItemList | A 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 Type | Explanation |
|---|---|
| Legacy Interface Configurations | Repos_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_PosYwith a capitalYis a third label, and no string for it exists inswkotor.exeon any object type. The engine’s own string is the lowercaseRepos_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.
- UTM-001 (Legacy Grid Coordinates): Informs when inventory items carry non-zero shop-grid coordinates;
LoadStorebuilds its UI on open and never reads them. - UTM-002 (Unknown Buy/Sell Flags): Warns when
BuySellFlaghas bits set outside the canonical buy (bit 0) and sell (bit 1) toggles. - UTM-003 (Legacy Store UI Fallback): Warns when
BuySellFlag == 0(missing or empty); the engine falls back to legacy UI behaviour and clampsMarkUpto 100.
Phase 2 (resource existence, requires LintContext)
Implemented under rakata_lint::rules::utm_range.
- UTM-004 (Resref Existence): Warns when
OnOpenStore(.ncs) or anyItemList[i].InventoryRes(.uti) does not resolve in the configured resource sources. The toolset-only top-levelResRef(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
| Field | Type | Engine | When absent |
|---|---|---|---|
ItemList[].Repos_PosX | WORD | never reads it: an older tool’s shop-grid coordinate; the engine builds its shop UI dynamically when opened and never reads these | not one constant; we substitute 0 |
ItemList[].Repos_Posy | WORD | never reads it: an older tool’s shop-grid coordinate; the engine builds its shop UI dynamically when opened and never reads these | not one constant; we substitute 0 |
ItemList[].Repos_PosY | WORD | never reads it: an older tool’s shop-grid coordinate; the engine builds its shop UI dynamically when opened and never reads these | not 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.
| Field | Type | When absent |
|---|---|---|
ResRef | CResRef | NOT EXAMINED; we substitute "" |
Tag | CExoString | stamps "" |
LocName | CExoLocString | stamps empty |
MarkUp | INT | stamps 0 |
MarkDown | INT | stamps 0 |
OnOpenStore | CResRef | stamps "" |
Comment | CExoString | NOT EXAMINED; we substitute "" |
ID | BYTE | NOT EXAMINED; we substitute 0 |
BuySellFlag | BYTE | keeps 3 |
ItemList | List | not one constant; we substitute container |
ItemList[].InventoryRes | CResRef | NOT EXAMINED; we substitute "" |
ItemList[].Dropable | BYTE | stamps 0 |
ItemList[].Infinite | BYTE | stamps 0 |
ItemList[].ObjectId | DWORD | stamps 2130706432 |