You are here

Basic Scripting (not so basic)

8 posts / 0 new
Last post
kevL's
Basic Scripting (not so basic)

This is too shoddy to present as a document, but since a couple individuals recently expressed an interest in scripting here's a bunch of stuff, mostly off the top of my head:



here's the script that taught me everything I know about scripting:

void main()
{
}



void is a return type. Every function needs a return type, even if it's void - a null return.

main() is the name of a function. It can be thought of as the entry point of the script. The engine enters the script at main(). The only other entry point function that NwScript allows is:

int StartingConditional()
{
}



Note that it returns an int, an integer value. The engine will interpret this as a TRUE or FALSE value - FALSE=0, TRUE=anythingelse. A StartingConditional() script is useful only in dialogs, where they can be used to determine conversation flow (think of it as start/show the dialog-node, the node that the script is placed as a Condition to).

So, main() scripts are far more common. The engine is designed with hookpoints called events; it can be told to handle an event by putting the name of a valid, compiled, accessible script in an object's event slot, using the toolset. Many if not all IG objects other than Items (see Appendix B) have hookable events.

Scripts are plain textfiles with extension .NSS
Compiling a script creates a binary file with extension .NCS, typically in the same directory with the .NSS file.


Different types of objects have different sets of events. For example, the module-object has an OnAcquireItem event, while a placeable-object has an OnSpellCastAt event. Note that both of those object-types also have an OnHeartbeat event, which is checked by the engine every 6 seconds by default (this can be changed for an object, but usually a pseudo-heartbeat* is implemented instead of mucking with the default 6 seconds).

*not covered.


The set of module-level events can be found under View|ModuleProperties|Scripts, and events for other objects are under their Properties|Scripts.


In general, object is a concept that can denote anything from an integer to a module. It basically means, a bunch of bits held together by the language that created it. But since object is also a very specific technical term in NwScript, let's talk about types.

NwScript implements the following types:

int
float
string
vector
effect
event
location
talent
itemproperty
object
struct

 

In order for a compiler to know what the heck is going on, all variables need to be declared with a type ASAP (see Appendix F). This is a declaration of variable i as type integer:

int i;

This is the definition of i with a value:

int i = 10;

Once declared, a variable can have a value assigned to it:

i = 0;


Functions can also be used to assign values to variables:

object oNearest = GetNearestObject();

So ... you've got an object. Now let's check if it's a player-controlled creature:

if (GetIsPC(oNearest))
{
}


If it is a player-controlled creature, the engine will run any code between the curly braces. If it's not, then the engine will skip it and move on through the rest of a script. Code-flow in NwScript is always sequential: one thing happens after another in the order they appear in a script.

However, there are exceptions to sequential code-flow. When a function is wrapped in a DelayCommand() function, even if the delay is 0 seconds, it will be postponed till after the script finishes. The function DestroyObject() also takes a delay parameter, and will be postponed till after the script that it was called by finishes.


Most functions have parameters that take arguments, that are passed into a function call, to better hone in on what you want the function to do. For example, the declaration of GetNearestObject() looks like this:

object GetNearestObject(int nObjectType=OBJECT_TYPE_ALL, object oTarget=OBJECT_SELF, int nNth=1);

To get the 2nd-nearest creature to an object with tag "table", the definition of oNearest looks like so:

object oNearest = GetNearestObject(OBJECT_TYPE_CREATURE, GetObjectByTag("table"), 2);

To make things read more clearly, that is the same as this:

object oTable = GetObjectByTag("table");
object oNearest = GetNearestObject(OBJECT_TYPE_CREATURE, oTable, 2);


If you're unsure whether a table will exist at the time your script runs, it can be checked for validity:
- note that this uses the 1st-nearest table, and uses a hardcoded function that combines the two previous functions

object oTable = GetNearestObjectByTag("table");
if (GetIsObjectValid(oTable))
{
    // it exists, so let's do something silly with the table:

    effect eFire = EffectVisualEffect(VFX_HIT_AOE_FIRE);
    ApplyEffectToObject(DURATION_TYPE_INSTANT, eFire, oTable);

    DestroyObject(oTable);
}


[aside] Comments:
// this is a comment.
/* this is
   also a comment */


Okay. You may have noticed that the table that caught fire and got destroyed was defined as the nearest object. This begs the question, the nearest object to WHAT exactly. Answer: the table is the nearest object to the object that ran the script. The object that runs a script can be found by using

OBJECT_SELF

For example, in a trigger's OnEnter script, the trigger-object itself can be defined as:

object oTrigger = OBJECT_SELF;

To get the table nearest to the trigger:

object oTrigger = OBJECT_SELF;
object oTable = GetNearestObjectByTag("table", oTrigger);


But wait. The second parameter of GetNearestObjectByTag() has a default value of OBJECT_SELF already:

object GetNearestObjectByTag(string sTag, object oTarget=OBJECT_SELF, int nNth=1);

Default values are assigned to function parameters with an equals sign followed by a constant. A constant is a variable that doesn't change. I'll let you figure that one out ....

Hardcoded constants are defined in the file 'nwscript.nss' along with all the hardcoded functions. When the compiler sees a scripted constant it replaces it with its literal value automatically.

A function parameter that has a default value does not have to be specified as an argument as long as no subsequent argument is passed in after it. That is, this is not allowed:

object oTable = GetNearestObjectByTag("table", , 2);

This is allowed:

object oTable = GetNearestObjectByTag("table", OBJECT_SELF, 2);

A note on GetNearestObjectByTag() vs. GetObjectByTag() - the latter searches the entire currently loaded module, while the former searches only the area that the object that runs the script (OBJECT_SELF) resides in. And GetNearestObject*() will not return itself btw.



right, fine. Let's talk code-flow ....

object oTable = GetObjectByTag("table");
if (GetIsObjectValid(oTable))
{
}
else // what to do if a table is not valid ->
{
}



if/else conditions can be strung together:

if (GetIsDay())
{
    // debug
    SendMessageToPC(GetFirstPC(), "it is Day");
}
else if (GetIsDusk())
{
    // debug
    SendMessageToPC(GetFirstPC(), "it is Dusk");
}
else if (GetIsNight())
{
    // debug
    SendMessageToPC(GetFirstPC(), "it is Night");
}
else //if (GetIsDawn()) // this check is redundant since it's the only possibility left
{
    // debug
    SendMessageToPC(GetFirstPC(), "it is Dawn");
}



The above method of debugging is *highly* effective. When you get a problem with a script the first thing to do is check if it's even running:

void main()
{
    SendMessageToPC(GetFirstPC(FALSE), "Run your_script by " + GetName(OBJECT_SELF) + " (" + GetTag(OBJECT_SELF) + ")");

    // code follows ->
    if (GetLocalInt(OBJECT_SELF, "local_int"))
    {
        SendMessageToPC(GetFirstPC(FALSE), ". value of local_int= " + IntToString(GetLocalInt(OBJECT_SELF, "local_int")));
    }
    else
        SendMessageToPC(GetFirstPC(FALSE), ". local_int is 0");
}



Notice I left the curly braces off of the else-block. This is allowed but is NOT recommended for n00bs since complicated code-sequences can really screw up and you won't have a clue what went wrong.

Another screw up is using = (assignment of a value to a variable) instead of == (equivalence check if two values are identical), or vice versa. DON'T DO IT. read: you will. So don't


for-loops

int i;
for (i = 0; i != 10; ++i)
{
    // this code will loop 10 times.
}


i starts at 0, is checked that it does not equal 10, then is incremented *after* the first pass, when it is again checked that it does not equal 10 *before* the second pass. This procedure repeats until i does equal 10, at which the for-loop exits immediately.


while-loops

int i = 0;
while (i != 10)
{
    ++i;
    // this code will loop 10 times.
}


Notice that the while keyword is merely a looped if statement. The engine would deal with an infinite loop by throwing a TMI error (Too Many Instructions).

[aside] A keyword is a reserved word in the language. They cannot be used as labels of variables or functions. Examples are int, object, if, while, void ....


[aside] do/while loops are not covered here. (hint: They always do at least one pass through their code-block, then the loop-condition is evaluated only *after* the first pass.)


Note that the iterator does not have to be an integer:

object oPlayerPC = GetFirstPC();
while (GetIsObjectValid(oPlayerPC))
{
    // this code will loop over all true PCs (ie. Owned Characters) that are
    // currently in the module.
    
    oPlayerPC = GetNextPC();
}



Note on incrementing by 1, all these result in the same thing:

int i;
i = i + 1;
i += 1;
++i; // the variable is incremented by one, then used in the expression
i++; // the variable is used in the expression, then incremented by 1


Decrementing by 1 works the same way, except it uses the - operator.


Multiplication and division operations have similar shortcuts:

int i, j;

i = i * j;
i *= j;

i = i / j;
i /= j;


The standard algebra rules apply: multiplication and division happens before addition and subtraction (unless bracketed ofc), and divide by zero is not allowed (although NwScript is forgiving).


switches

int iLocal = GetLocalInt(OBJECT_SELF, "local_int");
switch (iLocal)
{
    case 0:
        // run code here if the local is 0
        break;
    case 1:
        // run code here if the local is 1
        break;
    case 2:
        // run code here if the local is 2
        break;
    default:
        // run code here if (iLocal != 0 && iLocal != 1 && iLocal != 2)
        break;
}



switch w/ fall-through:

int iLocal = GetLocalInt(OBJECT_SELF, "local_int");
switch (iLocal)
{
    case 0:
        // run code here if (iLocal == 0)
    case 1:
        // run code here if (iLocal == 0 || iLocal == 1)
    case 2:
        // run code here if (iLocal == 0 || iLocal == 1 || iLocal == 2)
        break;

    default:
        // run code here if (iLocal != 0 && iLocal != 1 && iLocal != 2)
        break;
}


Note: switch/case statements (in NwScript) only work with integers. They are similar to but generally faster than (large) if/else blocks.


break and continue keywords

break; is used to immediately stop and exit a loop, or simply to exit a switch/case block.

string sRoster = GetFirstRosterMember();
while (sRoster != "")
{
    object oRoster = GetObjectFromRosterName(sRoster);
    if (GetIsObjectValid(oRoster) && !GetIsDead(oRoster))
    {
        // an instantiated roster-member is not dead
        SetLocalInt(GetModule(), "isRosterAlive", TRUE);
        break;
    }

    sRoster = GetNextRosterMember();
}



continue; is used to go back to the top of a loop, bypassing any code that's within the loop-block below it, typically causing the iterator to advance and get re-checked against the stop-condition.

string sRoster = GetFirstRosterMember();
while (sRoster != "")
{
    object oRoster = GetObjectFromRosterName(sRoster);
    if (!GetIsObjectValid(oRoster) || GetIsDead(oRoster))
    {
        // roster-member is either invalid or is already dead
        sRoster = GetNextRosterMember();
        continue;
    }

    // the roster-member is instantiated and not dead here, kill it
    effect eDeath = EffectDeath();
    ApplyEffectToObject(DURATION_TYPE_INSTANT, eDeath, oRoster);

    sRoster = GetNextRosterMember();
}


note: There are better ways to do that - it's just an example.


The return keyword:

return; causes a script to immediately exit - program execution returns to the engine. A void-returning function infers this at its final curly brace, but functions that specify any other return type require something to be returned explicitly.

int GetIsRosterAlive()
{
    if (GetLocalInt(GetModule(), "isRosterAlive"))
    {
        return TRUE;
    }
    return FALSE;
}



Operators (in no particular order):

=   assign value
==  equivalent to
!=  not equivalent to
>   greater than
<   less than
>=  greater than or equal to
<=  less than or equal to
&&  boolean AND
&   bitwise AND (both bits true)
&=  bitwise AND and assign
||  boolean OR
|   bitwise OR (either bit true)
|=  bitwise OR and assign
^   bitwise XOR (either bit true but not both)
^=  bitwise XOR and assign
~   bitwise NOT (invert bits)
.   dot-accessor
!   boolean NOT
+   addition or string concatenation
++  pre or post increment
+=  addition or string concatenation and assign
-   subtraction
--  pre or post decrement
-=  subtraction and assign
*   multiplication
*=  multiplication and assign
/   division
/=  division and assign
%   modulo
%=  modulo and assign
>>  bitwise shift right
>>= bitwise shift right and assign
<<  bitwise shift left
<<= bitwise shift left and assign
?:  ternary conditional operator


(hint: do a Google for a C-like tutorial) Be careful with operator precedence, and bracket (especially bitwise) operations accordingly.


The ternary operator. In the following definition of oPC, if the expression before the question-mark evaluates TRUE, oPC will be the value before the colon; if FALSE, the value after the colon is assigned instead:

object oPC = (GetPCSpeaker() == OBJECT_INVALID) ? OBJECT_SELF : GetPCSpeaker();

It's a different way of doing this:

object oPC = GetPCSpeaker();
if (!GetIsObjectValid(oPC))
{
    oPC = OBJECT_SELF;
}



Note that conditional expressions can be grouped together to test what you want:

object oCreature = GetNearestObject();
if ((GetObjectType(oCreature) == OBJECT_TYPE_CREATURE && GetCreatureSize(oCreature) >= CREATURE_SIZE_LARGE)
    || GetTag(oCreature) == "big_bad_boss")
{
    // this code will run if either of these conditions evaluated TRUE:
    // 1) the object-type is OBJECT_TYPE_CREATURE of CREATURE_SIZE_LARGE or CREATURE_SIZE_HUGE
    // 2) the object has tag "big_bad_boss"
}


Notice that parentheses should be used to subgroup distinct sets of conditions.


Tags are Case Sensitive! So are identifiers (ie. variables and functions).


The equivalence operators (==, !=) can evaluate variables of the same type only; eg, an int against an int, or an object against an object. The compiler will throw an error if there's a type mismatch.



Speaking of compilers: Please use Skywing's Advanced Script Compiler for NWN2

  • up
    100%
  • down
    0%
kevL's

APPENDIX A
Helper functions.

A subfunction can be written and called upon to help any other function.


- using an included file that contains the helper:
----
// file: 'inc_location.nss'

location GetSpecialLocation()
{
    object oWaypoint = GetWaypointByTag("wp_special");
    return GetLocation(oWaypoint);
}

----
// file: 'onenter_trigger.nss'

#include "inc_location"

void main()
{
    object oPC = GetEnteringObject();
    if (GetIsPC(oPC))
    {
        AssignCommand(oPC, ClearAllActions(TRUE));

        location lSpecial = GetSpecialLocation();
        AssignCommand(oPC, JumpToLocation(lSpecial));
    }
}

----

- not using an #include file:
----
// file: 'onenter_trigger.nss'

location GetSpecialLocation();

void main()
{
    object oPC = GetEnteringObject();
    if (GetIsPC(oPC))
    {
        AssignCommand(oPC, ClearAllActions(TRUE));

        location lSpecial = GetSpecialLocation();
        AssignCommand(oPC, JumpToLocation(lSpecial));
    }
}

location GetSpecialLocation()
{
    object oWaypoint = GetWaypointByTag("wp_special");
    return GetLocation(oWaypoint);
}

----

Alternately, the declaration of a helper function can be omitted if its definition comes before the function(s) that use it:
----
// file: 'onenter_trigger.nss'

location GetSpecialLocation()
{
    object oWaypoint = GetWaypointByTag("wp_special");
    return GetLocation(oWaypoint);
}

void main()
{
    object oPC = GetEnteringObject();
    if (GetIsPC(oPC))
    {
        AssignCommand(oPC, ClearAllActions(TRUE));

        location lSpecial = GetSpecialLocation();
        AssignCommand(oPC, JumpToLocation(lSpecial));
    }
}

----

The point is that the compiler needs to read and interpret scripting in a specific order.


takeaway: If you think that's rough ... ~pfft~ Fortunately the developers of NwScript made it very C-like and easy to get going.

  • up
    100%
  • down
    0%
kevL's

APPENDIX B
Events for Items.

These are fired by module-level events and/or itemproperties (i think..), using tag-based scripting. For a script to handle a tag-based event, its filename needs to start with "i_" and end with one of the following suffixes:

_aq - OnAcquireItem (module based, player only)
_ua - OnUnacquireItem (module based, player only)

_eq - OnPlayerEquipItem (module based, player only)
_ue - OnPlayerUnequipItem (module based, player only)

_ac - OnActivateItem (module based, player only)

_hc - OnHitCast (hit if a weapon, get hit if armor) (itemproperty based, incl/ NPCs)
_ci - OnCastSpellAt (itemproperty based i guess)

Between the prefix and suffix must be the tag of the item that an event will be handled for. Eg,

script: 'i_tag_ac'

Note that the suffixes are defined with stock values in 'x2_inc_switches', and the prefix is usually defined in the module-level OnModuleLoad event. Additionally, the tag-based scripting mode can be reverted to NwN1-style, also in the OnModuleLoad event (where tag-based scripting can be turned off completely as well).

In short, there has to be specific code in those module-level scripts to fire the (first 5) tag-based scripts, or itemproperties set on an item to fire the other (last 2) events.

  • up
    100%
  • down
    0%
kevL's

APPENDIX C
Structs.

A data-object of type struct is a data-structure. A struct can be declared to contain data-objects of other data-types. Like other types, it must be declared before it can be used, either at the top of a script or in an #include:

struct stCoins
{
    int iGold;
    int iSilver;
    int iCopper;
};



The types of the internals do not have to be the same. They are accessed with the . operator:

struct stCoins GetCoins()
{
    struct stCoins rCoins;

    rCoins.iGold   = 10;
    rCoins.iSilver = 5;
    rCoins.iCopper = 0;

    return rCoins;
}



They are passed as parameters into functions as follows:

int GetGoldCoins(struct stCoins rCoins)
{
    return rCoins.iGold;
}



Note that the native type vector is a struct of 3 floats:

void TellPosition(object oTarget)
{
    vector vPos = GetPosition(oTarget);

    SendMessageToPC(GetFirstPC(), "pos X= " + FloatToString(vPos.x));
    SendMessageToPC(GetFirstPC(), "pos Y= " + FloatToString(vPos.y));
    SendMessageToPC(GetFirstPC(), "pos Z= " + FloatToString(vPos.z));
}



Finally, a special note on the native type location. It is similar to a struct but instead of using the . operator to access its internals, three hardcoded functions access its parts:

vector GetPositionFromLocation(location lLocation);
object GetAreaFromLocation(location lLocation);
float GetFacingFromLocation(location lLocation);



Conversely, to construct a location:

location GetTargetLocation(object oTarget)
{
    object oArea = GetArea(oTarget);
    vector vTarget = GetPosition(oTarget);
    float fOrientation = GetFacing(oTarget);

    return Location(oArea, vTarget, fOrientation);
}


that's just an example. Ordinarily if you want the location of an object, use the hardcoded function:

location GetLocation(object oObject);
 

  • up
    100%
  • down
    0%
kevL's

APPENDIX D
A note on .2da's

Many of the integer constants that are found in 'nwscript.nss' refer to rows in the various .2da files. If you notice that a constant has not been defined for a row of a particular .2da that you want to refer to, either define a constant at the top of your script

const int BASE_ITEM_STEIN = 127;

or simply use the integer literal (the row #).

  • up
    100%
  • down
    0%
kevL's

APPENDIX E
Conversation scripts and console scripts.

Conversation scripts that can be used as Actions of a dialog-node are typically prefaced with "ga_" and use the entry function void main(). When used as the Conditions of a dialog-node, they are typically prefaced with "gc_" and use the entry function int StartingConditional().

They share a feature with console scripts, in that parameters can be specified that take arguments, which are passed into a script - this is not allowed with ordinary event-driven scripts. In the Conversation Editor, such parameters show up after the scriptname when Refresh is clicked. In the console, they are typed in as follows:

`
debugmode 1
rs test_console_script("string_var", 1)
debugmode 0
`


that could correspond to a script such as this:

// 'test_console_script'
void main(string sVariable, int iValue)
{
    SetLocalInt(GetModule(), sVariable, iValue);

    // test it:
    int iTest = GetLocalInt(GetModule(), sVariable);
    SendMessageToPC(OBJECT_SELF, IntToString(iTest));
}

  • up
    100%
  • down
    0%
kevL's

APPENDIX F
Scope.

It's not quite true that variables have to be declared ASAP. They have to be declared before they are used, they have to be declared inside of scope. Stuff that is in scope is usually the stuff between matching curly braces:

{
    string sString;
    // sString can be used anywhere before the next brace.
}


Scope includes any nested curly braces:

{
    string sString;

    {
        // sString can also be used here.
    }
}


BUT. Be warned that a variable with the same label can be declared in a subscope, and it will not be the same variable as the one in its superscope:

{
    string sString;

    {
        string sString;

        // sString used here is no longer the same as ...
    }
    // the sString used here.
}



A variable can also have script-scope, if it is declared before the functions that use it:

string sString;

void main()
{
    // sString can be used here.
}
// sString can also be used by any other functions that are defined in this file.



Note that if a variable is declared in an #include, its scope will increase even further. A good policy, however, is to keep your variables tightly scoped.


--
The syntax of NwScript is pretty straightforward once you get the hang of it. The difficult part is having to search through thousands of functions and constants, till you eventually realize the power of your fingertips.

And dealing with engine-quirks when you're sure "I'm doing everything correctly!"


I PLACE THE ABOVE MATERIAL IN THE PUBLIC DOMAIN. (in case anyone wants to organize it into some sort of [proper] tutorial)

  • up
    100%
  • down
    0%
Lance Botelle

Hi KevL,

Thanks for putting this together. There are one or two things in your posts I have often wondered about. maybe this will help me get my head around some of them. :)

Thanks, Lance.

  • up
    50%
  • down
    50%