TSLPatcher GFFList Syntax - OpenKotOR/PyKotor GitHub Wiki
This guide explains how to modify GFF files using TSLPatcher syntax. For the complete GFF file format specification, see GFF File Format. For general TSLPatcher information, see TSLPatcher's Official Readme. For HoloPatcher-specific information, see HoloPatcher README for Mod Developers.
The [GFFList] section in TSLPatcher's changes.ini lets you edit or add data inside GFF (Generic file format) files used across KotOR. You will use this to change items (UTI), creatures (UTC), dialogs (DLG), placeables (UTP), triggers (UTT), waypoints (UTW), areas (ARE), journal entries (JRL), paths (PTH), module/level info (IFO), and scripts data (GIT).
- Quick Start
- Cheatsheet
- Basic Structure
- File-Level Configuration
- Modifying Existing Fields
- Adding New Fields
- Field Types and Value Syntax
- Memory Token System
- Nested Structures
- Special Features
- Common Pitfalls and Troubleshooting
- Execution Order and Dependencies
- Complete Examples
- Add your file under
[GFFList]
[GFFList]
File0=my_item.uti- Create a section named exactly the file's name (e.g.
my_item.uti) and set where to save it:
[my_item.uti]
!Destination=override- Change an existing field by writing its path on the left (key), (e.g.
BaseItem) and the new value on the right (value) (e.g.28):
BaseItem=28
LocalizedName(strref)=12345
Comment(lang0)=Hello there- Add a brand-new field using
AddField#--> create another section for its details (e.g.new_property):
AddField0=new_property
[new_property]
FieldType=Struct
Path=PropertyList
Label=
TypeId=7-
Use tokens when a value comes from earlier steps:
ModelVariation=2DAMEMORY5
Description=StrRef10That's it. The rest of this page explains the knobs and dials you'll use as your files get more complex.
- Paths use backslashes (
\) to separate hierarchy levels:Parent\Child\Field - Lists use numbers (
\0,\1,\2, etc.) to index elements:RepliesList\0\Text(e.g.RepliesList\0\Text=Hello) - Localized strings use parentheses on the field name:
-
(strref)--> set the dialog.tlk reference-
(lang0)..(lang9)--> set per-language text
-
- vectors:
Position=1.5|2.0|3.0,Orientation=0.0|0.0|0.0|1.0 - Tokens as values:
- Tokens for dynamic field targets and list indices:
- In AddField:
2DAMEMORY#=ListIndexsaves where a struct was inserted -
2DAMEMORY#=!FieldPathsaves the full path to a field you just added - Later: use that
2DAMEMORY#in place of a field path to modify it
- In AddField:
[GFFList]
!DefaultDestination=override
!DefaultSourceFolder=. ; Note: `.` refers to the tslpatchdata folder (where changes.ini is located)
File0=example.dlg
Replace0=different_dlg.dlg
[example.dlg]
!Destination=override
!SourceFolder=.
!SourceFile=source.dlg
!ReplaceFile=1
; Modify existing fields
FieldName=123
NestedField\0\SubField=value
; Add new fields
AddField0=new_field_add
[new_field_add]
FieldType=Byte
Path=
Label=MyNewField
Value=42
; Use 2DA memory tokens
2DAMEMORY0=!FieldPath
2DAMEMORY1=2DAMEMORY2The [GFFList] section declares GFF (Generic File Format) files to patch. Each entry references another section with the same name as the filename.
| Key | Type | Default | Description |
|---|---|---|---|
!DefaultDestination |
string | override |
Default destination for all GFF files in this section |
!DefaultSourceFolder |
string | . |
Default source folder for GFF files. Relative path from mod_path (typically the tslpatchdata folder, which is the parent directory of changes.ini and namespaces.ini). When ., refers to the tslpatchdata folder itself. Path resolution: mod_path / !DefaultSourceFolder / filename
|
Each GFF file requires its own section (e.g., [example.dlg]).
| Key | Type | Default | Description |
|---|---|---|---|
!Destination |
string | Inherited from !DefaultDestination
|
Where to save the modified file (override or path\to\file.mod) |
!SourceFolder |
string | Inherited from !DefaultSourceFolder
|
Source folder for the GFF file. Relative path from mod_path (typically the tslpatchdata folder). When ., refers to the tslpatchdata folder itself. |
!SourceFile |
string | Same as section name | Alternative source filename (useful for multiple setup options using different source files) |
!ReplaceFile |
0/1 | 0 | If 1, overwrite existing file before applying modifications. If 0 (default), modify the existing file in place. |
!SaveAs |
string | Same as section name | Alternative filename to save as (useful for renaming files during installation) |
!OverrideType |
string | ignore |
How to handle existing files in Override when destination is an ERF or RIM container. Valid values: ignore (default), warn (log warning), rename (prefix with old_) |
Destination values:
-
overrideor empty: Save to the Override folder -
Modules\module.mod: Insert into a module capsule (MOD, ERF, or RIM; use backslashes for path separators) - Container paths must be relative to the game folder root
Source file resolution:
The patcher resolves source files in this order:
- If
!ReplaceFile=1or file doesn't exist at destination: Load frommod_path / !SourceFolder / !SourceFile(or section name if!SourceFilenot set) - Otherwise: Load existing file from destination location (override or container)
- Apply all modifications from the section
- Save to
!Destinationwith name!SaveAs(or section name if!SaveAsnot set)
To change an existing field's value, use the field name as the key:
[example.uti]
; Modify root-level field
LocalizedName=strref12345
; Modify nested field (use backslash to separate path components)
PropertiesList\0\Subtype=5
Comment(objref)=2DAMEMORY10
; Modify localized string strref
Comments(strref)=123
; Modify localized string substring (lang0 = English)
Comments(lang0)=Hello World
; Modify vector/orientation
Position=1.5|2.0|3.0
Orientation=0.0|0.0|0.0|1.0- Use backslash (
\) to separate hierarchy levels - Use numeric indices for list elements:
ListName\0\Field - Case-sensitive labels
- Parenthesis syntax for complex types:
-
FieldName(strref)for localized string StrRef -
FieldName(lang0)throughFieldName(lang9)for language/gender strings
-
Supported memory token formats:
-
[StrRef](Audio-and-Localization-Formats#string-references-strref)#- References TLK memory token -
2DAMEMORY#- References 2DA memory token
Use AddFieldN keys to define new fields. Each requires its own section:
[example.uti]
AddField0=new_item_property
[new_item_property]
FieldType=Word
Path=
Label=NewProperty
Value=123| Key | Type | Required | Description |
|---|---|---|---|
FieldType |
string | Yes | One of the following keywords (case as shown in tools): - byte - char - Word, Short, DWORD, Int, Int64 - double, float - ExoString, ResRef, ExoLocString - Binary, Struct, List - orientation, position |
Label |
string | Yes* | field name (max 16 alphanumeric characters, no spaces). Must be unique within the same STRUCT parent. |
Path |
string | No | field location in GFF hierarchy. Empty string (Path=) means root level. For nested AddField sections, if Path is empty or not specified, it inherits the path from the parent AddField. Use backslashes to separate hierarchy levels. |
Value |
varies | Conditional | field value (see field types below). Not used for Struct, List, or ExoLocString types. |
[StrRef](Audio-and-Localization-Formats#string-references-strref) |
int/string | LocString only |
TLK stringref value, [StrRef](Audio-and-Localization-Formats#string-references-strref)# token, 2DAMEMORY# token, or -1 (no dialog.tlk reference) |
TypeId |
int/string | Struct only | Struct type ID (numeric), ListIndex (auto-set to list index), [StrRef](Audio-and-Localization-Formats#string-references-strref)# token, or 2DAMEMORY# token |
lang# |
string | LocString only | Localized string entries where # is the language+gender ID (0-9). Use <#LF#> for linefeeds. |
AddField# |
string | No | Reference to another section for nested field addition |
2DAMEMORY# |
string | No | Store field path (!FieldPath), list index (ListIndex), or copy from another token (2DAMEMORY#) |
*Label is optional (blank) only when adding a STRUCT to a LIST field. All other field types require a label, including fields added inside structs.
There are two distinct scenarios when using AddField:
-
Adding a STRUCT to a LIST: When you want to add a new element to an existing LIST field
-
FieldType=Structis required -
Label=must be blank (LIST elements don't have labels) -
Path=must point to the LIST field name (e.g.,Path=RepliesList) - The struct will be appended to the end of the list
-
-
Adding a field to a STRUCT: When you want to add any field type (including structs) inside an existing or newly-created STRUCT
- Any
FieldTypeis allowed -
Label=is required (except when the struct itself is being added to a list) -
Path=can be empty to inherit from parent, or explicit to target a specific location
- Any
Critical distinction:
; Scenario 1: Adding a STRUCT to a LIST (note blank Label)
[new_list_entry]
FieldType=Struct
Path=MyList ; Points to the LIST field
Label= ; MUST be blank - list elements have no labels
TypeId=5
; Scenario 2: Adding a field INSIDE a struct (note Label is required)
[new_struct_field]
FieldType=Byte
Path= ; Can be empty (inherits) or explicit path
Label=MyField ; MUST have a label - fields have names
Value=42When adding nested fields via AddField#, child sections automatically inherit the parent's resolved path if their Path= is empty. This allows you to build complex nested structures without repeating full paths.
Basic path inheritance (struct within a struct):
[parent_struct]
FieldType=Struct
Path=PropertyList
Label=NewProperty
AddField0=child_field
[child_field]
FieldType=Byte
Path= ; Inherits "PropertyList\NewProperty" from parent
Label=SubField
Value=42Path inheritance with lists:
When adding a STRUCT to a LIST, the patcher automatically resolves the list index at runtime. Child fields added inside that gff struct inherit a path that includes the resolved index:
[example.dlg]
AddField0=new_reply
[new_reply]
FieldType=Struct
Path=RepliesList ; Target the RepliesList field
Label= ; Blank - adding struct TO list
TypeId=5
AddField0=reply_text ; Add a field INSIDE the newly-added struct
AddField1=reply_sound
[reply_text]
FieldType=ExoLocString
Path= ; Empty path inherits from parent
; Parent resolves to "RepliesList\{index}" where {index} is the position
; where the struct was added (e.g., "RepliesList\3")
Label=Text ; Required - adding field INSIDE struct
StrRef=-1
lang0=New reply option
[reply_sound]
FieldType=ResRef
Path= ; Also inherits "RepliesList\{index}"
Label=Sound ; Required - adding field INSIDE struct
Value=How path resolution works:
-
When you add a
STRUCTto aLISTwithPath=MyList, the patcher:- Finds the LIST field named "MyList"
- Appends the new struct to the end of that list
- The struct's position becomes its index (0-based: first element is 0, second is 1, etc.)
-
When child
AddFieldsections havePath=empty:- They inherit the parent's resolved path
- For structs in lists, this includes the dynamically-resolved index
- Example: If the struct was added as the 5th element (index 4), child fields inherit
MyList\4\{field}
-
You can override inheritance by explicitly setting
Path=in the child section
Example 1: Adding a Property to an Item (Struct within a Struct)
[example.uti]
AddField0=item_property_struct
[item_property_struct]
FieldType=Struct
Path=PropertyList ; Add struct to existing PropertyList
Label= ; Blank - struct is being added to list
TypeId=7
AddField0=property_subtype
AddField1=property_value
[property_subtype]
FieldType=Word
Path= ; Inherits "PropertyList\{index}\"
Label=Subtype ; Required - field inside struct
Value=15
[property_value]
FieldType=Int
Path= ; Inherits "PropertyList\{index}\"
Label=Value ; Required - field inside struct
Value=500Example 2: Adding a Nested Struct (Struct within a Struct at the Root Level)
[example.uti]
AddField0=outer_struct
[outer_struct]
FieldType=Struct
Path= ; Root level
Label=OuterContainer ; Required - field at root level
TypeId=100
AddField0=inner_struct
[inner_struct]
FieldType=Struct
Path= ; Inherits "OuterContainer" from parent
Label=InnerContainer ; Required - field inside struct
TypeId=200
AddField0=inner_field
[inner_field]
FieldType=Byte
Path= ; Inherits "OuterContainer\InnerContainer"
Label=Data ; Required - field inside struct
Value=42Example 3: Complex Dialog Entry (Struct in List with Multiple Nested Fields)
[example.dlg]
AddField0=dialog_entry
[dialog_entry]
FieldType=Struct
Path=EntryList ; Add struct to EntryList
Label= ; Blank - adding struct to list
TypeId=0
2DAMEMORY10=ListIndex ; Store index for later cross-referencing
AddField0=entry_text
AddField1=entry_speaker
AddField2=entry_replies
[entry_text]
FieldType=ExoLocString
Path= ; Inherits "EntryList\{index}\"
Label=Text ; Required
StrRef=StrRef50
lang0=Welcome to my shop!
[entry_speaker]
FieldType=ResRef
Path= ; Inherits "EntryList\{index}\"
Label=Speaker ; Required
Value=shop_keeper
[entry_replies]
FieldType=List
Path= ; Inherits "EntryList\{index}\"
Label=RepliesList ; Required
AddField0=reply_struct
[reply_struct]
FieldType=Struct
Path= ; Inherits "EntryList\{index}\RepliesList"
; Note: This struct is being added to the NEWLY CREATED RepliesList
Label= ; Blank - adding struct to the nested list
TypeId=0
AddField0=reply_text_field
[reply_text_field]
FieldType=ExoLocString
Path= ; Inherits "EntryList\{entry_index}\RepliesList\{reply_index}\"
Label=Text ; Required
StrRef=-1
lang0=Thank you!You can explicitly set Path= in child sections to override automatic inheritance:
[parent]
FieldType=Struct
Path=OuterList
Label=OuterStruct
AddField0=child1
AddField1=child2
[child1]
FieldType=Byte
Path= ; Inherits "OuterList\OuterStruct"
Label=InheritedPath
Value=1
[child2]
FieldType=Byte
Path=DifferentPath ; Explicit path overrides inheritance
Label=CustomPath
Value=2| field type | size | Range | Example |
|---|---|---|---|
| Byte (uint8) | 8-bit unsigned | 0 to 255 | Value=128 |
| Char (Int8) | 8-bit signed | -128 to 127 | Value=-50 |
| Word (uint16) | 16-bit unsigned | 0 to 65535 | Value=1024 |
| Short (int16) | 16-bit signed | -32768 to 32767 | Value=-4096 |
| DWORD (UInt32) | 32-bit unsigned | 0 to 4294967295 | Value=123456 |
| Int (int32) | 32-bit signed | -2147483648 to 2147483647 | Value=-1000000 |
| Int64 | 64-bit signed | -9223372036854775808 to 9223372036854775807 | Value=1234567890 |
| field type | size | Precision | Example |
|---|---|---|---|
| Float (Single) | 32-bit | ~7 digits | Value=3.14159 |
| Double | 64-bit | ~15 digits | Value=2.718281828 |
| field type | Description | Example |
|---|---|---|
| ExoString | null-terminated string | Value=Hello World |
| ResRef | Resource reference (max 16 chars) | Value=myscript |
| Binary | Binary data (hex/base64) |
Value=0xFF00FF00 or Value=base64data (HoloPatcher only) |
[localized_field]
FieldType=ExoLocString
Path=
Label=MyLocalizedString
StrRef=-1
lang0=Hello World
lang3=Bonjour le monde-
StrRef: Numeric value,StrRef#,2DAMEMORY#, or-1 -
lang#: Language+gender substring (see table below) - Use
<#LF#>for linefeeds/carriage returns in lang# values
[position_field]
FieldType=Position
Path=
Label=Location
Value=1.5|2.0|3.0Three float coordinates (X, Y, Z) separated by |.
[orientation_field]
FieldType=Orientation
Path=
Label=Rotation
Value=0.0|0.0|0.0|1.0Four float components (quaternion) separated by |.
[struct_field]
FieldType=Struct
Path=
Label=MyStruct
TypeId=123
AddField0=nested_field-
TypeId: Numeric type ID,ListIndex,StrRef#, or2DAMEMORY# -
AddFieldN: Child fields
[list_field]
FieldType=List
Path=
Label=MyList
AddField0=first_entry
AddField1=second_entry- Contains
AddFieldNentries for each element - Elements are typically STRUCTs without labels
Note: Only supported by HoloPatcher.
[binary_field]
FieldType=Binary
Path=
Label=BinaryData
Value=0xFF00FF00Supported formats (auto-detected):
-
Binary string:
Value=10101010(sequence of 0s and 1s, processed in 8-bit chunks) -
Hex string:
Value=0xFF00FF00orValue=FF00FF00(hexadecimal, even length required;0xprefix optional) -
Base64:
Value=SGVsbG8gV29ybGQ=(standard Base64 encoding)
Note: The Binary field type is a HoloPatcher extension and is not supported by classic TSLPatcher. Classic TSLPatcher does not support adding Binary fields via GFFList.
| ID | Language | Gender |
|---|---|---|
| 0 | English | Male |
| 1 | English | Female |
| 2 | French | Male |
| 3 | French | Female |
| 4 | German | Male |
| 5 | German | Female |
| 6 | Italian | Male |
| 7 | Italian | Female |
| 8 | Spanish | Male |
| 9 | Spanish | Female |
Store and retrieve 2DA data:
[example.uti]
; Store root-level field path
2DAMEMORY0=!FieldPath
; Copy value between tokens
2DAMEMORY1=2DAMEMORY2
; Store list index
AddField0=new_list_entry
[new_list_entry]
FieldType=Struct
Path=
Label=
TypeId=5
2DAMEMORY0=ListIndex2DAMEMORY# usage:
-
At file level:
-
2DAMEMORY#=!FieldPath- Store absolute field path -
2DAMEMORY#=2DAMEMORY#- Copy token value - Numeric ranges and 2DA operations not supported
-
-
In AddField sections:
-
2DAMEMORY#=ListIndex- Store list index -
2DAMEMORY#=!FieldPath- Store field path -
2DAMEMORY#=2DAMEMORY#- Copy token value
-
StrRef Tokens
Reference TLK entries:
[example.uti]
; Direct strref reference
LocalizedName=strref12345
; Use TLK memory token
LocalizedName=StrRef5
; In AddField with LocString
AddField0=localized_description
[localized_description]
FieldType=ExoLocString
Path=
Label=Description
StrRef=StrRef8; Use 2DA token as field value
[example.uti]
ModelVariation=2DAMEMORY5
; Use TLK token as field value
LocalizedName=StrRef3
; Use memory in nested fields
AddField0=dynamic_property
[dynamic_property]
FieldType=Word
Path=
Label=PropertyValue
Value=2DAMEMORY10When adding a STRUCT element to an existing LIST field, you must follow specific rules:
Required configuration:
-
FieldType=Struct- Only structs can be list elements -
Label=must be blank -LISTelements don't have labels -
Path=must point to the LIST field name (e.g.,Path=RepliesList) -
TypeId=specifies the struct's type ID (numeric,ListIndex, or token)
Example: basic STRUCT addition to LIST:
[example.dlg]
AddField0=new_reply_entry
[new_reply_entry]
FieldType=Struct
Path=RepliesList ; Must point to the `LIST` field name
Label= ; MUST be blank - list elements have no labels
TypeId=5 ; Struct Type ID
AddField0=reply_text ; Add fields INSIDE the newly-added struct
AddField1=reply_sound
[reply_text]
FieldType=ExoLocString
Path= ; Empty path inherits parent's resolved path
; Parent resolves to "RepliesList\{index}" automatically
Label=Text ; Required - fields inside structs have labels
StrRef=-1
lang0=New reply option
[reply_sound]
FieldType=ResRef
Path= ; Also inherits "RepliesList\{index}"
Label=Sound ; Required - fields inside structs have labels
Value=Understanding ListIndex resolution:
When you add a STRUCT to a LIST, the patcher automatically determines which position (index) it occupies:
- First struct added becomes index 0
- Second struct added becomes index 1
- And so on...
This index is used to construct the full path for any child fields. For example, if you're adding the 3rd struct to RepliesList:
- The struct itself is added to
RepliesList[2](0-based indexing) - Child fields with empty
Path=inheritRepliesList\2\{field_name}
Storing and using the ListIndex:
You can store the index where a STRUCT is added for later reference:
[new_reply_entry]
FieldType=Struct
Path=RepliesList
Label=
TypeId=5
2DAMEMORY0=ListIndex ; Store the index (e.g., 3) in 2DAMEMORY0Using ListIndex for TypeId:
Some GFF structures require the TypeId to match the ListIndex. Use TypeId=ListIndex:
[example.jrl]
AddField0=journal_category
[journal_category]
FieldType=Struct
Path=Categories
Label= ; Blank - adding struct to list
TypeId=ListIndex ; Auto-sets TypeId to the `ListIndex where STRUCT is added
AddField0=category_name
[category_name]
FieldType=ExoLocString
Path= ; Inherits "Categories\{index}\"
Label=Name ; Required - field inside struct
StrRef=StrRef100
lang0=My Custom QuestsComplete example: dialog entry with cross-references:
[example.dlg]
AddField0=new_entry
AddField1=new_reply
; Add a dialog entry
[new_entry]
FieldType=Struct
Path=EntryList
Label= ; Blank - adding struct to list
TypeId=0
2DAMEMORY5=ListIndex ; Store entry index (e.g., 5)
AddField0=entry_text
[entry_text]
FieldType=ExoLocString
Path= ; Inherits "EntryList\{entry_index}\"
Label=Text ; Required
StrRef=-1
lang0=Hello, traveler!
; Add a reply that references the entry
[new_reply]
FieldType=Struct
Path=RepliesList
Label= ; Blank - adding struct to list
TypeId=0
2DAMEMORY6=ListIndex ; Store reply index (e.g., 2)
AddField0=reply_text
AddField1=reply_entry_link
[reply_text]
FieldType=ExoLocString
Path= ; Inherits "RepliesList\{reply_index}\"
Label=Text ; Required
StrRef=-1
lang0=Thank you!
[reply_entry_link]
FieldType=DWORD
Path= ; Inherits "RepliesList\{reply_index}\"
Label=EntriesRepliesList ; Required - field inside struct
Value=2DAMEMORY5 ; Use stored entry index as valueKey rules summary:
- Adding
STRUCTtoLIST:Label=must be blank,Path=points to list name. - Adding a field to
STRUCT:Label=is required,Path=can be empty (inherits) or explicit. - Path inheritance: Child fields with empty
Path=automatically inherit the parent's resolved path, includingListIndexvalues. -
ListIndexresolution: Happens automatically at runtime based on insertion order. -
TypeId=ListIndex: Auto-setsTypeIdto match theListIndex. -
2DAMEMORY#=ListIndex: Stores the index for later use in cross-references or calculations.
This example demonstrates adding a LIST field containing STRUCT elements, with fields inside those STRUCTs:
[example.uti]
AddField0=item_properties
; First, add a new LIST field at the root level
[item_properties]
FieldType=List
Path= ; Empty - root level
Label=PropertyList ; Required - fields have labels
AddField0=property_struct ; Add a STRUCT element to the newly-created list
; Add a STRUCT element to the PropertyList (LIST elements have no labels)
[property_struct]
FieldType=Struct
Path= ; Inherits "PropertyList" from parent
; This STRUCT will be added to the LIST, so Path resolves to
; "PropertyList\{index}" where index is automatically determined
Label= ; MUST be blank - adding STRUCT to LIST
TypeId=7 ; Property STRUCT Type ID
AddField0=property_subtype
AddField1=property_value
; Add fields INSIDE the struct that was added to the list
[property_subtype]
FieldType=Word
Path= ; Inherits "PropertyList\{index}\" from parent struct
Label=Subtype ; Required - fields inside structs have labels
Value=8
[property_value]
FieldType=Int
Path= ; Inherits "PropertyList\{index}\" from parent struct
Label=Value ; Required - fields inside structs have labels
Value=123Step-by-step breakdown:
-
item_propertiescreates a newLISTfield named "PropertyList" at root level -
property_structadds aSTRUCTelement to thatLIST(hence blankLabel=) -
property_subtypeandproperty_valueadd fields inside theSTRUCT(hence requiredLabel=)
This pattern is common when adding new property lists, dialog entries, journal categories, and similar list-based structures.
Store and use field paths dynamically. This feature (added in TSLPatcher v1.2.7b9) allows you to add fields and then reference them later using 2DAMEMORY# tokens.
Storing a field path:
[example.dlg]
; First, add a field and store its Field Path
AddField0=dynamic_reply
[dynamic_reply]
FieldType=Struct
Path=RepliesList
Label=
TypeId=5
AddField0=text_field
2DAMEMORY0=!FieldPath ; Store the full Field Path to the "Text" field
[text_field]
FieldType=ExoLocString
Path= ; Inherits "RepliesList\{ListIndex}\"
Label=Text
StrRef=-1
lang0=Dynamic reply textUsing a stored field path:
After storing a path with 2DAMEMORY#=!FieldPath, you can use that token as a Field Path to modify the field:
; Modify the field using the stored path
2DAMEMORY0(strref)=StrRef50 ; Sets the StrRef of the field stored in 2DAMEMORY0
2DAMEMORY0(lang0)=Updated text ; Sets the lang0 substring of that fieldCopying field paths between tokens:
; Copy a field path from one token to another
2DAMEMORY1=2DAMEMORY0 ; Copy the path stored in 2DAMEMORY0 to 2DAMEMORY1Use cases:
- Dynamic Dialog Branches: Add new dialog entries/replies and cross-reference them using stored paths
- Self-Referencing structures: Create fields that need to reference other dynamically-added fields
- Conditional field Updates: Store multiple field paths and update them based on runtime conditions
Important: When using 2DAMEMORY#=!FieldPath in an AddField section, the stored path includes the field's label. For nested fields, the path is the full absolute path from the GFF root.
Some GFF structures require the STRUCT's TypeId to match its position (index) in the LIST. This is common in:
- Journal category lists (
.jrlfiles) - Certain dialog structures
- Other list-based structures where type ID corresponds to list position
Syntax:
Set TypeId=ListIndex (literal text, not a token) when adding a STRUCT to a LIST:
[example.jrl]
AddField0=journal_category
[journal_category]
FieldType=Struct
Path=Categories
Label= ; Blank - adding struct to list
TypeId=ListIndex ; Auto-sets Type ID to match list index
AddField0=category_name
[category_name]
FieldType=ExoLocString
Path= ; Inherits "Categories\{index}\"
Label=Name ; Required - field inside struct
StrRef=StrRef100
lang0=My Custom QuestsHow it works:
- When
TypeId=ListIndexis specified, the patcher automatically determines the index where theSTRUCTis added - The type ID is set to that numeric index (0, 1, 2, etc.)
- For example, if the
STRUCTbecomes the 5th element (index 4), the type ID will be set to 4
Note: TypeId=ListIndex is different from 2DAMEMORY#=ListIndex. The former sets the STRUCT's TypeId, while the latter stores the index in a memory token for later use.
ExclusiveColumn (in Nested 2DA Integration)
[GFFList] does not support 2DA-style ExclusiveColumn.
-
Case sensitivity: Field names are case-sensitive.
Comments≠comments. Use a GFF viewer to copy labels exactly. -
List indices:
LISTs start at 0. The first element is\0\, second is\1\, etc. -
Blank labels:
Label=blank is only valid whenFieldType=Structand adding thatSTRUCTto aLIST. All other field types require a label, including fields added insideSTRUCTs that are themselves inLISTs. -
Adding to list vs adding to struct: Confusing these two operations is a common mistake:
- Adding
STRUCTtoLIST:Label=blank,Path=points to list name - Adding field to
STRUCT:Label=required,Path=can be empty (inherits)
- Adding
-
Path inheritance confusion: Remember that when adding a
STRUCTto aLIST, child fields with emptyPath=inherit the resolved path including theListIndex. You don't need to (and shouldn't) manually specify the index. -
Container fields: Don't assign
Value=toSTRUCTorLISTfields—they are containers. Set values in their child fields instead. -
List index resolution: The index where a
STRUCTis added is automatically determined at runtime. You cannot manually set or predict the exact index ahead of time if other mods might addSTRUCTs to the sameLIST.
-
StrRef vs lang#: Use
FieldName(strref)=...for the StrRef value, andFieldName(lang#)=...for text substrings. Don't mix them on the same key. -
Multiple substrings: You can set multiple
lang#entries for the same field (e.g.,lang0=English,lang2=French). -
Line breaks: Use
<#LF#>for linefeeds inlang#values, not literal newlines.
-
Token initialization: Tokens must be set before use. Using
2DAMEMORY5before assignment results in an error. -
Execution order: Within GFFList, AddField sections run before field modifications. Store
!FieldPathwhen creating fields, then use the token to modify them. -
Token scope:
[StrRef](Audio-and-Localization-Formats#string-references-strref)#tokens are created in TLKList and available to GFFList.2DAMEMORY#tokens are file-scoped unless explicitly copied.
-
Source folder resolution:
.refers to thetslpatchdatafolder (wherechanges.iniis located). Don't explicitly specifytslpatchdatain the path. -
Path separators: Use backslashes (
\) in field paths:Parent\Child\Field. Forward slashes may not work consistently. -
Empty paths: For root-level fields, use
Path=(empty) or omit it entirely. Don't usePath=\orPath=..
- type compatibility: Ensure values match field types. Don't assign strings to integer fields or vice versa.
-
Missing fields: Attempting to modify a non-existent field will log an error. Use
AddFieldto create new fields first. -
Binary type: The
Binaryfield type is HoloPatcher-only and not supported by classic TSLPatcher.
- "Cannot parse 'key=value'": Invalid syntax for memory token assignment or field path
-
"field did not exist at path": Tried to modify a field that doesn't exist (use
AddFieldinstead) - "2DAMEMORY# was not defined before use": Memory token was referenced before being set
- "Label must be set for FieldType": Non-Struct field requires a label, or Struct in LIST requires blank label
- Enable verbose logging (
LogLevel=4) to see detailed path resolution and token assignments - Verify field paths using a GFF editor before writing patches
- Test
AddFieldsections before adding modifications that depend on them - Check memorytoken assignments match between
[2DAList]/[TLKList]and[GFFList]sections
Understanding execution order is crucial when your edits depend on earlier tokens or dynamically created fields.
Classic TSLPatcher execution order:
-
TLKList: Appends entries to dialog.tlk, creates
[StrRef](Audio-and-Localization-Formats#string-references-strref)#tokens - InstallList: Copies files to destination (ERF or RIM containers may be created here)
-
2DAList: Modifies 2DA files, creates
2DAMEMORY#tokens -
GFFList: Modifies GFF files (can use
[StrRef](Audio-and-Localization-Formats#string-references-strref)#and2DAMEMORY#tokens) -
CompileList: Preprocesses NSS scripts (replaces
#[StrRef](Audio-and-Localization-Formats#string-references-strref)#and#2DAMEMORY#tokens), then compiles - HACKList: Applies binary patches to NCS files
- SSFList: Modifies soundset files
Note: HoloPatcher moves [InstallList] ahead of [TLKList] so a whole dialog.tlk can be installed before TLK edits are applied. The classic ordering above still describes original TSLPatcher convention and PyKotor's INI serialization order.
Within the GFFList section:
Modifications within a single GFF file are processed in order:
- AddField sections are processed first (fields are created)
-
Memory assignments (
2DAMEMORY#=!FieldPath,2DAMEMORY#=ListIndex) are evaluated as fields are added -
Field modifications are processed last (can reference stored paths via
2DAMEMORY#memory tokens)
Best practices:
-
Add before modify: Use AddField to create structures, store their paths with
2DAMEMORY#=!FieldPath, then modify them using those tokens -
Token dependencies: Ensure tokens are set before use.
2DAMEMORY#tokens from 2DAList are available to GFFList - Container handling: If patching files into ERF or RIM containers, the container must exist (created by InstallList) or be built automatically by the patcher
Important notes:
- Script token preprocessing (in
[CompileList]) runs before[GFFList]to avoid interfering with!FieldPathassignments - If multiple GFF files reference the same memory tokens, they can share
StrRef#and2DAMEMORY#values across files - The
!OverrideTypesetting controls behavior when a file exists inOverridebut you're patching into an ERF/MOD/RIM container
Example 1: Simple UTI (Item Template) Modification
[GFFList]
File0=my_item.uti
[my_item.uti]
!Destination=override
; Modify existing fields
LocalizedName=strref50000
Description=strref50001
BaseItem=28
; Add new property
AddField0=new_property
[new_property]
FieldType=Struct
Path=PropertyList
Label=
TypeId=7
AddField0=subtype_field
AddField1=value_field
[subtype_field]
FieldType=Word
Path=
Label=Subtype
Value=15
[value_field]
FieldType=Int
Path=
Label=Value
Value=500Example 2: DLG (dialog file) with New Branches
[GFFList]
File0=my_dialog.dlg
[my_dialog.dlg]
!Destination=override
; Add new entry
AddField0=new_entry
[new_entry]
FieldType=Struct
Path=EntryList
Label=
TypeId=0
2DAMEMORY0=ListIndex
AddField0=speaker
AddField1=text
AddField2=replies
[speaker]
FieldType=ResRef
Path=
Label=Speaker
Value=npc_speaker
[text]
FieldType=ExoLocString
Path=
Label=Text
StrRef=-1
lang0=Welcome to my mod!
[replies]
FieldType=List
Path=
Label=RepliesList
AddField0=reply1
[reply1]
FieldType=Struct
Path=
Label=
TypeId=0
AddField0=reply_text
[reply_text]
FieldType=ExoLocString
Path=
Label=Text
StrRef=-1
lang0=Thank you!Example 3: UTC (creature template) with Dynamic data
[GFFList]
File0=new_creature.utc
[new_creature.utc]
!Destination=override
; Use 2DA memory token for appearance (set elsewhere)
AddField0=appearance_value
[appearance_value]
FieldType=Word
Path=
Label=Appearance_Type
Value=2DAMEMORY5
; Add localized name using TLK token
AddField0=name_field
[name_field]
FieldType=ExoLocString
Path=
Label=LocalizedName
StrRef=StrRef0
lang0=Custom CreatureExample 4: JRL (journal entry)
[GFFList]
File0=global.jrl
[global.jrl]
!Destination=override
; Add new category with ListIndex
AddField0=new_category
[new_category]
FieldType=Struct
Path=Categories
Label=
TypeId=ListIndex
AddField0=category_strref
[category_strref]
FieldType=ExoLocString
Path=
Label=Name
StrRef=StrRef100
lang0=My Custom Quests[GFFList]
File0=complex_item.uti
[complex_item.uti]
!Destination=override
; Add properties list with multiple entries
AddField0=properties_list
[properties_list]
FieldType=List
Path=
Label=PropertyList
AddField0=property1
AddField1=property2
AddField2=property3
[property1]
FieldType=Struct
Path=
Label=
TypeId=7
AddField0=prop_subtype
AddField1=prop_value
AddField2=prop_parameter1
AddField3=prop_parameter2
AddField4=prop_cost
[prop_subtype]
FieldType=Word
Path=
Label=Subtype
Value=12
[prop_value]
FieldType=Int
Path=
Label=Value
Value=10
[prop_parameter1]
FieldType=DWORD
Path=
Label=Parameter1
Value=0
[prop_parameter2]
FieldType=DWORD
Path=
Label=Parameter2
Value=0
[prop_cost]
FieldType=DWORD
Path=
Label=Cost
Value=0
[property2]
FieldType=Struct
Path=
Label=
TypeId=7
AddField0=prop_subtype2
AddField1=prop_value2
[prop_subtype2]
FieldType=Word
Path=
Label=Subtype
Value=25
[prop_value2]
FieldType=Int
Path=
Label=Value
Value=5
[property3]
FieldType=Struct
Path=
Label=
TypeId=7
AddField0=prop_subtype3
AddField1=prop_value3
[prop_subtype3]
FieldType=Word
Path=
Label=Subtype
Value=30
[prop_value3]
FieldType=Int
Path=
Label=Value
Value=15[GFFList]
File0=item.uti
[item.uti]
; Add struct to list, store index
AddField0=store_index_property
[store_index_property]
FieldType=Struct
Path=PropertyList
Label=
TypeId=7
2DAMEMORY0=ListIndex
; Later, use that index elsewhere
Index=2DAMEMORY0[GFFList]
File0=dialog.dlg
[dialog.dlg]
; Add entry with stored strref
AddField0=entry_with_strref
[entry_with_strref]
FieldType=Struct
Path=EntryList
Label=
TypeId=0
AddField0=localized_text
[localized_text]
FieldType=ExoLocString
Path=
Label=Text
StrRef=StrRef50
lang0=English text
lang2=French text
lang4=German text[GFFList]
File0=templates.uti
[templates.uti]
; Store value from 2DA
AddField0=from_2da
[from_2da]
FieldType=Word
Path=
Label=From2DA
Value=2DAMEMORY10
; Copy 2DA token
2DAMEMORY20=2DAMEMORY10
; Use copied token
AddField0=use_copied
[use_copied]
FieldType=Int
Path=
Label=Value
Value=2DAMEMORY20- Unique labels across each GFF hierarchy level
- Prefer memory tokens for dynamic values
- List indices start at 0
- Backslash path separators for nested paths
- Case-sensitive field names
- Verify file structure with a GFF editor
- Sort complex nested structures for clarity
- Use meaningful identifiers for AddField sections
Common mistakes:
- Modifying non-existent fields
- Invalid FieldType
- Missing required keys
- Circular memory references
- Invalid 2DAMEMORY/StrRef syntax
PyKotor validates configuration and logs errors during INI loading and patching.
Tested on:
- HoloPatcher
- TSLPatcher v1.2.10b
- PyKotor’s TSLPatcher implementation
- KotOR1/KotOR2 GFF files
field type compatibility:
- All standard types supported
- Nested structures supported
- Memory tokens supported
- Dynamic paths supported
- TSLPatcher Official Readme - Original TSLPatcher documentation
- HoloPatcher README for Mod Developers - HoloPatcher-specific features and improvements
-
pykotor/resource/formats/gff/gff_data.py- GFF data structure definitions -
pykotor/resource/formats/gff/io_gff.py- GFF file I/O implementation -
pykotor/tslpatcher/reader.py- INI configuration parsing (seeload_gff_list,add_field_gff,modify_field_gff) -
pykotor/tslpatcher/mods/gff.py- GFF modification logic (seeModificationsGFF,AddFieldGFF,ModifyFieldGFF) -
pykotor/tslpatcher/patcher.py- Main patcher execution flow
GFF file types
Common GFF-based file types you can modify:
-
.are- area files .dlg- Dialogs.git- Module instance files.ifo- Module info.jrl- Journal entries.pth- AI Pathing files.utc- Creature templates.utd- Doors.ute- Encounters.uti- Item templates.utm- Merchants.utp- Placeable templates.uts- Sounds.utt- Trigger templates.utw- Waypoint templates
-
1.2.10b (2007-09-19): Fixed ExoLocString substring linefeed handling (use
<#LF#>for newlines) - 1.2.9b (2007-08-13): Changed behavior when adding duplicate fields--now modifies existing field instead of skipping
- 1.2.8b10 (2006-12-10): Bug fixes for required file checks
-
1.2.8b6 (2006-10-03): Added
!OverrideTypesupport for ERF and RIM destinations -
1.2.7b9 (2006-07-23): Dynamic field paths - Added
!FieldPathsupport for storing and using field paths via2DAMEMORY#tokens - 1.2.7b4 (2006-05-11): Multiple setups support improvements
- 1.2.6b3 (2006-03-09): SSF soundset file modification support added
- 1.2a (2006-01-10): AddField support - Initial support for adding new fields to GFF files
- Binary field type: Support for adding/modifying Binary fields (not in classic TSLPatcher)
- GFF File Format -- GFF structure and field types
- TSLPatcher's Official Readme -- TSLPatcher overview and change log
- TSLPatcher HACKList Syntax -- 2DA and text patching
- HoloPatcher README for Mod Developers -- HoloPatcher usage
- Container-Formats#key -- Resource resolution and override order
- Community sources and archives -- DeadlyStream, LucasForums for GFF modding examples
For issues or questions, check PyKotor’s GitHub Issues or the KotOR modding forums.