NCS File Format - OpenKotOR/PyKotor GitHub Wiki
NCS โ Compiled Script Bytecode
NCS files contain compiled NWScript bytecode โ the executable form of the scripting language that drives game logic in Knights of the Old Republic and The Sith Lords (NCS L254, xoreos ncsfile.cpp). Scripts control dialogue branching, trigger responses, combat behavior, cutscene sequencing, and virtually every dynamic system in the game. The bytecode runs inside a stack-based virtual machine inherited from the Aurora engine (NCSInstruction L637, reone ncsreader.cpp L28, KotOR.js NWScriptInstance.ts L32), where the "server" context (game world simulation) executes scripts and the client handles display and input.
The format is shared across Aurora engine games (Neverwinter Nights, Jade Empire, etc.), though KotOR adds game-specific action routines (NCSBinaryReader.load L56, xoreos-tools ncsfile.cpp, NorthernLights NCSReader.cs, Kotor.NET NCS.cs L9). The sections below focus on KotOR-specific behavior. Scripts are compiled from NSS source code and triggered by script hooks on GFF resources โ DLG dialogue files, GIT area instances, UTC creatures, UTD doors, UTP placeables, and IFO module definitions. Like all resources, NCS files are resolved through the standard resource resolution order (override -> MOD/SAV -> KEY/BIF).
File Structure Overview
| Offset | Size | Description |
|---|---|---|
| 0 (0x00) | 4 | Signature "NCS " |
| 4 (0x04) | 4 | Version "V1.0" |
| 8 (0x08) | 1 | Program size marker opcode (0x42) |
| 9 (0x09) | 4 | Total file size (big-endian UInt32) |
| 13 (0x0D)+ | -- | Stream of bytecode instructions |
- The VM executes sequential instructions; control-flow opcodes (
JMP,JZ,JSR) adjust the instruction pointer. - KotOR introduces no custom container sections--scripts are a flat stream.
All major reverse-engineered engines decode the same structure: reone, xoreos, and NorthernLights, among others catalogued on Home. KotOR.js runs bytecode through its own NWScript stack machine viaNWScriptInstance.tsL32+ andNWScriptStack.tsL31+, with per-opcode byte layout matching native VMs. - The program size marker at offset 8 (
0x42) is not a real instruction but a metadata field containing the total file size. Execution begins at offset 13 (0x0D) after the header; the complete instruction set is specified in the Torlack NWScript spec (xoreos-docsspecs/torlack/ncs.html) and is independently verified by PyKotor (NCSBinaryReader.loadL56+), xoreosncsfile.cppL342โL350, xoreos-toolsncsfile.cppL116โL125, reonencsreader.cppL28โL40, and Kotor.NETNCS.csL9+.
Stack-Based Virtual Machine
NWScript uses a stack-based VM where all operations work on a stack rather than CPU registers. Stack grows downward with negative offsets, 4-byte aligned elements.
Stack pointer (SP): SP = (stackPtr + 1) * -4. Stack positions are negative multiples of 4 (e.g., -4, -8, -12).
Stack layout example:
graph TD
SP[SP (top)] --> S1["-4: j: 1"]
S1 --> S2["-8: i: 12"]
S2 --> S3["-12: (previous frame)"]
/*
SPpoints to the topmost value on the stack.- Stack grows downward (from higher to lower negatives).
- Each node shows the stack offset, variable name, and value. */
Stack position calculations:
- Byte offset to position:
-offset / 4(e.g., -12 --> position 3) - Byte size to elements:
size / 4(e.g., 12 bytes --> 3 elements)
Global variables: accessed via base pointer (BP). The #globals routine initializes globals before main(), then SAVEBP saves current SP as BP. Functions access globals via CPTOPBP/CPDOWNBP. RESTOREBP restores previous BP.
Stack entry types:
- Constants: Immutable values (
CONSTx) read from instructions -- int, float, string, object - Variables: Assignable stack slots created via
RSADDx, modified viaCPDOWNSP/CPDOWNBP - structures: Composite types spanning multiple positions (vectors = 3 positions/12 bytes, custom = 4-Byte multiples)
Lifecycle: Create (CONSTx, RSADDx, CPTOPSP) --> Modify (CPDOWNSP, INCxSP) --> Consume (operations, MOVSP) --> Destroy (DESTRUCT)
Decompilation tracking:
-
Reference Counting: Track variable usage per stack instance
-
Assignment Status: Distinguish initialized vs uninitialized variables
-
type Inference: Infer types through operation chains
-
structure Recognition: 12-Byte copies --> vectors (z, y, x order), other multiples of 4 --> custom structures
-
Variable Naming: Generate names from type + position or infer from usage patterns
-
xoreos
ncsfile.cppL105โL172 (SP/BP) -
xoreos L389โL394 (globals)
-
xoreos L1039โL1060 (SAVEBP/RESTOREBP)
Header
| Name | type | offset | size | Description |
|---|---|---|---|---|
| file type | char | 0 (0x00) | 4 | "NCS " (must match exactly, ASCII bytes: 0x4E 0x43 0x53 0x20) |
| file Version | char | 4 (0x04) | 4 | "V1.0" (must match exactly, ASCII bytes: 0x56 0x31 0x2E 0x30) |
| size Marker | uint8 | 8 (0x08) | 1 | Program size marker opcode (0x42) |
| Total file size | UInt32 | 9 (0x09) | 4 | Total file size in bytes (big-endian) |
The header is 13 bytes total. The size marker (0x42) is not a real instruction but a metadata field. All implementations validate that this byte equals 0x42 before reading the size field. The actual instruction stream begins at offset 13 (0x0D).
Header validation:
All implementations validate:
- File Signature:
"NCS V1.0"(bytes[0x4E, 0x43, 0x53, 0x20, 0x56, 0x31, 0x2E, 0x30]) - Size Marker at Offset 8: 66 (
0x42) (metadata, not an instruction) - File Size (big-endian UInt32 at offset 9) โค actual file size
- Seek on Offset 13 (0x0D) to begin parsing
Reject file if any validation fails.
reone/src/libs/script/format/ncsreader.cpp:28-40(header reading and validation)xoreos/src/aurora/nwscript/ncsfile.cpp:333-350(includes validation of 66 (0x42) marker)xoreos-tools/src/nwscript/ncsfile.cpp:106-125(header parsing utilities)NorthernLights/Assets/Scripts/ncs/NCSReader.cs:876-886(66 (0x42) marker handling)NCS.csL9+ (header reading)Libraries/PyKotor/src/pykotor/resource/formats/ncs/io_ncs.py:62-93(NCS instruction definitions)
Instruction Encoding
Every Instruction is stored as:
<Bytecode: *uint8*> <Qualifier: *uint8*> <Arguments: *variable*>
PyKotor reuses the NWScript Opcode table; each Opcode accepts a specific Qualifier/Argument layout described below.
Bytecode
Bytecode selects the fundamental Instruction (stack manipulation, arithmetic, logic, control flow, etc.). KotOR supports the standard NWN Opcodes plus Bioware extensions such as STORE_STATE. See Libraries/PyKotor/src/pykotor/resource/formats/ncs/ncs_data.py:70-142.
Qualifier
Qualifier refines the Instruction to specific Operand types (e.g., IntInt, FloatFloat, VectorVector).
Example: ADDxx with Qualifier IntInt performs integer addition, while the same Opcode with Qualifier FloatFloat adds floats.
Qualifier type values:
Unary types (single operand):
0x03- Integer (I) - 4 bytes0x04- Float (F) - 4 bytes0x05- String (S) - 4 bytes (pointer to string data)0x06- Object (O) - 4 bytes (object ID)0x10- Effect (E) - 4 bytes0x11- Event (V) - 4 bytes0x12- Location (L) - 4 bytes0x13- Talent (T) - 4 bytes0x10-0x1F- Engine Types (ETs) (Reserved range for game-specific types)
Binary types (two operands):
0x20- Integer, Integer (II) - 4 bytes each0x21- Float, Float (FF) - 4 bytes each0x22- Object, Object (OO) - 4 bytes each0x23- String, String (SS) - 4 bytes each (pointers)0x24- Structure, Structure (TT) - variable size (must be multiple of 4)0x25- Integer, Float (IF) - 4 bytes each0x26- Float, Integer (FI) - 4 bytes each0x30- Effect, Effect (EE) - 4 bytes each0x31- Event, Event (VE) - 4 bytes each0x32- Location, Location (LE) - 4 bytes each0x33- Talent, Talent (TE) - 4 bytes each0x30-0x39- Engine type pairs (ET pairs) (reserved range)0x3A- Vector, Vector (VV) - 12 bytes each (3 floats: x, y, z)0x3B- Vector, Float (VF) - 12 bytes + 4 bytes0x3C- Float, Vector (FV) - 4 bytes + 12 bytes
Special type values:
0x00- Void (V) (no type, used for return types)0x01- Stack (S) (internal type marker)
Type size information:
- All primitive types (int, float, string pointer, object ID, engine types) are 4 bytes
- Vectors are 12 bytes (3 consecutive floats)
- Structures have variable size but must be 4-byte aligned
- The TT (structure) qualifier type is used for comparing ranges of elements on the stack, specifically for structures and vectors. When used with
EQUALTTorNEQUALTT, it requires a 2-byte size field indicating how many bytes to compare (must be a multiple of 4).
Type system details:
The qualifier byte system allows the same opcode to operate on different data types. Type qualifiers are organized into ranges:
- Unary Types (
0x03-0x1F): Single operand types - Binary Types (
0x20-0x3C): Two operand type combinations - Special Types (
0x00,0x01): Void and internal stack marker
Vectors types are special: they occupy 3 stack positions (12 bytes) but are treated as a single composite type. When operations involve vectors, all three float components are processed together.
References:
reone/src/libs/script/format/ncsreader.cpp:42-190xoreos/src/aurora/nwscript/ncsfile.h:131-177NCS.csL9+NorthernLights/Assets/Scripts/ncs/NCSReader.csNWScriptInstruction.tsL79+
Arguments
Instruction arguments follow the qualifier byte and vary by instruction type. All multi-byte values are stored in big-endian byte order.
Argument format patterns:
-
No Arguments (2 bytes total: opcode + qualifier):
RSADDx,LOGANDII,LOGORII,INCORII,EXCORII,BOOLANDII,EQUALxx(non-structure),NEQUALxx(non-structure),GEQxx,GTxx,LTxx,LEQxx,SHLEFTII,SHRIGHTII,USHRIGHTII,ADDxx,SUBxx,MULxx,DIVxx,MODII,NEGx,COMPI,RETN,NOTI,SAVEBP,RESTOREBP,NOP
-
Signed 32-bit Integer (6 bytes total: opcode + qualifier + 4 bytes):
MOVSP: Stack pointer adjustment offset (signed, allows positive and negative adjustments)JMP,JSR,JZ,JNZ: Relative jump offset (signed, from start of instruction, allows forward and backward jumps)INCxSP,DECxSP,INCxBP,DECxBP: Stack/base pointer offset (signed, negative offsets access stack elements)CONSTO: Object ID constant (signed 32-bit:0x00000000=OBJECT_SELF[scriptdefs.pyL9],0x00000001=OBJECT_INVALIDin KotOR [scriptdefs.pyL10])
-
Unsigned 32-bit Integer (6 bytes total: opcode + qualifier + 4 bytes):
CONSTI: Integer constant (stored as unsigned 32-bit big-endian, but may represent signed integer values in the range -2ยณยน to 2ยณยน-1)
-
Signed 32-bit Integer Pair (10 bytes total: opcode + qualifier + 4 bytes + 4 bytes):
-
32-bit float (6 bytes total):
CONSTF: Float constant (IEEE 754 big-endian)
-
string (variable length: 2 bytes length + string data):
CONSTS: 2-byte length prefix (big-endian uint16) followed by string bytes (ASCII)
-
Stack Copy Operations (8 bytes total: opcode + qualifier + 4 bytes offset + 2 bytes size):
CPDOWNSP,CPTOPSP,CPDOWNBP,CPTOPBP: Signed 32-bit stack offset + unsigned 16-bit size- Stack offset conversion: Negative byte offsets are converted to stack positions by dividing by 4 (e.g., offset -4 becomes position 1, offset -8 becomes position 2)
- size field indicates number of bytes to copy (must be multiple of 4 for alignment)
-
Engine Function Call (5 bytes total: opcode + qualifier + 2 bytes routine + 1 byte arg count):
ACTION: Unsigned 16-bit routine number + unsigned 8-bit argument count- Routine number indexes into the engine's function table (actions data)
- Argument count specifies how many stack elements (not bytes) to pass to the function
-
Destruct Operation (8 bytes total: opcode + qualifier + 2 bytes + 2 bytes + 2 bytes):
DESTRUCT: format:[0x21][qualifier][uint16 size][signed16 stackOffset][uint16 sizeNoDestroy]size: Total number of bytes to destroy from stackstackOffset: Signed 16-bit offset from stack pointer indicating where destruction begins (converted to position by dividing by 4)sizeNoDestroy: Number of bytes to preserve (not destroyed) within the destruction range- Used for complex stack cleanup when removing multiple elements while preserving specific values (e.g., when unpacking structures or vectors)
-
Struct Comparison (4 bytes total: opcode + qualifier + 2 bytes):
EQUALTT,NEQUALTT: format:[0x0B/0x0C][0x24][uint16 size]
Jump offset calculation: Jump offsets are relative to the start of the jump instruction itself, not the next instruction. The offset is a signed 32-bit integer allowing both forward and backward jumps.
Byte order:
All multi-byte values in NCS files are stored in big-endian (network byte order):
-
16-bit values โ most significant byte first:
-
32-bit values โ most significant byte first:
-
This applies to: offsets, sizes, constants, jump targets, and all numeric arguments
Instruction parsing:
Standard process: Read opcode + qualifier --> Determine argument format via lookup --> Read arguments (0 to variable bytes) --> Advance IP by total instruction size --> Repeat until EOF
Variable-length instructions:
CONSTx(0x04): Qualifier determines type: Int (4B), Float (4B IEEE 754), String (2B length + data), Object (4B)CPDOWNSP/CPTOPSP/CPDOWNBP/CPTOPBP(0x01/0x03/0x26/0x27): 4B offset + 2B sizeJMP/JSR/JZ/JNZ(0x1D/0x1E/0x1F/0x25): 4B signed offset (relative to instruction start)DESTRUCT(0x21): 2B size + 2B stackOffset + 2B sizeNoDestroySTORE_STATE(0x2C): 4B size + 4B sizeLocalsACTION(0x05): 2B routine + 1B argCountEQUALTT/NEQUALTTwith qualifier 0x24: 2B size (multiple of 4)
All multi-byte values: big-endian
Execution state:
- IP: Instruction pointer (starts at 0x0D)
- SP: Stack pointer (top of data stack)
- BP: Base pointer (globals area, set by
SAVEBP/RESTOREBP) - Return Stack: Separate stack for
JSR/RETNaddresses
Execution loop: Parse instruction --> Execute (manipulate stack/IP/BP, call functions) --> Advance IP --> Repeat until termination
Instruction sizes: 2B base + arguments: None (2B), Int/float/Object (6B), String (2+N B), Jump (6B), Stack copy (8B), ACTION (5B), DESTRUCT (8B), STORE_STATE (10B), Struct compare (4B)
Static sizing enables: instruction list building, jump target resolution, code coverage, static analysis
References:
xoreos/src/aurora/nwscript/ncsfile.cpp:194-1649reone/src/libs/script/format/ncsreader.cpp:42-190NCS.csL9+NorthernLights/Assets/Scripts/ncs/NCSReader.csxoreos-tools/src/nwscript/ncsfile.cppNWScriptInstance.tsL32+xoreos/src/aurora/nwscript/ncsfile.h:86-280
Instruction Encoding Examples
Example 1: Integer Constant
Bytes: [0x04][0x03][0x00][0x00][0x00][0x2A]
โโโฌโโ โโโฌโโ โโโโโโโโโโโโฌโโโโโโโโโโโ
CONST Int 42 ([big-endian](https://en.wikipedia.org/wiki/Endianness))
Pushes integer value 42 onto the stack.
Example 2: String Constant
Bytes: [0x04][0x05][0x00][0x05][0x48][0x65][0x6C][0x6C][0x6F]
โโโฌโโ โโโฌโโ โโโฌโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ
CONST Str length=5 "Hello"
Pushes string "Hello" onto the stack.
Example 3: Jump Instruction
Bytes: [0x1D][0x00][0x00][0x00][0x00][0x10]
โโโฌโโ โโโฌโโ โโโโโโโโโโโโฌโโโโโโโโโโโ
JMP qualifier offset=16 bytes forward
Jumps 16 bytes forward from the start of this instruction.
Example 4: Stack Copy Operation
Bytes: [0x01][0x00][0xFF][0xFF][0xFF][0xFC][0x00][0x04]
โโโฌโโ โโโฌโโ โโโโโโโโโโโโฌโโโโโโโโโโโ โโโฌโโ
CPDOWNSP qualifier offset=-4 bytes size=4
Copies 4 bytes from top of stack to offset -4 (one element down).
Example 5: Engine Function Call
Bytes: [0x05][0x00][0x00][0x01][0x02]
โโโฌโโ โโโฌโโ โโโฌโโ โโโฌโโ
ACTION qualifier routine=1 argCount=2
Calls engine routine #1 with 2 arguments (which must be on stack in reverse order).
Example 6: float Constant
Bytes: [0x04][0x04][0x40][0x49][0x0F][0xDB]
โโโฌโโ โโโฌโโ โโโโโโโโโโโโฌโโโโโโโโโโโ
CONST Float 3.14159... ([big-endian](https://en.wikipedia.org/wiki/Endianness) [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754))
Pushes float value approximately 3.14159 (ฯ) onto the stack.
Example 7: Object Constant (OBJECT_SELF)
Bytes: [0x04][0x06][0x00][0x00][0x00][0x00]
โโโฌโโ โโโฌโโ โโโโโโโโโโโโฌโโโโโโโโโโโ
CONST Object OBJECT_SELF (0x00000000)
Pushes OBJECT_SELF constant onto the stack.
Example 8: Conditional Jump (JZ)
Bytes: [0x1F][0x00][0xFF][0xFF][0xFF][0xF0]
โโโฌโโ โโโฌโโ โโโโโโโโโโโโฌโโโโโโโโโโโ
JZ qualifier offset=-16 bytes backward
Jumps 16 bytes backward if top of stack is zero (consumes the integer from stack).
Instruction Categories
| Category | Opcodes |
|---|---|
| Stack | CPDOWNSP, CPTOPSP, MOVSP, CPDOWNBP, CPTOPBP, SAVEBP, RESTOREBP, INCxSP, DECxSP, INCxBP, DECxBP |
| Constants | CONSTI, CONSTF, CONSTS, CONSTO, and typed variants |
| Arithmetic | ADDxx, SUBxx, MULxx, DIVxx, MODxx, NEGx |
| Bitwise/Logic | LOGANDxx, LOGORxx, INCORxx, EXCORxx, BOOLANDxx, NOTx, COMPx, SHLEFTxx, SHRIGHTxx, USHRIGHTxx |
| Comparison | EQUALxx, NEQUALxx, GTxx, GEQxx, LTxx, LEQxx |
| Control Flow | JMP, JSR, JZ, JNZ, RETN, STORE_STATE, DESTRUCT |
| Function Calls | ACTION (invokes engine-exposed script functions) |
Detailed Instruction Descriptions
Stack manipulation:
CPDOWNSP(0x01): Copy bytes from top of stack down to specified offset. format:[0x01][qualifier][signed32 offset][uint16 size]. SP remains unchanged. Used to write local variables.CPTOPSP(0x03): Copy bytes from specified offset to top of stack. format:[0x03][qualifier][signed32 offset][uint16 size]. SP increases bysizebytes. Used to read local variables.MOVSP(0x1B): Adjust stack pointer by specified value. format:[0x1B][qualifier][signed32 offset]. Used to allocate/deallocate stack space for local variables.CPDOWNBP(0x26): Copy bytes from base pointer down to specified offset (for global variables). format:[0x26][qualifier][signed32 offset][uint16 size]. SP remains unchanged. Used to write global variables.CPTOPBP(0x27): Copy bytes from specified offset relative to base pointer to top of stack (for global variables). format:[0x27][qualifier][signed32 offset][uint16 size]. SP increases bysizebytes. Used to read global variables.SAVEBP(0x2A): Save current BP and set BP to current stack position. format:[0x2A][qualifier]. Used when entering a new function scope.RESTOREBP(0x2B): Restore BP from previousSAVEBP. format:[0x2B][qualifier]. Used when exiting a function scope.RSADDx(0x02): Reserve space on stack for a variable of specified type. format:[0x02][qualifier]. SP increases by 4 bytes (or 12 for vectors). Qualifier determines type:0x03=int,0x04=float,0x05=string,0x06=object,0x10-0x1F=engine types.INCxSP(0x24): Increment variable on stack at specified offset. format:[0x24][qualifier][signed32 offset]. Qualifier0x03=int,0x04=float.DECxSP(0x23): Decrement variable on stack at specified offset. format:[0x23][qualifier][signed32 offset]. Qualifier0x03=int,0x04=float.INCxBP(0x29): Increment global variable at specified base pointer offset. format:[0x29][qualifier][signed32 offset].DECxBP(0x28): Decrement global variable at specified base pointer offset. format:[0x28][qualifier][signed32 offset].
Stack operation details:
- offsets: Always negative (e.g., -4, -8), positive = invalid
- Copy ops: Signed 32-bit offset + unsigned 16-bit size (must be 4-byte multiple)
MOVSP: Adjust SP (positive = deallocate, negative = allocate)- BP ops: Identical to SP ops but use base pointer (set by
SAVEBP, points to globals) - Multi-element copies (size > 4): May indicate composite types (vectors, structs)
CPDOWNSP/CPDOWNBP: Store to stack (SP unchanged, source = top of stack)CPTOPSP/CPTOPBP: Load from stack (SP increases by copied size)
Position conversion:
- byte offset --> position:
-offset / 4(e.g., -8 --> 2, -12 --> 3) - size --> elements:
size / 4(e.g., 12B --> 3 elements) - position --> offset:
-position * 4
Examples: offset -4 = position 1 (top), -8 = position 2, vector (12B) at -12 = positions 1-3
References:
xoreos/src/aurora/nwscript/ncsfile.cpp:458-545reone/src/libs/script/format/ncsreader.cpp:52-97xoreos/src/aurora/nwscript/ncsfile.cpp:105-172NCS.csL9+NorthernLights/Assets/Scripts/ncs/NCSReader.csNWScriptInstance.tsL32+
Constants:
All use opcode 0x04, qualifier determines type:
CONSTI(0x03):[0x04][0x03][uint32](6B), SP+4, unsigned but can represent signed (-2ยณยน to 2ยณยน-1)CONSTF(0x04):[0x04][0x04][float32](6B), SP+4, IEEE 754 big-endianCONSTS(0x05):[0x04][0x05][uint16 len][ASCII](2+N B), SP+4 (pointer only, content stored separately), no null terminatorCONSTO(0x06):[0x04][0x06][signed32](6B), SP+4, special:0x00000000=OBJECT_SELF,0x00000001/0xFFFFFFFF=OBJECT_INVALID
Parsing: Read opcode (0x04) --> qualifier --> type-specific args (UInt32/float32/string length+data/signed32)
Behavior: Read value --> Create immutable constant entry --> Push onto stack (SP+4) --> Remains until consumed/removed
References:
xoreos/src/aurora/nwscript/ncsfile.cpp:500-545reone/src/libs/script/format/ncsreader.cpp:60-73NCS.csL9+NorthernLights/Assets/Scripts/ncs/NCSReader.csxoreos-tools/src/nwscript/ncsfile.cppNWScriptDef.tsL12+
Arithmetic operations:
All arithmetic operations consume operands from the top of the stack and place the result back on the stack. SP increases by result size and decreases by total operand sizes.
ADDxx: Addition (supports II, IF, FI, FF, SS, VV)SUBxx: Subtraction (supports II, IF, FI, FF, VV)MULxx: Multiplication (supports II, IF, FI, FF, VF, FV)DIVxx: Division (supports II, IF, FI, FF, VF)MODII: Modulus (integer only)NEGx: Negation (supports I, F)
Bitwise and logical operations:
LOGANDII(0x06): Logical AND of two integersLOGORII(0x07): Logical OR of two integersINCORII(0x08): Bitwise Inclusive OR of two integersEXCORII(0x09): Bitwise Exclusive OR of two integersBOOLANDII(0x0A): Boolean/Bitwise AND of two integersNOTI(0x22): Logical NOT of an integerCOMPI(0x1A): One's Complement of an integerSHLEFTII(0x11): Shift Integer Left by specified number of bitsSHRIGHTII(0x12): Shift Integer Right by specified number of bits (signed)USHRIGHTII(0x13): Unsigned Shift Integer Right by specified number of bits
Comparison operations:
EQUALxx(0x0B): Test for EQUALity. format:[0x0B][qualifier][optional: uint16 size for TT]. Supports II, FF, SS, OO, TT, and engine types. For TT (struct) qualifier, includes 2-byte size field (must be multiple of 4). Pops two operands from stack, compares them, pushes 1 if equal else 0.NEQUALxx(0x0C): Test for iNEQUALity. Format:[0x0C][qualifier][optional: uint16 size for TT]. Same asEQUALxxbut pushes 1 if not equal else 0.GTxx(0x0E): Greater Than. format:[0x0E][qualifier]. Supports II, FF. Pops two operands (top is right operand), pushes 1 if left > right else 0.GEQxx(0x0D): Greater Than or Equal. format:[0x0D][qualifier]. Supports II, FF. Pops two operands, pushes 1 if left >= right else 0.LTxx(0x0F): Less Than. format:[0x0F][qualifier]. Supports II, FF. Pops two operands, pushes 1 if left < right else 0.LEQxx(0x10): Less Than or Equal. format:[0x10][qualifier]. Supports II, FF. Pops two operands, pushes 1 if left <= right else 0.
Comparison operation details:
- Comparison operations pop two operands from the stack and push a single integer result (1 for true, 0 for false)
- For binary comparisons (
GT,GEQ,LT,LEQ), the top of stack is the right operand, and the value below it is the left operand - structure comparisons (
EQUALTT,NEQUALTT) perform byte-by-byte comparison of the specified number of bytes - The size field for structure comparisons must be a multiple of 4 to maintain alignment
- string comparisons compare string pointers (4-byte values), not string contents directly
References:
xoreos/src/aurora/nwscript/ncsfile.cpp:712-768(Comparison OPCode Implementation)reone/src/libs/script/format/ncsreader.cpp:102-105(Comparison Instruction Parsing)NCS.csL9+ (Comparison Operation Handling)
Control flow:
JMP(0x1D): Unconditional jump to relative offset. format:[0x1D][qualifier][signed32 offset]. offset is relative to the start of the JMP instruction. Used for loops and unconditional branches.JSR(0x1E): Jump to subroutine at relative offset. format:[0x1E][qualifier][signed32 offset]. Pushes return address onto return stack, then jumps. Used for function calls within the script.JZ(0x1F): Jump if top of stack is zero. format:[0x1F][qualifier][signed32 offset]. Consumes integer from top of stack (SP decreases by 4). If value is zero, jumps to offset; otherwise continues to next instruction.JNZ(0x25): Jump if top of stack is non-zero. format:[0x25][qualifier][signed32 offset]. Consumes integer from top of stack (SP decreases by 4). If value is non-zero, jumps to offset; otherwise continues to next instruction.RETN(0x20): Return from subroutine. format:[0x20][qualifier]. Pops return address from return stack and jumps to it. Used to exit script subroutines.DESTRUCT(0x21): Destroy elements on stack, preserving specified element. format:[0x21][qualifier][uint16 size][signed16 stackOffset][uint16 sizeNoDestroy](8 bytes total). This instruction performs complex stack cleanup by removingsizebytes from the stack starting atstackOffset, but preservessizeNoDestroybytes within that range. ThestackOffsetis a signed 16-bit value converted to a stack position by dividing by 4. Used for complex stack cleanup operations, particularly when unpacking structures or vectors where only specific elements need to be preserved. The preserved element is moved to the top of the stack after destruction.
DESTRUCT operation details:
The DESTRUCT instruction is used in scenarios where multiple stack elements need to be removed, but a specific element within that range must be preserved:
- The instruction identifies a range of stack elements starting at
stackOffsetand extending forsizebytes - Within that range,
sizeNoDestroybytes starting at a calculated position are preserved - All other bytes in the range are removed from the stack
- The preserved element is moved to the top of the stack (SP position 1)
- This is commonly used when unpacking structures: the structure occupies multiple stack positions, but only one field needs to be extracted
The stackOffset parameter is a signed 16-bit value that is converted to a stack position by dividing by 4. The offset is relative to the current stack pointer, with negative values indicating positions deeper on the stack. The size parameter indicates the total number of bytes to destroy, and sizeNoDestroy indicates how many bytes within that range should be preserved.
Example: If a structure occupies positions 3-5 (12 bytes) and only the middle element (position 4) needs to be preserved, DESTRUCT would remove positions 3 and 5 while keeping position 4, then move it to the top.
DESTRUCT format:
- Opcode:
0x21 - Qualifier: Typically
0x00(not used for type determination) - Arguments:
[uint16 size][int16 stackOffset][uint16 sizeNoDestroy](6 bytes total, 8 bytes including opcode and qualifier) - The preserved element is extracted from the destruction range and placed at the top of the stack, effectively replacing the destroyed elements with just the preserved value.
During decompilation, DESTRUCT identifies structure field accesses (preserved element position --> field).
STORE_STATE(0x2C): Save stack state for delayed execution. format:[0x2C][qualifier][int32 size][int32 sizeLocals](10B). Used withDelayCommand. size = total stack bytes, sizeLocals = local variable bytes. Separates temp values from persistent locals. Decompilers typically emit as-is with comments.
Control flow details:
- Jump offsets: Relative to instruction start (not end), signed 32-bit (ยฑ2GB range)
JSR: Separate return stack (not data stack), return addr = after JSRRETN: Pop return addr from return stack, empty stack = main/errorJZ/JNZ: Consume top int (removed regardless of jump)DESTRUCT: Atomic multi-element removal with preservation (structure unpacking)
Examples: JMP @100 +20 --> 120, JZ @200 -16 --> 184, JSR @300 +50 --> 350 (return addr 306)
References:
xoreos/src/aurora/nwscript/ncsfile.cpp:712-768reone/src/libs/script/format/ncsreader.cpp:81-91NCS.csL9+NorthernLights/Assets/Scripts/ncs/NCSReader.csxoreos-tools/src/nwscript/ncsfile.cppKotOR.js/src/nwscript/NWScript.tsL39+
Engine function calls:
ACTION(0x05): format:[0x05][0x00][uint16 routine][uint8 argCount](5B). Routine = index into engine function table (K1/K2 differ). Args in reverse order (last on top). Engine removes args, pushes return value. vectors = 3 floats (x, y, z). Synchronous execution.
Function table:
Engine-specific mapping: routine number --> function (name, params, return type). Used for decompilation (routine 1 --> GetFirstObject). Invalid routine = error.
Actions data file: Lists engine functions, format: return_type function_name(param_type param, ...). Example: int GetFirstObject(int nObjectFilter, object oArea);
The decompiler parses this file to build a lookup table mapping routine numbers (indices in the file) to function signatures. This allows the decompiler to generate readable function calls instead of raw ACTION instructions with numeric routine IDs.
See NSS File Format for complete documentation of nwscript.nss, function definitions, and KotOR-specific functions/constants.
References:
xoreos/src/aurora/nwscript/ncsfile.cpp:643-660(ACTION opcode implementation)reone/src/libs/script/format/ncsreader.cpp:74-77(ACTION instruction parsing)NCS.csL9+ (ACTION instruction handling)NorthernLights/Assets/Scripts/ncs/NCSReader.cs(engine function call processing)xoreos-tools/src/nwscript/ncsfile.cpp(actions data parsing for decompilation)
Special instructions:
NOP(0x2D): No-operation, used as placeholder for debugger. format:[0x2D][qualifier]. Does nothing, SP remains unchanged.- Program size marker (0x42): Always found at offset 8 in NCS file header. format:
[0x42][uint32 fileSize]. This is not a real instruction and is never executed. It contains the total file size in bytes (big-endian). All implementations validate this marker before parsing instructions.
Note: All multi-byte values in NCS files are stored in big-endian byte order. This includes all integers, floats, offsets, and size fields.
Special instruction details:
NOP(0x2D) is a no-operation instruction that does nothing. It is typically used as a placeholder by debuggers or during script compilation. The qualifier is typically0x00.- The program size marker (0x42) at offset 8 is never executed as an instruction. It is a metadata field that all implementations check before parsing the instruction stream. If this byte is not
0x42, implementations should reject the file as invalid.
References:
xoreos/src/aurora/nwscript/ncsfile.cpp:342-350(header validation including 0x42 check)xoreos-docs/specs/torlack/ncs.html(Torlack's original NCS specification)NorthernLights/Assets/Scripts/ncs/NCSReader.cs:876-886(0x42 marker handling)NCS.csL9+ (NOP instruction handling)Libraries/PyKotor/src/pykotor/resource/formats/ncs/ncs_data.py:144-242(instruction definitions)
Control Flow and Jumps
- Jump instructions store relative offsets; readers resolve them to absolute positions.
- PyKotor's NCS class tracks instruction objects and rewires the
jumpattribute so tooling can walk the control-flow graph without recomputing offsets. - Subroutines use
JSR(push return address) andRETN(pop address and jump back). STORE_STATE/DESTRUCTmanage VM save/restore for suspended scripts (cutscenes, dialogs); the semantics are identical in Reone and Kotor.NET, while KotOR.js mirrors them for deterministic playback.
Subroutine Calls
Calling conventions:
Script subroutines: Reserve Return Space --> Push args (reverse) --> JSR --> Subroutine removes args --> Return value at reserved location
Engine routines: Push args (reverse) --> ACTION (routine #, arg count) --> Engine removes args --> Engine pushes return
Example: int j = DoSomeScriptSubroutine(12, 14);
Before: SP --> -4: Arg1(12), -8: Arg2(14), -12: Return(??), -16: j(??)
After: SP --> -4: Return(??), -8: j(??)
Example stack layout for engine routine call:
Before call to int j = DoSomeEngineRoutine(12, 14);:
*SP* --> -4: *Arg1*: 12
-8: *Arg2*: 14
-12: j: ??
After call:
*SP* --> -4: *Return*: ??
-8: j: ??
Jump Instructions
All jump instructions use relative offsets from the start of the jump instruction itself:
JMP: Unconditional jump to offsetJZ: Jump if top of stack is zero (consumes the integer from stack)JNZ: Jump if top of stack is non-zero (consumes the integer from stack)JSR: Jump to subroutine (pushes return address, then jumps)
The offset is a signed 32-bit integer stored in big-endian format, allowing forward and backward jumps within the script. The offset is calculated as: target_address - instruction_address.
Jump resolution: offsets resolved to absolute instruction pointers during parsing. PyKotor's NCS class stores direct instruction references in jump attribute for control-flow graph traversal. Format: Signed 32-bit big-endian, target = instruction_addr + offset, forward (+) or backward (-).
Jump offsets and resolution behavior are implemented in PyKotor's instruction model (ncs_data.py L244-L421) and reader (io_ncs.py L262-L269), with matching parser/execution behavior visible in reone ncsreader.cpp L81-L85, xoreos ncsfile.cpp L712-L768, and Kotor.NET NCS.cs L9+.
Cross-reference: implementations
Reference implementations include reone ncsreader.cpp, xoreos ncsfile.cpp, xoreos-tools ncsfile.cpp, Kotor.NET NCS.cs L9+, NorthernLights NCSReader.cs, KotOR.js NCSResource.ts, kotor-savegame-editor ncs.ts, and xoreos-docs ncs.html. These implementations use the same opcode space (0x01-0x2D, marker 0x42), qualifier range (0x03-0x3C), and big-endian encoding.
Instruction sizes: Base 2B + args: None (0B), Int/Float (4B), String (2+N B), Stack copy (6B), ACTION (3B), DESTRUCT (6B), STORE_STATE (8B), Struct compare (2B)
Compatibility: All implementations use identical opcodes (0x01-0x2D, 0x42), qualifiers (0x03-0x3C), and big-endian encoding, ensuring cross-engine compatibility when paired with the appropriate ACTION routine table.
Decompilation overview:
Core analyses: Stack tracking (variable assignments/reads via copy ops), Control flow (jumps --> if/loops/switches), Subroutine analysis (JSR --> function calls, infer params/returns), Variable naming (type + position or usage patterns), Dead code elimination
Analysis passes:
- Parse: Bytecode --> instructions + control flow graph (jumps as edges)
- Subroutines: Identify boundaries via
JSR/RETN, analyze separately - type Inference: Operations reveal types (
ADDII--> ints, calls --> engine function table) - Prototyping: Infer subroutine signatures from usage (may need multiple passes for recursion)
- Stack Tracking: Track state per instruction (variables, types, assignment), clone/merge at control flow joins
- structure Recognition: 12-byte copies --> vectors, other multiples of 4 --> custom structs
- Control Flow: Jumps -->
if/loops (forward jumps = conditionals, backward = loops), multi-target = switch - Code Gen: Emit source with named variables, typed declarations, high-level constructs
- Cleanup: Remove dead code, optimize names, format
Stack analysis details:
- Variables:
CPDOWNSP/CPDOWNBP= writes,CPTOPSP/CPTOPBP= reads, offset identifies variable, size determines primitive vs composite - Scope:
MOVSP= function entry/exit (negative = allocate, positive = deallocate) - structures: Vectors (12B) = z/y/x order, nested structures require hierarchy tracking
- Globals: BP operations, separate from locals (SP operations)
- State Merging: Converging paths require merging stack states (variables on all paths preserved)
- Reference Counting: Track usage per stack instance, zero refs + unassigned = placeholder
- Return values: Track separately, identified from stack state before RETN/*JSR
Decompiler behavior described above is visible in xoreos-tools ncsfile.cpp, xoreos-tools decompiler.cpp, reone ncsreader.cpp, Kotor.NET NCS.cs L9+, xoreos ncsfile.cpp, kotor-savegame-editor ncs.ts, and NorthernLights NCSDecompiler.cs.