[Tutorial] Morrowind Scripting for Dummies

Morrowind Scripting For Dummies

This file is hosted at Morrowind Modding History, and is a complete guide to how to write and edit scripts in Morrowind.


MSFD, 9th edition, still contains a few errors (some inherited from the original Construction Set manual), but is otherwise generally correct and is the most comprehensive guide available. When looking up a function in MSFD do read beyond the description and through its notes and comments, which contain valuable information.


There is no undocumented feature by specifying the player after the function (->Activate, player). The addition of ", player" is simply ignored, and it does the same thing as "Activate" alone. In the given example, "container"->Activate will work, but only if the container has been manually opened in the session before, and loaded within 72 hours.

The note "creatures killed with curse spell effects on them cause all other creatures of that type to have the same curse on them": this has nothing to do with the creature being killed or the effect being of the Curse type. Just like NPCs, abilities, diseases or curses added to a creature ID by dialogue results or their local script will be added to any new instance of the creature.

AiActivate can not make a NPC drink a potion.
It will only pick up objects in the cell; does not check for objects in inventory. See also notes below about what [reset] does.
note: Morrowind Code Patch feature "Scriptable potion use" lets you do this with the Equip function instead

The following isn't true if a [reset] argument is used: "When you leave a cell with an Actor that is just performing its AITravel command, or if you rest, the script will never detect the GetAIPackageDone signal". But keep in mind that when AiTravel is skipped by resting, the engine doesn't check path collision, so the NPC may be teleported to a spot where it's stuck (like inside another NPC) and where it may not acknowledge that it has reached its destination, or only after a delay or when the player reenters the cell.
the undocumented argument for Ai functions (AiActivate, AiFollow, AiEscort...):
It changes the behaviour of the "package done" flag. GetAiPackageDone normally only returns 1 for one frame, when the Ai function has been executed. The [reset] argument of Ai functions is optional. If it is given with any value (0 or 1), once the package is executed, GetAiPackageDone will NOT reset to 0 until a new Ai command is called on that NPC (if the new Ai command is called while in MenuMode, GetAiPackageDone will reset when out of MenuMode). The point of this is that scripted Ai sequences won't break if the single "package done" frame is skipped for any reason - in vanilla it would be for instance skipped if the PC waits/rests during the execution of AiTravel.
does not "check if the target is currently in the crosshair".
Its function for the player is exactly the same as for NPCs: checking if the target ID is engaged in combat by the calling actor.
For NPCs, GetTarget returns 1 not only in combat with the target, but also (if the target is the player) while they speak a Hello voice.

Modifies or defines the reputation (not reaction) modifier for members of the specified faction towards the PC. This affects advancement and Reputation checks for NPCs of the same faction, but doesn't directly affect their Disposition (only Rank affects the NPC's disposition if the NPC's faction is favorably disposed towards the player's faction). Reputation (player's and NPC's) is also involved in Persuasion success formulas.

The simple declaration of the variable doesn't usually prevent combat reactions for NPCs with standard AI values for Fight (30) and Alarm (90). The report that "an NPC with a script that uses this variable will not attack on its own accord. If you don’t want the Actor to remain passive you have to manually StartCombat" seems to be consistently true only for NPCs with less than 76 Alarm, or after a script stops that NPC from attacking the player at least once with Stopcombat and Setfight. For NPCs that are supposed to be passive, Stopcombat needs to be used under OnPCHitMe. For NPCs that are not supposed to be passive, using StartCombat manually is still recommended, as there are cases where OnPCHitMe does prevent normal NPCs from fighting back, even with more than 30 Fight and more than 90 Alarm (seems to be related to crime having happened in an interior outside of the reaction range of these NPC, and gets reset if PayFine is used with a guard nearby).
PlayGroup , LoopGroup
Unlike what the note states about "crosswired" animation, function names do not give different results if they are used in the console or in scripts. They seemed to give different results because of scripts compiled in old mods (the compiled script is saved in the plugin along with the text of the script by the CS; what the game processes is not the text of the script but the compiled version). The explanation is that these scripts were compiled in an older version of the CS, before a change in Bloodmoon: see this page for the affected Opcodes. The NPC animation explorer linked in MSFD (MMH link) is one of these older mods and needs to be recompiled, otherwise it will fail or play wrong animations.

Position , PositionCell
Can NOT take variables, local or not. See additional notes on PositionCell below for more quirks.

Rotate , RotateWorld
While "Rotate" doesn't have the same issue as RotateWorld with not updating the object's coordinates for GetAngle, it will make both position and angle changes NOT persistent if the game is saved and reloaded in the same cell. See Move , MoveWorld about this. See SetAngle , GetAngle about the order of rotations.

Unlike what the warning states, it does not re-run other scripts, and scripts could not "be run more than once in the same frame".

The condition for a value set by SetJournalIndex to persist is not simply for the journal to be "defined in the "info" section of the dialogue window": the player must have received an actual journal entry for that journal ID using the function "Journal" before (can be any valid entry in the same journal, greater or lesser index value doesn't matter). Otherwise, if no valid entry was given in that journal before SetJournalIndex is used, the issue is not only that the value will be lost when a savegame is reloaded: if a save is reloaded to an earlier point (before using SetJournalIndex) without exiting the game, the value set by SetJournalIndex will still be there; if the game has been exited before reloading, then the value will revert to the last valid entry.

The fact that it "stops combat for all actors involved" is only true in the situation where StartCombat was used to make one idle NPC attack another (StopCombat will stop combat for both), or if NPCs were following each other and one was made to attack another/the player by script. StopCombat will not make several NPCs stop attacking the player if they attacked separately;
If NPC1 first had StartCombat on player, and then also had StartCombat on NPC2, once NPC2 reacts by attacking, StopCombat on NPC1 will stop NPC1 from attacking the player but will not stop NPC2 from attacking NPC1 (who will then attack NPC2 back).

Targeted scripts
"Object_ID"->StartScript "Script_ID"  does work from dialogue results, not just scripts. Will not "only work to target the actor the player is in dialogue with"; provided an instance of the target exists, it is possible to specify a different ID in dialogue results.

Displaying variables and text defines in a message box
The digit designation for %g is not ignored. Precision and significant digits for g, G are as described for the C language.

Referencing variables on other objects and scripts
- The reported limitation to writing remote variables into another one "Note that the reverse does not work: Set local_variable to MyObject.variable ;this doesn't work!" - is incorrect in the final version of the game. See the vanilla script "FraldCounter", which does update the variable.
- The cell limitation "Set MyObject.variable to (...) will only work if the cell containing the target object/(script) has previously loaded" is incorrect, at least for NPCs. Their local variables can be remotely set even if their cells were never loaded before.
- If the object isn't a NPC or creature, it needs "References persist" for this to work. For compiled scripts, as always, one instance of the object must be placed in a cell in the CS and the first instance is the only one affected.

Functions which are spelled differently than documented
getInvisible -> getInvisibile is outdated ("fixed in a later patch or Tribunal?" according to uesp)

Additional notes:


- Note that the range is a spherical radius and doesn't mean the NPC won't wander outside of that area, but that the NPC will pick its destination within that range. The NPC can stray far outside of that range in order to reach a spot that IS within the range, like another floor.
- "To let an Actor stand in one spot you can use: AIWander, 0, 0, 0" - note that if no parameters are given for idle chances, they will be considered as 0, so while standing the actor will have no animations besides idle breathing. The most common idle chances in the game's content files are AiWander AIWander, 0, 0, 0, 0, 60, 20, 10, 10, 0, 0, 0, 0

Without any scripts involved, AiTravel can also be used in NPC AI packages - you can put several "Travel" packages and have a basic patrol route included in the NPC's parameters. If you put Wander packages with non-0 duration between the travel packages they'll wander at that point of the cycle then resume the travel patrol route (wander is in in-game hours at has to be at least 1 to not be infinite).

Cast , ExplodeSpell
Creatures with empty spellcast animations can not be made to cast spells by scripts, even though the AI can make them cast their own spells or enchantments. Examples of creatures with no cast animations: ash slaves, ash ghouls. Examples of creatures with cast animations: dremora.
Scripted casting can be used by most objects, like activators and persistent statics, but the casting object must have collision, otherwise the engine gives the error "Scale parameters for Magic Effect ... are bad". Note that the same error will be given if actors hit by spell effects are lacking collision, and that using SetScale to make an object very small can result in it having no collision and giving the error.

- Disabled statics do keep their collision in interiors as well as exteriors and need to be reloaded/repositioned;
- On the caution about disabling lights: changing the position of lights instead as suggested is a good workaround, but additional warnings on scripted lights: for lights that do not have a mesh, changes to their positions and script variables will persist through reloading games or starting a new game without quitting first. Adding a mesh (EditorMarker, invisible/collisionless) to the light seems to solve this issue.

- Note that objects created by these functions aren't affected by ingame lighting until the player exits and reloads the cell.
- has been fixed by MCP: "When used on NPC's, the items are removed correctly from the NPC, but still dropped at the player's feet."
- has been fixed by MCP: "If the player drops an item he doesn't have, his weight will still be reduced by the weight of that item - same as the encumbrance bug for RemoveItem"

- is an extremely slow function if many different NPCs and creatures are recorded in the save. Make sure not to call it every frame.
- GetDeadCount will not increment if normal death animations do not play out fully (see also PlayGroup , LoopGroup). GetDeadCount increments at the end of the death animation, on the same frame as OnDeath and can be checked at the same time (it will return an incremented value if checked under OnDeath).

If the objects between which distance is checked are in different cells (different interiors, or one exterior and one interior), the value returned by getdistance will be the maximum possible value, 340282346638528860000000000000000000000.00. This gives you an easy way to check if a NPC was left in a different cell.

OnDeath / GetHealth
OnDeath returns 1 only at the end of a normal, non-scripted death animation. MSFD suggests GetHealth as an alternative to OnDeath. However, a NPC can be dead with a positive amount of health if they were healing, because magic effects still apply during death animations. While there may (?) need to be at least one frame on which the NPC's health is < 1 for them to die, using GetHealth is not a reliable way to tell that a NPC is dead afterwards.

The function would return eval errors for cell names over 51 characters due to a bug with "if" conditions. This has been fixed by MCP.

Waiting or loitering (resting where sleep is forbidden) doesn't count as sleep. To detect it you can instead check if ingame time (GameHour) changes within MenuMode.

Note that using this function will modify then inscribe the current state of *all* faction properties of the first faction ID into the player's save. Includes faction and rank names, requirements, etc., which will overwrite the properties edits of any new mod that wasn't already installed when the function was used, and will definitively apply the properties edits of any mod that was already installed even if that mod is later removed. The same is true of NPC functions Mod/SetFight, Mod/SetFlee, Mod/SetAlarm, AddSpell, RemoveSpell, which record all of the NPC's properties into the save (even if there is no change or no existing spell to remove).

Move , MoveWorld
If a CS reference (an object instance placed in a cell in the CS, not created in the game) is moved with only Move or MoveWorld, its position will be reset if the game is reloaded in a different cell. Don't use timers to control Move or MoveWorld if position matters, because the player may save and reload while the object is moving, which could reset its position but not the timer. Assuming you use position checks, if you want the object's new position to be persistent, use "SetPos" at the end of its movement (see comments on SetPos for cases where SetPos alone may not persist). If other functions like "Rotate" are also used in the script, there are cases where SetPos changes will not persist when saving and reloading in the same cell. When testing, keep in mind that reload persistence works differently if combinations of SetPos, Move, Rotate... are used in the console, as opposed to used normally by local or global scripts: you can't rely on console tests.
For GetDistance not updating with Move, see the note in MSFD at GetDistance.

- "An item is usually activated when you press the spacebar to "use" it": it's also activated when another actor is made to activate it with AiActivate.
- "so only one script can report OnActivate successfully, and you should only have one OnActivate call in your script at each moment": This also means it's bad practice to use it in a targeted global script, and it should be reserved for the object's local script.
- "LoadDoors -> Open (If used with OnActivate)": this made CellChanged not return 1 in the loaded cell, but has been fixed by MCP.

PlaceAtPC , PlaceAtMe
Note that objects created by these functions aren't affected by ingame lighting until the player exits and reloads the cell. Doesn't apply to actors.
For creatures with particle effects: when a creature is created ingame by a script (as opposed to a creature or lev.list placed in the CS), constant particles that ignore parent animation and trail behind the parent (example: flame atronach body particles) aren't displayed until the game is saved and loaded or, in interiors, until the player looks towards coordinates 0,0,0 where the particles are placed.

PlayGroup , LoopGroup
- "Playgroup" leaves the actor frozen after the animation if it doesn't loop: use "PlayGroup idle" to reset the Ai controller and the actor will then resume normal behaviour. "Playgroup" loops the animation by default if it has a loop. Using "Loopgroup" with a finite number of loops normally lets the actor resume its behaviour after the loops have played.
- Neither PlayGroup not LoopGroup will play the parts of animations between 'loop stop' and 'stop'
- If PlayGroup or LoopGroup interrupt death animations, OnDeath will not return 1 and GetDeadCount will not increase (unless "Playgroup idle" is used to reset and let a full death animation play from beginning to end). If a NPC is scripted to die (SetHealth 0) during a scripted PlayGroup or LoopGroup animation, OnDeath and GetDeadCount will not update either.
- Paralysis doesn't stop scripted animations, neither does the function SkipAnim.

PositionCell , Position
- for actors: always use AiWander just after PositionCell, otherwise the NPC's AI coordinates will not change, and the NPC will reset to its previous AI coordinates if you rest in the same cell (other conditions to reproduce: rest must be in the same session, and the cell the NPC was in before PositionCell must be a cell that has been loaded in the same session before)
- for actors: there are unwanted effects if PositionCell follows certain AI functions too closely in time. A delay between half a second and one second is the minimum you need after StopCombat; without that delay the NPC can decide to walk back towards its earlier StopCombat position (if using PositionCell to move to another cell, it can look like the NPC is jittering). NPCs may return to previously given AiWander positions after PositionCell if a new Ai target isn't given.
- for non-actor objects: when PositionCell is used, if the object instance was already in the destination cell, the new position won't persist when exiting the program and reloading unless the instance has been marked as edited in the save, for instance by calling "enable" on it. Not an issue when PositionCell moves an object to a different cell.
- The following should be fixed by the MCP: If PositionCell was used on a CS NPC instance that had never been loaded in the game before, in order to move it into either the current cell or a cell that had been loaded before, the NPC would appear without its local script and could cause crashes on activation.

MCP fixes the bug that returned up to 1100 for specific values (65... 84) or crashed for multiples of 256.

As long as an external script is used, removing a normal amount of scripted items from the player's inventory should not cause errors even if there are several. However, RemoveItem can only remove one at a time. The report "if the player has two or more copies of an object with an attached script in their inventory, using RemoveItem on that Object ID will frequently corrupt data for one of the remaining copies" is probably an unrelated error, or related to containers.

SetAngle , GetAngle
SetAngle X, Y doesn't relate to X, Y angles in the Construction Set because the SetAngle function doesn't apply rotations in the same order as anything else. The engine and the CS use the object's local axes and rotate in the ZYX order. SetAngle uses local axes too but instead rotates in the XYZ order. By default GetAngle returns the angle value set in the CS, but returns the value set in the game if SetAngle is used, which is why you could use GetAngle and get the CS value 10, then use SetAngle to 10 which rotates the object in a different position, then use GetAngle again and still get the same value 10.
note: Morrowind Code Patch fix "Get/SetAngle enhancement" lets you use SetAngle with U, V, W axes, which are mapped to X, Y, Z but applying a ZYX rotation axis order matching the rest of the engine. The patch also allows you to work with an actor's actual rotation instead of the actor's initial rotation.

- For actors, the same warning as PositionCell applies: always use AiWander just after using SetPos to change a NPC's coordinates, otherwise the NPC's AI coordinates will not change, and the NPC will reset to its previous AI coordinates if you rest in the same cell.
- Corpses can be moved with SetPos if the corpse is afterwards (after SetPos) disabled and enabled.
- For non-actor objects (similar warning to PositionCell for non-actors moved within the same cell): the new position won't properly persist when reloading a save if there isn't anything that marks the instance as edited in the save. Examples of marking an instance as edited: calling "enable" or "disable" on it, or changing the value of any local variable in its script. Examples of moving an instance with SetPos *without* marking it as edited: remotely calling SetPos on a unique instance with "references persist" from another script, using SetPos in an instance's local script that uses a global variable and has no local variables... When testing, keep in mind that reload persistence works differently if combinations of SetPos, Move, Rotate... are used in the console, as opposed to used normally by local or global scripts: you can't rely on console tests. For testing results, note that using Move or Rotate before SetPos on the same frame may also result in the object's SetPos being saved.

Only skips frames of "natural" animations, doesn't interrupt scripted animations called by PlayGroup or LoopGroup.

Making Actors lie down
- 0 Fatigue: ModFatigue and ModCurrentFatigue will also fail to make a NPC fall down if they are triggered in the script by GetAiPackageDone, meaning on the exact frame an Ai function (such as AiTravel) ends. If a reset argument was given to the Ai function (see [reset] above), the NPC will also get back up and start spinning.
- "Note that some character animation groups may not work as expected with Bloodmoon": no, this was a compilation issue with old mods, see notes on PlayGroup.
- "Note also that the NPC's upper body may not play the correct animation": has been essentially fixed by MCP.
- "If you do use PlayGroup, you will also need to stop NPCs using normal voices"... setting Hello to 0 takes care of greeting voices and of another issue: NPCs turning around to face the player even while their animation is playing. Being hostile will still make them turn to face the player if they aren't paralysed.

Result field scripts
- Voice dialogue (Attack, Hit...): if any input (except comments) is present in the result field and the voice is triggered while in MenuMode, an error message is given: "Trying to RunFunction index greater than function count". Although harmless, the error will prompt the player to click "Yes" twice or it will close the game. If dialogue or the console is open and a NPC's health is set to 0, the error can be given by a Hit voice. In the course of normal play, this can happen for Attack voices, since they can be triggered by SetFight and StartCombat in dialogue results (dialogue is in MenuMode). NPCs who have Attack voices with scripted results can use their local script to detect MenuMode and record it in a local variable as a condition for the voices to exclude. Since Greeting results apply on the first frame of dialogue (before a script can detect MenuMode), that local variable should first be updated in the greeting's results if they include StartCombat.

Referencing variables on other objects and scripts
- As always, for this to work in a compiled script, an instance of the targeted unique object must exist (placed in the CS), and have "references persist" if it's not an actor. When used in dialogue results though (non-compiled script), any existing instance will work, even if it was created ingame and didn't exist in the content files. See also errors in the same section listed above at "Referencing variables on other objects and scripts".
- If for some reason a script refers to its own variables using the "objectID".variable syntax, for instance a local script used by several objects that wants to do something to only one of these objects,
and if you haven't compiled these variables yet (or accidentally saved this script by moving to another script in the window without compiling it), the "variable not found" error will prevent you from compiling the script, and it won't be able to see its own variables until it's compiled... You need to comment out the references to its own variables, compile, then uncomment and compile again. Something similar happens for self-terminating scripts, but StopScript lets you compile the first time without commenting out the self-reference.

Script with style for safer scripting
p162 (p153 [no fix] Return) "Keep track of your Return function uses. The Return function is inherently dangerous – remember that it will stop anything in that script below that line from being executed. Use it, but use it sparingly."
Return should optimally be used as much as possible. While it's true that forgetting where you've used "return" will make scripting harder for you, return is what makes scripts efficient. It should always be used at the top for any conditions under which the script isn't supposed to run, especially on disabled objects (being disabled doesn't stop the rest of the script from running every frame!). Even if lines are inside a condition that is false, the compiler will still read them all at runtime unless there is Return above them. While "reading" the lines that are meant to be skipped isn't as slow as truly running them, it's still slower than using Return.

Cleaning up your mod
p163 "TESCS will remember you looked at the things if you moved them the slightest bit in the game world, or if you ever hit an “OK” button":
that's because when editing the properties of an object instance in a cell, you must click Cancel to avoid editing the object definition.
The TESAME link is dead. http://mw.modhistory.com/download-95-5289
Enchanted Editor link is dead. http://mw.modhistory.com/download-37-1662
Keep in mind that Enchanted Editor is reported to sometimes corrupt large files by failing to save them completely; saving several times and making sure the size of the saved file is correct seems to avoid the issue for users.
The "change indicators found in the details tab" such as DIAL and INFO aren't exclusive to TESAME and there are more than the list given there, they're TES3's data record types.

On References Persist
p165 "Indirect" referencing by functions: "Some functions may be exceptional in this respect.": besides GetDistance, the other function is AiActivate. "Direct" referencing is not only RefObject->Enable, but also RefObject.Variable in Set lines or in conditions:
  Set RefObject.Variable to ...
  if ( RefObject.Variable ... )
See p29 "Referencing variables on other objects and scripts" and its comments here.

GetArmorType + dialogue results or Choice
- GetArmorType seems to generally fail and return -1 in dialogue results if it's in a condition -- If ( player->GetArmorType, 0 == -1 ) always passes.
- On certain systems GetArmorType may generally fail in dialogue results and return -1 (why? helms quickly switched before dialogue seem to permanently trigger the issue). On other systems it may fail in dialogue results only if: it's in an If condition, or if it comes after a Choice function, but not if it comes before the Choice (why?). The bug does not seem to happen with other functions like GetWeapon or GetItemCount.

Displaying variables and text defines in a message box
^PCRank and ^NextPCRank belong in the same list as ^Faction and ^Rank, which as noted later won't work - they can't get a speaker's faction in script messageboxes, even if a faction NPC is explicit or implicitly targeted by its local script.

Adding a dialogue topic
"If another mod adds a topic with the same ID" is not a risk for hyperlinks; "an ID of which your topic's ID is a subset" is a risk as noted.
Topic hyperlinks are not retroactive: a topic will not be linked in a dialogue line if no entry was available before that dialogue line's results (Example: if there is no available entry before a Journal ID >= 10 condition, the topic will not be hyperlinked in the dialogue line that gives Journal ID 10 for the first time in its results).
Topics added in greeting results with Addtopic won't appear in the topic list immediately (Example: see NPC M'Aiq the Liar's first greeting: the topics it adds only appear after a new dialogue frame; a new frame happens when a topic is used or when he's greeted a second time; a new frame can be forced with Forcegreeting).

Equip and beast races
The "Equip" function bypasses beast race restrictions on NPCs, and can make them equip items that take the head or feet slots (equipment changes apply out of MenuMode). It can also bypass them on the player if it's used outside of MenuMode AND isn't the first Equip function called on the frame (first AddItem and Equip another item, then Additem and Equip the head/foot-slot item).

Soul trap detection and GetEffect/GetSpellEffects
GetEffect, sEffectSoultrap only returns 1 if the player had a soulgem in their inventory when the spell hit its target. It doesn't matter whether the soulgem was empty or filled, or large enough to contain the soul or not, or if it's removed after the spell hit its target. With no soulgem in inventory the function always returns 0.

Right eval, Left eval errors
MSFD does have comments on them, the exact errors aren't searchable in the document because it's missing spaces; search for "RightEval", "LeftEval".
"Left" or "Right" is the side of the statement that isn't accepted by the compiler, for instance to the left or right of the == sign. You may not see this error when compiling in the CS but it will come up when running the script in the game. Besides functions declared as variables, another example of that error is when the compiler gets the wrong object type for a function (common when the same ID is accidentally shared by a NPC and a script, or a NPC and a journal...)

Some bugs (/features) that MSFD warns against are fixed by the most recent versions of the MCP, including:
- CellChanged not returning 1 for scripted teleporting or magic teleporting is fixed by MCP,
- "CellChanged doesn't always trigger, even if the player enters the cell via a normal teleport door": this comment likely referred to what happens when entering an interior through a load door that had a script with OnActivate on it, a known bug with the Vivec Arena (this is also fixed by MCP)
- RemoveItem subtracting weight from the character's encumbrance even if the amount of items is not in the character's inventory (is fixed by MCP; the hack of removing negative weight items can no longer increase encumbrance)
- PlayGroup / LoopGroup being unreliable and having different animation on upper body and lower body is fixed by MCP
- Random function returning up to 1100 for specific values (65... 84) or crashed for multiples of 256 is fixed by MCP
- PlaceItem / PlaceItemCell: "placed NPC disappeared if I saved and reloaded before going to the cell they were placed in" is fixed by MCP "PlaceItem fix"
- The 34th local variable is fixed by MCP "Script expression parser fix"