Tes4Mod:Mod File Format/SCPT
A SCPT record holds information on a single script.
Subrecords known to occur in the SCPT record include (confirmed):
- EDID (required): Editor ID (variable length string)
- SCHR (required): Script data (20 bytes)
- SCDA (required): Compiled script data (variable length)
- SCTX (required): Script text (variable length string, not always nul terminated)
- SLSD (optional, mult): Variable data (24 bytes, confirmed)
- SCVR (optional, mult): Variable name, follows a SLSD (variable length string)
- SCRO (optional, mult): Global variable reference (formid, confirmed)
- SCRV (optional, mult): Ref variable data (one for each ref declared) (dword)
Appears to hold 20 bytes (confirmed) of basic script data .
|RefCount||4 (dword)||Number of references used script. This includes 'ref' local variables and direct references (eg: player). This should equal the number of SCRO and SCRV subrecords in the script.|
|CompiledSize||4 (dword)||Size of the compiled data (SCDA subrecord)?|
|VariableCount||4 (dword)||The last local variable index used in the script. For a new script this value will be 0. For a new script with a single local this will be 1. This index will always increase for a single script and never decrease. For example, if you create a new script, add a local variable, compile, delete the local, and recompile this value will remain at 1. If you add another local variable it will increase to 2. This is likely to prevent external references to a local variable from becoming invalid.|
|Type||4 (dword)||Type of script
Holds the compiled script data. Note that an empty script (with just a "scriptname ...") has a 4-byte SCDA with a value of 0x00000001D.
The compiled script consists of a list of 2-byte opcodes, each followed by the 2-byte length of its arguments, optionally followed by the 2-byte number of arguments and the arguments itself. Arguments with a variable length (strings, RPN-encoded expressions and so on) have an additional 2-byte length prepending them. The only notable exception to this rule is the SetCurrentReference pseudo-instruction (represented as a single dot in the script editor), which consists only of its opcode (0x001C) and the 2-byte index of the SCRO subrecord to be used.
SCRO subrecords in expressions are referenced using 0x72, followed by the 2-byte index of the corresponding subrecord. SLSD/SLVR subrecords use 0x73, followed by the index. The first subrecord has the index 1.
|1D 00 00 00|
Note: The name of the script doesn't get used in the compiled script.
|1C 00 01 00||53 10 0A 00 02 00||72 02 00||6E||3C 00 00 00|
SCDA Output Codes
The following is a work in progress and likely contains errors. Text in the format [Name(N)] indicates variable data (N) bytes in size.
|Script Text||Compiled Bytes||Notes|
|ScriptName||1D 00 00 00||Length of a scriptname is always 00 00|
|Begin OnActivate||10 00 08 00 02 00 [BlkLen(4)] 00 00||08 00 is the [ModeLen]. [BlkLen] is the length in the bytes of the following begin block compiled data. [BlkLen] does not include the two trailing 00 00 bytes.|
|Begin GameMode||10 00 06 00 00 00 [BlkLen(4)]||06 00 is the [ModeLen].|
|end||11 00 00 00|
|return||1E 00 00 00|
|if [Expression]||16 00 [CompLen(2)] [JmpOps(2)] [ExpLen(2)] [ExpBytes(N)]||[CompLen] is the size of the entire comparison bytes following it. [JmpOps] is the number of operations to the next elseif/else/endif statement following the if block (used to jump over opcodes if the expression evaluates to false). [ExpLen] is the length of the comparison expression following it. [ExpBytes] is the compiled comparison expression data.|
|elseif [Expression]||18 00 ...||Same as if statement except for the first opcode.|
|else||17 00 02 00 [JmpOps(2)]||02 00 is the length of the [JmpOps] which is 2 bytes.|
|[Function]||[FuncCode(2)] [ParamBytes(2)] [ParamCount(2)] [Parameters(N)]||[ParamBytes] is the size of the parameter data following it, including the [ParamCount]. For functions with no input parameters the [ParamBytes] value is 00 00 and the [ParamCount] is not included. Note that not all functions follow this format. See Script Functions for opcodes and other function details.|
|[RefFuncParam]||72 [Index(2)]||When a reference (eg: player) is used as a function input parameter the output bytes reference an one-based index to a SCRO subrecord which holds the formid of the reference. For example, 72 01 00 means the first SCRO subrecord.|
|[StringFuncParam]||[Length(2)] [Text(N)]||The text does not include a terminating NUL character.|
|[LongFuncParam]||6E [Value(4)]||Holds the binary value of the long integer.|
|[FloatFuncParam]||7A [Value(8)]||The float value is stored as a double 64-bit number.|
|[Axis]||'X'/'Y'/'Z'||An axis value (setpos/getpos) is simply output as a single uppercase character.|
|male||00 00||Function parameter for GetIsSex.|
|female||01 00||Function parameter for GetIsSex.|
|[ActorValue]||[Word Index]||The actor value used in functions like GetActorValue is output as a word index. See Actor Values for a list of specific index values.|
|[QuestStage]||[WordValue]||A quest stage function parameter is output as a 2-byte word value.|
|[CrimeType]||[WordValue]||A crime type function parameter is output as a 2-byte word. Valid values are 0-4.|
|[reference].||1C 00 [Index(2)]||For a reference call (eg: player.[Function]) the index is a 1-based value into a SCRO subrecord in the script which holds the formid of the reference. For example, 1C 00 01 00 is the first SCRO subrecord.|
|set [Var] to [Expression]||15 00 [Length(2)] [Var(N)] [ExpLength(2)] [Expression(N)]||Expression is an ASCII string in postfix notation.|
|[SetShortLocal]||73 [Index(2)]||[Index] is the variable index as stored in the SLSD subrecord.|
|[SetLongLocal]||73 [Index(2)]||[Index] is the variable index as stored in the SLSD subrecord.|
|[SetFloatLocal]||66 [Index(2)]||[Index] is the variable index as stored in the SLSD subrecord.|
|[SetRefLocal]||66 [Index(2)]||[Index] is the variable index as stored in the SLSD subrecord.|
|[SetGlobal||47 [Index(2)]||[Index] is a 1-based index to a SCRO subrecord.|
|[SetRef].[LocalVar]||72 [Index(2)] [LocalVarData(3)]||[Index] is a 1-based index to a SCRO subrecord containing the reference formid. [LocalVarData] is the appropriate 3 bytes of data for the local variable in the script of the reference. Note that the index used in the [LocalVarData] is for the target script and not the current script.|
|MessageBox [String]||00 01 [Length(2)] 01 00 [StrParam(N)] 00 00 00 00||The string is not NUL terminated. [Length] will equal the string length plus 8. [StrParam] is output like a regular string parameter with a 2-byte string length prefix.|
|MessageBox [String] [Button1]...[ButtonN]||00 01 [Length(2)] 01 00 [StrParam(N)] 00 00 [ButCount(2)] 01 00 [But1StrParam(N)] 01 00 [But2StrParam(N)] ...||[StrParam] and [ButNStrParam] are output like a regular string parameter with a 2-byte string length prefix. Each button string is also prefixed with a 01 00 (unknown purpose).|
|MessageBox [String] [Var1]..[VarN]||00 01 [Length(2)] 01 00 [StrParam(N)] [VarCount(2)] [VarData(N)] 00 00||[VarData] uses the same format as variables found in set/if statements.|
SCDA Expression Codes
Expression codes, used in set and if statements, use a somewhat different format that the rest of the SCDA data. Expressions are parsed using a simple stack and infix notation.
|[push]||20||The 20 output value (space character) represents a push operation for the following variable or data.|
|[Function]||58 [FuncCode(2)] [ParamBytes(2)] [ParamCount(2)] [Parameters(N)]||[ParamBytes] is the size of the parameter data following it, including the [ParamCount]. For functions with no input parameters the [ParamBytes] value is 00 00 and the [ParamCount] is not included.|
|[Number]||[Number]||Constant numbers are output exactly as they are.|
|[RelOp]||[RelOp]||Relational operators (==, >, >=, <, <=, !=, &&, ||) are output exactly as they are.|
|[ShortLocal]||73 [Index(2)]||[Index] is the variable index as stored in the SLSD subrecord.|
|[LongLocal]||73 [Index(2)]||[Index] is the variable index as stored in the SLSD subrecord.|
|[FloatLocal]||66 [Index(2)]||[Index] is the variable index as stored in the SLSD subrecord.|
|[RefLocal]||66 [Index(2)]||[Index] is the variable index as stored in the SLSD subrecord.|
|[Reference]||5A [Index(2)]||[Index] is the reference index of the SCRO/SCRV subrecord. Note that this is just for standalone references.|
|[Global]||47 [Index(2)]||[Index] is the 1-based index of the SCRO subrecord that contains the global formid.|
|[ref].[function]||72 [Index(2)] [FuncData...]||[Index] is the 1-based index of the SCRO subrecord that contains the global formid.|
|[ref].[LocalVar]||72 [Index(2)] [LocalVarData(3)]||[Index] is a 1-based index to a SCRO subrecord containing the reference formid. [LocalVarData] is the appropriate 3 bytes of data for the local variable in the script of the reference. Note that the index used in the [LocalVarData] is for the target script and not the current script.|
|-[Term]||20 [TermData(N)] 20 7E||The unary negation operator seems to use the ~ character rather than the minus sign (likely to indicate that only one stack variable should be popped).|
SLSD and SCVR records together represent the local variables of the script. SLSD provides general info, while SCVR gives the name of the variable.
SLSD appears to hold 24 bytes (confirmed) of data related to a local variable definition.
|Index||4 (dword)||Unique index for each local variable in the script.|
|Unknown||4 (dword)||Usually 0?|
|Unknown||4 (dword)||Usually 0?|
|Unknown||4 (dword)||Usually 0?|
|Type||4 (dword)||Type of variable (flags)
|Unknown||4 (dword or float)||Usually 0xCDCDCDCD (uninitialized data?). Also looks like a float in some scripts.|
SLSD/SCVR pairs appear in the order in which they're defined in the script at compile time. When compiling a script, the compiler will use index already assigned to the variable if it already exists in the SLSC/SCVR pair listing. If the variable is not in the list, then the next available index is assigned -- where next available index is numVars (from SCHR) plus 1.
- Changing the type of an existing variable will not affect its index number.
- Reordering variable definition will not affect their indices, though it will affect the ordering of the SLSD/SCVR pairs.
- Renaming a variable will result in a new index number being assigned, which means that savegames will effectively have the value reset. (Since savegames remember variable value by index.)
- Deleting a variable, compiling and then reading the variable will cause the redefined variable to have a new index number. This is because when the variable is deleted, it is removed from the SLSD/SCVR pair list, and so the compiler is not able to find its old index number when the variable is redefined.
- Renaming a variable will likely cause other scripts attempting to access it to lose it -- until they too are modified to use the new name and recompiled.
Note: It appears that the savegames store all numbers (despite their short, long, float designation) as doubles (64 bit floats). Refs are stored differently, but in the same space(?)
It seems that problems can arise if their is a conflict over what a given index means. This can happen if:
- a master defines a script with 4 variables
- a mod overrides the script and adds 2 more variables
- the master then is updated to add another variable
- player starts playing with master version 2, then loads mod (compiled on version 1 of master script), since there is now a conflict over what variable index 5 refers to.
This might in particular be a problem when a float is reinterpreted as a ref value. It is suspected that this conflict was the source of some CTDs when version 1.3 of OOO (built on top of unpatched Oblivion) was used with savegames using patched Oblivion.
There will be one SCRV subrecord for each local ref variable used in the script. They seem to follow at the end of the script data after all the SLSD/SCVR pairs. The SCRV data is a 4 byte dword (confirmed) and seems to hold the ref variable index as stored in the variable's SLSD subrecord.
There will be one 4-byte SCRO subrecord for each object reference used in the script. The value of the SCRO is the formid of the object. The one exception is an SCRO value of 0x00000014 which actually refers to the player (whose true formid is 0x00000007).