GFF Items and Economy - OpenKotOR/PyKotor GitHub Wiki
Items, merchants, journals, and factions form the game’s economy and progression systems. UTI defines every equippable or consumable object [UTI], UTM describes store inventories and pricing, JRL tracks quest progress visible to the player, and FAC controls inter-faction hostility and friendship.
Part of the GFF File Format Documentation.
UTI files define item templates for all objects in creature inventories, containers, and stores. Items range from weapons and armor to quest items, upgrades, and consumables. UTI files are loaded with the same resource resolution order as other resources (override, MOD/SAV, KEY/BIF).
Official Bioware Documentation: For the authoritative Bioware Aurora Engine Item format specification, see Bioware Aurora Item Format.
For mod developers:
- To modify GFF/UTI files in your mods, see the TSLPatcher GFFList Syntax Guide.
- For general modding information, see HoloPatcher README for Mod Developers.
Related formats:
-
TLK — localized names
-
Visual variants resolve through:
PyKotor models UTI templates with the dedicated UTI data class and read_uti / write_uti helpers on top of the shared binary GFFBinaryReader.load pipeline, while Holocron Toolset exposes the same item fields through its uti.py editor. Other toolchains keep UTI in the generic GFF family as well, including reone's gff.cpp and gffreader.cpp, KotOR.js's GFFObject.ts, Kotor.NET's GFF.cs, and xoreos's Aurora GFF stack.
| Field | Type | Description |
|---|---|---|
TemplateResRef |
ResRef | Template identifier for this item |
Tag |
CExoString | Unique tag for script references |
LocalizedName |
CExoLocString | Item name (localized) |
Description |
CExoLocString | Generic description |
DescIdentified |
CExoLocString | Description when identified |
Comment |
CExoString | Developer comment/notes |
| Field | Type | Description |
|---|---|---|
BaseItem |
int32 | Index into baseitems.2da (defines item type) |
Cost |
UInt32 | Base value in credits |
AddCost |
UInt32 | Additional cost from properties |
Plot |
byte | Plot-critical item (cannot be sold/destroyed) |
Charges |
byte | Number of uses remaining |
StackSize |
word | Current stack quantity |
ModelVariation |
byte |
model variation index [uti.py L82] |
BodyVariation |
byte | Body variation for armor [uti.py L81] |
TextureVar |
byte |
texture variation for armor [uti.py L83] |
BaseItem types (from baseitems.2da); row index into the 2DA defines item type.
| Field | Type | Description |
|---|---|---|
PropertiesList |
List | Item properties and enchantments |
Upgradable |
byte | Can accept upgrades (KotOR1 only) |
UpgradeLevel |
byte | Current upgrade tier (KotOR2 only) |
PropertiesList Struct fields:
-
PropertyName(word): Index intoitempropdef.2da -
Subtype(word): Property subtype/category -
CostTable(byte): Cost table index -
CostValue(word): Cost value -
Param1(byte): First parameter -
Param1Value(byte): First parameter value -
ChanceAppear(byte): Percentage chance to appear on randomly generated loot; default 100 [uti.pyL169]
Property types are defined entirely in itempropdef.2da; the PropertyName field indexes into this table which maps each row to its subtype table, cost table, and parameter tables. PyKotor serializes each UTIProperty without enumerating property-type names in code — all valid property names derive from the 2DA at runtime [uti.py L165–L176].
| Field | Type | Description |
|---|---|---|
WeaponColor (KotOR2) |
byte | Blade color for lightsabers |
WeaponWhoosh (KotOR2) |
byte | Whoosh sound type |
Lightsaber colors (KotOR2 WeaponColor):
- 0: Blue, 1: Yellow, 2: Green, 3: Red
- 4: Violet, 5: Orange, 6: Cyan, 7: Silver
- 8: White, 9: Viridian, 10: Bronze
| Field | Type | Description |
|---|---|---|
BodyVariation |
byte | Body model variation [uti.py L81] |
TextureVar |
byte |
texture variation [uti.py L83] |
ModelVariation |
byte |
model type [uti.py L82] |
ArmorRulesType (KotOR2) |
byte | Armor class category |
Armor model Variations:
- Body + texture Variation: Creates visual diversity
- Armor adapts to wearer's body type and gender
-
appearance.2dadefines valid combinations
| Field | Type | Description |
|---|---|---|
Plot |
byte | Cannot be sold or destroyed |
Stolen |
byte | Marked as stolen |
Cursed |
byte | Cannot be unequipped |
Identified |
byte | Player has identified the item |
Plot Item Behavior:
- Immune to destruction/selling
- Often required for quest completion
- Can have special script interactions
| Field | Type | Description |
|---|---|---|
Upgradable |
byte | Item accepts upgrade items |
Upgrade Mechanism:
- Weapon/armor can have upgrade slots
- Player applies upgrade items to base item
- Properties from upgrade merge into base
- Referenced in
upgradetypes.2da
| Field | Type | Description |
|---|---|---|
UpgradeLevel |
byte | Current upgrade tier (0-2) |
WeaponColor |
byte | Lightsaber blade color |
WeaponWhoosh |
byte | Swing sound type |
ArmorRulesType |
byte | Armor restriction category |
KotOR2 Upgrade Slots:
Weapons and armor may have upgrade slots defined in baseitems.2da.
| Field | Type | Description |
|---|---|---|
ModelVariation |
byte | Base model index [uti.py L82] |
BodyVariation |
byte | Body model for armor [uti.py L81] |
TextureVar |
byte |
texture variant [uti.py L83] |
model Resolution:
Item model names are derived from the ModelResRef column in baseitems.2da, combined with the ModelVariation index. textures follow the same naming convention.
| Field | Type | Description |
|---|---|---|
PaletteID |
byte | Toolset palette category |
Comment |
CExoString | Designer notes/documentation |
Toolset Integration:
-
PaletteIDorganizes items in editor - Does not affect gameplay
- Used for content creation workflow
PyKotor parses UTI via construct_uti [uti.py L128], which deserializes each GFF field into the UTI data object.
Armor identification uses the ARMOR_BASE_ITEMS frozenset ({35,36,37,38,39,40,41,42,43,53,58,63,64,65,69,71,85,89,98,100,102,103}) defined in [uti.py L19] to distinguish armor from weapons when computing visual variants. PaletteID and Comment are toolset-only fields not used at game runtime.
- GFF File Format - Binary layout and data types
- GFF Creature and Dialogue - UTC and DLG types
- GFF Spatial Objects - UTD, UTP, UTT, UTE, UTS, UTW, PTH types
- GFF Module and Area - ARE, GIT, IFO types
- baseitems.2da - Item type definitions
- TSLPatcher GFFList Syntax - Modding GFF/UTI with TSLPatcher
- Bioware Aurora Item Format - Official BioWare specification
Part of the GFF File Format Documentation.
UTM files are GFF resources with root content type UTM (GFFContent.UTM) that define merchant templates: localized name, pricing (mark up / mark down), buy/sell flags, optional OnOpenStore script hook, and an ItemList of stock lines. Module store instances in the GIT reference a UTM template via the area’s store list (PyKotor maps GIT stores to ResourceType.UTM; instance wrapper GITStore L967–L1000). UTMs resolve like other resources: override -> MOD/SAV -> KEY/BIF.
Official Bioware Documentation: See Bioware Aurora Store Format for Aurora-era store semantics; KotOR field names below match PyKotor’s construct_utm / dismantle_utm and the class docstring’s LoadStore notes.
For mod developers:
Global merchant metadata also lives in merchants.2da (data table, not the per-template GFF).
| Field | GFF type | Role |
|---|---|---|
ResRef |
ResRef | Template resref (file stem). |
LocName |
CExoLocString | Localized merchant name. |
Tag |
CExoString | Tag for scripts / identification. |
MarkUp |
int32 | Markup when selling to the player (integer; divide by 100 for percentage multiplier [ModuleStore.ts L56]). |
MarkDown |
int32 | Markdown when buying from the player (integer; divide by 100 for percentage multiplier [ModuleStore.ts L52]). |
OnOpenStore |
ResRef | Script executed when the store UI opens. |
Comment |
CExoString | Authoring comment. |
BuySellFlag |
byte | Bit 0 = can buy from player; bit 1 = can sell to player [construct_utm L127–L128]. |
ItemList |
List | Stock entries (see below). |
ID |
byte | Legacy field; default 5 in PyKotor [utm.py L83]; emitted only when use_deprecated=True in dismantle_utm L172. |
Each list element is a struct with (at minimum) the fields PyKotor reads and writes:
| Field | GFF type | Role |
|---|---|---|
InventoryRes |
ResRef | Item template (UTI) resref. |
Infinite |
byte | Infinite stock when non-zero. |
Dropable |
byte | Droppable flag (PyKotor writes the field only when true — dismantle_utm L213–L214). |
Repos_PosX |
uint16 | Repository grid X (writer uses slot index — dismantle_utm L164). |
Repos_PosY |
uint16 | Repository grid Y (writer uses 0 — dismantle_utm L165). Note: reone reads this field as Repos_Posy (lowercase 'y'); PyKotor writes Repos_PosY (capital 'Y'). |
PyKotor documents and round-trips merchant templates through UTM, construct_utm, dismantle_utm, read_utm, and write_utm on top of the shared GFFBinaryReader.load path, and the same merchant-root-as-GFF approach appears in reone's gffreader.cpp and gff.cpp, KotOR.js's GFFObject.ts, Kotor.NET's UTM.cs plus GFF.cs, and xoreos's Aurora GFF loader stack.
- GFF-File-Format — container layout
- GFF-GIT — store instances in areas
-
GFF-UTI — item templates referenced by
InventoryRes - Bioware-Aurora-Store — Aurora store documentation
- Container-Formats#key — resource resolution
Part of the GFF File Format Documentation.
JRL files define the structure of the player's quest journal. They organize quests into categories and track progress through individual journal entries. JRL files are loaded with the same resource resolution order as other resources (override, MOD/SAV, KEY/BIF).
Official Bioware Documentation: For the authoritative Bioware Aurora Engine Journal format specification, see Bioware Aurora Journal Format.
For mod developers:
- Journal updates are typically driven by DLG Quest/QuestEntry and scripts (
AddJournalQuestEntry).
See also:
Related formats:
PyKotor represents journals through JRL, JRLQuest, JRLEntry, construct_jrl, read_jrl, and write_jrl, tags them as GFFContent.JRL, and decodes them through the same shared GFFBinaryReader.load path that Holocron Toolset builds on in its dialogue and module editors. Other implementations also keep JRL inside the generic GFF family, including reone's gff.cpp and gffreader.cpp, KotOR.js's GFFObject.ts, Kotor.NET's GFF.cs, and xoreos's Aurora reader stack.
JRL files contain a list of Categories (Quests), each containing a list of EntryList (States).
| Field | Type | Description |
|---|---|---|
Categories |
List | List of quests |
| Field | Type | Description |
|---|---|---|
Tag |
CExoString | Unique quest identifier |
Name |
CExoLocString | Quest title |
Comment |
CExoString | Developer comment |
Priority |
uint32 | Sorting priority (0=Highest, 4=Lowest) [JRLQuestPriority L93–98]; default LOWEST when constructing a new JRLQuest object [jrl.py L64] |
PlotIndex |
int32 | Legacy plot index |
PlanetID |
int32 | Planet association (unused) |
EntryList |
List | List of quest states |
Priority Levels:
- 0 (Highest): Main quest line
- 1 (High): Important side quests
- 2 (Medium): Standard side quests
- 3 (Low): Minor tasks
- 4 (Lowest): Completed/Container
| Field | Type | Description |
|---|---|---|
ID |
uint32 | State identifier (referenced by scripts/dialogue) |
Text |
CExoLocString | Journal text displayed for this state |
End |
uint16 | 1 if this state completes the quest [jrl.py L153] |
XP_Percentage |
float (single) | XP reward multiplier for reaching this state [jrl.py L155] |
Quest Updates:
- Scripts use
AddJournalQuestEntry("Tag", ID)to update quests. - Dialogues use
QuestandQuestEntryfields. - Only the highest ID reached is typically displayed (unless
AllowOverrideHigheris set inglobal.jrllogic). -
End=1moves the quest to the "Completed" tab.
- global.jrl: The master journal files for the entire game.
- Module JRLs: Not typically used; most quests are global.
-
XP Rewards:
XP_Percentagescales thejournal.2daXP value for the quest.
- GFF-File-Format -- Generic format underlying JRL
- GFF-DLG (Dialogue) - Quest/QuestEntry updates from conversations
- NCS File Format - Scripts that call journal API
- Bioware Aurora Journal Format - Official journal specification
FAC files are GFF-based format files that store faction definitions and reputation relationships between factions in KotOR modules. The file is typically named repute.fac in modules. FAC files are loaded with the same resource resolution order as other resources (override, MOD/SAV, KEY/BIF).
Official BioWare Documentation: For the authoritative BioWare Aurora Engine Faction Format specification, see Bioware Aurora Faction Format.
Source: This documentation is based on the official BioWare Aurora Engine Faction Format PDF, contained in xoreos-docs: specs/bioware/Faction_Format.pdf.
A Faction is a control system for determining how game objects interact with each other in terms of friendly, neutral, and hostile reactions. Faction information is stored in the repute.fac file in a module or savegame. This file uses BioWare's Generic File Format (GFF), and the GFF FileType string in the header of repute.fac is "FAC ".
Related Files:
-
repute.2da- Default faction standings (see 2DA File Format) -
repadjust.2da- Reputation adjustment values (see 2DA File Format)
PyKotor describes module faction state with FACFaction, FACReputation, FAC, construct_fac, read_fac, and write_fac, labels the root as GFFContent.FAC, and parses it through the shared GFFBinaryReader.load; Holocron Toolset exposes the same repute.fac data where its module and generic GFF workflows surface faction editing. Other engines again keep FAC as ordinary GFF, including reone's gff.cpp and gffreader.cpp, KotOR.js's GFFObject.ts, Kotor.NET's GFF.cs, and xoreos's Aurora stack.
The top-level GFF struct contains two lists:
| Label | Type | Description |
|---|---|---|
| FactionList | List | List of Faction Structs (StructID = list index). Defines what Factions exist in the module. |
| RepList | List | List of Reputation Structs (StructID = list index). Defines how each Faction stands with every other Faction. |
Each Faction Struct in the FactionList defines a single faction. The StructID corresponds to the faction's index in the list, which is used as the faction ID in reputation relationships.
| Label | Type | Description |
|---|---|---|
| FactionName | CExoString | Name of the Faction. |
| FactionGlobal | WORD | Global Effect flag. 1 if all members of this faction immediately change their standings with respect to another faction if just one member of this faction changes it standings. 0 if other members of a faction do not change their standings in response to a change in a single member. |
| FactionParentID | DWORD | Index into the Top Level Struct's FactionList specifying the Faction from which this Faction was derived. The first four standard factions (PC, Hostile, Commoner, and Merchant) have no parents, and use 0xFFFFFFFF as their FactionParentID. No other Factions can use this value. |
KotOR modules typically contain the following standard factions (in order):
-
PC (Player) - Index 0, Parent:
0xFFFFFFFF -
Hostile - Index 1, Parent:
0xFFFFFFFF -
Commoner - Index 2, Parent:
0xFFFFFFFF -
Merchant - Index 3, Parent:
0xFFFFFFFF -
Defender - Index 4, Parent:
0xFFFFFFFF(KotOR 2 only)
Each Reputation Struct in the RepList describes how one faction feels about another faction. Feelings need not be mutual. For example, Exterminators might be hostile to Rats, but Rats may be neutral to Exterminators, so that a Rat would only attack a Hunter or run away from a Hunter if a Hunter attacked the Rat first.
| Label | Type | Description |
|---|---|---|
| FactionID1 | DWORD | Index into the Top-Level Struct's FactionList. "Faction1" |
| FactionID2 | DWORD | Index into the Top-Level Struct's FactionList. "Faction2" |
| FactionRep | DWORD | How Faction2 perceives Faction1. 0–10 = Faction2 is hostile to Faction1, 11–89 = Faction2 is neutral to Faction1, 90–100 = Faction2 is friendly to Faction1 [FactionManager.ts L131–L141]; default 50 when field absent [fac.py L134] |
| Range | Relationship | Description |
|---|---|---|
| 0–10 | Hostile | Faction2 will attack Faction1 on sight [IsHostile L132]. |
| 11–89 | Neutral | Faction2 is neutral to Faction1. No automatic aggression [IsNeutral L136]. |
| 90–100 | Friendly | Faction2 is friendly to Faction1. Will not attack and may assist [IsFriendly L140]. |
For the RepList to be exhaustively complete, it requires N*N elements, where N = the number of elements in the FactionList. However, the way that the PC Faction (FactionID2 == 0) feels about any other faction is actually meaningless, because PCs are player-controlled and not subject to faction-based AI reactions. Therefore, any Reputation Struct where FactionID2 == 0 (i.e., PC) is not strictly necessary, and can therefore be omitted from the RepList.
Thus, for the RepList to be sufficiently complete, it requires N*N - N elements, where N = the number of elements in the FactionList, assuming that one of those Factions is the PC Faction.
In practice, however, the RepList may contain anywhere from (NN - N) to (NN - 1) elements, due to a small idiosyncrasy in how the toolset generates and saves the list. When a new faction is created, up to two new entries may appear for the PC Faction.
From all the above, it follows that a module that contains no user-defined factions will have exactly NN - N Faction Structs, where N = 5. Modules containing user-defined factions will have more. The maximum number of Faction Structs in the RepList is NN - 1, because the Player Faction itself can never be a parent faction.
The repute.2da file defines default faction standings. Each row corresponds to a faction ID, and columns represent how that faction feels about other factions.
Rows (by faction ID):
- Row 0: Player
- Row 1: Hostile
- Row 2: Commoner
- Row 3: Merchant
- Row 4: Defender (KotOR 2 only)
Columns:
-
LABEL- String: Programmer label; name of faction being considered by the faction named in each of the other columns. Row number is the faction ID. -
HOSTILE- Integer: How the Hostile faction feels about the other factions -
COMMONER- Integer: How the Commoner faction feels about the other factions -
MERCHANT- Integer: How the Merchant faction feels about the other factions -
DEFENDER- Integer: How the Defender faction feels about the other factions
Note: Do not add new rows to repute.2da. They will be ignored.
The repadjust.2da file describes how faction reputation standings change in response to different faction-affecting actions, how the presence of witnesses affects the changes, and by how much the changes occur.
Rows (action types - hardcoded, do not change order):
- Attack
- Theft
- Kill
- Help
Columns:
-
LABEL- String: Programmer label; name of an action. -
PERSONALREP- Integer: Personal reputation adjustment of how the target feels about the perpetrator of the action named in the LABEL. -
FACTIONREP- Integer: Base faction reputation adjustment in how the target's Faction feels about the perpetrator. This reputation adjustment is modified further by the effect of witnesses, as controlled by the columns described below. Note that a witness only affects faction standing if the witness belongs to a Global faction. -
WITFRIA- Integer: Friendly witness target faction reputation adjustment. -
WITFRIB- Integer: Friendly witness personal reputation adjustment. -
WITFRIC- Integer: Friendly witness faction reputation adjustment. -
WITNEUA- Integer: Neutral witness target faction reputation adjustment. -
WITNEUB- Integer: Neutral witness personal reputation adjustment. -
WITNEUC- Integer: Neutral witness faction reputation adjustment. -
WITENEA- Integer: Enemy witness target faction reputation adjustment. -
WITENEB- Integer: Enemy witness personal reputation adjustment. -
WITENEC- Integer: Enemy witness faction reputation adjustment.
Note: Do not change the order of rows in repadjust.2da. Adding new rows will have no effect.
from pykotor.resource.generics.fac import read_fac
# Read from file
fac = read_fac("module/repute.fac")
# Access factions
for i, faction in enumerate(fac.factions):
print(f"Faction {i}: {faction.name}")
print(f" Global Effect: {faction.global_effect}")
print(f" Parent ID: {faction.parent_id}")
# Access reputations
for rep in fac.reputations:
print(f"Faction {rep.faction_id2} perceives Faction {rep.faction_id1} as: {rep.reputation}")from pykotor.resource.generics.fac import FAC, FACFaction, FACReputation, write_fac
fac = FAC()
# Add standard factions
pc = FACFaction()
pc.name = "PC"
pc.global_effect = False
pc.parent_id = 0xFFFFFFFF
fac.factions.append(pc)
hostile = FACFaction()
hostile.name = "Hostile"
hostile.global_effect = True
hostile.parent_id = 0xFFFFFFFF
fac.factions.append(hostile)
# Add reputation relationship
rep = FACReputation()
rep.faction_id1 = 1 # Hostile
rep.faction_id2 = 0 # PC
rep.reputation = 5 # Hostile (0-10 range)
fac.reputations.append(rep)
# Write to file
write_fac(fac, "output/repute.fac")- Faction IDs correspond to list indices in the FactionList
- The PC faction (index 0) typically has no reputation entries where FactionID2 == 0, as PC reactions are player-controlled
- Standard factions use
0xFFFFFFFFas their parent ID - Reputation values outside the 0-100 range may cause undefined behavior
- Global factions propagate reputation changes across all members when one member's reputation changes
- GFF File Format - Parent GFF format
- 2DA-repute - Default faction standings table
- 2DA File Format - repadjust.2da and repute.2da structure
- Bioware Aurora Faction - Official faction specification
- GFF File Format -- Binary container format and GFF family overview
- GFF Creature and Dialogue -- UTC and DLG types
- GFF Spatial Objects -- Placeables, doors, triggers, encounters, waypoints
- GFF Module and Area -- ARE, GIT, IFO module data
- Bioware Aurora Items Economy and Narrative -- Official specification
- Concepts -- Resource resolution and shared vocabulary