Morrowind Scripting for Dummies

From Project Tamriel Wiki
Jump to navigation Jump to search

Morrowind Scripting for Dummies, 9th edition, is the most comprehensive Morrowind scripting guide available.

It is hosted at Morrowind Modding History (archive link) and Morrowind Nexus.

Although this book is the most complete mwscript reference available, it still contains errors and omissions (some inherited from the original Construction Set manual). The foreword already alludes to this by noting that different versions of the game might have contained different bugs. In addition to any Bethesda fixes, the MCP has also fixed a great number of potential crashes, potentially rendering some of the advice in the guide out of date.

When looking up a function in Morrowind Scripting for Dummies, do read beyond the description and through its notes and comments, which contain valuable information. Also check the Known Errors and Additional Notes below (these were mostly compiled by Rotouns).

Known Errors


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.


Can not make a NPC drink a potion.

It will only pick up objects in the cell; does not check for objects in inventory. AiActivate doesn't work if you give a reset argument. As always, only the first object found will be targeted, so the object needs to be unique and have "references persist" ticked if the AiActivate line is called in a compiled script

Note: The Morrowind Code Patch feature Scriptable potion use lets you make a NPC drink a potion with the Equip function instead. This always worked for the player, and what MCP does is extend it to work with NPCs too.


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 (AiTravel, AiActivate, AiFollow, AiEscort...):

  • For AiTravel, 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), there are two effects for AiTravel. First, once the AiTravel 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 AiTravel 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. The second effect for AiTravel is that in cells without pathgrids, without the reset argument, when the actor collides with any static object GetAiPackageDone returns 1 once and the travel package stops; but with the argument, GetAiPackageDone doesn't stop returning 1 and the actor will keep trying to reach its destination.
  • For AiWander: the [reset] argument has no effect on GetAiPackageDone. Wander packages restart at the end of their duration with or without the reset parameter, and when resting until after the end of an AiWander cycle, GetAiPackageDone doesn't stay 1.
  • For AiActivate: the function does not work if a [reset] argument is given.


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

The duration during Hello is inconsistent in the original engine (1 second after reloading into a cell, otherwise the duration of the first voice ever spoken before, or 3.4 seconds when first loading an interior).


"influences NPC disposition" is by default incorrect. The value of the GMST that would control this: fDispCrimeMod = 0


Modifies or defines the reputation (not reaction) modifier for members of the specified faction towards the PC. This affects advancement 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).

Note: Faction reputation is completely separate from general reputation and is not involved in the dialogue function PC Reputation; similarly, only general reputation, player's and NPC's, is involved in Persuasion success formulas.


The simple declaration of the variable does not 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 NPCs, and gets reset if PayFine is used with a guard nearby).


Is nonfunctional. Since soul gems are hardcoded Misc items, the only way for it to work would have been to add a script with it to the default soul gems, but even then the variable is not detected to be set when a soul gem is used and deleted during enchanting. Azura's star script is also not what makes it work.

PC Clothing Modifier

"The value of your equipment changes the disposition of people in the game" is incorrect. While the base game files can randomly show favourable greeting text when equipment value is high, actual disposition is unmodified and those same NPCs can therefore still alternate between laudatory and hateful greetings.

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 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 does not 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 only correct for compiled scripts. With non-compiled scripts (dialogue results or console), the local variables of unique NPCs and creatures and reference persistent objects can be remotely set even if their cells were never loaded before, as long as an instance does exist in a cell. It's the functions SetStat, ModStat... that don't work for actors that haven't been loaded in the past 72 hours.
  • If the object isn't a NPC or creature, it needs "References persist" to be referenced in another script, whether it is loaded or not. For compiled scripts, as always, one instance of the object must be placed in a cell in the CS and the first instance found 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

AddItem, RemoveItem

  • Usage with containers: containers become unique clones of their base object when they are opened, not when they are loaded in a cell. Adding or removing items before a container is ever opened affects the base object record, after it's been opened it only affects the instance.
  • Usage with generic actors (creatures, or NPCs there are multiple instances of, such as guards): similarly to containers, adding or removing from an actor that was never loaded anywhere before will affect the actor's base object record, therefore applying the effect to every actor loaded thereafter. Adding or removing from an actor ID that has been loaded before will apply the effect to the earliest loaded instance, and to no other. Note: there is an apparent exception with actors placed by Morrowind.esm in the cell outside the starting ship, namely Seyda Neen, seem to get preloaded even if normal character generation was skipped. The unique actors among those then behave as expected of loaded actors even if clones of them are created, but while the three "Imperial Guard" instances placed in Seyda Neen also act as if loaded and are not targeted by later ingame base record changes, nonetheless remotely adding to or removing from "Imperial Guard" even while in the cell itself behaves as if none had ever been loaded and applies the effect to any later instance besides those in Seyda Neen.
  • Adding a negative amount of an object or leveled list creates maxcount objects.
  • Without MCP, using Additem with leveled lists would otherwise work as expected on NPCs that were not loaded yet or on containers the contents of which were not changed by the player – otherwise, the leveled list would become visible in the inventory using the default missing icon and become interactable as a "NO NAME GIVEN!" object.
  • With MCP, usage with leveled lists otherwise works as expected.


[More info needed:] Repositioning an object and targeting it with AiActivate for travel pathing may inconsistently lead to "Exception encountered saving AI Pack" errors?

In cells without pathgrids, unlike AiTravel, AiActivate doesn't get stuck when colliding with static objects, and is usually better able to reach the target. However, AiActivate isn't usually able to cross exterior cell borders (activating the player seems to be an exception);


Outside of scripting, Follow can also be used in NPC AI packages. If AiFollow is under an AiWander package (with infinite 0 duration) it only makes the actor join the target in combat, whereas if AiFollow is on top it actually makes the actor follow its target when in range.


Outside of scripting, AiTravel can also be used in NPC schedule 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). Note however that in cells with no pathgrids they have a drawback compared to scripted AiTravel: these NPC schedule AiTravel packages behave like scripted AiTravel with no [reset] argument and will be interrupted at the first collision with a static object (the CS accordingly gives a warning when adding travel packages in a cell without pathgrids). AiTravel with a [reset] argument will keep trying to reach the destination.

AiTravel and AiActivate need a target within 7168 game units distance of the actor's current AI coordinates, otherwise they won't have the desired effect (they can still interrupt the current ai package or combat and make the actor return to their AI coordinates). These coordinates are reset when the actor is given new Ai targets, or when the actor follows the player (with AiFollow - pursuing in combat doesn't count as changing Ai coordinates). If the actor has never been moved by Ai functions, these center coordinates are the actor's default initial position (if the actor was placed in the CS in the same cell with a wander package) or the actor's current position if it was created ingame.


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.

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.

ExplodeSpell also works for "on self" spells, not only "on touch".

Scripted casting can be used by most objects, like activators and (persistent and unique) 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.

Scripted casting should not be used by LIGHT type objects, because it triggers a looping cast sound that cannot be stopped. The issue is probably related to the option those objects have for looping fire sounds.


Should never be used in Persuasion results: it will only direct to whatever topic was used last.


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.

Disable resets a NPC's voice line delays. A NPC that is disabled and enabled again (that can be done invisibly in the same frame) gets immediately prompted for a Hello voice if the player is within distance of the NPC's Hello parameter.


  • Note that objects created by these functions are not 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."


ForceGreeting allows you to bypass limitations for hostile NPCs (that you normally cannot talk to by activating them), but it still will not make you talk to a NPC that is knocked down or knocked out – "knocked down" comes from high weapon damage, "knocked out" starts with the first frame fatigue becomes negative.

Note about scripted animations: these states can only be triggered by damage or negative fatigue and not by scripted KnockOut or KnockDown animations - see Playgroup, Loopgroup; however, if the actor's fatigue was or becomes negative during a scripted animation, they will remain in a knocked out state during the animation even after their fatigue has become positive, until the scripted animation is finished and normal AI animation begins, or until changing cells or reloading the game. The same goes if the actor was in a knocked down animation before a scripted animation started.

ForceSneak, ClearForceSneak, GetForceSneak

Not usable on creature actors, even ones that use NPC animations or have the appropriate animations themselves.

ForceRun, ClearForceRun, GetForceRun, ForceJump, ClearForceJump, GetForceJump, ForceMoveJump, ClearForceMoveJump, GetForceMoveJump

Similarly not usable on creatures, even ones that use NPC animations or have the appropriate animations themselves.


Note that the value GetButtonPressed returns is consumed on use – the value HAS to be saved in a separate variable to be able to detect which of several choices was used – and only one script should check GetButtonPressed at a time. If several scripts check it, only the first would get a value other than "-1".

Due to this, you shouldn't start an arbitrary MessageBox with choices during MenuMode (unless it is triggered by inventory or dialogue actions: those cannot happen during a different MessageBox). As a rule, a MessageBox with choices that is delayed or triggered by a timer should always wait until the player is out of MenuMode.


Can never be 5 – Pursue is never an active AI package.


  • This 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 to the end (a scripted death animation does not count, 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.


GetItemCount can detect leveled lists if they have not been resolved yet. Resolved refers to when the list's conditions and chances are applied, at which point it is replaced by items if any. For NPCs, leveled lists are resolved as soon as the NPC is loaded (when entering the cell they are in). For containers, leveled lists are resolved only when the container is opened.

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) does not count as sleep. To detect it you can instead check if ingame time (GameHour) changes within MenuMode.

GetStat, ModStat, SetStat

GetStat, SetStat, ModStat, GetSkill, SetSkill, ModSkill... do not have any effect or return any value on actors that have not been loaded in the past 72 hours. The effects are also reset after 72 hours (see The "72-Hours Bug").

GetWeaponDrawn, GetSpellReadied

Both always returns 0 on a creature that doesn't use biped animations.

GetWeaponDrawn seems to return 0 until weapons are actually attached in drawing animations. This explains the few frames' delay in the note which will also more generally apply when switching between weapons.


When a lockpick or probe was equipped, GetWeaponType would return 0, the same as if a short blade was equipped. This could cause problems with weapon detection.

With the Morrowind Code Patch, GetWeaponType returns -2 for lockpicks and -3 for probes.

GetCollidingActor, GetCollidingPC, HurtCollidingActor

Does not trigger when colliding with the top of an object: see GetStandingActor, GetStandingPC, HurtStandingActor.


Does not work on containers, only actors. Returns not only 0 or 1, but the number of soul gems of all types that have the specified soul.


About giving messages with one or more choices during MenuMode: a MessageBox with choices is generally safe when it is triggered immediately by a player action (like doing something in the inventory or showing a dialogue result), because those could not happen with another choice message already on the screen. Otherwise, you should take care not to trigger the message with choices during MenuMode, because there could already be a different one. See also the warning about GetButtonPressed.


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 ModRegion and of NPC functions Mod/SetFight, Mod/SetHello, Mod/SetFlee, Mod/SetAlarm, AddSpell, RemoveSpell, SetHealth which record all of the actor's base record properties into the save, even if there is no change or no existing spell to remove.

ModHello, SetHello

Note that Hello must typically be set to 0 during scripted NPC tasks, which can otherwise be interrupted for a dozen seconds by Hello.

While the Hello parameter cannot force a voice greeting, a NPC that is disabled and enabled again in the same frame gets immediately prompted for a Hello voice if the player is within the parameter's distance.

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. Do not 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 cannot rely on console tests.

For GetDistance not updating with Move, see the note in MSFD on GetDistance.


  • "An item is usually activated when you press the spacebar to "use" it": it is 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 is 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.


Detects objects dropped on the floor only, not in a container. Manual player dropping only: the Drop function does not trigger OnPCDrop.


OnPCHitMe cannot register the first 4 "friend hits" on a follower.

Note: there is a Friend Hit dialogue filter for voices, although voices are not a reliable workaround for this and cannot be triggered during another voice.

PlaceAtPC, PlaceAtMe

Note that objects created by these functions are not affected by ingame lighting until the player exits and reloads the cell. Does not apply to actors.

For creatures with particle effects: when a creature is created ingame by a script (as opposed to a creature or leveled list placed in the CS), constant particles that ignore parent animation and trail behind the parent (example: flame atronach body particles) are not 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.

Static objects placed with these functions first have no collision until the cell is reloaded; having enable called on them is one way to force their collision to work.

PlayGroup , LoopGroup, Animating objects p69

  • Playgroup leaves the actor frozen after the animation if it does not 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 nor LoopGroup will play the parts of animations between loop stop and stop
  • They play all animations except those of the root bone, which means the actor will animate in place and not move or rotate even if the animation is supposed to change their coordinates (like WalkForward).
  • Only animation groups without spaces in their name can be called by these functions (the weapon attack or weapon follow groups cannot).
  • 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.
  • On paralysis: however, a vanilla engine trick can use paralysis to freeze the scripted animation by ending it: first paralysing the actor like with Setparalysis 1, then PlayGroup with the desired anim, then PlayGroup idle during the scripted anim, which leaves it frozen in its current pose.

PlaySound, PlaySound3DVP

The volume parameter must be between 0.0 and 1.0, the pitch parameter can also be over 1. Changing pitch does change the speed at which the sound is played.

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 where that position isn't reachable, it can look like the NPC is jittering). NPCs may return to previously given AiWander positions after PositionCell if a new Ai target is not given.
  • For player: when positioning the player into a different cell, the condition chain that PositionCell is inside of can lose its state: the script resumes executing where it stopped, it does not restart from the beginning, but it forgets already checked conditions in the chain so any "else" or "elseif" won't work properly. You should either add Return at the end of the condition block that uses PositionCell, or divide the condition chain after that block with Endif. The bug always happens when teleported into a cell that was never loaded before, but it can also happen in already loaded cells if nothing in the cell was edited in a special way before (unlike with other bugs, calling Enable or Disable on objects does not count as edits for this, looting objects or killing actors do not count as edits either, but changing a FRMR's local variable value does, if it is not changed back to 0). A similar issue exists with ForceGreeting. Example script for the issue:
   short doonce
   if ( doonce == 1 )
   elseif ( doonce == 0 )
       set doonce to 1
       player->PositionCell 0 0 0 0 "Ghostgate, Tower of Dusk"
       MessageBox "you should not see this message but you will"
  • For non-actor objects: when PositionCell is used, if the object instance was already in the destination cell, the new position will not 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 or having one of its local variables set. 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.


The values of local variables in actor scripts (but not their stats) are kept when they're resurrected. Resurrect will also keep running any ongoing scripted animation (PlayGroup, LoopGroup) from the original actor, but will reset AI packages, like coordinates and wander, to their original default.

New instances created by Resurrect also seem to preserve the full ingame ID of the actor (including the last 8 digits), which means that a resurrected actor can be targeted by compiled scripts that use its ID, just like the original instance (and unlike duplicate instances created with functions such as PlaceAtMe).

SetAngle, GetAngle

SetAngle X, Y does not relate to X, Y angles in the Construction Set because the SetAngle function does not 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: The Morrowind Code Patch optional feature (not a fix enabled by default) 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.


  • Objects marked for deletion are deleted immediately if they are new instances that were created while in the game (such as with PlaceAtMe), or deleted upon loading a saved game if they are instances defined in a plugin/master file. In the latter case, the player may already no longer collide with that object upon re-entering the cell.
  • If an object is effectively deleted while colliding with the player, the engine will crash.


  • 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 will not 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 cannot 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.


For objects with particles: in the vanilla engine, the effects of changing scale will apply twice for the particles (probably because the change applies both from the particles node and from the emitter), but for some objects (if the particles node does not have 128 in flags, example: light_torch_01) the particles will have no scale change until the game is reloaded. For objects that are made of more than particles, large changes to scale will look bad since the effect will be squared for particles compared to the rest of the object (fire will be too big for the shaft of a scaled up torch, or too small if scaled down).


Only skips frames of "natural" animations, does not interrupt scripted animations called by PlayGroup or LoopGroup. (See the earlier comments on PlayGroup and LoopGroup for a trick that does freeze scripted animations.)


TypeEnum values 8 and 9 are added by Bloodmoon (snowy weather and blizzard).

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.
  • Similarly, triggering StartCombat in the results of a Hello voice forces the inventory menu open and gives the "Trying to RunFunction index greater than function count" error, potentially leading to more AI breakage for the NPC/current cell.
  • The abovementioned errors amount to the engine trying to run a result script within a result script. The issue gets fixed by the current BETA VERSION of MCP 2.5b12, but is not in the stable version used by the public as of this writing [check if this is the case now].

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 is not an actor. When used in dialogue results though (non-compiled script), any existing instance will work, even if it was created in-game 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 have not 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 will not be able to see its own variables until it is 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 is true that forgetting where you have 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 is not supposed to run, especially on disabled objects (being disabled does not 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 is not as slow as truly running them, it is 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 is 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.

The Enchanted Editor link is dead.

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 are not exclusive to TESAME and there are more than the list given there; they are 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.

Sound file formats

p142: naturally the formats are the common 22050 Hz / 44100 Hz, not kHz.

Time related global variables

p143: missing one of the special globals listed on p211 – Float "TimeScale" (30). Sets the ratio of real-time/game-time.

Define random Actor movement

  • Idle8 is supposed to be "pensive", not heartburn.
  • "Human" (not beast) female: also has a different Idle4, without shaking.
  • "Khajiit female" – is all beasts and not female specific.

GetArmorType + dialogue results or Choice

  • GetArmorType seems to generally fail and return -1 in dialogue results if it is 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 is 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 will not work – they cannot 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 will not 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 is 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 is not the first Equip function called on the frame (first AddItem and Equip another item, then Additem and Equip the head/foot-slot item).

Equipment sharing and other companion functions

set companion to 1 also enables inventory sharing if the creature is activated while knocked out: without sneaking, activating the creature when its fatigue is <= 0 will directly open its inventory.

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 is 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, but the exact errors are not searchable in the document because it is missing spaces; search for "RightEval", "LeftEval".

"Left" or "Right" is the side of the statement that is not 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, but that are fixed by the most recent versions of the MCP
  • 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 (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"
  • Testing player blight disease ("Any time the current weather is blight, the player will have blight diseases added to them without any effects"... "make the player effectively immune to those diseases") is fixed by MCP.
  • The 34th local variable is fixed by MCP "Script expression parser fix."