BubaDragon - Guide to Debugging

BubaDragons guide to NWNScript Debugging

Abstract

Troubleshooting NWNScript can be a challenging and time-consuming affair. Approaching this task in an organized manner can reduce some of the frustration in tracking down and fixing bugs in scripts. This document presents a basic set of heuristics for troubleshooting scripts, and walks through a sample scenario.

Intended Audience

It is assumed that readers of this document are familiar with the Aurora Toolset and have a basic understanding of the scripting language. It is beyond the scope of this document to teach scripting basics, rather this document endeavors to present an organized repeatable process for discovering why a NWNScript is failing and how to correct that failure.

What is Troubleshooting?

In programming troubleshooting, or debugging* is the process of locating and fixing or bypassing errors in computer code. To debug a program is to start with a problem, isolate the source of the problem, and then fix the problem. Here then is the essence of what to do when errors occur in NWNScript:

·Identify the problem

·Isolate the problem

·Fix the problem

·Test your fix for the problem.

Alas such clarity and simplicity is much easier to state than to accomplish. What follows is my humble attempt to outline my method for troubleshooting NWNScript code and modules.

Basic Troubleshooting Guidelines

Suspect the obvious

Before you try to blame the Aurora Toolset, curse the developers at BioWare, or accuse moon wobble for your script failure, investigate some of the more likely causes:

·Check for assignment (=) when you meant to test for equality (==)

·Make sure every statement is terminated with a semicolon (;)

· Check for typos and proper function calls

· Check that the variable you assign (foo) is the variable you call (Not “Foo”)

· Make sure the script is getting called

It sounds simple but I can’t tell you how many times I have solved problems and never executed the code containing the solution.

Localize the error.

Sometimes this alone makes the solution obvious. Narrow the problem down first to a method, then a section of code, finally a single line of code.

Make simple test cases. Create a small module with only enough objects to test the problem.

Check your facts.

Check your assumptions, question what you “know” you know. Look at function calls in the toolset. Find a working script and examine it. Explore all the parameters of a function.

Learn a bit about the toolset and the language.

Explore some articles on event-based programming. Understanding the fundamentals will go a long way to giving you the tools to solving your script problems.

Scripting is an exercise in logic.

It is both an art and science to translate an idea for a behavior into a series of instructions to execute that behavior. Folks, if it were easy anybody would do it. Developers would not get the big bucks, and all software would work flawlessly the first time it was compiled; but they do and it doesn’t, ever.

Don’t let this discourage you however, the fact you are reading this or have picked up the toolset indicates that you have the particular bent required for accomplishing this task. Face it; you are doing this for fun right? Stop for a moment and think about how twisted that really is, laugh at your frustration and dive back in.

Debugging Tools

The first thing some of you are going to say is “What debugging tools?” My answer is the same ones that Bjorn had when writing C++, mainly the one between your ears. Add to that this a short list of available tools, and you will be as prepared as any Neanderthal is to hunt a mammoth.

Print Statements

How does one check the value of a parameter? Simple, print it out. Have the object speak it using

SpeakString(“Some String “+IntToString(iVariable));

SpeakString(“More String”+FloatToString(fVariable));

SpeakString(“The object is named”+GetTag(oObject));

//etc…

Another option is to write the variable to the log file using

void PrintString(string sMessage).

You knew there was a log file right? Well there is and it can be found where you installed NWNights in the \Logs directory, it is called nwclient1.txt and can be opened with any text editor. (I have a shortcut to the file on my desktop…)

The Script Editor

Perhaps the most useful and informative of the debugging tools you have to work with is the script editor itself. The script editor can provide a wealth of information about functions and NWNScript types like Object, Effect, and Vector. Look for the types in the parameter list and know how these types are created and instantiated.

Contained within the NWN Script Editor is the compiler; it is your friend. Every error that is generated by the compiler will have a line reference, and in it’s own limited way, a description of the error. Some of the more esoteric errors I have encountered are:

· ERROR: UNKNOWN STATE IN COMPILER

This particular error is most likely caused by unmatched parenthesis or scooping braces.

· ERROR: NO SEMICOLON AFTER EXPRESSION

This error will highlight a line which if scrutinized will have a semicolon after it. The reason for this is that often the line above the line that generated the error will actually have the error.

· ERROR: DECLARATION DOES NOT MATCH PARAMETERS

Guess what, you need to go back to the function definition and review the parameter list. Either the wrong parameter type or a missing parameter causes this error.

· ERROR: PARSING VARIABLE LIST

One of my personal favorites only because it is so descriptive… Usually this is an indication that you have misspelled a function name on the right side of an assignment statement.

The best thing about the compiler is that it always shows you where the problem is. Never forget that. Examine the line the compiler indicates and the line immediately before it for errors. If only all of the NWNScript errors were so easily found.

BioWare Message Boards

Guess what, there is a strong possibility that any question you may have has been asked, and answered, on the scripting forum, the custom content forum, or the toolset forum. Get in the habit of searching the forums BEFORE asking the question. It’s a waste of bandwidth to keep posting the same questions over, and over, and over again. Besides it makes you a target for a flame…

The NWN Scripting Lexicon

You knew that had to be in here right? This is as fine a set of API documentation to come out of a community source as any I have ever seen, and I have seen a few. Get it, load it, love it, and tell your friends about it.

A Troubleshooting Example

What follows is an actual event (as best as I remember it) that illustrates the principles outlined above.

Note: this occurred before the Lexicon was even an idea I had heard of. Had the Lexicon been available I would have simply searched it and found my answer.

Start with the problem

My goal was to discover how to turn the lights off in a particular tile, so first thing I did was to search the tool for functions using the keyword "light", this of course returned the following functions:

GetTileMainLight1Color

GetTileMainLight2Color

GetTileSourceLight1Color

GetTileSourceLight2Color

RecomputeStaticLighting

SetTileMainLightColor

SetTileSourceLightColor

Looking at each function in turn yielded the following data:

// Get the color (TILE_MAIN_LIGHT_COLOR_*) for the main light 1 of the tile at

// lTile.

// - lTile: the vector part of this is the tile grid (x,y) coordinate of the tile.

int GetTileMainLight1Color(location lTile)

// Get the color (TILE_MAIN_LIGHT_COLOR_*) for the main light 2 of the tile at

// lTile.

// - lTile: the vector part of this is the tile grid (x,y) coordinate of the

// tile.

int GetTileMainLight2Color(location lTile)

// Get the color (TILE_SOURCE_LIGHT_COLOR_*) for the source light 1 of the tile

// at lTile.

// - lTile: the vector part of this is the tile grid (x,y) coordinate of the

// tile.

int GetTileSourceLight1Color(location lTile)

// Get the color (TILE_SOURCE_LIGHT_COLOR_*) for the source light 2 of the tile

// at lTile.

// - lTile: the vector part of this is the tile grid (x,y) coordinate of the

// tile.

int GetTileSourceLight2Color(location lTile)

// All clients in oArea will recompute the static lighting.

// This can be used to update the lighting after changing any tile lights or if

// placeables with lights have been added/deleted.

void RecomputeStaticLighting(object oArea)

// Set the main light color on the tile at lTileLocation.

// - lTileLocation: the vector part of this is the tile grid (x,y) coordinate of

// the tile.

// - nMainLight1Color: TILE_MAIN_LIGHT_COLOR_*

// - nMainLight2Color: TILE_MAIN_LIGHT_COLOR_*

void SetTileMainLightColor(location lTileLocation, int nMainLight1Color, int nMainLight2Color)

// Set the source light color on the tile at lTileLocation.

// - lTileLocation: the vector part of this is the tile grid (x,y) coordinate of

// the tile.

// - nSourceLight1Color: TILE_SOURCE_LIGHT_COLOR_*

// - nSourceLight2Color: TILE_SOURCE_LIGHT_COLOR_*

void SetTileSourceLightColor(location lTileLocation, int nSourceLight1Color, int nSourceLight2Color)

Isolate the Problem

Nothing rocket science yet, nei? Obviously the functions I wanted to use were

A glance at their parameter list and reading the function notes showed they needed a location object (needed for a vector object containing the “grid (x,y) coordinate of the tile”) and two integers (For the light colors). That threw me for a loop, what was a “grid coordinate”?

Although I had no idea what a grid coordinate was, I knew that it would be part of a location object. So the next thing to do was research the location object. Searching for location yielded more results than I wanted to look at, but some of these showed promise like:

// Get the area's object ID from lLocation.

object GetAreaFromLocation(location lLocation)

// Get the location of oObject.

location GetLocation(object oObject)

// Get the position vector from lLocation.

vector GetPositionFromLocation(location lLocation)

and of course the all important constructor...

// Create a location.

location Location(object oArea, vector vPosition, float fOrientation)

So now I knew something about the location object, I was also able to guess that SetTileMainLightColor was going to use some form of GetPositionFromLocation to know which area to affect. While I was still lost about what a “grid coordinate” was, I knew how to create a location that would contain one. The pieces of this puzzle were beginning to fall together.

Leaving aside the problem of the grid coordinate I began to research light colors. I knew that these constants were all prefaced with TILE_MAIN_LIGHT_COLOR_*, and poking around in the constants section of the toolset provided me with the whole set of these. Since I hate to type and I try to avoid function lines that wrap I simply looked in the nwscript file for the area where these constant values were defined to select a color to test with. After locating this area I settled on Bright White, which was defined by:

int TILE_MAIN_LIGHT_COLOR_BRIGHT_WHITE = 3;

At this point to make any more progress I had to know what a “grid coordinate” was, and how it was formatted. So I started playing around with writing the location of a switch to the log file using PrintString(string sMessage). Soon I had the switches location, and I was passing it directly to the SetLight function using…

location lLocation = getLocation(OBJECT_SELF);

SetTileMainLightColor(lLocation, 3, 3);

This off course failed, so I opened the editor and was positioning my cursor over the reported location of the switch when I saw something on the status line... A message that read

Mouse(x:n, y:n) Grid(Row:n, Col:n) Tile(<tileSet_tile_name>),

Then it dawned on me that there was a relationship between Mouse(x:n, x:n) and Grid(Row:n, Col:n), the Row and Col were the integer portion of ([x|y]/10) <read that as: x OR y DIVIDED_BY 10>. So now I knew that the tiles were referenced, from the bottom left to upper right starting at 0,0.

This revelation gave me the information I needed to reference a specific tile, but how about the area? Again a trusty search in the toolset using the keyword “area” revealed the following useful functions:

// Get the area that oTarget is currently in

// * Return value on error: OBJECT_INVALID

object GetArea(object oTarget)

// Get the area's object ID from lLocation.

object GetAreaFromLocation(location lLocation)

Noting that an area is a type of object I needed to reexamine the things I could do with an object. So I searched on object and found:

// Get the nNth object with the specified tag.

// - sTag

// - nNth: the nth object with this tag may be requested

// * Returns OBJECT_INVALID if the object cannot be found.

object GetObjectByTag(string sTag, int nNth=0)

Which I figured had to be there, this confirmed that if I knew the areas tag I could retrieve the area by its tag. Since I would be creating the area, I would know it’s tag. Now I had all the parameters for:

SetTileMainLightColor(location, int, int)

Those being simply; a Location object containing a Vector of the Tiles position in the Area, and two color integers which were defined in the file nwscript.

Fix the Problem

So now I had all the pieces to put together my little puzzle. I knew that in order to reference a specific tile I needed the following:

1) A reference to the area the tile was in

2) The grid coordinates of the tile itself

Next I created a 3X3 area and named it "foo" and in the area properties I turned the lights off (torchlight only). Then I created the following script:

void main()

{

object oArea = GetObjectByTag("foo");

vector vLocation = Vector(1.0, 1.0, 0.0);

location lTile = Location(oArea, vLocation, 0.0);

SetTileMainLightColor(lTile, 3, 3);

}

Imagine my surprise when it compiled! Placing this script in the OnUsed event for a switch, and running the module in the client, can you imagine my joy when the lights came on? I figured that the next step was to create an algorithm to give me the area an object was in, and then return a new location that would reference the grid the object was in. But, I realized that I didn't need to do that, If I wanted to turn all the lights off in an area I could simply use two nested for loops to run me through my area's grid, ala:

for (int x = 0; x < MaxX; x++)

for (int y = 0; y < MaxY; y++)

tileVector.x = x

tileVector.y = y

tileLocation = Location( oArea, tileVector, 0.0)

SetTileMainLightColor( tileLocation, color1, color2)

SetTileSourceLightColor( tileLocation, color3, color4)

Test the Fix for the Problem

Next came the test of my solution. Browsing nwscript for the TILE_LIGHT_MAIN_COLOR_* section I found this line:

int TILE_MAIN_LIGHT_COLOR_BLACK = 1;

I of course, like anyone else assuming that black was off; made a quick test of my solution using TILE_MAIN_LIGHT_COLOR_BLACK the results of which were a bit discouraging. Sure it got dark, but not black like I wanted. So now I had a new problem to troubleshoot. Figuring out what off was. So I started a mini debug cycle; I had this problem fairly well defined and isolated. In order to turn the lights off I needed to know what off was. Setting the lights in my test are to off (torchlight only) and using tiles that had no secondary light sources (torches, braziers, etc.) gave me the effect I wanted, but what was that color? Scanning the editor for color functions I was reminded of:

int GetTileMainLight1Color(location lTile)

int GetTileMainLight2Color(location lTile)

int GetTileSourceLight1Color(location lTile)

int GetTileSourceLight2Color(location lTile)

It was apparent that I could simply ask the tile what the value of off was, with the following code:

object oArea = GetObjectByTag("foo");

location lTile = Location(oArea, 1.0, 1.0, 0.0);

PrintString("Main Light 1 color is "+IntToString(GetTileMainLight1Color(lTile)));

PrintString("Main Light 2 color is "+IntToString(GetTileMainLight2Color(lTile)));

PrintString("Source Light 1 color is "+IntToString(GetTileSourceLight1Color(lTile)));

PrintString("Source Light 2 color is "+IntToString(GetTileSourceLight2Color(lTile)));

Running this on my test module and checking the log showed entries of:

Main Light 1 color is 255

Main Light 2 color is 255

Source Light 1 color is 255

Source Light 2 color is 255

So now I knew what off was. A quick test of SetTileMainLightColor(lTile, 255, 255) and SetTileSourceLightColor(lTile, 255, 255)verified that I now had the correct value to turn off the lights. This concluded my mini-debug cycle.

I now had all the information I needed to turn the lights off on a tile, or group of tiles. To recap I needed:

  1. A reference to the area object where the tile I want to affect is.
  2. The tiles location on the area grid

(Being the integer portion of a location on that grid divided by 10).

  1. A new location containing 1 and 2.
  2. An integer value (or predefined constant) for the new light color.

The final step was to put this into code, and test the solution again. Since I wanted to maximize the reusability of this code I needed to create the most generic script I could with the information I gathered. The code wound up looking something like this:

void ChangeLights(int c1, int c2, int c3, int c4)

{

string sAreaTag = "Crypt"; //Change this to the name of the area

float maxX = 3.0; //Max Width. Change this for your module

float maxY = 3.0; //Max Height Change this for your module

//vector needed to locate each tile on the map

vector vTile = Vector(0.0, 0.0, 0.0);

//The first tile in the area (location[0,0])

location lTile = Location(GetObjectByTag(sAreaTag, 0), vTile, 0.0);

//variables needed for the while loop.

float fx = 0.0;

float fy = 0.0;

int notDone = TRUE;

while (notDone)

{

while (fx < maxX)

{

vTile.x = fx;

vTile.y = fy;

lTile = Location(GetObjectByTag(sAreaTag, 0), vTile, 0.0);

SetTileMainLightColor (lTile, c1, c2);

SetTileSourceLightColor (lTile, c3, c4);

fx += 1.0;

}

fy += 1.0;

fx = 0.0;

if (fy >= maxY)

{

notDone = FALSE;

}

}

RecomputeStaticLighting(GetObjectByTag(sAreaTag));

}

My plan was that this code would be stored in a file by itself, and called via ChangeLights(int c1, int c2, int c3, int c4) in another script. The ChangeLights code would need to be referenced in the calling script via a #include statement. (This is more a style thing for me, I have a library of lots of little functions that I can combine together into a larger file to reduce the number of #include statements in my scripts. A large number of small scripts intimidates me less than one or two monster scripts, especially when I know that the smaller scripts have been tested.)

Finally after much tribulation I could exclaim, “Viola, the light switch is born!” (And don’t think I didn’t).

Conclusion

The first step is identifying the problem:

Define the problem in one or two sentences. Be as concise as you can. While you do not have to write the problem down, you will need to keep your efforts directed towards that one problem. Don’t try to solve everything at once. Script debugging can be a bit like eating a chocolate elephant, you can only do it one bite at a time.

Next isolate the problem:

Build the smallest test case for this problem you can build that exhibits the problem. Search your script and the toolset for all the variables that are being used, not only the ones you think affect your problem. Check the module and every object for every script that is in the test case. Explore every function and the parameters for the function. For each of the parameters explore its constructor. Note how it gets initialized and the expected value at the time it is passed in.

Next fix the problem:

If you have performed the first two steps you can now start to check everything in your test case. If your problem involves objects that are spawned with scripts attached to them by default, rip them out. Run your test and slowly add them back in. Check the NWN BioBoard for others who have experienced your troubles. Chances are you are not doing anything that someone else has not done yet. As time goes on that possibility gets even more remote. (Remember to set your search parameters to go back to July 28 2002; the default search is a month back). If all of that fails to provide clarity to your issue make a post to the NWN forums, include as much information as you know or think you know. The more complete your post the more likely you are to get a timely, correct answer.

Finally test your solution:

While the fix may have worked in your test module, does it still work in a bigger module? I like to test my code in a custom module that I did not write, or even one of the single player chapters (like Chapter 1).

While all of this does not provide you with push button debugging, it does present a tried and true method of fixing programming errors. As I said before folks scripting is not easy, but for those twisted enough to pursue it, scripting can be fun and rewarding.

Footnotes:

*Grace Murray Hopper, working at Harvard University on the Mark II computer, found the first computer bug in a relay. She taped it into the logbook of the computer, thereafter whenever the machine stops they tell director Howard Aiken that they are “Debugging” the computer. The very first computer bug still exists in the National Museum of American History of the Smithsonian Institution




 author: Michael Nork, editor: Charles Feduke, additional contributor(s): Boris Maréchal