You are here

Script debugging tutorial

image1
image2

This tutorial should help you understand what the debugging is, how to use it, how to use ingame debugging command SpawnScriptDebugger() and generally, how to be selfsufficient in solving issues in your scripts. This tutorial is applicable on all scripting languages, not just NWScript.

What is debugging?

Debugging is a method how to find out what is really happening in your script, what are the variables' values and values returned from functions in your scripts. This is a main method of solving issues in script - debugging helps you find out why the script doesn't work the way you expected. After you know the reason, fixing the issue is easy.

 

How to use debugging?

Most often, scripters are using some print function, in NWN1 this would be SendMessageToPC or WriteTimestampedLogEntry. Many scripters has their own debugging functions that uses combination of the above functions and/or has extra features such as print the message only if local variable DEBUG is 1 or something like that.

For this tutorial I will be using a 4 custom functions of my own. These functions are inside inc_debugging attached to this tutorial. They don't have the "debug level" functionality so scripter is supposed to remove them after he makes sure everything works as supposed.

But here are two tips for making custom debugging functions:

  • using GetFirstPC() for the oPC parameter in the PC specific functions allows you to drop the oPC parameter in your custom debugging function, use it for scripts that are not working with PC and generally make the debugging functions easier/faster to use
  • using a parameter int DebugLevel=1 or something like that allows you to show/hide the debugging messages without need to remove them from code, that is usually done in a way that you give yourself some method of raising the debuglevel such as DM Tool item. The actual debug level is Local variable with specified name

 

the include library inc_debugging contains 4 functions:

//prints sText: 'nInt'" into all logs and as floating text over head
void DebugInt(string sText, int nInt);

//prints sText: 'sString'" into all logs and as floating text over head
void DebugString(string sText, string sString);

//prints "sText: 'sString1' 'sString2' 'sString3' 'sString4' 'sString5'" into all logs and as floating text over head
void DebugStrings(string sText, string sString1, string sString2, string sString3="", string sString4="", string sString5="");

//prints "sText: area name, X,Y,Z and orientation angle" into all logs and as floating text over head
void DebugLoc(string sText, location lLocation);

 

We will be using them to make our scripts to debug what is going on in our script.

 

So, assume we have a script or function that is faulty somehow. First we need to open the script in editor. I will use the balord ondeath fireball explosion, the script nw_s3_balordeth.nss as an example.

#include "NW_I0_SPELLS"    
void main()
{
    //Declare major variables
    object oCaster = OBJECT_SELF;
    int nMetaMagic = GetMetaMagicFeat();
    int nDamage;
    float fDelay;
    effect eExplode = EffectVisualEffect(VFX_FNF_FIREBALL);
    effect eVis = EffectVisualEffect(VFX_IMP_FLAME_M);
    effect eDam;
    //Get the spell target location as opposed to the spell target.
    location lTarget = GetLocation(OBJECT_SELF);
    //Limit Caster level for the purposes of damage
    //Apply the fireball explosion at the location captured above.
    ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eExplode, lTarget);
    //Declare the spell shape, size and the location.  Capture the first target object in the shape.
    object oTarget = GetFirstObjectInShape(SHAPE_SPHERE, RADIUS_SIZE_HUGE, lTarget, TRUE, OBJECT_TYPE_CREATURE | OBJECT_TYPE_DOOR);
    //Cycle through the targets within the spell shape until an invalid object is captured.
    while (GetIsObjectValid(oTarget))
    {
       //Fire cast spell at event for the specified target
        SignalEvent(oTarget, EventSpellCastAt(OBJECT_SELF, SPELL_FIREBALL));
        //Get the distance between the explosion and the target to calculate delay
        fDelay = GetDistanceBetweenLocations(lTarget, GetLocation(oTarget))/20;
        if (!MyResistSpell(OBJECT_SELF, oTarget, fDelay))
        {
            //Adjust the damage based on the Reflex Save, Evasion and Improved Evasion.
            nDamage = GetReflexAdjustedDamage(50, oTarget, GetSpellSaveDC(), SAVING_THROW_TYPE_FIRE);
            //Set the damage effect
            eDam = EffectDamage(nDamage, DAMAGE_TYPE_FIRE);
            if(nDamage > 0)
            {
                // Apply effects to the currently selected target.
                DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eDam, oTarget));
                //This visual effect is applied to the target object not the location as above.  This visual effect
                //represents the flame that erupts on the target not on the ground.
                DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oTarget));
            }
         }
       //Select the next target within the spell shape.
       oTarget = GetNextObjectInShape(SHAPE_SPHERE, RADIUS_SIZE_HUGE, lTarget, TRUE, OBJECT_TYPE_CREATURE | OBJECT_TYPE_DOOR);
    }
}

Lets say the script is not applying damage and you want to know why.

There are multiple ways of where to put the debugging and what to put inside it. I would recommend to first put a debugging function into each brackets, ie:

#include "inc_debugging"
#include "NW_I0_SPELLS"
void main()
{
    DebugString("balor ondeath","start");
    //Declare major variables
    object oCaster = OBJECT_SELF;
    int nMetaMagic = GetMetaMagicFeat();
    int nDamage;
    float fDelay;
    effect eExplode = EffectVisualEffect(VFX_FNF_FIREBALL);
    effect eVis = EffectVisualEffect(VFX_IMP_FLAME_M);
    effect eDam;
    //Get the spell target location as opposed to the spell target.
    location lTarget = GetLocation(OBJECT_SELF);
    //Limit Caster level for the purposes of damage
    //Apply the fireball explosion at the location captured above.
    ApplyEffectAtLocation(DURATION_TYPE_INSTANT, eExplode, lTarget);
    //Declare the spell shape, size and the location.  Capture the first target object in the shape.
    object oTarget = GetFirstObjectInShape(SHAPE_SPHERE, RADIUS_SIZE_HUGE, lTarget, TRUE, OBJECT_TYPE_CREATURE | OBJECT_TYPE_DOOR);
    //Cycle through the targets within the spell shape until an invalid object is captured.
    while (GetIsObjectValid(oTarget))
    {
        DebugString("balor ondeath, inside while loop, target:",GetName(oTarget));
       //Fire cast spell at event for the specified target
        SignalEvent(oTarget, EventSpellCastAt(OBJECT_SELF, SPELL_FIREBALL));
        //Get the distance between the explosion and the target to calculate delay
        fDelay = GetDistanceBetweenLocations(lTarget, GetLocation(oTarget))/20;
        if (!MyResistSpell(OBJECT_SELF, oTarget, fDelay))
        {
            DebugString("balor ondeath, inside myresistspell",GetName(oTarget));
            //Adjust the damage based on the Reflex Save, Evasion and Improved Evasion.
            nDamage = GetReflexAdjustedDamage(50, oTarget, GetSpellSaveDC(), SAVING_THROW_TYPE_FIRE);
            //Set the damage effect
            eDam = EffectDamage(nDamage, DAMAGE_TYPE_FIRE);
            if(nDamage > 0)
            {
                DebugString("balor ondeath, inside nDamage>0",GetName(oTarget));
                // Apply effects to the currently selected target.
                DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eDam, oTarget));
                //This visual effect is applied to the target object not the location as above.  This visual effect
                //represents the flame that erupts on the target not on the ground.
                DelayCommand(fDelay, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oTarget));
            }
         }
       //Select the next target within the spell shape.
       oTarget = GetNextObjectInShape(SHAPE_SPHERE, RADIUS_SIZE_HUGE, lTarget, TRUE, OBJECT_TYPE_CREATURE | OBJECT_TYPE_DOOR);
    }
    DebugString("balor ondeath","end");
}

When you run a game and kill balor you should see this:

From the messages we can determine that:

  1. the explosion found 3 targets in AoE.
  2. all targets failed resistance check
  3. all targets failed reflex saves
  4. script affected caster itself (i gave the script to the linu laneral and killer her)
  5. script ignored SR of my main character completely

With these informations we should now know what the bug is or at least which function causes it. In our case the script seems ignoring Spell Resistance and the bug is somewhere inside the function MyResistSpell.

We can now proceed two ways:

First, we can put this line into the script:

 DebugInt("balor ondeath, result of myresistspell:",MyResistSpell(OBJECT_SELF, oTarget, fDelay));

This will give us the actual value of the MyResistSpell. (This will print value 0) or we can apply debugging into MyResistSpell.

I won't follow further in this example as the issue here is known - it is because vanilla function ResistSpell returns -1/0 when used outside of the spellscript. Ie. this will never work and if you would want to make it functional you would have to make your own fake SR check.

This should give you rough idea of how to use debugging in script and how to use the messages you get ingame to find and fix the issue. This methodology is applicable on all programming, you just need to use different functions and different means how to activate the problematic part of the code.

 

SpawnScriptDebugger

IMPORTANT NOTE: This functionality is currently missing in NWN:EE and might not even be restored! So right now, this applies only for 1.69.

This is the actual reason I am writing this tutorial. It seems nobody except me knows about it?

So this is internal method of debugging scripts provided by Bioware, it requires two things:

1) Use function SpawnScriptDebugger() somewhere in your script or inside function called by your script.

2) Compile the script with debugging informations. To do that, you need to go into script editor settings and check Generate Debug Information When Compiling Scripts checkbox. (And re-compile the script if it was compiled without it)

Then all you have to do is to press F9 to run the module and activate the script. After that happens, you will see something like this:

The game client is stopped and the execution of the script is also stopped untill you loop the script step by step or you hit Quit button. And the debugger will highlight currently processed line.

The button Step Into will go deeper into custom functions, the button step Over won't.

This is extremely helpful and much easier to use, it will show you values of all variables and when they change, it will show you which function runs and which doesn't.

It however has some limitations:

  • it doesn't run in server mode
  • it doesn't show values of global variables (thus not very usefull for debugging CPP spellscripts for example)
  • it might break with TMI in script (iirc I had to shut it down via task manager when I ran into TMI with debugger active)
  • it will negatively influence player's gaming experiences if you ever forgot to remove it from scripts
  • the *.ndb files generated by compiling are very large and will increase size of the module greatly

So when using this method of debugging, you should:

  • make sure you always remove the SpawnScriptDebugger(); command from your scripts after you are done debugging.
  • toggle the Generate Debug Information When Compiling Scripts checkbox after you are done debugging.
  • avoid compiling all scripts while the above checkbox is enabled
  • delete the *.ndb files from your module time from time

 

I wanted to upload a video example of the SpawnScriptDebugger but due to the way it works (separate program) it turned out to be impossible to capture...

Migrate Wizard: 
First Release: 
  • up
    100%
  • down
    0%