Tes3Mod:Scripting Pitfalls

Introduction
This page documents common and/or unsuspected scripting pitfalls. These problems may occur due to limitations in the script compiler, bugs in the Morrowind executable, or unexpected side effects of game execution.

Syntax
ForceGreeting Fallthrough
If you use a "forceGreeting" from within an if block, use a return after it. Otherwise the script will fall continue to execute the remaining elseif/else tests, rather than bypassing them. This is a bug in the script execution process.
 

Fractional Numbers
A literal float specified without digit before the decimal point will cause a runtime error.
Floats in the range: -1  

Inconsistent Commas
The scripting language is fairly forgiving in terms of comma usage or not, but the inconsistent use of commas can cause difficulties:

 

Furthermore, commas in functions used within an if(), while(), or the Set statement can result in the compiled output including the commas even though they are not needed. Although the statement may still execute correctly, it is recommended not to use commas in functions because of this behavior.

 

The comma can be used as an alternative separator instead of a space in some places, as well as in some console commands, this can lead to two separators where only one is required if a comma and a space are used to separate the same thing.

Object Names
Don't start object names with an underscore unless you properly use them in quotes. Otherwise, some script operations will fail. You can get around this error by ensuring the name is always enclosed in quotes in the script.

 

You may experience similar problems when using object names starting with a number. 

 

This also applies to the script ID. However, since the script ID cannot be enclosed in quotes in its own script it is recommended not to start script names with a number or underscore.

Spaces
Unlike other languages proper use of spaces is important in several situations. Omitting the spaces can result in scripts seeming to compile correctly but the output compiled data will be corrupt and not working as expected in some cases.

Comparison Operator
One space must be included on each side of the comparison operator in if() and while() statements.
 

Brackets and Operators
Brackets and operators should have one space on each side of them when used in if(), while(), or set statements. 
 

Fix Operator
When using the fix operator, either put the "fixed" object in quotes or don't allow spaces between it and the fix operator. Otherwise, you'll experience a runtime script error at some random and usually unrelated script.
 

Variables
34th Variable
If you have a script with 34 or more variables of the same type (short, long or float), the 34th variable of that type can cause errors when used. For that reason, most scripters call it "DoNotUse" or something like that to remember that they should not use it. The reason for this problem seems to be that the ASCII character 34 is the double quote character.

 
Instead of that you can do like this to prevent errors:
 
If the variable types are mixed, you should do like this:
 
Note that now the "bad" variable isn't the 34th, but the 34th short and the 34th float

Remote Access of Variables
;Edit Needed
The following is not totally correct. You can access local variables of another object (usually), but you cannot access local variables of a global script. However, I think that there are some limitations on object variable reference (like you can't use them in an expression?) For local variables of a global script, if you attempt to access them, they'll simply return 0.
Also the GetAttribute, GetSkill approach is probably unworkable, since these functions return zero if the NPC has not been encountered in the last 72 gamehours.
Might want to break this section out into a "How to Communicate State" article.
----
One serious limitation of the scripting language is that there's no obvious way to check the variable on another script.
While you can SET another local script's variables using the following format:
 
or SET another global script's variables (from inside another local/global script and even from a dialog result) using the following format:
 
the reverse is not possible:
 
 

So, with all those limitations, how do you get a value from another script?  There are a few options:
# Use a global variable instead.  This is the quickest and easiest method, but it has its own pitfalls.  (see below)
# Implement some sort of call-back script.  Have a flag variable in the script to be called that tells that script to send its value back to you.  Set the flag from your calling script when you want to check the other script's value.  This will require at least a 1-frame delay, however, so you'll have to structure your script so that it waits until the other script has executed its call-back.
# Use some other universally-accessible property of the object.  Certain properties of objects can always be accessed remotely, though in most cases these properties are being used for other things, but in certain special cases, you can use them.  Examples:
#*   If the object is an NPC or a creature, you'll have to be aware that GetAngle will only work with the Z axis, and only if they are paralyzed.  (Otherwise, they may rotate on their own.)  Also, the GetPos will only work with X and Y unless the entity can fly or has a Levitate spell on them, as the Z value will keep resetting them to the floor.  Needless to say, they must be either paralyzed or burdened to keep them from moving.  If you don't want to have to deal with the object moving or re-scaling, you can access these properties on a 3rd-party object, such as a ring hiding in the wall or something.  ("References Persist" must of course be checked on the called object if it isn't an NPC or a creature, and the object must be unique, or the script may become confused.)
#*   GetSkill will of course only work on NPCs, and GetAttribute will only work on NPCs and creatures.  You'd want to choose an Attribute or Skill that won't be noticed, so Personality, Willpower, or Endurance would be a good choice for Attributes.  Skills you have more choice.  Try using, say, Conjuration, on an NPC that doesn't have any Conjuration spells.  Obviously this method has a slight weakness in that you may get false results if the player casts a spell that raises or lowers the target's Attributes or Skills.
#*  Only works on NPCs, creatures, and containers.  And obviously you wouldn't want the player to be able to access the inventory of the entity in question while using this method.  If it is an NPC or creature, the items added should be scripted to be removed when they are killed.  Also, NPCs can be pick-pocketed, so any item you use should in that case be an article of clothing (not armor) that they would wear.  Or the NPC in question should be hiding in a wall where you can't access them directly.  (A hidden container works well for this also.)
#*   Only works on NPCs and creatures.  If the entity is paralyzed or burdened, you can change their AI package (AIFollow, AIWander, AIEscort, etc.) without worrying about the side effects.  (Just make sure they're far enough from any doors that they won't follow you through them.)
#*   Works to store a binary (0 or 1) variable on an object by enabling/disabling it.  Will work on anything, but of course the object disappears when disabled, so it should be something hidden in a wall so that you don't see it.

Functions
Parenthesizing
Functions used in compound if(), while() and set expressions need to be surrounded in parentheses. If the parentheses are omitted it may result in a strange compile-time error or in the compiled data not being output correctly which may result in the script not working as intended in some situations. Don't forget to include spaces on both sides of the parentheses and function.
 

Moon Phase in Interior Cells
The moon phase detection commands GetMasserPhase and GetSecundaPhase are not updated in interior cells. Used inside, they'll always deliver the values that were true for the last visit to an exterior cell.


AIWander
The AIWander function is not output like any other function. The officially stated syntax for the function is:
 

but the compiled output for the function always omits the [Idle2] parameter and adds an extra optional [Idle10] parameter before the last [Reset]. For example:

 

It is assumed, but not confirmed, that the idle parameters are merely shifted right by one from the stated syntax. When using the function you may wish to add an 'extra' parameter in the [Idle2] location.

An additional problem with the AiWander function is with the last Reset parameter. Unlike the other AI functions the reset parameter in this function behaves differently and somewhat randomly. If not used in a single script the reset value is output as the byte 01 in the compiled data. If used one or more times in a single script its value can be 00, 01, or 05 with the value changing between compiles. It is recommended that reset is not used with the AiWander function untils its purpose can be confirmed.


PlayGroup and LoopGroup
These two functions take an Animation Group as their first parameter. When compiled this group text value is converted into the animation group opcode, a 16-bit integer value. At some point the opcodes for the animation groups, hardcoded in the application, were changed, likely with one of the expansions or patches (not yet confirmed which one). Most of the animation groups changed opcodes at this point with a few dozen new opcodes introduced.

The problem is that scripts using PlayGroup/LoopGroup compiled in the older version of the Construction Set contain the old opcodes. If this script is run in a newer version of the game the animation group opcodes in that script may not correspond to the ones the script author intended. The script must be recompiled with a new version of the Construction Set for the opcodes to be updated (although then this new script cannot be run in older versions of the game).

This bug is not yet confirmed and there may well be a hidden mechanism to convert between old and new opcodes.

Global Scripts

StartScript from Deleted Object
If you start a global script from an object that may be deleted, fix it to something permanent like the player. Otherwise, you'll get an CTD when the object is deleted if the global script is still running.

For example, NOM 2.12 has a script on NOM tisane pots that starts a global script that typically ends up running for a while. But the tisane pot can be picked up and put in inventory, which is handled by deleting the tisane pot that had been dropped. 
  

Note: All global scripts except some startup scripts have are associated with some reference. This is the reference that is assumed by the script whenever the script uses a fixable function without using specifying a fix object. If you don't specify the reference using the fix operator, then the global script inherits the reference of the script that starts it.

StopScript Delay
Don't count on stopScript stopping execution immediately. StopScript signals Morrowind not to run the global script again. But it does not stop the current execution of the script. 

When calling stopScript from the script being stopped, either make sure that the remaining script is okay to be executed, or use a return statement to force the script to stop executing immediately.

Variable Persistence
Don't rely on persistence of global script variables -- unless those script are guaranteed to run at least once during each load session. If a global script is not run during a load session, then it's variables will not be saved during the save process. 

If the script is not guaranteed to run, then:
Store its variables elsewhere. E.g. in a global variable.
"Heartbeat" the script. I.e., force it run once per each load session. This can be done with a startup script.

Here's the bare bones of a heartbeat
 


Unary Operators
The unary operators are a + or - sign in front of a symbol in an expression, for example:
     set local to -6                     ;Unary -
     set local to 1 - 6                  ;Regular subtraction -
     set local to -global                ;Unary -
     set local to -(global1 + global2)   ;Unary -
For simple statements like the above examples the CS compiles them properly, although it generally converts them to multiplications by -1.
     set local to -1      => set local to -1 * 1 
     set local to +15     => set local to 15 +
     set local to -global => set local to -1 * global
     if  local == -1      => if  local == -1            ; Not changed in if statements
More complex expressions may actually be output incorrectly, for example:
                    set local1 to -(-1 -9) * -local2 / -local3
     Outputs As:    set local1 to 9 * -local2 / -local3
     
                    if     ( -testmisc2.local1 == 10 )		
     Outputs As:    if -1 *(  testmisc2.local1 == 10 )
It is recommended that you don't use the unary - except in simple expressions and use explicit multiplication (* -1) for more complex expressions.