TSLPatcher HACKList Syntax - OpenKotOR/PyKotor GitHub Wiki
TSLPatcher HACKList Syntax Documentation
This guide explains how to modify NCS files directly using TSLPatcher syntax. For the complete NCS file format specification, see NCS File Format. For general TSLPatcher information, see TSLPatcher's Official Readme. For HoloPatcher-specific information, see HoloPatcher README for Mod Developers.
Overview
The [HACKList] section in TSLPatcher's changes.ini file enables you to modify compiled NCS files (NWScript bytecode, historically called "Neverwinter Compiled Script" but used identically in KotOR) 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:
In PyKotor's current implementation, HACKList is a first-class emitted section: TSLPatcherINISerializer.serialize() writes it explicitly via _serialize_hack_list() after the earlier data and structure passes, but TSLPatchDataGenerator.generate_all_files() does not synthesize NCS payloads the way it does TLK, 2DA, GFF, or SSF resources. In practice that means HACKList normally operates on compiled scripts that already exist in tslpatchdata, are copied in as install assets, or are produced by the compile-stage workflow before the binary patch pass runs [TSLPatcherINISerializer.serialize(), _serialize_hack_list, TSLPatchDataGenerator.generate_all_files(), Deadly Stream TSLPatcher page].
- Patching numerical values in existing compiled scripts
- Injecting dynamically-generated string references (StrRef) and 2DA memory values (2DAMEMORY)
- Performing surgical modifications to hardcoded constants
- Updating scripts to reference new TLK entries or 2DA row numbers/indexes
Important: HACKList is executed after [CompileList] during patcher execution, allowing compiled scripts to be modified after compilation if needed.
Table of Contents
- Overview
- Basic Structure
- file-Level Configuration
- Supported Value Syntaxes
- Modifying Byte Sequences
- File and Offset Resolving
- Usage Examples
- Best Practices & Cautions
- Troubleshooting
- See Also
Basic Structure
[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:2DAMEMORY10
The [HACKList] section declares NCS files to modify. Each entry references another section with the same name as the filename.
file-Level Configuration
Top-Level Keys in [HACKList]
| 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 |
file Section Configuration
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 folderModules\module.mod: Insert into an ERF/MOD/RIM container- 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.
Token Types and Data Sizes Syntax
Each modification requires specifying an offset and a value. values can include type specifiers to control data size.
Syntax
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
Supported value types
| 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.
Endianness
All multi-byte values are written in big-endian (network byte order), which is standard for KOTOR's binary formats.
type Compatibility Notes
Historical background: TSLPatcher originally distinguished between strref and strref32 (and 2damemory vs 2damemory32), but PyKotor's implementation unifies these:
[StrRef](Audio-and-Localization-Formats#string-references-strref)#tokens are automatically handled as 32-bit values2DAMEMORY#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.
Memory Token Integration Syntax
[HACKList] section integrates seamlessly with TSLPatcher's memory token system, allowing dynamic value injection from other patch sections.
StrRef Tokens
Reference values stored in [TLKList] section 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=StrRef5
Use cases:
- Injecting dynamically-added dialog.tlk string references
- Patching scripts to reference custom text entries
- Updating hardcoded string IDs to mod-added entries
2DA Memory Tokens (2DAMEMORY#)
Reference values stored in [2DAList] section memory tokens:
; 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=2DAMEMORY1
Use 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.
Offset Calculation Syntax
Determining the correct byte offset is the most critical aspect of [HACKList] section usage.
NCS files structure (NCS file)
The structure of a NCS file is as follows:
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.
Finding offsets with DeNCS
DeNCS (Decompiler for NCS) is a Java-based disassembler that can help you locate exact byte offsets in NCS files.
Using DeNCS
- 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
Example Disassembly
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.
Common Instruction Opcode Layouts
| 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 |
Hex vs Decimal Offsets
Both formats are supported:
- Hexadecimal:
0x20,0x100,0xFF - Decimal:
32,256,255
Use hexadecimal for convenience when working with byte-aligned operations (e.g. 0x50 is 80 in decimal).
Examples Syntax
Example 1: Modifying a Hardcoded Integer
Replace a hardcoded constant in a NCS compiled script:
[HACKList]
File0=combat_script.ncs
[combat_script.ncs]
; At offset 0x50, change a damage value from 10 to 50
0x50=u16:50
Example 2: Injecting Dynamic TLK Reference
Inject a dynamically-added StrRef value:
[TLKList]
StrRef1=My New Dialog Entry
[HACKList]
File0=dialog_script.ncs
[dialog_script.ncs]
; At offset 0x100, inject the StrRef value
0x100=StrRef1
Example 3: Patching Multiple Values
Modify 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:60
Example 4: Using 2DA Memory Values
Inject a dynamically-added 2DA row index:
[2DAList]
Add2DALine1=spells.2da
[Add2DALine1]
2DAMEMORY5=RowIndex
[HACKList]
File0=spell_handler.ncs
[spell_handler.ncs]
; Inject the row number at offset 0x88
0x88=2DAMEMORY5
Example 5: Advanced Multi-Type Patching
Combine different data sizes and token/value 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:0xDEADBEEF
Example 6: Saving to Container (!DefaultDestination)
Save modified scripts to a module container:
[HACKList]
!DefaultDestination=Modules\mymod.mod
File0=modified_script.ncs
[modified_script.ncs]
!Destination=Modules\mymod.mod
ReplaceFile=1
; Multiple modifications
0x50=u16:100
0x60=StrRef5
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=StrRef99
2. Patching Spell/Item Row IDs/Indexes
When adding new spells or items, existing scripts may need to reference them by row index or label:
[2DAList]
Add2DALine1=spells.2da
[Add2DALine1]
2DAMEMORY7=RowIndex
[HACKList]
File0=spell_handler.ncs
[spell_handler.ncs]
; Inject the new spell's row number
0x88=2DAMEMORY7
3. Adjusting Combat Values
Modify damage, duration, or other gameplay values without recompiling (e.g. 0x30=u16:50 is the damage value, 0x50=u16:60 is the duration value, 0x70=u16:10 is the cooldown value) in a NCS compiled script:
[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:10
4. Enabling Debug Flags
Some scripts have debug flags that can be enabled (e.g. 0x20=u8:1 is the debug flag):
[HACKList]
File0=debug_script.ncs
[debug_script.ncs]
; Enable debug flag (assuming it's a boolean at 0x20)
0x20=u8:1
5. Fixing Known Bugs
Patch 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:1000
Troubleshooting Syntax
Offset Calculation Errors
Problem: The 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) (e.g.
CONSTIis an opcode,-4is an operand) - Ensure you're not overwriting opcodes accidentally (e.g.
CONSTIis an opcode, not a value, so0x0E=u16:100is incorrect, it should be0x0E=u16:100) - Verify big-endian byte order for multi-byte values (e.g.
u16:0x1234is0x12 0x34, not0x34 0x12)
Memory Token Not Defined (StrRefN/2DAMEMORYN)
Problem: StrRefN/2DAMEMORYN was not defined before use.
Solutions:
- Ensure the token is defined in
[TLKList]/[2DAList]before[HACKList]execution - Check the token number for typos (e.g.
StrRef1instead ofStrRef10) - 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.
Wrong Data Size
Problem: Script crashes or behaves unexpectedly after patching.
Solutions:
- Verify you're using the correct data size (
u8/u16/u32/StrRefN/2DAMEMORYN) - Check DeNCS output to confirm the operand size
- Ensure you're not truncating large values with
u8/u16 - Verify signed vs unsigned behavior for large values (e.g.
u32:0x80000000is a negative number)
File Not Found (!SourceFile)
Problem: File not found error during patching.
Solutions:
- Verify
!SourceFilepoints to the correct filename (e.g.source.ncs) - Check
!DefaultSourceFolderand!SourceFolderpaths (e.g.tslpatchdata\source.ncs) - Ensure the source file exists in the tslpatchdata folder (e.g.
tslpatchdata\source.ncs) - Verify the file extension is
.ncs
Archival Insertion Issues (!Destination)
Problem: Modified script not appearing in ERF/MOD/RIM container.
Solutions:
- Verify
!Destinationpath uses backslashes - Check the bioware container exists before insertion (e.g.
Modules\mymod.mod) - Ensure the destination folder structure is correct
- Verify the
ReplaceFilesetting (0= skip if exists,1= overwrite) (0= skip if the file exists,1= overwrite the existing file)
Technical Details
Execution Order
HoloPatcher processes patch lists in this order:
[InstallList](install files)[TLKList](add TLK text or sound entries or create StrRef memory tokens)[2DAList](modify 2DA files or create 2DAMEMORY memory tokens)[GFFList](modify GFF files or create 2DAMEMORY memory tokens from!FieldPath)[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 edited if needed.
All memory tokens from [TLKList] and [2DAList] are available during [HACKList] processing.
Byte-Level Writing
All multi-byte values are written in big-endian format:
u16:0x1234writes12 34u32:0x12345678writes12 34 56 78- Bytes are written from most significant to least significant
ReplaceFile Key Behavior
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=1
ReplaceFile=0 means "skip if file exists", while ReplaceFile=1 means "overwrite existing file".
This non-prefixed ReplaceFile spelling is a legacy HACKList syntax quirk preserved for compatibility.
Compatibility Notes
- PyKotor's
[HACKList]implementation is compatible with TSLPatcher v1.2.10b+ - All NCS versions V1.0 are supported
- Container insertion works for ERF, MOD, and RIM formats
- Memory tokens from TLKList and 2DAList are fully supported
!FieldPathis not supported (only numeric values)
See also
- TSLPatcher GFFList Syntax - GFF modifications
- TSLPatcher's Official Readme - General TSLPatcher documentation
- DeNCS Documentation - NCS disassembler
- NCS Instruction Reference - Detailed bytecode documentation
- Community sources and archives -- DeadlyStream, LucasForums for
[HACKList]/binary patching
Advanced Topics
Offset Alignment
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
Inserting vs Modifying
Important: [HACKList] can only modify existing bytes. It cannot:
- Insert new bytes (files would shift offsets)
- Delete bytes (files would shrink)
- Resize instruction arrays
It can only overwrite existing bytes.
For structural changes, use [CompileList] to recompile from NSS source.
Debugging Tips
Enable verbose logging to see [HACKList] operations:
[Settings]
LogLevel=4
This 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
Conclusion
[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.