TSLPatcher HACKList Syntax - NickHugi/PyKotor GitHub Wiki
The [HACKList] section in TSLPatcher's changes.ini file enables you to modify compiled NCS (Neverwinter Compiled Script) bytecode files directly at the binary level. This advanced feature allows precise byte-level modifications to script files without recompiling from NSS source code, making it ideal for:
- Patching numerical values in existing compiled scripts
- Injecting dynamically-generated string references (StrRefs) and 2DA memory values
- Performing surgical modifications to hardcoded constants
- Updating scripts to reference new TLK entries or 2DA row numbers
Important: HACKList is executed after [CompileList] during patcher execution, allowing compiled scripts to be modified after compilation if needed.
- Basic Structure
- File-Level Configuration
- Token Types and Data Sizes
- Memory Token Integration
- Offset Calculation
- Examples
- DeNCS Reference
- Common Use Cases
- Troubleshooting
[HACKList]
!DefaultDestination=override
!DefaultSourceFolder=. ; Note: `.` refers to the tslpatchdata folder (where changes.ini is located)
File0=myscript.ncs
Replace0=otherscript.ncs
[myscript.ncs]
!Destination=override
!SourceFile=source.ncs
!SaveAs=mypatched.ncs
ReplaceFile=0
; Byte-level modifications
0x15=12345
32=u16:2DAMEMORY1
64=u8:255
0x100=StrRef5
0x200=u32:2DAMEMORY10The [HACKList] section declares NCS files to modify. Each entry references another section with the same name as the filename.
| Key | Type | Default | Description |
|---|---|---|---|
!DefaultDestination |
string | override |
Default destination for all NCS files in this section |
!DefaultSourceFolder |
string | . |
Default source folder for NCS files. This is a relative path from mod_path, which is typically the tslpatchdata folder (the parent directory of the changes.ini file). The default value . refers to the tslpatchdata folder itself. Path resolution: mod_path / !DefaultSourceFolder / filename
|
Each NCS file requires its own section (e.g., [myscript.ncs]).
| 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 NCS 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 to load |
!SaveAs or !Filename
|
string | Same as section name | Final filename to save as |
ReplaceFile |
0/1 | 0 |
Note: Unlike other patch lists, HACKList uses ReplaceFile (without exclamation point) |
Destination Values:
-
overrideor empty: Save to the Override folder -
Modules\module.mod: Insert into an ERF/MOD/RIM archive - Use backslashes for path separators
Important: The ReplaceFile key in HACKList does NOT use an exclamation point prefix. This is unique to HACKList compared to other patch lists.
Each modification requires specifying an offset and a value. Values can include type specifiers to control data size.
offset=value
offset=type:value-
offset: Decimal number (e.g.,
32) or hexadecimal (e.g.,0x20) -
type (optional): One of
u8,u16, oru32to specify data width - value: Numeric value, token reference, or hex literal
| Value Format | Type | Size | Description |
|---|---|---|---|
| Numeric (no prefix) | u16 | 2 bytes | 16-bit unsigned integer (default) |
u8:123 |
u8 | 1 byte | 8-bit unsigned integer (0-255) |
u16:12345 |
u16 | 2 bytes | 16-bit unsigned integer (0-65535) |
u32:123456 |
u32 | 4 bytes | 32-bit unsigned integer |
StrRef0 |
strref | Varies* | Reference to TLK string from memory |
StrRefN |
strref32 | 4 bytes | 32-bit signed TLK reference (CONSTI) |
2DAMEMORY1 |
2damemory | Varies* | Reference to 2DA memory value |
2DAMEMORYN |
2damemory32 | 4 bytes | 32-bit signed 2DA reference (CONSTI) |
*strref and 2damemory without explicit sizes default to strref32 and 2damemory32 respectively in PyKotor's implementation.
All multi-byte values are written in big-endian (network byte order), which is standard for KOTOR's binary formats.
Historical Background: TSLPatcher originally distinguished between strref and strref32 (and 2damemory vs 2damemory32), but PyKotor's implementation unifies these:
-
StrRef#tokens are automatically handled as 32-bit values -
2DAMEMORY#tokens are automatically handled as 32-bit values
If you need legacy 16-bit compatibility, use explicit type specifiers like u16:StrRef5, though this is not typically necessary.
HACKList integrates seamlessly with TSLPatcher's memory token system, allowing dynamic value injection from other patch sections.
Reference values stored in TLKList memory:
; In TLKList section, this would define StrRef5
StrRef5=123456
; In HACKList, inject it into bytecode
[HACKList]
File0=myscript.ncs
[myscript.ncs]
; At offset 0x100, write the StrRef value
0x100=StrRef5Use Cases:
- Injecting dynamically-added dialog.tlk string references
- Patching scripts to reference custom text entries
- Updating hardcoded string IDs to mod-added entries
Reference values stored in 2DAList memory:
; In 2DAList section, this would store a row number
Add2DALine1=appearance.2da
[Add2DALine1]
2DAMEMORY1=RowIndex
; In HACKList, inject it into bytecode
[HACKList]
File0=myscript.ncs
[myscript.ncs]
; At offset 0x50, write the 2DA memory value
0x50=2DAMEMORY1Use Cases:
- Injecting dynamically-added 2DA row numbers
- Patching appearance/spell IDs to reference new rows
- Updating hardcoded IDs to mod-added entries
Important Limitation: !FieldPath values are NOT supported in HACKList. Only numeric memory values can be used.
Determining the correct byte offset is the most critical aspect of HACKList usage.
Byte Offset Description
----------- --------------------------------------------
0x00-0x03 File signature: "NCS " (ASCII)
0x04-0x07 Version: "V1.0" (ASCII)
0x08 Magic byte: 0x42
0x09-0x0C Total file size (4 bytes, big-endian)
0x0D+ Compiled bytecode instructions
The header is 13 bytes (0x0D), so the first instruction byte is at offset 0x0D.
DeNCS (Decompiler for NCS) is a Java-based disassembler that can help you locate exact byte offsets in NCS files.
- Load your NCS file in DeNCS
- Disassemble to view instruction-level operations
- Identify the target instruction and note its byte offset
- If modifying an instruction's operand, add to the instruction's offset:
- For CONSTI operands: offset + 1 (skip the opcode byte)
- For other operands: depends on instruction type
Offset Inst Args
------ ---- ----
0x0D NOP
0x0E CONSTI 10000
(opcode at 0x0E, operand at 0x0F-0x12)
0x13 CPDOWNSP -4
0x15 CONSTS "Hello World"
(opcode at 0x15, string offset at 0x16-0x19)
To modify the CONSTI value at 0x0E, you'd patch bytes 0x0F-0x12.
| Instruction | Opcode Size | Operand Size | Example Offset to Patch |
|---|---|---|---|
CONSTI |
1 byte | 4 bytes | offset + 1 |
CONSTF |
1 byte | 4 bytes | offset + 1 |
CONSTS |
1 byte | 4 bytes | offset + 1 |
CPDOWNSP |
1 byte | 4 bytes | offset + 1 |
ACTION |
1 byte | 4 bytes | offset + 1 |
JMP |
1 byte | 4 bytes | offset + 1 |
JZ |
1 byte | 4 bytes | offset + 1 |
Both formats are supported:
-
Hexadecimal:
0x20,0x100,0xFF -
Decimal:
32,256,255
Use hexadecimal for convenience when working with byte-aligned operations.
Replace a hardcoded constant in a compiled script:
[HACKList]
File0=combat_script.ncs
[combat_script.ncs]
; At offset 0x50, change a damage value from 10 to 50
0x50=u16:50Inject a dynamically-added string reference:
[TLKList]
StrRef1=My New Dialog Entry
[HACKList]
File0=dialog_script.ncs
[dialog_script.ncs]
; At offset 0x100, inject the StrRef value
0x100=StrRef1Modify several offsets in the same file:
[HACKList]
File0=spell_script.ncs
[spell_script.ncs]
; Patch spell ID at 0x30
0x30=u16:123
; Patch damage amount at 0x50
0x50=u32:999
; Patch duration at 0x70
0x70=u16:60Inject a dynamically-added 2DA row number:
[2DAList]
Add2DALine1=spells.2da
[Add2DALine1]
2DAMEMORY5=RowIndex
[HACKList]
File0=spell_handler.ncs
[spell_handler.ncs]
; Inject the row number at offset 0x88
0x88=2DAMEMORY5Combine different data sizes and token types:
[HACKList]
File0=complex_script.ncs
Replace0=old_script.ncs
[complex_script.ncs]
ReplaceFile=1
; 8-bit flag value
0x20=u8:1
; 16-bit numeric literal
0x30=u16:4096
; 32-bit StrRef from memory
0x40=StrRef10
; 32-bit 2DA memory reference
0x50=2DAMEMORY3
; Direct hex value
0x60=u32:0xDEADBEEFSave modified scripts to a module archive:
[HACKList]
!DefaultDestination=Modules\mymod.mod
File0=modified_script.ncs
[modified_script.ncs]
!Destination=Modules\mymod.mod
ReplaceFile=1
; Multiple modifications
0x50=u16:100
0x60=StrRef5DeNCS provides comprehensive NCS disassembly capabilities for locating exact byte offsets. Understanding its output is essential for HACKList usage.
- Instruction-level disassembly: See each bytecode instruction
- Offset mapping: Exact byte positions for each instruction
- Operand extraction: View data embedded in instructions
- Jump resolution: Understand control flow
Offset Instruction Args
------ ------------- ----
0x0D NOP
0x0E CONSTI 0x00002710 (10000)
0x13 CPDOWNSP -4
0x15 CONSTI 0x00000064 (100)
0x1A CPDOWNSP -4
0x1B ACTION 0x00401048 (AddObjectToInventory)
0x20 MOVSP -4
0x22 RETN
Stack:
Before NOP: []
After NOP: []
...
To modify the CONSTI at 0x0E, you'd patch bytes 0x0F-0x12 (the 4-byte operand).
Many scripts follow predictable patterns you can target:
Setting a constant value:
CONSTI <value>
CPDOWNSP -4
This pushes a 4-byte integer onto the stack. The value is at offset +1.
Calling a function:
ACTION <function_pointer>
The function pointer is a 4-byte address at offset +1.
Conditional jumps:
JZ <offset>
The jump offset is a 4-byte signed integer at offset +1.
Many vanilla scripts have hardcoded StrRef values. HACKList lets you redirect them to mod-added entries:
[TLKList]
; Add your custom string
StrRef99=New Dialog Text
[HACKList]
File0=old_dialog.ncs
[old_dialog.ncs]
; Replace hardcoded StrRef 12345 with your new one
0x100=StrRef99When adding new spells or items, existing scripts may need to reference them:
[2DAList]
Add2DALine1=spells.2da
[Add2DALine1]
2DAMEMORY7=RowIndex
[HACKList]
File0=spell_handler.ncs
[spell_handler.ncs]
; Inject the new spell's row number
0x88=2DAMEMORY7Modify damage, duration, or other gameplay values without recompiling:
[HACKList]
File0=combat_init.ncs
[combat_init.ncs]
; Change damage from 10 to 50
0x30=u16:50
; Change duration from 30 to 60 seconds
0x50=u16:60
; Change cooldown from 5 to 10 seconds
0x70=u16:10Some scripts have debug flags that can be enabled:
[HACKList]
File0=debug_script.ncs
[debug_script.ncs]
; Enable debug flag (assuming it's a boolean at 0x20)
0x20=u8:1Patch bugs in vanilla scripts without distributing modified source:
[HACKList]
File0=buggy_script.ncs
[buggy_script.ncs]
; Fix incorrect check value
0x50=u16:1
; Fix incorrect comparison
0x70=u32:1000Problem: Patched value doesn't seem to take effect
Solutions:
- Verify the offset using DeNCS
- Check if you're modifying the correct bytes (instruction vs operand)
- Ensure you're not overwriting opcodes accidentally
- Verify big-endian byte order for multi-byte values
Problem: StrRefN was not defined before use
Solutions:
- Ensure the token is defined in TLKList/2DAList before HACKList execution
- Check the token number for typos
- Verify token definition syntax in the appropriate section
Important: HACKList executes after CompileList and after TLKList and 2DAList in HoloPatcher, so memory tokens should be available.
Problem: Script crashes or behaves unexpectedly after patching
Solutions:
- Verify you're using the correct data size (u8/u16/u32)
- Check DeNCS output to confirm operand size
- Ensure you're not truncating large values with u8/u16
- Verify signed vs unsigned behavior for large values
Problem: File not found error during patching
Solutions:
- Verify
!SourceFilepoints to correct filename - Check
!DefaultSourceFolderand!SourceFolderpaths - Ensure source file exists in tslpatchdata folder
- Verify file extension is
.ncs
Problem: Modified script not appearing in ERF/MOD/RIM archive
Solutions:
- Verify
!Destinationpath uses backslashes - Check archive exists before insertion
- Ensure destination folder structure is correct
- Verify
ReplaceFilesetting (0 = skip if exists, 1 = overwrite)
HoloPatcher processes patch lists in this order:
- InstallList (install files)
- TLKList (add dialog entries)
- 2DAList (modify 2DA files)
- GFFList (modify GFF files)
- CompileList (compile NSS to NCS)
- HACKList (modify NCS bytecode) ← You are here
- SSFList (modify soundset files)
Important: This differs from TSLPatcher's original order, where HACKList executes before CompileList. HoloPatcher runs CompileList first to allow scripts to be compiled and then potentially edited. This order change is intentional and should not affect mod compatibility in practice.
All memory tokens from TLKList and 2DAList are available during HACKList processing.
All multi-byte values are written in big-endian format:
-
u16:0x1234writes12 34 -
u32:0x12345678writes12 34 56 78 - Bytes are written from most significant to least significant
Unlike other patch lists, HACKList's ReplaceFile key does not use an exclamation point:
; CORRECT (HACKList syntax)
ReplaceFile=1
; INCORRECT (this is for other sections)
!ReplaceFile=1ReplaceFile=0 means "skip if file exists", while ReplaceFile=1 means "overwrite existing file".
I have no idea why this is the exclusive instance of Stoffe's variables that doesn't use exclamation-point syntax but whatever.
- PyKotor's HACKList implementation is compatible with TSLPatcher v1.2.10b+
- All NCS versions V1.0 are supported
- Archive insertion works for ERF, MOD, and RIM formats
- Memory tokens from TLKList and 2DAList are fully supported
-
!FieldPathis not supported (only numeric values)
- TSLPatcher-GFFList-Syntax.md - GFF file modifications
- TSLPatcher's Official Readme - General TSLPatcher documentation
- DeNCS Documentation - NCS disassembler
- NCS Instruction Reference - Detailed bytecode documentation
When working with NCS bytecode, be aware of alignment requirements:
- Instructions start on any byte boundary (no alignment enforced)
- Operands follow immediately after opcodes
- Multi-byte values are written as-is without padding
Important: HACKList can only modify existing bytes. It cannot:
- Insert new bytes (files would shift offsets)
- Delete bytes (files would shrink)
- Resize instruction arrays
For structural changes, use CompileList to recompile from NSS source.
Enable verbose logging to see HACKList operations:
[Settings]
LogLevel=4This will show detailed output like:
Loading [HACKList] patches from ini...
HACKList myscript.ncs: seeking to offset 0x20
HACKList myscript.ncs: writing unsigned WORD (16-bit) 12345 at offset 0x20
HACKList modifications are very fast since they're simple binary writes. However:
- Large files with many patches may take slightly longer
- Archive insertion requires archive rewriting
- Always test thoroughly as byte-level modifications can break scripts
Never patch untrusted NCS files without verifying their contents. Malicious bytecode modifications could:
- Execute arbitrary code
- Corrupt save games
- Crash the game
- Exploit vulnerabilities
Always validate offsets and values before distribution.
HACKList provides powerful byte-level control over compiled NCS scripts, enabling surgical modifications without source code access. While it requires understanding NCS bytecode structure and careful offset calculation, it's essential for advanced modding scenarios involving dynamic value injection and hardcoded constant patching.
For most modding needs, CompileList (NSS source compilation) is preferred. HACKList should be reserved for cases where source code is unavailable or where byte-level precision is required.