Learn to script! (Tut post, Very long, with pictures!)

Started by Cruzel, September 27, 2008, 07:48:19 PM

Previous topic - Next topic

Cruzel

People here on EFU:A are very creative individuals, but not all of you are computer savvy, and I've seen more than one of you expressing interest in making things in the toolset, but you're shit with scripting. Hopefully after this post you'll have a basic and easily buildable understanding of how to script.

[hide="Part 1, variables, basic functions, and adding your content to a module"]
This will be a somewhat long post, and hopefully informative.

This will sort of be a follow along thing. You can simply read it if you want, but I have learned it is so much easier to learn from a set of instructions if you actually carry them out as you read them. As such, you will need to have your toolset open for this.

Once you have your toolset open, you will want to click here;



Once you have this window open, a window should pop up. It might take a few seconds, but when it comes up it should look like this;



Let's take a minute to explain this whole window, before we get into the nerdy part. Let's call this window the DE, or Development Environment

To explain the poor labels;

1; The main window of our DE, where we enter all of our code.

2; The toolbar, Right now we only need to pay attention to the left four; Save and compile, Open new, Open existing, and close. The three to the right are also saving buttons, but let's worry about that later on.

3; Possibly one of the most useful parts of the toolset. Think of this list as your own little scripting library. It contains all of bioware's default functions, constants and variables. The box on top of it allows you to type in text, and the list will shorten to all matches to your text, even if partial. For example, say you wish to find a function that you don't know the exact name for, but know it starts with 'Get'. You type 'Get' in this box, and the list will display all the commands that have 'Get' anywhere in their name. We'll come back to this soon, though.


That's the DE in a nutshell. Let's move onto some real code.

As soon as you open the toolset, you will see


Void main ()
{

}

Let's break this down.

Line 1; Void main ()

Void is basically telling the computer that you are beginning a function.
'Main' is the name of this function. the two brackets are in place for any arguements, in this case, there are none so the brackets are empty. Every script needs a 'main' function, but you can have more than one function in a script. We'll get to that later though, for basic stuff you will only need the main function.

It is important to note the difference between a '(' and '{' in programming. a '(' is used in commands to define variables, arguements, or other user defined values, with some exceptions. A '{' is generally used to separate blocks of code, marking the start and end of functions, and in If.. else if..End if Selection statements . Most of that was probably gibberish to you, hopefully it will become clear soon.

Line 2; {

This line is marking the beginning of the function main
Your code will begin on line 3, in the empty space. The end of your code will be marked with a '}' to show the function is closed.


In programming, there are preset values, called constants. These can be set by the system or the user in the DE (or IG, if you want to), and cannot be changed once set. (generally). There are also values which can be set in the DE, but also changed at any time when the module/program is running. These are called variables There are three basic types of variables we are going to go through quickly.

String; A string contains letters, numbers, symbols. It stores this data as is, for display/changing by the module. A common example; The sending scripts. While a little more complex, basically they just copy what you say as a string, and then the script reads the string and sends it to all players in range.

Integer; Integers are numeric data. They cannot store text. If you try to save a string as an integer, your script will return an error. So just don't do it. An integer is designed to be changed on the fly, and you will probably use them most of all, so it's important to get familiar with using integer's quickly.

Boolean; Bool what? Boolean is the most basic type of variable. it follows computer logic, in that something is either true or false. 1, or 0. Boolean is most often used to check if something is true. For example checking to make sure a player is in the area before running a certain script.

There are in NWScript, variables called locations. These should be somewhat obvious, but they are data storing where something is. This allows us to target specific places, as well as specific things. Locations are used most often in teleportation, creation, and destroying of things.



Let's demonstrate a bit of these now, shall we?

Click on line 3, to our nice open space. Press Enter a few times to expand this space a bit. Back on line three, Let's type this text;

object oPC = GetItemActivator(); Let's break this down. object Objects are exactly that. They are PC's, items, placeables, or monsters. We use objects to tell the computer there is something the script will react to or affect. Naturally, just telling that to the computer isn't enough. We need to tell the computer what to affect. In this case, the object is 'oPC' 'o' is a general prefix used to mark an object. This could be read as 'object PC' if you really wanted to. You can get creative with the names if you want, but it's best to remain simple for your own sake/the sake of anyone reading your code. try to name your objects in a way that will let it be obvious what it is affecting.

Now that the computer knows that the object will be called 'oPC', we need to tell it how to find out what the object actually is. we do this with - You guess it, an equals sign. 'oPC = ' Here we have 'GetItemActivator()' This means that the value of oPC will be set to the person that uses an item. This command is generally used on an item activation script. For example you use an item as a PC named Steve. If this command is used, the object oPC becomes the PC named Steve, so any commands afterwards that affect oPC, will affect Steve if the command has any IG effect.

And last, we have the semicolon. These are placed at the end of a line to basically tell the computer that the line of code is finished, and a new line of instructions is following.

Remember when defining to the computer what an object is, you can use a whole ton of different commands to set the object to many different things. Areas, monsters. Get creative, all the commands are on the right of the DE. Use them!

Moving on. Now that we have an object in our script, let's actually do something with it. Underneath it, let's type this;


FloatingTextStringOnCreature("This is some sort of scripting marvel!", oPC);

What this does, is makes - you guessed it; Floating text above Steve's head that says 'This is some sort of scripting Marvel'. Let's break it down.

The function "FloatingTextStringOnCreature" Sends a message to the combat window, and floats above the PC's head. This is the main portion of our 'Function'. After the function, comes the brackets. These little dudes mark the begining of our 'arguements', which affect what our function will affect, and how. Some commands need arguements, some don't. If you click on a function from the list, you will get a breif text message near the bottom of your screen. It will look something like this;

// Display floaty text above the specified creature.
// The text will also appear in the chat buffer of each player that receives the
// floaty text.
// - sStringToDisplay: String
// - oCreatureToFloatAbove
// - bBroadcastToFaction: If this is TRUE then only creatures in the same faction
//   as oCreatureToFloatAbove
//   will see the floaty text, and only if they are within range (30 metres).
void FloatingTextStringOnCreature(string sStringToDisplay, object oCreatureToFloatAbove, int bBroadcastToFaction=TRUE)
The message will explain what the base function does, and how to use it. Note that you do not need to type it like shown, all the time. Some arguements are optional, like 'Int broadcastToFaction'. We can leave this out, and the computer will automatically assume the value is false, since we did not give a value. Also note that you do not need to type 'string' or 'object' before your arguements. It can be as simple as
FloatingTextStringOnCreature("Text", oPC);
The types listed are mainly to tell you what type of data you need to use for your arguements. It is important to remember to separate your arguements with a comma, to let the engine know the next is finishing. Close your brackets, and you are done with your arguements.

Let's add up what we have so far; In your code window, you should now have on your screen this;

[code]
void main()
{
object oPC = GetItemActivator();
FloatingTextStringOnCreature("This is some sort of scripting marvel!", oPC);
}
Surprisingly enough, this is a working script, you're done. You could add this into the game, and it would work. Click the save and compile button. You will get a message saying "script compiled successfully" at the bottom of your screen. Good work, dude.

Let's say we wanted to change this script, though, so it only affected say.. Bob, and not Steve. How would we do this? Well, first we need to tell the computer that it needs to check the Person who is using the item. Then we need to tell it what to do if one person uses it, but not another. We do this with selection statements, or conditional statements. Selection statements check to see if a condition is met, before executing the script.

Let's do one of these, and break it down.

First, you begin with this. Press enter a few times and give yourself a few lines of blank space underneath the line that says object oPC = GetItemActivator();
After you have done this, add this underneath it in the blank space.

string sName = GetName(oPC);
This line tells the engine that the string 'sname" will use the GetName function on the object oPC. Meaning the engine will find the object oPC, and then return it's name. This will only work if we tell the engine what oPC is first, which we have done in the line before this. Now that we have the PC's name as something the engine recognizes, let's put it to use.
Add this underneath your string line;

if (sName == "Steve")
{

}
Here we have the 'if' statement. The 'if' marks the beginning of a conditional. The script inside it will only run IF the conditions are met, hence the name. Then we have (sName == "steve") This is our condition, which we enclose within brackets. In this one, we call the string "sName" and compare it to the text Steve. If the name is Steve, then our condition is met and the script will continue. If it is not, the script will bypass this section of code and continue on to anything after the } of the If statement.Note; There are different ways to use this. In this statement we use '==' for Equal to. You can also use '>=' for greater than or equal to, '<=' for less than or equal to, or '!=' for Not equal to. (also note you do not put a semicolon after the conditional brackets)

Assuming the script moves forward, we need to tell it do actually do something with this code. since we want the script to NOT fire when steve uses the item, we will add this line, on the blank line between the { and } in your if statement.

return ;
return; is sort of like telling the script to stop. There are other uses, but we'll get to them in a later tutorial.

Let's move onto how we then tell it what to do if the name is NOT steve.
Since we have our If, and that condition is not met, we can add another condition. to do this, we use 'else if'. It means if the first condition is not met, but this one is, it executes this part of the script instead. If neither are met, nothing will happen from either of them, and the script will continue as if they did not exist.

Add this line in a blank line undernearth the } of your first if statement.

else if (sName == "Bob")
{
FloatingTextStringOnCreature("This is some sort of scripting marvel, Bob! Too bad Steve sucks too much to see it.", oPC);
}
 
Exactly like the other one, we tell the engine to look for something else instead of the first condition. Instead of "steve" we check for the name to be Bob. If it matches, the section of code inside will fire. In this case, sending the message to bob.


And that's it. You have a script that will only fire for a certain person. There is another part of conditionals I did not touch on, the else statement. This is essentially the same as the other two, except it does not use arguements or a conditional. It literally means 'everything else'. if the conditionals you specifiy in the 'if' and 'else if' (you can have as many else if's as you like, by the way) , if these conditions are not met, the else will. You typically use this to send a message to a PC or so when a condition is not met.
For example the EFU quest system. When you start a quest, it checks if your party is valid, 'if' someone has already done it, it does not let you take it 'else if' your party is too big, it will not let you take it. 'elseif' your level is too high or low, it will not let you take it. 'else' it will allow you take it. (it may not work precisely that way in their scripts, but that is a basic way to explain the logic of the statements in use)

For now, you have a working script. If you want to test it out, the quickest way to do this is by following these instructions;

Compile and save your script. It should look like this, and you should not get any errors.

void main()
{
object oPC = GetItemActivator();
string sName;
if (sName == "Steve")
{
 return ;
}
else if (sName == "Bob")
{
FloatingTextStringOnCreature("This is some sort of scripting marvel, Bob! Too bad Steve sucks too much to see it.", oPC);
}
}
Once compiled, it's ready to be used IG. Here's how you do that.

Double click an area name, in the bar on the left of your screen. (close the scripting window after you saved your script. Make sure you remember what you named it!)

The area list will look like this.



Then you need to create the item which will run your script.

Click on the sword, then custom. Right click any of the categories, and press "new"



Follow the prompts, create your item and decide where it will go in your pallete. I chose 'tutorial', but you can really place it anywhere you want.
Make sure you check the box that says "Launch item properties" in the last screen of the item creation wizard. It just saves time.

Now, a screen should pop up. Look to this part of it;



The name can be anything you want. But for now, name the tag of the item, whatever you saved your script as. (it's case sensitive, btw) So if you saved your script as killpuppies, you set your tag as killpuppies . Simple, right?

Great. Now press the "Properties" tab, and look for the "cast spell" property. Click the + beside it, and then scroll down to "Unique power"
Click the > to add it to the item, then the 'edir property' at the bottom. Set it to unlimited uses/day, then exit out of the prompts using the okay button, to save your item.






Place it on the ground in your area, and you're done the item bit.

then click edit, out of the menu bar. Click module properties.




This screen should pop up. Click 'Edit' on the one that says "OnActivateItem"


One line 35(Or the line -just- above the last }) add this;

ExecuteScript(GetTag(GetItemActivated()), OBJECT_SELF);
Save and compile the script, then leave the script window. Press OK to save the changes to your events, and then Press "Build" in the menu. Press "build Module" from inside it, and then click "build" on the menu that pops up. Once it finishes, click "done" Save your module, and press F9. It will launch NWN and take you to your area. try your item to see if it works!

Try your tool a few times. It probably won't work. try to figure out why, for a bit. See if you can find the problem, and fix it!

Hopefully you can see the problem, and have fixed it by now. If you found this helpful, great! If you find it unhelpful, or gibberish, I tried to make it as simple as possible, sorry




[/hide]

[hide="Part 2; Working with strings."]We know what a string is from the last section, but what if we want to get a specific part of a string? What if we are looking for a specific word, or we want to only read part of a string?

No problem, to do this, we work with something called 'Substrings'. A substring is a string within a string, and the functions to work with them are really simple. Open up your toolset and lets work with some examples.

Let's define a couple strings.
string sFirstString = "The test string";
string sNewString;
So we have our firststring and a newstring, but we didn't assign a value to the second. Let's do that now, using a substring. Let's set this as "Test string" Here is how you would do it;

sNewString = GetSubString(sFirstString, 5, 11);
Let's break that one down. We define the sNewString as a substring. The first arguement, sFirstString tells the engine to read the variable SFirststring for it's value, thus making the first arguement "The Test String". It's easier and more efficient to use the variable name rather than typing it out again.

The second arguemnt, '5', is telling the engine to start the substring at the 5th character in the string. In this case, the 't' of test. The last arguement is how many characters after the start of the substring we want to include in it. In this case, we include 11 characters after the 't' in test.

Pretty simple, right? However, say you don't know how long the string will be, what do you do then? Using the above example, we change the line " sNewString = GetSubString(sFirstString, 5, 11);"

to this;

sNewString = GetSubString(sFirstString, 5, GetStringLength(sFirstString) - 5);
Let's break this down. As our second arguement, we added two more arguement things. Why does this work? The Function GetStringLength, actually counts as an integer, because it checks to see how many characters are in the string. in this case, the value will be 16. So what's with the -5? We are telling the engine to subtract five from the length of the string, because our substring starts five characters in. This means that the 'end' arguement of our GetSubString function, will equal 11, and give us the same substring as the first example.

Still pretty easy, right?

Now say you only wanted to get the first three characters in a string. You could just as easily use the GetSubString, but there is also another function called GetStringLeft, (and simlarily, GetStringRight)

Using these is simple. Let's say you wanted to get 'The' from our example first string. You could go about it by doing this;

sNewString = GetStringLeft(sFirstString, 3);
That's it, you tell it to retreive the value of sFirststring, and then your sNewstring value becomes the first three characters from the left of sFirststring, or 'The'.

Similarly using GetStringRight returns similar results.

sNewString = GetStringRight(sFirstString, 6);
The above would return the 6 characters from the right, making sNewString's value = 'string'

Really simple stuff, right?


Let's say you wanted to check for a certain string. This is easy to do, using an 'if' statement. Like this;
if (sFirstString== "The string you want")
{
etc..
}
However, NWScript is 100% case sensitive. If you don't specify, if there is a capital letter the string will not match and your code inside the if will not fire. What if we don't want it to be case sensitive, though? This is also easy; Here is how you do it.

if (GetStringLowerCase(sFirstString)=="the string you want")
{
etc..
}
Using the GetStringLowercase(), you converted sFirstString into all lowercase, even if sFirstString was "WrItTeN lIkE tHiS" it would still come out "written like this". Very easy stuff.

There is also a GetStringUpperCase(), which does the exact opposite, and is used in the same way.

Let's move onto another subject now; Combining variables into strings. In one of the above examples we combined a variable into an equation to subtract a value, which is very similar.

Lets assume we still have our first two strings defined, sFirstString and sNewString. Define a new string called sSecondString.

string sFirstString = "the test string";
string sSecondString = "And another test string";
string sNewString;
Let's combine these two;

string sNewString = sFirststring + " " + sSecondString;
Notice a few things; Firstly, we did not use quotations around our entire new string. the quotations mark 'raw text' which is data you enter yourself. If you leave these out, the engine will assume that the text is a variable and look for a variable in your script to get the value from. If the variable is defined but has no value, the value is "" (which is the same as 0, or nothing) if the variable is notdefined, you will get an error when you try to compile and save your script.

Notice we used a simple + sign. This isn't actual math, but we're still adding values of the variables together. Also notice that I added a " ". This is not always necessary; if you know for a fact your string will be added to another, add a space at the end or the beginning, depending on where your script will place it; Without the + " " +, the value of sNewString would be 'The test stringAnd another test string'. It doesn't look very nice that way, does it? One way or another, we should be sure to add a space.

You can use the + to add any type of variable, but when you add an integer to a string, you will get an error. This is because of the way the engine handles the different data types, and you cannot mix the two. When adding an integer to a string, you need to use the function IntToString(). This converts the integer to string format so that you can add the integer's value to your string. An example; First we define an integer to go with our strings.

int nTestInt = 55;
sNewString = sFirstString + " " + "and a number, too ;" + IntToString(nTestInt);
Notice how I added a raw string there too; There is no limit to how many things you can add to a string, but if I remember there is a theoretical limit to string size in NWN; However if I recall this limit is very very large, and not one you are likely to reach. Limiting string size is good practice though, and you should try to keep them short and to the point. After the raw string, we use the IntToString() to convert nTestInt to a string format, where it will become part of the string, returning sNewString to us as; "a test string and a number, too;55" [/hide]

[hide="Part 3, Counters and Loops"]

Let's say you want to say, Set an Integer on ten different, but very similar placeables (Let's say, ten plants side by side). How would you go about doing this? Seriously, think about it for a minute. You probably came up with defining each of the integers for each plant separately. This is correct, and doing each plant on it's own will work, and there is nothing wrong with that. But doing that produces very long and inefficient scripts. We can actually simplify all ten plants into less than ten lines of code. We do this using a 'loop'. A loop is a term in programming, which does exactly like the name implies. It repeats a section of code over and over again. However, if we don't tell a loop that it eventually needs to stop, The script will stop itself after looping a certain amount of times. This happens very fast, in less than a second, because the computer can run the script/loop thousands of times per second. This is what happens when you see a "TOO MANY INSTRUCTIONS" error in nwn, it means a loop did not stop properly.

Let's take a minute to explain loops. There are a couple different 'types of loops. there is a 'While...' loop, a 'Do..' and a 'For...' loop that we will visit here. Each has it's own situations where it is most useful, and it's up to you to use your judgement on which you need.

A 'While' loop, will run as long as the condition specified is met. This usually looks like this.

while (YourCondition)
   {
   YourFunctions
   }
Once this loop reaches the closing bracket, it will return to the 'While' line, and check for the condition again. If it is still met (or not met, if you specify such) the loop will continue to jump back up to the 'while' line as long as the condition is met/ not met. Once met/not met, it will continue as normal. These are usually run when you are checking for something, such as a PC being in the area, or checking to see if an object is valid.


The other type of loop, the 'For..' is based on integers we call counters. You need to have an integer defined for this loop, and compare it to another value. Each step through this loop will update the counter, until the condition is no longer met. This type of loop is best used when you want to run the loop a specific number of times, or based off a variable.
Let's look at one of these now.int YourInt;
for (YourInt = 0; YourInt < 5; YourInt++)
{
 YourCode
}
Let's break this one down. First we declared the integer YourInt, then we started the For() loop.
The loop's brackets contain three parts, each separated by a semi-colon. First, we define the value of our counter, which is YourInt. Second, is our condition, which the loop will continue to loop until it is no longer true. In this case, as long as YourInt is smaller than 5, our loop will continue. Lastly, we have our counter updating. In this I used YourInt++, which will add 1 to the value of YourInt each time the loop starts again.

You can use other math to change the counter, if you want. Subtraction, etc. It's really up to you how you use them.


Lastly, we have the 'Do..' loop. This loop is different from the others slightly; The other loops operate almost like 'if..' statements, only starting and continuing as long as their condition is met/ not met. The do loop will always execute at least once, because it's condition statement is at the end of the loop, not the beginning. It will execute your code, and then go back and repeat it if the condition is met, or just keep going normally if the condition is not met. These are best used when you want your code to fire no matter what at least once, and then loop as needed.

Let's look at one of these;

int YourInt;
Do
{
//Your Code here
YourInt++;

} while (YourInt < 10);
The Do loop is simple to look at, the only key thing to note is that you must update the counter in the loop or use a break(explained in a later part in more detail) The while portion checks the counter and then starts the loop. Unlike the 'For' loop, you cannot update the counter inside the brackets, it needs to be done within the loop's code itself.


Now that we have explained the basic loop types, let's combine parts 1 and 2 of the tutorial and solve the plant problem at the beginning of this part, using each different loop as an example.

If you want to follow along in the toolset, make an area, and create 10 objects tagged 'plant_1' to 'plant_10' and place them in your area. Make an item that will fire the script as well. (Revisit Part one if you forgot how)

[hide="Solve using 'While' "]

To solve using a 'while' loop, this is one way you could approach it; Since the plants are tagged in order, our loop simply has to count them; Here is how we do it;


 //Declare our variables
object oPlant = GetObjectByTag("plant_1");
int nPlant = 1;

// Set up our loop, making sure our object oPlant  exists and is valid.
// This makes sure that we don't go over and keep looping for plants that don't exist.
   while (GetIsObjectValid(oPlant))
      {
        // Set the Integer we want on the plant.
        SetLocalInt(oPlant, "PlantInt", 1);
        // The integer is set, now we want to set it up to cycle to the next plant.
        // First we update the plant counter
        nPlant ++;
        // Then we update the plant tag by adding the plant number onto the string that checks the tag.
        oPlant = GetObjectByTag("plant_"+ IntToString(nPlant));

        }
Notice how we combine the use of an underscore and then using the + IntToString(), we update the plant's object tag without needing to typo out a number for each one. For only a couple this isn't really needed, as copying/pasting and then changing the numbers 1-10 really isn't that hard. But in programming it's best to err on the side of efficiency, because if you want to add to your content later, with this script as it is now, you never need to update the script to set the integer, as long as the tags remain plant_Number format. Nifty, huh?
[/hide]

[hide="Show 'for' loop"]


object oPlant = GetObjectByTag("plant_1");
int nPlant;
// Set up our loop, setting the counter at 0.
   for (nPlant = 1; nPlant <10; nPlant++)
      {
        // Set the Integer we want on the plant.
        SetLocalInt(oPlant, "PlantInt", 1);

        // The integer is set, now we want to set it up to cycle to the next plant.
        // by adding the plant number onto the string that checks the tag.
        oPlant = GetObjectByTag("plant_"+ IntToString(nPlant));

      }
This one is pretty much the same as the other, but we need to make sure we set the counter at 1, otherwise the step from 0 to 1 might mess up any other functions you have based on the counter. It's good practice to start the counter at whatever number matches what you're trying to do.

[/hide]

I'm not going to do a 'Do..' example because it's basically the same as the 'while' example.Hopefully you have followed along enough and I've explained enough that you can make your own 'Do' loop for this. It, see if it works. If you think you got it working and really want to make sure, try adding this line inside your loops, if you don't know how to manually check an integer from the client;

SetName(oPlant, "NewName_" + IntToString(nPlant));
This will change the name of the plant_1-10 items to Newname_1, newname_2, etc.. This will let you know for sure if the script works properly. (Note there is a bug with SetName that sometimes the name does not change on your screen until you reload the area)

[/hide]


Still pretty simple, right? More to come soon.


A useful tool, a VERY useful tool for learning about functions is the NWN Lexicon, found at http://www.nwnlexicon.com/ <- this website has information on like every function in NWN. Very useful, most contain examples of how to use them, etc. No scripter should go without.
Another tool for learning how to put them all together, is Lilac Soul's script generator. You can use this to build scripts, simply by telling it what you want to do. It's a very good learning resource. It can't do everything, but it can teach you a LOT about how to use the functions together. Use it to make a script, and then try to change it around, or write a similar one without it. You will learn very quickly. The only downside to Lilac is that they are difficult to merge with other systems, etc. But for your own personal use, they are very handy.

http://nwvault.ign.com/View.php?view=Other.Detail&id=625

Cruzel


Cruzel

Updated again, imo.

(Since I seem to be reaching the character limit for posting???, It's better if you don't reply to this so I can use consecutive posts to convey the information and whatnot. If you think this is cool, give me a shout on IRC or in a PM to say so, or if you think I missed something (or spot a mistake) send me a PM.  

Cheers!