Morrowind Mod:Scripting Pitfalls

The UESPWiki – Your source for The Elder Scrolls since 1995
Jump to: navigation, search

Introduction[edit]

This page documents common and 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[edit]

ForceGreeting Fallthrough[edit]

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 and else tests, rather than bypassing them. This is a bug in the script execution process.

if ( test1 )
  wr_testNPC->ForceGreeting ;--BAD! This will fall through and messagebox will print.
else
  messagebox "Didn't forcegreet"
endif

if ( test1 )
  wr_testNPC->ForceGreeting ;--Okay. Following return will prevent fallthrough.
  return
else
  messagebox "Didn't forcegreet"
endif

Fractional Numbers[edit]

A literal float specified without a digit before the decimal point will cause a runtime error. Floats in the range: -1 < 0 < 1 should be specified with a leading 0, or you'll experience a runtime error.

if ( number == .2 ) ;--BAD!
...

if ( number == 0.2 ) ;--Okay

Inconsistent Commas[edit]

The scripting language is forgiving with comma usage, but inconsistent use of commas can cause problems:

This will work:
Player->PositionCell, -1396, 124, 3312, 90, "Cell ID"

and this will work:
Player->PositionCell -1396 124 3312 90 "Cell ID"

but this will sometimes have problems:
Player->PositionCell -1396, 124, 3312, 90 "Cell ID"

Furthermore, commas in functions used within an if(), while(), or 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.

This will output normally:
     set TestVar to ( player->GetPos X )
     if ( ( GetFactionReaction faction_01 "faction_02" ) >= 50 )

but the following will output with extra commas in the compiled data:
     set TestVar to ( player->GetPos, X )
     if ( ( GetFactionReaction, faction_01, "faction_02" ) >= 50 )

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[edit]

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.

_wr_testNPC->ForceGreeting    ;BAD: Will cause a runtime error

"_wr_testNPC"->ForceGreeting  ;Good, proper use of quotes.
wr_testNPC->ForceGreeting     ;Good

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

123wr_testNPC->ForceGreeting     ;BAD: May cause runtime/compile errors

"123wr_testNPC"->ForceGreeting   ;Good, proper use of quotes.
wr_testNPC->ForceGreeting        ;Good

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[edit]

Unlike other scripting 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 work as expected in some cases.

Comparison Operator[edit]

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

if ( SomeVar>=10 )       ;BAD
if ( SomeVar >= 10 )     ;GOOD

while (Count!=20)        ;BAD
while ( Count != 20 )     ;GOOD

Brackets and Operators[edit]

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

if(SomeVar >= 10)        ;BAD
if ( SomeVar >= 10 )     ;GOOD

set SomeVar to (10*Global01/(Global02+92))            ;BAD
set SomeVar to ( 10 * Global01 / ( Global02 + 92 ) )  ;GOOD

Fix Operator[edit]

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.

set strength to wr_testNPC -> getStrength ;--BAD!

set strength to wr_testNPC->getStrength ;--Okay.
set strength to "wr_testNPC" -> getStrength ;--Okay.

Variables[edit]

34th Variable[edit]

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.

short Var1
short Var2
short Var3
short Var4
...
short Var33
short Var34
short Var35

if ( Var34 == 0 )
    Set Var34 to 1;this line and the one above can cause odd errors
endif

Instead of that you can do like this to prevent errors:

short Var1
short Var2
short Var3
short Var4
...
short Var33
short DoNotUse
short Var34
short Var35

if ( Var34 == 0 )
    Set Var34 to 1;now it will work fine since the Var34 actually is the 35th
endif

If the variable types are mixed, you should do like this:

short Var1S
float Var1F
short Var2S
float Var2F
...
short Var33S
float Var33F
short DoNotUseS
float DoNotUseF
short Var34S
float Var34F

Note that now the "bad" variable isn't the 34th, but the 34th short and the 34th float.

Remote Access of Variables[edit]

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 game hours.
  • 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:

set object_ID.variable to 1 ;--Okay.

or set another global script's variables (from inside another local or global script and even from a dialogue result) using the following format:

set global_script_ID.variable to 1 ;--Okay.

the reverse is not possible:

set local_var to object_ID.variable ;--BAD!
set local_var to global_script_ID.variable ;--BAD!
set object_ID.variable to object_ID.variable + 1 ;--BAD!
object_ID->set variable to variable + 1 ;--BAD!
if ( object_ID.variable == 1 ) ;--Okay.

So, with all those limitations, how do you get a value from another script? There are a few options:

  1. Use a global variable instead. This is the quickest and easiest method, but it has its own pitfalls. (see below)
  2. 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.
  3. 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:
    • GetPos (X,Y,Z), GetAngle (X,Y,Z), GetScale. 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.)
    • GetAttribute, GetSkill. 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.
    • GetItemCount. (use "addItem" and "removeItem" to alter the variable) 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.)
    • GetCurrentAIPackage. 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.)
    • GetEnabled. 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[edit]

Parenthesizing[edit]

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.

if ( player->GetStrength > 10 )        ;BAD
if ( ( player->GetStrength ) > 10 )    ;GOOD

set LocalVar to ( GetFactionReaction faction_01 "faction_02" - 100 ) * 1.2         ;BAD
set LocalVar to ( ( GetFactionReaction faction_01 "faction_02" ) - 100 ) * 1.2     ;GOOD

Moon Phase in Interior Cells[edit]

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[edit]

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

AiWander [Range] [Duration] [Time] [Idle2] [Idle3] ... [Idle9] [Reset]

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:

The function call:
   AiWander 100 1000 0 10 20 30

Will result in the compiled data like:
   [Opcode] 100 1000 0 20 30

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 to not use reset with the AIWander function until its purpose can be confirmed.

PlayGroup and LoopGroup[edit]

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 or LoopGroup compiled in the older version of the Construction Set contain the old opcodes. If this script runs 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 be a hidden mechanism to convert between old and new opcodes.

Global Scripts[edit]

StartScript from Deleted Object[edit]

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 a 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.

elseif ( state == 30 )
  startScript NOM_tisane ;--BAD! Will cause CTD when tisane pot is deleted.
...

elseif ( state == 30 )
  player->startScript NOM_tisane ;--Okay. No crash on tisane pot deletion.

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[edit]

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[edit]

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

begin wr_testHeartbeatSS
set wr_test1.heartbeat to 1
stopscript wr_testHeartbeatSS
end

begin wr_test1
short heartbeat
short persist1
short persist2

if ( heartbeat )
  set heartbeat to 0
  stopScript wr_test1
endif
...
end


Unary Operators[edit]

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.