×
Меню
Индекс

MSFD My first Scripting tutorial

If you are completely new to scripting and programming in general, the thought of using TES script might be a little daunting – I have therefore written an extended tutorial that will walk you through making your first script. I will also explain the main elements of the scripting language as we go. There will be other explanations on the way, but the key instructions will be in bold print.
 
Let's get going!
We start by opening the script editor: Start up the TES Construction Set, open the Morrowind.esm file and then select Edit Scripts from the Gameplay menu to open the scripting window.
The scripting window
You enter the script editor either by selecting Gameplay – Edit Scripts from the menu, by clicking the edit script button (the pencil) in the taskbar or by accessing it from an Object or NPC dialogue, by clicking the button with the ellipses […] next to the script field. The editor window is pretty basic:
 
 
 
Let's have a look at the buttons in the taskbar, from left to right: Open lets you select a script to edit. Save error checks the current script and compiles it or gives out error messages – note, however, that the plugin and thus the script is not really saved to disk at this time. When programming large scripts frequently use the save command in the main TESCS window after you have saved the script here, just in case the TESCS crashes.
Note that if you edit the script and suddenly hit "save plugin" to backup in the middle of the work, your updated script will NOT be saved with it. You must save it manually first. Also, if you just close script window, it doesn’t mean that script will be saved. You must take care of it yourself (Thanks to Kir for this tip).
Forward and Backward arrows jump to the next or previous script, respectively (alphabetical order). If you give your scripts a common tag, that will make it easier to jump between the different scripts of your project, e.g. start every script name with AA_Scriptname this will put them right at the beginning of the list and keep them neatly together. Compile all recompiles all scripts (what's this good for? I don't really know). Finally, the delete button deletes a script and the last "arrow down" button closes the script window.
The help menu gives quick access to the function and command pages of the helpfile (of moderate utility, hence the creation of this manual!)
You can cut, copy, and paste from and into the editor window by using the Windows standards ctrl-c for copy, ctrl-x for cut and ctrl-v for paste.
What do we want?
Before we really start writing our tutorial script we should decide what we want it to do. For this tutorial I decided we are going to make a Riddle Chest: The chest will ask a riddle and only the right answer will open the chest. If the player provides the wrong answer, a trap will go off, hurting the player, and the chest can't be opened.
That’s a fairly complex undertaking, but we will take it step by step.
Writing a script
Ok, once you've got the Edit Script window open, click into the main part of the window. That is where you will write your script.
Naming a script: Begin and End
First of all we must give our script a name – every script must start with the declaration of this name. So please type:
 
Begin my_first_script
 
into the editor window. Note the underscores: Your name should be one word. Also note that the script language is not case sensitive, so Begin could also be written without the capital letter: begin. This name is the handle by which the script will be known in the TESCS. Try hitting the save button now: you will get an error message about "you need to end your script with end scriptname.
So, for the editor to recognize the script we also need to indicate an end: next, write
 
End
 
in a new line below the above. As you see we can omit putting the name of the script in this line again, just end will do. When you hit safe now, you will see the name appear in the title bar of the script editor, indicating that the script has been accepted. This is the shortest script possible - and of course it doesn’t do anything at all.
Detecting an action by the player
Next, we need a way to determine whether the player tries to open the chest. In TES Script we distinguish between Objects, Functions and Commands –
Objects are all the things in the game world, be they visible objects, creatures, NPCs or just sounds.
Functions are the all the "words" of TES Script that let us either manipulate these objects or let us gather information about them.
Commands are those "words" that structure the scripting language, but do not operate on any Game objects – an example is the word "Begin" we used to tell the script editor about the name of our script.
To tell the game which object it is supposed to perform a given function on we can use the "arrow", or "fix" : -> (really just a hyphen and a greater-than sign). You specify the object for the function on the left (we also call this the calling object) and the function to be performed on the right:
 
Object_ID->function, [parameters]
 
A function may or may not have parameters. Parameters could be other object ID's, numbers and in some cases, variables.
 
What we need for our riddle chest is the OnActivate function: this is an informative function that tells us whether the player has "activated" an object in the game world or not. This function returns a value of 1 (which means "true" in programming terms) if the object has been activated, that means the player targets it and presses the "use" button (space, by default). So what we need to do is check if OnActivate becomes "true" anytime in the game. So edit your script to look like this:
 
Begin my_first_script
 
If ( OnActivate == 1 )
     ; here we will enter what happens when the chest is opened
endif
 
End
 
A couple more things need to be explained here: The "if" command is there to check a condition – whenever the expression in the parentheses is "true" the following lines of code will be executed until the "endif" command is encountered. The "==" checks if an expression (in our case the "OnActivate" function) on the left of it is equal to the expression on the right of it (in our case to 1). If you forget the endif command after an if command, the editor will complain with an error message. The ";" semicolon denotes a comment – whatever you write behind the semicolon will be ignored when the script is run. If you ever write larger scripts you should learn to love this possibility.
Writing text and obtaining decisions from the player
Now we want our trapped chest to ask the player a riddle. For this we use the MessageBox function that allows us to display some text on the screen and also to display choices that the player can select from. Unfortunately Morrowind has no option to have the player type in the answer to our riddle, so we will have to give multiple choices. The line for that could read:
 
MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith"
 
The first text is the text actually displayed in the box, the other texts, separated by commas tell the game to make "buttons", with the text given displayed.
But how do we ensure that the riddle is asked only the first time we try to open the chest and not every time? We now come to a very central point: the use of do-once conditions and state variables. Most of the problems that beginners encounter with scripting for Morrowind have their roots in misunderstanding how the scripts are actually executed and how scripts should accordingly be structured. So let's have a look at this.
How local scripts are executed
Every script that is attached to an Object or an NPC (local script) is executed every frame the game displays on screen while the cell with the object is active (indoors only the cell the NPC is currently in is active, outdoors the PC’s cell and all adjacent cells are active). So the complete script (not just one line of it) is executed 10-60 times a second or however fast your computer runs the game! It is best to imagine every local script wrapped in a big “while-loop”:
 
while (Object is in active Cell)
 
[Your script code]
 
endwhile
 
This is the reason why the following script spits out a continuous stream of messages (if attached to an Object or NPC in the same cell as the player). Try it, if you want:
 
Begin Message_script
 
MessageBox "Thousands of useless Messages"
 
End Message_script
 
This example is relatively harmless, but imagine what happens if you would use a line of code that adds an item to the players inventory, or places a monster next to him, etc.!
For this reason, “Do Once” constructions are very essential and something you will probably use a lot while scripting for Morrowind. So, let's go on with our tutorial script: we need to declare a variable and use it to make sure the message is only displayed once. Change the script to the following:
 
Begin my_first_script
 
Short controlvar
 
If ( OnActivate == 1 )
If ( controlvar == 0)
          MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith"
     Set controlvar to 1
endif
endif
 
End
 
(Please note that the MessageBox command should be in one line in the editor!)
 
"Short controlvar" declares a new variable I called "controlvar", of type short. For the moment it's enough to know that this is variable that will contain integers (whole positive or negative numbers). A variable is a "placeholder" that can take on different values. The if command we already know, the set command is new, but simple enough – it sets our variable that had the value 0 before (all variables start out at zero when declared) to 1. This, in connection with the if  ( controlvar == 0 ) command provides a do-once condition – the next frame the script is executed after the variable was set to 1 the if condition will be false and the message box will not be displayed again.
Now our script is already capable of being run, so lets test it:
•     Save the script and close the script editor window.
•     Go to the TES construction set, Object window, select the container tab and open "chest_small_01".
•     Change the ID of the chest to "tutorial_chest"
•     In the script dropdown field, select my_first_script
•     Save the object as a new object, save the mod, and quit the construction set. Start Morrowind and load a savegame.
•     Now bring down the console (usually the ~ key, or whatever you have to the left of the "1" on the main keyboard) and in the console window, type:
PlaceAtPC tutorial_chest 1,1,1
and hit return.
Take a step back (err, let your player character step back, that is!); you should now have a little chest sitting on the floor right in front of you. Clicking on it should bring up our message, which should look like this:
 
 
 
Clicking on those buttons will just close the messagebox for the moment, and clicking on the chest again, nothing should happen either – which is good, it means our do-once condition works.
Ok, leave Morrowind and go back to the editor, and load your plugin again.
We now need to figure out which answer the player selects, and script appropriate reactions for right and wrong answers. The function to test the selected answer is "GetButtonPressed". This function returns a number depending on which of the buttons of a message box has been clicked on with the mouse. It will return "0" for the first button ("bat" in our example) and 1, 2, 3 etc. for the following buttons, in the order you listed them in the messagebox function. While no answer has been selected, the function will return –1, so we have to take care of that, too.
The "Activate" function will make our chest open, in fact activate will simply trigger the standard action that would usually be performed when you "use" the selected object – e.g. doors would swing open, NPCs would initiate dialogue etc.
The following update to our script also demonstrates how you can use a control variable to force MW to process functions one after the other, although the complete script is processed every frame of the game: simply increment the control variable and test it in a series of if – elseif statements. This is a very safe way of scripting for MW – it may not always be necessary, but it's safe.
 
Please edit the script to the following:
 
Begin my_first_script
 
Short controlvar
Short button
 
If ( OnActivate == 1 )
If ( controlvar == 0)
     MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith"
     Set controlvar to 1
elseif controlvar > 1
     activate
endif
endif
 
if (controlvar == 1)
     set button to GetButtonPressed
     if ( button == -1 )
          return
     elseif ( button == 2)
          MessageBox "The answer was correct"
          Activate
          set controlvar to 2
     else
          MessageBox "The answer was wrong"
          set controlvar to -1
     endif
endif
 
End
 
Take a look at the part that starts with "if (controlvar == 1)". We have set controlvar to 1 as soon as the chest got activated. Now we test for which button is being pressed. We do this by assigning the new variable "button" a value returned by GetButtonPressed. Since the script is still running, even while the game seemingly pauses to await your decision, we first test if no button has been selected yet – return tells the game engine to stop processing the script for this frame.
Our correct answer was "wind" which corresponds to button number two – if button number two gets pressed, we will tell the player that he gave the right answer, and "activate" will open the chests inventory in the usual way.
All other values of button mean that the player has selected a wrong answer, so we can use the "else" command here. In this case we tell the player what a fool he was and the chest is not activated.
Now look at the little addition at the top of the script:
 
If ( OnActivate == 1 )
If ( controlvar == 0)
     MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith"
     Set controlvar to 1
elseif  controlvar > 1
     activate
endif
endif
 
This means that, whenever the chest is activated in the future, it will only open if controlvar is greater than 1. Check above: when the player provides the wrong answer in the riddle, controlvar is set to –1, so he will never be able to open the chest. But if he knew the right answer, controlvar is set to 2, and from now on the player can open the chest as often as he likes. Save and run your plugin, and test as described above.
 
Your first bug
Now, you will probably have noticed that the script does almost – but not quite – what we wanted. After clicking the correct answer, the chests inventory doesn’t open as intended. Now, the logic above seemed fine, so what is wrong? Let's try the following (change the corresponding part of your script according to the fragment below):
 
if (controlvar == 1)
     set button to GetButtonPressed
     if ( button == -1 )
          return
     elseif ( button == 2 )
          MessageBox "The answer was correct"
          set controlvar to 2
     else
          MessageBox "The answer was wrong"
          set controlvar to -1
     endif
elseif ( controlvar == 2 )
     Activate
endif
 
See how I moved the activate command to the section that tests for controlvar == 2? This provides a cleaner sequence of events, and as I mentioned above, this can be very important when scripting for Morrowind – always try to avoid doing too many things at once! Well, run and test it.
Great, now the inventory opens as we wanted, but what is this? The cursor is real slow, and we can't close the inventory! Look above – controlvar was set to two, and remains there, we do not change it again – therefore the game now gets continuous "Activate" commands each time the script is processed (every frame)! That’s why we can't close the inventory – it gets reopened immediately. So change the following part of the script:
 
elseif ( controlvar == 2 )
     Activate
     Set controlvar to 3
endif
 
Test again: now everything works the way we wanted. I hope I have not confused you with the above excursion into the process of debugging, but it is a very important thing to know about – you will constantly have to rethink your scripts and try different ways of doing it to be successful.
 
What is still missing? The trap effect of course!
Putting a spell on the player
Our chest will put a curse on the player if he fails to answer the riddle.
First go to the spellmaking tab in the editor, right click and select "new". Give the spell the ID "Frost_Curse", name it "Frost Curse" and make it type "curse" then give it a magnitude of e.g. 1-5. It should look like in the picture below.
 
 
 
 
Now, we need to put this curse on the player. For this we use the AddSpell function. After some time we will remove the curse again using the RemoveSpell function, for this we need to make a timer. Edit your script again:
 
Begin my_first_script
 
Short controlvar
Short button
Float timer
 
If ( OnActivate == 1 )
If ( controlvar == 0)
     MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith"
     Set controlvar to 1
elseif  controlvar > 1
     activate
endif
endif
 
if (controlvar == 1)
     set button to GetButtonPressed
     if ( button == -1 )
          return
     elseif ( button == 2)
          MessageBox "The answer was correct"
          Activate
          set controlvar to 2
     else
          MessageBox "The answer was wrong"
          Player->AddSpell, "Frost_Curse"
          set controlvar to -1
     Endif
elseif ( controlvar == 2 )
     Activate
     Set controlvar to 3
elseif ( controlvar == -1 )
     Set timer to ( timer + GetSecondsPassed )
     if timer > 10
          Player->RemoveSpell, "Frost_Curse"
          set controlvar to -2
     endif
endif
 
End
 
Let's go over this. Player->AddSpell, "Frost_Curse" puts the curse we created earlier on the player. Note how we need to use "Player->" to make sure the effect really targets the player. Otherwise we would curse the chest (which is the default object, because the script is attached to it), which wouldn’t make a lot of sense…
 
This bit:
 
Float timer
Set timer to ( timer + GetSecondsPassed )
     if timer > 10
 
…that is how you make a timer in Morrowind. GetSecondsPassed is a function that returns the time in seconds that has passed since the last frame. Since this is usually a fraction of a second (as the script gets called every frame), it is only natural that we need a float variable for this purpose – a variable that can store numbers with decimals. So when the timer has been running for 10 seconds we remove the curse again, and make sure we do this only once:
          Player->RemoveSpell, "Frost_Curse"
          set controlvar to -2
 
Ok, save and test your mod. Works fine now, doesn’t it? Well, almost. Try the following: let yourself be cursed, and then open your inventory. Wait. See how the curse terminates after some time, without hurting you? Of course: the script is still running, but spell effects are only calculated while in game, not while you are in the menu. We don’t want the player to get off the hook so easily, so we need to put something in our script that stops it from processing when we are in menu mode. Luckily there is the MenuMode function, which returns 1 when you enter the menu. So we can put this at the start of our script:
 
If ( MenuMode == 1 )
     Return
Endif
 
Remember, return tells the game to stop processing the script for this frame.
Ok, now we have our final working script. Congratulations! If you want, experiment a little more with this script: Put the chest into a location in the game and lock it. Then try unlocking it (Unlock function) with the script in addition to activating it. Try adding a sound, when the riddle is successfully solved (e.g. PlaySound3D, "skillraise"). Try using the "Cast" function instead of "AddSpell". Previous users of the Tutorial have spotted a potential bug in it: what happens if the player leaves the area with the scripted chest before the curse is removed again? How would you fix this?
 
This is the final script:
 
Begin my_first_script
 
Short controlvar
Short button
Float timer
 
If ( MenuMode == 1 )
     Return
Endif
 
If ( OnActivate == 1 )
If ( controlvar == 0 )
     MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith"
     Set controlvar to 1
elseif ( controlvar > 1 )
     activate
endif
endif
 
if ( controlvar == 1 )
     set button to GetButtonPressed
     if ( button == -1 )
          return
     elseif ( button == 2 )
          MessageBox "The answer was correct"
          Activate
          set controlvar to 2
     else
          MessageBox "The answer was wrong"
          Player->AddSpell, "Frost_Curse"
          set controlvar to -1
     Endif
elseif ( controlvar == 2 )
     Activate
     Set controlvar to 3
elseif ( controlvar == -1 )
     Set timer to ( timer + GetSecondsPassed )
     if timer > 10
          Player->RemoveSpell, "Frost_Curse"
          set controlvar to -2
     endif
endif
 
End
 
How to learn more
After this tutorial you may ask yourself how to continue with learning how to script. A good way is to look at the example scripts in this guide or at scripts that are already in the game (either form Bethesda or from mods). Try to find a script that is similar to what you want to do and then copy the script and change it to fit your needs. Read the general information below and the descriptions of the functions you may need to do what you have planned. The ordering of the functions into thematic groups should help you to find the right ones.
Finally, the official forums are a great place to find information (use the search function) or to get help on a specific problem. The rest is practicing, practicing, practicing