While it seems that there is almost no end to the power of BioWare's NWScript language, it does have its limitations. Despite all of the features it does have, most of us with programming experience in NWScript's parent language, C, wonder where the arrays have gone. And they have a valid concern: arrays are fundamental to most programming languages, and it can be very hard to accomplish some tasks without arrays, and for others it's just plainly impossible.
Fortunately for us, the scripting language has enough power to let the shrewd coder get around this limitation, and simulate a C/C++/Java array.
Background: What is an array?
Since I do not wish to be exclusive to those of us with programming experience outside of the Neverwinter world, let me briefly explain what an array does, and why not having them is so stifling.
In NWScript, when we wish to remember a value, we declare a variable and assign it a value, like this:
int n = 5;
Later, we want to change the value of our variable, n. We say;
n = 6;
The variable n immediately assumes the value 6 and forgets that it used to store 5. This is fairly intuitive. A variable has a name and a value, in this case its name is n and its value is 6.
An array is like a variable, but instead of having one name and one value; an array has one name and many values.
The first line looks familiar. It's the declaration of the variable, a, whose type is integer array. In our previous example, the type of n was integer.
The brackets [ ] are used to denote the size of a when it's declared. So, a [5] means I want an array that contains 5 values.
When the brackets are used to assign values, they are accessing the numbered elements of the array. This number in the brackets is called an index (plural, indices). Note that in C, array indices always start at 0. So, if you have an array of size 5, the valid indices are 0, 1, 2, 3, 4.
Think of an array like a list of integers, all referred to by a common name (in this example, a). To access specific elements of the list, we refer to these specific elements by a number, which you can think of as their place in the list. If I want to access the first element, I use a[0]. If I want to access the last element in the list, I use a[4].
No Arrays in NWScript
Though NWScript is modeled very closely after C, the BioWare team explicitly left arrays out of the language. Why?
A likely reason is that programs get very upset when you attempt to use an invalid index. Meaning, if I have an array of size 10, and I attempt to access element 50 well, there is no element 50. So what happens?
In C, your program will crash, with an error message along the lines of, "array index out of bounds."
This is a run-time error. A compile-time error is one that is caught by the compiler (like bad syntax, for example). A run-time error occurs when the code is valid, but something happens during the execution of the program that causes it to fail, without the compiler being able to detect it. Consider this block of code:
int a[10];
int i;
for (i = 0; i < 50; i++)
SpeakString("Here's my value: " + IntToString(a[i]), TALKVOLUME_TALK);
Since the index of an array is of type integer, compilers allow you to replace the literal digit with a variable, like i.
As soon as i reaches 10, and a[10] is accessed, the program will fail, since a[10] does not exist (recall that an array of size 10 has indices 0, 1, 2, 3, 4, 5, 6, 7, 8, 9).
Why doesn't the compiler catch this vulnerable code and fix it?
Some compilers can, but most are just not that smart. And, this is a simple case. You can do all sorts of things to cause an array index to go out of bounds. It's not the compiler's job to catch all of these possibilities, because to find the error it would, in effect, need to execute the code. That's a chicken-egg problem. So unfortunately, this kind of debugging is always dedicated to the coder.
It's for this reason that I suspect the BioWare team explicitly removed direct access to arrays to scripters. There are ways of preventing the game itself from crashing when a script calls a bad array index, but how would you be able to successfully debug code involving arrays? It would be very messy.
How to Simulate Array Functionality Anyway
BioWare excluded arrays from the specs of NWScript. But they gave us something even better: local variables.
The local variables in NWScript are attached to objects, and the call to the functions to set and get these items look like this:
// Set oObject's local object variable sVarName to nValue
void SetLocalObject(object oObject, string sVarName, object oValue)
The first parameter is the object on which you wish to set this variable. The second parameter is a string which represents the name of this variable, and the third is the value itself.
// Get oObject's local object variable sVarName
// * Return value on error: OBJECT_INVALID
object GetLocalObject(object oObject, string sVarName)
The first parameter is the object from which you wish to retrieve the variable. The second parameter is the string representing the name of the variable.
These local variables are tremendously powerful.
Unlike script variables which need to be declared before using, these are a different animal altogether. You don't have to set them up specifically in some kind of initialization script. These variables can be "set" and "get" on the fly from anywhere, in any script, at any time.
How this might work internally (Programmers only!)
You might ask, "How do these variables work?" How does the script engine generate them on the objects so that they are so robust, and dynamic, and fool-proof?
It seems to boggle the mind. But there is a way.
For those of you who are familiar with dynamic memory in C and C++, you know that one problem with arrays is that they can't be easily resized at run-time. Their sizes must be established prior to compile. However, dynamic arrays can be created at run-time using manual memory allocation (malloc/dealloc in C and new/delete in C++).
Since it appears that there is no limit to how many local variables of each type you declare on an object, my guess is that each object in the game has arrays of the data types allowed by SetLocal * and GetLocal *, meaning each object comes equipped with a pointer to an int, float, string, and object.
Those pointers are assigned to a dynamic array each time a new local variable of each type is defined. So, if I have defined 3 local ints, the object probably has a dynamic array of size 3 stored in memory.
But, as you know, local variables are accessed by strings.
What some of you may not know is a technique called "mapping" (also known as associative arrays).
This is a process by which you can create arrays that are essentially indexed by data types other than ints. In C++, this is quite easy to do with objects and overloaded [ ] operators. In C, it's slightly trickier. Not knowing which language NWN is programmed in, I can only speculate here.
But if I were designing this system, internally I would have dynamic arrays which represent the values I've stored. And, I would set up some functions that will map a string (like the variable name) to an integer. That integer would represent the index of the dynamic array in which the variable's data is stored.
The Good News
Since local variables are referenced with strings, you can play with the variable names quite easily. That allows you to be very creative with an automatic naming sequence for these local variables which can simulate an array.
Consider this: in order to access elements in an array, you need to know two things.
First, you need to know the name of the array variable.
Second, you need to know the index of the element you're looking for.
Given that both of these two things can be known, and our ability to use local variables is quite extensive, we can clone an array by creating a series of local variables which contains a common name and its index.
Now, to get down and dirty, and by that mean give you an example of what I'm talking about.
Let's imagine we have a book keeper, whose job it is to keep a record of everyone who visits the library in your module. Before the player can get past the book keeper, he must talk to the book keeper and sign in. But, you don't want the player to have to sign in more than once; if he signs in the first time, he is allowed to enter and exit the library at will; a trivial example but one that illustrates well what we're talking about.
I'm going to assume this script is being fired from a conversation, so in order to refer to the person who is signing in, I'm going to use the function GetPCSpeaker(), which will return an object referring to the player.
The Book Keeper is going to be a variable called oBookie. The Book Keeper comes from a template I've defined in my module whose tag is "tag_bookie."
I've initialized oBookie with this line:
object oBookie = GetObjectByTag("tag_bookie");
This could be done in many ways. This also assumes that there is only one bookie, or if there were many, he was the last one placed down in the module. If we want to talk about a specific bookie, we would need to add an extra parameter to GetObjectByTag, specifically a number which refers to the nth bookie in the module. Also note that the toolset has this backward, so that the first bookie you put down in the toolset is the highest number, whereas the last one you placed is #0.
In order to clone an array, we are going to create a series of string variables, representing a player's name, with the following naming convention:
SIGNED_NAMES_xxx
where xxx is a number. Think of that number as you would think of an array index. In C, I might say SIGNED_NAMES[xxx] to access the nth element. Here, I'm going to get a local variable called SIGNED_NAMES_XXX.
While not exactly the same, it sure is close.
We need one very important piece of information. That information is how many names the bookie's recorded so far. We'll call that local variable NUM_NAMES.
So, let's see what the script that would be called when a player is signing up would look like. Remember, this is assuming that this script is fired from within a conversation.
// Bookie script
// signing up a new player
void main() {
// if "NUM_NAMES" hasn't been set yet, the get will return 0,
// and n = 0
int n = GetLocalVariable(oBookie,"NUM_NAMES");
// increment n by one, since we want to register a name
n++;
string varName = "SIGNED_NAMES_" + IntToString(n);
// create a new local variable at "index" n
SetLocalVariable(oBookie,varName,GetName(GetPCSpeaker()));
// set the number of records to n
SetLocalVariable(oBookie,"NUM_NAMES",n);
}
Surprisingly enough, that is simply all there is to it! Surprised?
Now, let's go over how you would "cycle through" your "array" when you want to check if the player has already signed in. Rather than write a full script here, I'll write you a function which will return a Boolean value representing whether or not the name appears in his list.
// cycling through the bookie's "array"//
int IsNameInList(string pName) {
// return TRUE if name is found in list
// return FALSE if name is not found in list
int i; // counter variable
int num = GetLocalInt(oBookie,"NUM_NAMES"); // "size" of "array"
// if num = 0, this for loop won't execute
for(i = 1; i <= num; i++) {
string var = "SIGNED_NAMES_" + IntToString(i);
string s = GetLocalInt(oBookie,var);
if (s == pName) return i;
}
return 0;
}
While this isn't perfect ANSI C, it gets the job done.
Note that in C, a statement is TRUE if its value != 0. That means that 5 is a "true" number. The only int which represents "false" is 0. So, this function can be used like a true/false function, or it can be used to trap the "index" of where the name was found in the list. If that confuses you, just replace "return i;" with "return TRUE;" and "return 0; " with "return FALSE; ".
Conclusion
So, while "arrays" as you know them might not be available directly, with this little trick you can create a series of variables with "indexed" names with very little trouble, and get all of the functionality of an array.
I hope you find this useful and informative. If you're having trouble, or discover anything quirky about this method, please e-mail me.