TSLPatcher GFFList Syntax - OpenKotOR/PyKotor GitHub Wiki

TSLPatcher GFFList Syntax Documentation

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.

Overview

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).

Table of Contents

Quick Start

  1. Add your file under [GFFList]
[GFFList]
File0=my_item.uti
  1. 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
  1. 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
  1. 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
  1. Use tokens when a value comes from earlier steps:

ModelVariation=2DAMEMORY5
Description=StrRef10

That's it. The rest of this page explains the knobs and dials you'll use as your files get more complex.

Cheatsheet

  • 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:
    • [StrRef](Audio-and-Localization-Formats#string-references-strref)# --> a TLK token you set elsewhere
    • 2DAMEMORY# --> a 2DA token you set elsewhere
  • Tokens for dynamic field targets and list indices:
    • In AddField: 2DAMEMORY#=ListIndex saves where a struct was inserted
    • 2DAMEMORY#=!FieldPath saves the full path to a field you just added
    • Later: use that 2DAMEMORY# in place of a field path to modify it

Basic structure

[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=2DAMEMORY2

The [GFFList] section declares GFF (Generic File Format) files to patch. Each entry references another section with the same name as the filename.

file-Level Configuration

Top-Level Keys in [GFFList]

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

file Section Configuration

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:

  • override or 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:

  1. If !ReplaceFile=1 or file doesn't exist at destination: Load from mod_path / !SourceFolder / !SourceFile (or section name if !SourceFile not set)
  2. Otherwise: Load existing file from destination location (override or container)
  3. Apply all modifications from the section
  4. Save to !Destination with name !SaveAs (or section name if !SaveAs not set)

Modifying Existing Fields

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

Field Path Syntax

  • 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) through FieldName(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

Adding New Fields

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

AddField Section Structure

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.

Understanding Struct vs. Field Addition

There are two distinct scenarios when using AddField:

  1. Adding a STRUCT to a LIST: When you want to add a new element to an existing LIST field

    • FieldType=Struct is 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
  2. Adding a field to a STRUCT: When you want to add any field type (including structs) inside an existing or newly-created STRUCT

    • Any FieldType is 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

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=42

Path Inheritance for Nested Fields

When 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=42

Path 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:

  1. When you add a STRUCT to a LIST with Path=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.)
  2. When child AddField sections have Path= 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}
  3. You can override inheritance by explicitly setting Path= in the child section

Complete Examples: Adding to Lists vs. Adding to Structs

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=500

Example 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=42

Example 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!

Overriding Path Inheritance

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 Types and Value Syntax

Integer Types

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

Float Types

field type size Precision Example
Float (Single) 32-bit ~7 digits Value=3.14159
Double 64-bit ~15 digits Value=2.718281828

String and Resource Types

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)

Complex Types

ExoLocString (Localized String)

[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 (Vector3 - Three-dimensional Vector)

[position_field]
FieldType=Position
Path=
Label=Location
Value=1.5|2.0|3.0

Three float coordinates (X, Y, Z) separated by |.

Orientation (Vector4 - Four-dimensional Vector)

[orientation_field]
FieldType=Orientation
Path=
Label=Rotation
Value=0.0|0.0|0.0|1.0

Four float components (quaternion) separated by |.

Struct (Struct - A collection of fields)

[struct_field]
FieldType=Struct
Path=
Label=MyStruct
TypeId=123
AddField0=nested_field
  • TypeId: Numeric type ID, ListIndex, StrRef#, or 2DAMEMORY#
  • AddFieldN: Child fields

List (List - A collection of elements)

[list_field]
FieldType=List
Path=
Label=MyList
AddField0=first_entry
AddField1=second_entry
  • Contains AddFieldN entries for each element
  • Elements are typically STRUCTs without labels

Binary (Binary - Binary data)

Note: Only supported by HoloPatcher.

[binary_field]
FieldType=Binary
Path=
Label=BinaryData
Value=0xFF00FF00

Supported formats (auto-detected):

  1. Binary string: Value=10101010 (sequence of 0s and 1s, processed in 8-bit chunks)
  2. Hex string: Value=0xFF00FF00 or Value=FF00FF00 (hexadecimal, even length required; 0x prefix optional)
  3. 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.

Language/Gender IDs for CExoLocString (Localized String)

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

Memory Token System

2DAMEMORY Tokens

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=ListIndex

2DAMEMORY# usage:

  1. At file level:

    • 2DAMEMORY#=!FieldPath - Store absolute field path
    • 2DAMEMORY#=2DAMEMORY# - Copy token value
    • Numeric ranges and 2DA operations not supported
  2. 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

Using Memory Tokens as values

; 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=2DAMEMORY10

Nested structures

Adding STRUCTs to Existing LISTs

When 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 - LIST elements 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= inherit RepliesList\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 2DAMEMORY0

Using 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 Quests

Complete 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 value

Key rules summary:

  1. Adding STRUCT to LIST: Label= must be blank, Path= points to list name.
  2. Adding a field to STRUCT: Label= is required, Path= can be empty (inherits) or explicit.
  3. Path inheritance: Child fields with empty Path= automatically inherit the parent's resolved path, including ListIndex values.
  4. ListIndex resolution: Happens automatically at runtime based on insertion order.
  5. TypeId=ListIndex: Auto-sets TypeId to match the ListIndex.
  6. 2DAMEMORY#=ListIndex: Stores the index for later use in cross-references or calculations.

Adding Complete Nested structures

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=123

Step-by-step breakdown:

  1. item_properties creates a new LIST field named "PropertyList" at root level
  2. property_struct adds a STRUCT element to that LIST (hence blank Label=)
  3. property_subtype and property_value add fields inside the STRUCT (hence required Label=)

This pattern is common when adding new property lists, dialog entries, journal categories, and similar list-based structures.

Special Features

Dynamic field Paths (2DAMEMORY# with !FieldPath)

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 text

Using 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 field

Copying field paths between tokens:

; Copy a field path from one token to another
2DAMEMORY1=2DAMEMORY0         ; Copy the path stored in 2DAMEMORY0 to 2DAMEMORY1

Use 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.

Using ListIndex for TypeId

Some GFF structures require the STRUCT's TypeId to match its position (index) in the LIST. This is common in:

  • Journal category lists (.jrl files)
  • 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 Quests

How it works:

  • When TypeId=ListIndex is specified, the patcher automatically determines the index where the STRUCT is added
  • The type ID is set to that numeric index (0, 1, 2, etc.)
  • For example, if the STRUCT becomes 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.

Common Pitfalls and Troubleshooting

Field and Path Issues

  • Case sensitivity: Field names are case-sensitive. Commentscomments. 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 when FieldType=Struct and adding that STRUCT to a LIST. All other field types require a label, including fields added inside STRUCTs that are themselves in LISTs.
  • Adding to list vs adding to struct: Confusing these two operations is a common mistake:
    • Adding STRUCT to LIST: Label= blank, Path= points to list name
    • Adding field to STRUCT: Label= required, Path= can be empty (inherits)
  • Path inheritance confusion: Remember that when adding a STRUCT to a LIST, child fields with empty Path= inherit the resolved path including the ListIndex. You don't need to (and shouldn't) manually specify the index.
  • Container fields: Don't assign Value= to STRUCT or LIST fields—they are containers. Set values in their child fields instead.
  • List index resolution: The index where a STRUCT is added is automatically determined at runtime. You cannot manually set or predict the exact index ahead of time if other mods might add STRUCTs to the same LIST.

Localized String Syntax

  • StrRef vs lang#: Use FieldName(strref)=... for the StrRef value, and FieldName(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 in lang# values, not literal newlines.

Memory Tokens

  • Token initialization: Tokens must be set before use. Using 2DAMEMORY5 before assignment results in an error.
  • Execution order: Within GFFList, AddField sections run before field modifications. Store !FieldPath when 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.

Path and Source Configuration

  • Source folder resolution: . refers to the tslpatchdata folder (where changes.ini is located). Don't explicitly specify tslpatchdata in 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 use Path=\ or Path=..

Field Type Issues

  • 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 AddField to create new fields first.
  • Binary type: The Binary field type is HoloPatcher-only and not supported by classic TSLPatcher.

Common Error Messages

  • "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 AddField instead)
  • "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

Debugging Tips

  • Enable verbose logging (LogLevel=4) to see detailed path resolution and token assignments
  • Verify field paths using a GFF editor before writing patches
  • Test AddField sections before adding modifications that depend on them
  • Check memorytoken assignments match between [2DAList]/[TLKList] and [GFFList] sections

Execution Order and Dependencies

Understanding execution order is crucial when your edits depend on earlier tokens or dynamically created fields.

Classic TSLPatcher execution order:

  1. TLKList: Appends entries to dialog.tlk, creates [StrRef](Audio-and-Localization-Formats#string-references-strref)# tokens
  2. InstallList: Copies files to destination (ERF or RIM containers may be created here)
  3. 2DAList: Modifies 2DA files, creates 2DAMEMORY# tokens
  4. GFFList: Modifies GFF files (can use [StrRef](Audio-and-Localization-Formats#string-references-strref)# and 2DAMEMORY# tokens)
  5. CompileList: Preprocesses NSS scripts (replaces #[StrRef](Audio-and-Localization-Formats#string-references-strref)# and #2DAMEMORY# tokens), then compiles
  6. HACKList: Applies binary patches to NCS files
  7. 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:

  1. AddField sections are processed first (fields are created)
  2. Memory assignments (2DAMEMORY#=!FieldPath, 2DAMEMORY#=ListIndex) are evaluated as fields are added
  3. 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 !FieldPath assignments
  • If multiple GFF files reference the same memory tokens, they can share StrRef# and 2DAMEMORY# values across files
  • The !OverrideType setting controls behavior when a file exists in Override but you're patching into an ERF/MOD/RIM container

Complete Examples

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=500

Example 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 Creature
[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

Example 5: Complex Nested Structure

[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

Common Patterns

Pattern 1: Store and Reference ListIndex

[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

Pattern 2: Dynamic Localized Strings

[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

Pattern 3: Cross-Reference Memory Tokens

[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

Best Practices

  1. Unique labels across each GFF hierarchy level
  2. Prefer memory tokens for dynamic values
  3. List indices start at 0
  4. Backslash path separators for nested paths
  5. Case-sensitive field names
  6. Verify file structure with a GFF editor
  7. Sort complex nested structures for clarity
  8. Use meaningful identifiers for AddField sections

Error Handling

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.

Compatibility

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

Additional Resources

Documentation

Source Code References

  • 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 (see load_gff_list, add_field_gff, modify_field_gff)
  • pykotor/tslpatcher/mods/gff.py - GFF modification logic (see ModificationsGFF, AddFieldGFF, ModifyFieldGFF)
  • pykotor/tslpatcher/patcher.py - Main patcher execution flow

GFF file types

Common GFF-based file types you can modify:

Version History

TSLPatcher Versions

  • 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 !OverrideType support for ERF and RIM destinations
  • 1.2.7b9 (2006-07-23): Dynamic field paths - Added !FieldPath support for storing and using field paths via 2DAMEMORY# 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

HoloPatcher Extensions

  • Binary field type: Support for adding/modifying Binary fields (not in classic TSLPatcher)

See also


For issues or questions, check PyKotor’s GitHub Issues or the KotOR modding forums.

⚠️ **GitHub.com Fallback** ⚠️