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

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

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.

Position , PositionCell
Can NOT take variables, local or not.

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

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. Otherwise, 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.

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

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!" - seems to be 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.

Additional notes:


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. The point of this is that scripted Ai sequences won't break if the "package done" frame is skipped for any reason - in vanilla it's for instance skipped if the PC waits/rests during the execution of AiTravel.

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.

- 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 but before it (it will return an incremented value if checked under OnDeath).

- 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 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. The same is true of NPC functions Mod/SetFight, Mod/SetFlee, Mod/SetAlarm, which record all of the NPC's properties into the save.

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. If you want the object's new position to be persistent, use "SetPos" at the end of its movement. Alternatively, using "Enable" will also flag the object as changed and make the new coordinates persist.

PlaceAtPC , PlaceAtMe
Note that objects created by these functions aren't affected by ingame lighting until the player exits and reloads the cell.

- 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 after StopCombat, without which the NPC can decide to walk back towards its earlier StopCombat position. NPCs may return to previously given AiWander positions after PositionCell if a new Ai target isn't given.
- 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.

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.

Cast , ExplodeSpell
Creatures with empty spellcast animations can not be made to cast spells by scripts, even though they can cast their own spells or enchantments. Examples of creatures with no cast animations: ash slaves, ash ghouls. Examples of creatures with cast animations: dremora.

PlayGroup , LoopGroup
- Use "PlayGroup idle" to reset the Ai controller when NPC animations get stuck.
- 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.

The simple declaration of the variable doesn't prevent combat reactions for NPCs with standard a standard 30 Fight value. 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" is only true for NPCs with 0 Fight.

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.

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.

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

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.

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