Scripting Tips, Tricks, and Pitfalls

5 years worth of scripting knowledge at your disposal

Throughout my years as a modder, I've learned A LOT about scripting. Probably a LOT more than I ever wanted to know. Morrowind's scripting engine is, to put it lightly, a little lackluster. Often times things simply cannot be done. Sometimes errors occur unpredictably and with no real cause, and then disappear just as quickly. These things can be extremely frustrating to a would-be modder. Luckily, there's almost always a way to outsmart the engine. I hope that these tips and tricks as well as information about pitfalls will help make your scripting experience less painful. Mostly, I'll be focusing on pitfalls so you don't have to waste your time trying something that will never work.

I will assume, for the sake of space, that you are using this guide in conjunction with GhanBuriGhan's Scripting Guide, therefore I will NOT provide information on function arguments and will not restate limitations already stated in his guide. Please treat this as an addendum to his guide!

Also, please understand that, while this guide was written to be as readable as possible, the scripting theory discussed within is very advanced and is aimed at those who already thoroughly understand the Morrowind Scripting Engine. Noobies to the scripting engine may have a great deal of difficulty understanding my guide.

Guide to Navigating the Guide

  • Strong red text denotes a limitation or pitfall that you REALLY need to know about. Pay attention, as these could be, and often are, deal-breakers.
  • Dark red text denotes a casual warning that should be kept in mind, but will not usually change things.
  • Dark green text indicates a workaround. If the specific situation that I state doesn't apply to your script, the workaround may not be of any use to you. Green text will be the most in-depth and hard-to-understand part of this guide, due to the fact that my workarounds often push the limits of both the scripting engine and the modder's patience.


  • Offset, Courier font text indicates a sample script.

    StartCombat

    Perhaps one of the most aggravating commands I've ever dealt with. Here's why:

  • You MUST already have a reference of the object to be attacked placed in the game world in order to call StartCombat on it (or your script won't compile).
  • StartCombat will ONLY target the reference of the object that you placed in the game world. If you placed multiple, it will target only the first.
  • StartCombat will NOT target any references created by PlaceAtMe or PlaceAtPC
  • Trying to work around this by calling SetDelete, 1 on the first reference and then using PlaceAtMe or PlaceAtPC does NOT work.
  • DO NOT rely on the results of console testing with this function! It behaves VERY DIFFERENTLY when called in the console. It works exactly like it should when called in the console, being that it detects duplicated references AND references placed by PlaceAtMe or PlaceAtPC. If this function worked in the engine like it worked in the console, I'd be a much happier camper, and the array of things that could be achieved in mods would be a LOT wider. Too bad.
  • So, if you need to attack something/someone and the location is not predetermined, then you must place a single reference of the object first in some random cell, then call a PositionCell command on it to place it where you need it in order for it to be properly attacked. Which brings us to our next set of concerns.

    PositionCell

    This can be a very powerful function if used properly. Unfortunately, it's got plenty of drawbacks:

  • GhanBuriGhan, in his indispensable scripting guide, stated that it will accept local float variables (after expansion packs were installed). This I have NEVER found to be true. Don't depend on PositionCell being able to work with variables. It's very picky.
  • Positioning an actor in a cell far from their original one will cause some problems. They won't be properly loaded in the current cell, so you either have to exit and re-enter the cell, or make sure that the actor is moved from a position close to their original cell.
  • An actor who has been "celljumped" and not reloaded in the current cell will CRASH the game if activated and most likely will not perform scripted actions as expected.
  • OK, so how could we make the aforementioned situation work, where we need to attack an NPC using StartCombat, but we aren't sure what cell the event will take place in? For the most part, this is impossible to do. However, as with anything else, you can usually find a work around. So try this.

  • Place a UNIQUE target object in every cell in which the encounter is a possibility.
  • Place a script on each of the target objects to either disable them immediately on cell load, or create a new spell that is type "curse" and has effect invisibility, then call AddSpell on the actor. I don't think this will work on creatures.
  • From the attacker script, first detect which cell the encounter is happening in by using GetPCCell with a set of elseifs. Alternatively, you could call a GetDistance to each unique target object to figure out which one is actually valid, i.e. is in the current cell.
  • From the attacker script, call a Position (PositionCell won't be necessary since they are in the same cell) function on the target object. Remember, using exact coordinates instead of variables is reccommended, so you might have to do a little tweaking to get this to work (i.e. you may have to restrict the encounter to a specific area).
  • Make sure you enable the target object and/or RemoveSpell the invisibility curse.
  • Call the StartCombat function from the attacker, and have fun.


  • Sample Target Script


    begin Sample_Target_Script

    short DoOnce
    short Method

    set Method to 1; Use this for the disable method
    ;set Method to 2; Use this for the invisibility method

    if ( DoOnce != 1 )
        set DoOnce to 1
        if ( Method == 1 )
            Disable
        else
            AddSpell, Sample_Curse_Invisibility
        endif
    endif

    end


    Sample Attacker Script


    begin Sample_Attacker_Script

    short DoOnce
    short Method
    short Attack

    set Method to 1; Use this for the disable method
    ;set Method to 2; Use this for the invisibility method

    ;Please note that NONE of the coordinates used in the Position commands are real! You'll have to find those yourself!

    ; set Attack to 1 when you are ready for him to attack
    if ( Attack == 0 )
        return
    endif

    if ( GetPCCell "Balmora" == 1 )
        target_balmoraref->Position,-1657,65789,546,0
        if ( Method == 1 )
            target_balmoraref->Enable
        else
            target_balmoraref->RemoveSpell, Sample_Curse_Invisibility
        endif
    elseif ( GetPCCell "Suran" == 1 )
        target_suranref->Position,3579,2217,840,0
        if ( Method == 1 )
            target_suranref->Enable
        else
            target_suranref->RemoveSpell, Sample_Curse_Invisibility
        endif
    endif

    ; You get the idea, right?

    end

    Variables, Data Storage

    There have been a lot of misconceptions over the years about the best way to store data in-game. Now obviously, if the variable will only need to be accessed locally, i.e. within the object's own script or dialogue with the object, then all you need is a local variable. Declare the variable in the object's script and never worry about it again. But what if we need other objects to be able to access this variable? There are a few ways to do this. We'll look at why you should or should not use each.

  • Keep the variable local and read it externally by using objectref.localvariable
  • Create a dummy NPC and set his health, magicka, fatigue, or any given stat/skill to the value of the variable, and then read it externally using dummynpc->GetHealth.
  • Create a global variable and then read it externally as you normally would (the difference is that you don't have to declare it locally since it's already declared globally).
  • Create a dummy object and set it's scale to the value of the variable, then read it externally using dummyobject->GetScale
  • From that list, one method is correct and three methods are incorrect. I'm sure you can probably guess the correct method. That's right, it's the global variable method. Let's look at why each of the other three are incorrect.

  • Local Variable: You just can't depend on being able to read a local variable of another reference from an external script. GhanBuriGhan said that it was impossible and would not work in his guide, unfortunately, this is incorrect. Sometimes, it WILL work using the above method, which can lull you into a false sense of security. But it's not dependable! Sometimes it will cause Right/Left Evals, other times it just won't work. Don't do it.
  • Dummy NPC: Wastes a lot more memory space than you need to waste, besides, it's virtually the same as creating a global variable...but in this case you're creating more like 50 global variables, since there are MANY variables linked to NPCs that must be persistent throughout the game.
  • Dummy Object: Scale will reset on load, object must be persistent anyway (which == the creation of around 5 globals). Just don't do it.
  • So if you have to use globals, how often should you try to avoid using them? In truth, never. My newest Farmer Mod will make use of somewhere around 200 globals variables. I ran a test .esp created by a macro that inserted 10,000 new global variables into the game. The result? No detectable slow down, not trouble reading/writing to the globals, all is well. If you want proof, the file is here. In summary, don't be afraid of global variables!

    "Targeted" Scripts

    In his guide, GhanBuriGhan introduced the concept of "targeted" scripts. These scripts were global scripts run on an object. In other words, a local script could start a global script by calling StartScript, "scriptname". The difference, however, between these targeted scripts and a normal global script, is that functions like GetHealth, AiTravel, or StartCombat all worked inside these scripts and treated the functions as if they were called on the local object that originally called the script. To be sure, this provides on of the most immensely useful tools at your disposal as a scripter. Learning to use targeted scripts can be the key to overcoming many of the scripting engine limitations. There are, however, some warnings/limitations:

  • Only one instance of the same targeted script can be running at any given time - meaning only one object can call the script at a time. We'll look at ways to overcome this later.
  • The script will continue running until StopScript is called, so remember to call it when you're done with whatever you needed the script to do.
  • Targeted scripts CANNOT access the local variables of the calling object.
  • One of the problems encountered with targeted scripts is the issue of accessing local variables. Although targeted scripts can call functions properly, they CANNOT access the local variables of the calling object. Targeted scripts essentially have their own set of local variables, completely separate from the calling object. This makes sense, and also provides opportunity in itself. It does, however, make the issue of transferring data between the calling object and the targeted script difficult. Here are some workarounds to try:

  • Use global variables. Set the global variable to the desired value in the calling object's script right before it calls StartScript, the read the value in the targeted script.
  • Use health, magicka, fatigue, etc. of the calling object to transfer values (SetHealth to write, GetHealth to read).
  • Use item counts to transfer values. AddItem to write, GetItemCount to read.
  • Though all of these methods will work, I find the item count method to be particularly useful. Why? Well, in some cases, globals actually won't update their values fast enough for the targeted script to read properly. I haven't studied this thoroughly, but that's the conclusion that I draw from seeing the global variable method fail in some applications. The item count method is best for transferring small values, i.e. the value of a variable that could only have 3 possible values. It is NOT effective for transferring a large value. Here's how to do it:

    Let's say we want to transfer the value of the variable State to the targeted script. State has three possible values: 1, 2, and 3.
  • Create 3 new lights. Name them something like transfer_state1, transfer_state2, and transfer_state3. Set the radius to 0 and make sure "Can Carry" is unchecked. Set the color to black.
  • Why use lights? Isn't that a bit strange? Yes, but lights will NOT show up in the inventory of the object. Imagine that an NPC running this script gets killed and the player opens the corpse. He sees random misc. objects with strange scripting labels. Weird, huh? Well, with lights, he won't see them. They don't appear in the inventory.
  • In the calling object's script, perform three checks for the value of State. If it's 1, add the object transfer_state1. If it's 2, add the object transfer_state2, etc.
  • In the targeted script, perform three GetItemCount checks. One for transfer_state1, one for transfer_state2, and one for transfer_state3. If the count is greater than 0 for any of them, set State to the appropriate amount.
  • Now let's say that you want to run targeted scripts from multiple objects at any given moment. You already know that you can only run one instance of a global script at a time. So how do we work around this? Make sure that only one is called at a time. Use a global variable to determine if the script is "busy", or is currently being run. Alternatively, if you need "high bandwidth," so to speak, on these scripts, you can create copies of the same script with different names (target_script_1, target_script_2, etc.) and call them separately. As long as they have different names, they can be running at the same time. Now let's combine the workarounds:

  • Create 2 extra copies of the targeted script with different names. You should have 3 copies total. This method will work with however many copies you want, it just depends on how many objects you think will need to run the script simultaneously.
  • Create 3 global variables to keep track of which scripts are running. Each global corresponds to one of the copies of the targeted script.
  • Create a simple if/elseif case list in the calling object script that checks for a free targeted script instance (one that isn't running) and then executes the script. Remember to set the global variable to 1, to indicate that the script is now in use, before calling StartScript. See the example below if this isn't making sense.
  • In each of your targeted scripts, make sure that they reset their corresponding global variables to 0 AND call StopScript when they are finished processing.


  • Sample Calling Object Script


    begin sample_callingobject

    ; This simple demonstration will wait until one of the
    ; global targeted scripts is free, then will run it once.
    ; It will return after the targeted script has been run once.

    ; A good example of this situation would be if you
    ; had a target script that randomized all the attributes
    ; of an NPC; you only want to call it once.

    short RanScript

    if ( RanScript == 1 )
        return
    endif

    if ( Global_IsRunning_Script1 == 0 )
        set Global_IsRunning_Script1 to 1
        set RanScript to 1
        StartScript, "sample_TargetScript1"
    elseif ( Global_IsRunning_Script2 == 0 )
        set Global_IsRunning_Script2 to 1
        set RanScript to 1
        StartScript, "sample_TargetScript2"

    elseif ( Global_IsRunning_Script3 == 0 )
        set Global_IsRunning_Script3 to 1
        set RanScript to 1
        StartScript, "sample_TargetScript3"
    endif

    end


    Sample Targeted Script


    begin sample_TargetScript1

    ; This simple target script will randomize the calling NPC's
    ; health to a value between 50 and 150. Notice that at the end,
    ; it resets the IsRunning variable and stops itself.

    ; When making copies of this script, you would have to be sure
    ; to change the numbers at the end of the global and at the
    ; end of the script name to match the number of the script your
    ; working with.

    float MyRand
    set MyRand to random, 100
    set MyRand to ( 50 + MyRand )
    SetHealth, MyRand

    set Global_IsRunning_Script1 to 0
    StopScript, "sample_TargetScript1"

    end


    Running From Nowhere!

    Recently one of the strangest things ever to happen to me in my years of scripting happened during the development of the Farmer Mod. A particular targeted script started firing from nowhere. It fired when it wasn't supposed to be firing, at completely random and unexpected times. What's worse, when I added the line SetHealth, 0 to the top of my script, I realized that the suspect actor whom I thought had been misfiring it was actually taking no part in running the script. It's extremely unlikely that this will happen to you. But if it does, here's what to do:

  • Go to the scripting editor and click delete. Now select the script that is randomly firing. If the editor asks you if you are sure and tells you that the script is used x number of times, you've probably just found the culprit. Targeted scripts aren't attached to objects in the editor, they are fired from other scripts, which the editor does not take into consideration when it cites the number of script references. This means that somehow, due to an editor glitch (or human error), your targeted script has actually been attached to objects.
  • Go through each tab in the object window, sort by script, and try to find any objects that possess your targeted script. If they do, bingo, remove the script from the object and continue the process until you have found all x references cited by the editor.
  • Make sure you restore your deleted script by deleting it again if you actually confirmed the deletion in the first place.
  • Why would the editor randomly attach a seemingly random script to seemingly random objects? I'm not sure, but it's almost certainly the strangest thing ever to happen to me in the CS. In my case, the script had attached itself to no less than 6 other individual objects. What the heck!?!

    Right Eval, Left Eval, Infix to Postfix

    Certainly the most dreaded errors one will encounter in scripting are 'Right Eval,' 'Left Eval', and 'Infix to Postfix.' These errors do not show in the compiler, rather, they will only spring up when the dirty line of code causing them fires. Which means, in many cases, one may be able to successfully test much of a script until a certain area fires. In his guide, GhanBuriGhan cites specific causes for these errors that may cause them. I have always found, however, that differentiating between the three errors is virtually useless, as any dirty line can seemingly cause any of the three dreaded errors. Here are some tips if you find yourself in a predicament with one of these errors:

  • 34th variable - make sure you NEVER use the 34th variable declared in the script. Shorts and floats count separately, so it's helpful to keep the short declaration section separate from the float declaration section, so that counting each is easier. Declare the 34th variable as "DontUse" or something similar, to remind yourself not to use the variable. Attempting to set a 34th variable to any value will almost certainly yield an error. Usually a Right Eval.
  • Undeclared Variable - when you're dealing with a lot of different scripts and use similar variable names among them, especially temporary variables, it's easy to forget to declare a variable, and luckily, the compiler will usually catch it. However, if the variable has been declared in ANY other script in the game, the compiler will treat it as declared and will not give a compile error, even though the variable hasn't actually been declared in the script. This will usually yield a Right Eval.
  • Together, these two make up about 90% of the evals that I see occurring in my own scripts.