Dynamic Item Properties - Subraces

The following example demonstrates the use of dynamic item properties in creating a flexible, completely data (2da) driven subrace system.


Technical Overview / Features


This system works by creating a creature hide on entering player characters. The creature hide then gets the item properties added for the subrace the player chose when creating his character.


Featureset:


* completely data driven with all data residing on the server
* add and remove subraces easily without changing code or recompiling
* enforces the players subrace field to be valid
* restrict subraces to member of certain races (i.e. only elves can become drow's)


_inc_subraces.nss


This file is the heart of the subrace system. Please note that it is a part of a much larger 'work in progress' and some functions were pulled from other files to make it run independently.


//------------------------------------------------------------------------------
//                       S u b r a c e    D e m o
//------------------------------------------------------------------------------
/*
                            _inc_subraces.nss
    The purpose of this file is to demonstrate how to make use of dynamic item
    properties and the bioware item property include file to create a highly
    flexible, 2da driven subrace system.
    Subrace 2DA Definition
    Row     Field      Purpose
    0       id         Unique Identifier (0 = no subrace)
    1       hide       reference to creature hide item (**** = x2_it_emptyskin)
    2       properties Comma seperated list of properties to add to the hide
    3       race       Racial type this subrace is limited to (-1 = no limit)
*/
//------------------------------------------------------------------------------
// author: georg zoeller; version: 2003-Oct-21; (c) 2003 Bioware Corp.
//------------------------------------------------------------------------------

// * Include the Bioware item property include.
#include "x2_inc_itemprop"
//#include "_inc_core"
//------------------------------------------------------------------------------
//                             C O N S T A N T S
//------------------------------------------------------------------------------
// * Set to TRUE if you feel you have too much memory and want to speed up 2da reads
const int TD_CFG_PERF_CACHE_ALL_2DA_READS = FALSE;

//------------------------------------------------------------------------------
//                             C O N S T A N T S
//------------------------------------------------------------------------------
const int GZ_SUBRACE_2DAROW_ID        = 0;
const int GZ_SUBRACE_2DAROW_HIDE      = 1;
const int GZ_SUBRACE_2DAROW_PROPS     = 2;
const int GZ_SUBRACE_2DAROW_RACELIMIT = 3;

const int GZ_SUBRACE_ERROR_UNKNOWN = -1;
const int GZ_SUBRACE_ERROR_INVALID_RACE = -2;
//------------------------------------------------------------------------------
//                         C O N F I G U R A T I O N
//------------------------------------------------------------------------------
// * Use these constants to change the names of the 2da files that hold all
// * information on subraces and properties
const string GZ_SUBRACE_CONST_PROP_2DA = "gz_properties";
const string GZ_SUBRACE_CONST_DEF_2DA  = "gz_subraces";
//------------------------------------------------------------------------------
//                            I N T E R F A C E
//------------------------------------------------------------------------------
// * Returns the creature hide object used for managing the subrace properties
// * on the player. If the player does not have a creature yet, create and
// * equip one, before returning it.
object SubraceGetOrCreateCreatureHide(object oPC, string sSubRace);
// * Returns the item property defined in row nPropRow of the 2da file in
// * GZ_SUBRACE_CONST_PROP_2DA
itemproperty SubraceGetItemPropertyFrom2DA(int nPropRow);
// * Reads GZ_SUBRACE_CONST_DEF_2DA and returns value for sSubrace in n2DARow
// * n2DARow possible values:
// *   GZ_SUBRACE_2DAROW_ID        - Subrace ID (unique id)
// *   GZ_SUBRACE_2DAROW_HIDE      - ResRef of Hide Item
// *   GZ_SUBRACE_2DAROW_PROPS     - Comma seperated properties list
// *   GZ_SUBRACE_2DAROW_RACELIMIT - Racial Type Limitation
string SubraceGetRaceData(string sSubrace, int n2DARow);
// * Perform the subrace check and perform all necessary operations on the
// * player to make the subrace found work on the character. If no valid subrace
// * is found, reset the players subrace field to a null-string
// * This function is intended to be called from the OnClientEnter event of a
// * module but will work from anywhere else as well.
int SubraceDoSubraceCheck(object oPC);

//------------------------------------------------------------------------------
//                        I M P L E M E N T A T I O N
//------------------------------------------------------------------------------

//------------------------------------------------------------------------------
//                        +++ I M P O R T E D  from _inc_debug +++
//------------------------------------------------------------------------------
const int TD_DEBUG = TRUE;
const int TD_DEBUG_WARNINGS = 1; // 1 - all, 2 - log only
void DebugMsg(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified");
void DebugWriteLog(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified");
void DebugWriteWarning(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified");
void LogWriteMessage(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified");
void LogWriteWarning(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified");
void DebugWriteLog(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified")
{
    if (TD_DEBUG)
    {
        WriteTimestampedLogEntry(sDomain +"::"+sFunction+ "() - " + sMessage);
    }
}
void DebugWriteWarning(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified")
{
    if (TD_DEBUG_WARNINGS>0)
    {
        WriteTimestampedLogEntry("WARN: "+ sDomain +"::"+sFunction+ "() - " + sMessage);
    }
}
void DebugMsg(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified")
{
    if (TD_DEBUG)
    {
        SendMessageToPC(GetFirstPC(),sDomain +"::"+sFunction+ "() - " + sMessage);
    }
    DebugWriteLog(sMessage, sDomain, sFunction )  ;
}
void DebugWarning(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified")
{
    if (TD_DEBUG_WARNINGS == 1)
    {
        SendMessageToPC(GetFirstPC(),"WARN " +sDomain +"::"+sFunction+ "() - " + sMessage);
    }
    DebugWriteWarning(sMessage, sDomain, sFunction )  ;
}
void LogWriteMessage(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified")
{
       WriteTimestampedLogEntry(sDomain +"::"+sFunction+ "() - " + sMessage);
}
void LogWriteWarning(string sMessage, string sDomain = "[Global]", string sFunction = "Unspecified")
{
       WriteTimestampedLogEntry("WARN: "+ sDomain +"::"+sFunction+ "() - " + sMessage);
}

//------------------------------------------------------------------------------
//                        +++ I M P O R T E D  from _inc_tools +++
//------------------------------------------------------------------------------
string GZGet2DAString(string s2DA, string sCol, int nRow, int bAlwaysCache = FALSE)
{
   int bCache = (bAlwaysCache || TD_CFG_PERF_CACHE_ALL_2DA_READS);
   string sCache = "GZ_2DA_" +s2DA + "+" +sCol + "+" + IntToString(nRow);
   string sRet = GetLocalString(GetModule(),sCache);
   if (sRet != "")
   {
        DebugWriteLog("HIT: " + sCache+": " + sRet ,"_inc_tool","GZGet2DAString");
        return sRet;
   }
   DebugWriteLog("MISS: " + sCache,"_inc_tool","GZGet2DAString");
   sRet = Get2DAString(s2DA,sCol, nRow);
   if (sRet != "" && bCache)
   {
        SetLocalString(GetModule(),sCache,sRet);
        DebugWriteLog("Write: " + sCache + ": " + sRet,"_inc_tool","GZGet2DAString");
   }
   return sRet;
}
int GZGet2DAInt(string s2DA, string sCol, int nRow, int bAlwaysCache = FALSE)
{
    int nRet = StringToInt(GZGet2DAString(s2DA, sCol, nRow, bAlwaysCache));
    return nRet;
}
//------------------------------------------------------------------------------
//                        +++ I M P O R T E D  from _inc_player +++
//------------------------------------------------------------------------------

void PlayerSendStrRef(object oPlayer, int nStrRef)
{
    SendMessageToPCByStrRef(oPlayer,  0x01000000 +nStrRef);
}

//------------------------------------------------------------------------------
//                        I M P L E M E N T A T I O N
//------------------------------------------------------------------------------


//------------------------------------------------------------------------------
// Georg Zoeller, Oct 2003
// Returns the creature hide object used for managing the subrace properties
// on the player. If the player does not have a creature hide yet, create and
// equip one, before returning it. If the player has a creature hide, it's
// resref is compared against the definition in the 2da and it is replaced if
// the resref's don't match
//------------------------------------------------------------------------------
object SubraceGetOrCreateCreatureHide(object oPC, string sSubRace)
{
    //--------------------------------------------------------------------------
    // Get definition of creature hide
    //--------------------------------------------------------------------------
    string sRefHide = GZGet2DAString( GZ_SUBRACE_CONST_DEF_2DA, sSubRace, GZ_SUBRACE_2DAROW_HIDE);
    object oHide = GetItemInSlot(INVENTORY_SLOT_CARMOUR,oPC);
    int bOverride;
    if (GetResRef(oHide) != sRefHide)
    {
        DestroyObject(oHide);
        bOverride = TRUE;
    }
    //--------------------------------------------------------------------------
    // If there is no creature hide already create one...
    //--------------------------------------------------------------------------
    if (!GetIsObjectValid(oHide) || bOverride)
    {
            //------------------------------------------------------------------
            // Create creature hide, based on 2da definition
            //------------------------------------------------------------------
             if (sRefHide == "")
            {
                //--------------------------------------------------------------
                // Empty String in the 2da, default to empty creature hide
                //--------------------------------------------------------------
                sRefHide = "x2_it_emptyskin";
            }
            oHide = CreateItemOnObject(sRefHide, oPC);
            //------------------------------------------------------------------
            // Some NWN creature hides are inv
            //------------------------------------------------------------------
            SetIdentified(oHide,TRUE);
            AssignCommand(oPC,ActionEquipItem(oHide,INVENTORY_SLOT_CARMOUR));
    }
    return oHide;
}
//------------------------------------------------------------------------------
// Georg Zoeller, Oct 2003
// Returns the item property defined in row nPropRow of the 2da file in
// GZ_SUBRACE_CONST_PROP_2DA
//
// Note: I wrapped ItemProperty ID #0 to 255, so I can use 0 as invalid
//------------------------------------------------------------------------------
itemproperty SubraceGetItemPropertyFrom2DA(int nPropRow)
{
    int nPropID = GZGet2DAInt("gz_properties", "PropertyID", nPropRow);
    itemproperty ip;
    int nParam1;
    int nParam2;
    int nParam3;
    int nParam4;
    if (nPropID>0)
    {
        //----------------------------------------------------------------------
        // We interprete 255 as 0 so we can use 0 as invalid before this point
        //----------------------------------------------------------------------
        if (nPropID == 255)
        {
            nPropID = 0;
        }
        nParam1 = GZGet2DAInt(GZ_SUBRACE_CONST_PROP_2DA, "Param1", nPropRow);
        nParam2 = GZGet2DAInt(GZ_SUBRACE_CONST_PROP_2DA, "Param2", nPropRow);
        nParam3 = GZGet2DAInt(GZ_SUBRACE_CONST_PROP_2DA, "Param3", nPropRow);
        nParam4 = GZGet2DAInt(GZ_SUBRACE_CONST_PROP_2DA, "Param4", nPropRow);
        //----------------------------------------------------------------------
        // Generate the item property from the parameters
        //----------------------------------------------------------------------
        ip = IPGetItemPropertyByID(nPropID,nParam1, nParam2, nParam3, nParam4);
    }
    if (!GetIsItemPropertyValid(ip))
    {
        //----------------------------------------------------------------------
        // Some message into the log to make debugging easier
        //----------------------------------------------------------------------
        DebugWriteWarning("_inc_subraces::SubraceGetItemPropertyFrom2DA - invalid item property created, values: " +
                                 "ID: " + IntToString(nPropID) + " p:" + IntToString(nParam1) + ", "+ IntToString(nParam2) + ", " +
                                 IntToString(nParam3) + ", " + IntToString(nParam4), "_inc_subraces","SubraceGetItemPropertyFrom2DA" );
    }
    return ip;
}

//-----------------------------------------------------------------------------
// This is the heart of the subrace system. This function determines if the pc
// has a valid subrace, meets all requirements for that subrace and, and finally
// adds the subrace specific item properties to the player's creature hide slot
// The parameter sSetSubraceTo can be specified to overwrite the player's
// current value in the SubRace field, effectively forcing another subrace.
//-----------------------------------------------------------------------------
int SubraceApplyPlayerSubrace(object oPC, string sSetSubraceTo = "")
{
    //--------------------------------------------------------------------------
    // If sSetSubraceTo was set, overwrite existing subrace with a new one
    //--------------------------------------------------------------------------
    if (sSetSubraceTo != "")
    {
        SetSubRace(oPC,sSetSubraceTo);
    }
    //--------------------------------------------------------------------------
    // Determine the 2da row and retrieve the subrace id
    //--------------------------------------------------------------------------
    string sSubRace   = GetStringLowerCase(GetSubRace(oPC));
    int    nSubraceId = GZGet2DAInt(GZ_SUBRACE_CONST_DEF_2DA, sSubRace, GZ_SUBRACE_2DAROW_ID,TRUE );
    //--------------------------------------------------------------------------
    // Get the PCs creature hide object.
    //--------------------------------------------------------------------------

    if (nSubraceId == 0 && sSubRace != "")
    {
        return  GZ_SUBRACE_ERROR_UNKNOWN ;
    }
    if (nSubraceId>0)
    {
        //----------------------------------------------------------------------
        // Enforce Racial Restrictions if existant.
        //----------------------------------------------------------------------
        int nReqRacialType = GZGet2DAInt( GZ_SUBRACE_CONST_DEF_2DA, sSubRace, GZ_SUBRACE_2DAROW_RACELIMIT );
        if (nReqRacialType != -1)
        {
            if (GetRacialType(oPC) != nReqRacialType)
            {
                return GZ_SUBRACE_ERROR_INVALID_RACE;
            }
        }
        object oHide = SubraceGetOrCreateCreatureHide(oPC,sSubRace);
        itemproperty ip;
        //----------------------------------------------------------------------
        // Determine and add all item properties that define this subrace
        // to the creature hide
        //----------------------------------------------------------------------
        string sProps = GZGet2DAString( GZ_SUBRACE_CONST_DEF_2DA, sSubRace, GZ_SUBRACE_2DAROW_PROPS);
        string sTemp;
        int nTemp;
        int nPos = FindSubString(sProps,",");
        while (nPos!= -1)
        {
            //------------------------------------------------------------------
            // if multiple properties have been specified seperated by ,
            // add all of them
            //------------------------------------------------------------------
            sTemp = GetSubString(sProps,0,nPos);
            nTemp = StringToInt(sTemp);
            sProps = GetSubString(sProps,nPos+1, GetStringLength(sProps) - GetStringLength(sTemp)-1);
            nPos = FindSubString(sProps,",");
            if (nTemp >0)
            {
                //--------------------------------------------------------------
                // Retrieve item property and add it to the hide, ignoring any
                // existing item property of the same type
                //--------------------------------------------------------------
                ip = SubraceGetItemPropertyFrom2DA(nTemp);
                IPSafeAddItemProperty(oHide, ip, 0.0f, X2_IP_ADDPROP_POLICY_IGNORE_EXISTING, FALSE,FALSE);
            }
            else
            {
                nPos = -1;
            }
         }
        nTemp = StringToInt(sProps);
        if (nTemp >0)
        {
            //------------------------------------------------------------------
            // Retrieve item property and add it to the hide, ignoring any
            // existing item property of the same type
            //------------------------------------------------------------------
            ip = SubraceGetItemPropertyFrom2DA(nTemp);
            IPSafeAddItemProperty(oHide, ip, 0.0f, X2_IP_ADDPROP_POLICY_IGNORE_EXISTING, FALSE,FALSE);
        }
    }
    //--------------------------------------------------------------------------
    // return the subrace id to the calling script. 0 indicates that no valid
    // subrace was found
    //--------------------------------------------------------------------------
    return nSubraceId;
}
//------------------------------------------------------------------------------
// Reads GZ_SUBRACE_CONST_DEF_2DA and returns value for sSubrace in n2DARow
// n2DARow possible values:
//   GZ_SUBRACE_2DAROW_ID        - Subrace ID (unique id)
//   GZ_SUBRACE_2DAROW_HIDE      - ResRef of Hide Item
//   GZ_SUBRACE_2DAROW_PROPS     - Comma seperated properties list
//   GZ_SUBRACE_2DAROW_RACELIMIT - Racial Type Limitation
//------------------------------------------------------------------------------
string SubraceGetRaceData(string sSubrace, int n2DARow)
{
    return GZGet2DAString( GZ_SUBRACE_CONST_DEF_2DA, sSubrace, n2DARow);
}
//------------------------------------------------------------------------------
// Perform the subrace check and perform all necessary operations on the
// player to make the subrace found work on the character. If no valid subrace
// is found, reset the players subrace field to a null-string
// This function is intended to be called from the OnClientEnter event of a
// module but will work from anywhere else as well.
//------------------------------------------------------------------------------
int SubraceDoSubraceCheck(object oPC)
{
    int nRet = SubraceApplyPlayerSubrace(oPC);
    if (nRet == GZ_SUBRACE_ERROR_UNKNOWN )
    {
        SendMessageToPC(oPC,"Unknown subrace selected");
        SetSubRace(oPC,"");
    }
    else if (nRet == GZ_SUBRACE_ERROR_INVALID_RACE)
    {
        SendMessageToPC(oPC,"Your subrace can not be member of the race you selected");
        SetSubRace(oPC,"");
    }
    else if (nRet != 0)
    {
        SendMessageToPC(oPC,"Valid subrace detected - character adjusted.");
    }
    return nRet;
}

Module OnEnter Script


Put the following code in the module's OnClientEnter Script to make the subrace system take effect:


     // This initializes subrace support on the player.
     // Detailed documentation provided within _inc_subraces.nss
     //--------------------------------------------------------------------------
     SubraceDoSubraceCheck(oPC);

Note: Don't forget to put #include "_inc_subraces.nss" up at the top of the script file.


gz_subraces.2da


This file holds the definition of each subrace. It is a design side only 2da file, thus it needs to be present only on the server. Please note that the data in this file is for demonstration purposes only, it does not contain the appropriate data for each of the subraces.


2DA V2.0
    Label       drow              tiefling 
0   id          1                 2      
1   hide        ****              nw_it_creitem196
2   properties  7,27              ****  
3   requireRace 1                 6        

For each subrace you want to have, you need a new column in this 2da, that uses the lowercase name of the subrace as name. The rows in this 2da file are defined as follows:


0 - id - a unique ID for your subrace, which MUST NOT be 0
1 - hide - the resref to the creature hide item. if you leave this field empty(****), the game will automatically create an empty skin
2 - properties - a comma seperated list of item properties that are going to be added to the creature hide. No spaces are allowed between the numbers and commas. Each number represents a line in gz_properties.2da
3 - requireRace - A race id (row number from races.2da) that is allowed to take this subrace. I.e. by putting a 1 [elf] into this row, only elves can take this subrace. If anyone else enters this subrace into their subrace field, it is automatically reset.


gz_properties.2da


This file is used to define the item properties that the system knows. The line indices for these numbers are put into the properties row of 'gz_subraces.2da'.


2DA V2.0
    Label          PropertyID   Param1   Param2   Param3    Param4    Param5   
0   ****           ****         ****     ****     ****      ****      ****
1   FtWhirlwind    12           29       ****     ****      ****      ****
2   FtDisarm       12           28       ****     ****      ****      ****
3   AbilityStr+1   255          0        1        ****      ****      ****
4   AbilityStr+2   255          0        2        ****      ****      ****
5   AbilityStr+3   255          0        3        ****      ****      ****
6   AbilityStr+4   255          0        4        ****      ****      ****
7   AbilityDex+1   255          1        1        ****      ****      ****
8   AbilityDex+2   255          1        2        ****      ****      ****
9   AbilityDex+3   255          1        3        ****      ****      ****
10  AbilityDex+4   255          1        4        ****      ****      ****
11  AbilityCon+1   255          2        1        ****      ****      ****
12  AbilityCon+2   255          2        2        ****      ****      ****
13  AbilityCon+3   255          2        3        ****      ****      ****
14  AbilityCon+4   255          2        4        ****      ****      ****
15  AbilityInt+1   255          3        1        ****      ****      ****
16  AbilityInt+2   255          3        2        ****      ****      ****
17  AbilityInt+3   255          3        3        ****      ****      ****
18  AbilityInt+4   255          3        4        ****      ****      ****
19  AbilityWis+1   255          4        1        ****      ****      ****
20  AbilityWis+2   255          4        2        ****      ****      ****
21  AbilityWis+3   255          4        3        ****      ****      ****
22  AbilityWis+4   255          4        4        ****      ****      ****
23  AbilityCha+1   255          5        1        ****      ****      ****
24  AbilityCha+2   255          5        2        ****      ****      ****
25  AbilityCha+3   255          5        3        ****      ****      ****
26  AbilityCha+4   255          5        4        ****      ****      ****
27  AbilityStr-1   27           0        1        ****      ****      ****
28  AbilityStr-2   27           0        2        ****      ****      ****
29  AbilityDex-1   27           1        1        ****      ****      ****
30  AbilityDex-2   27           1        2        ****      ****      ****
31  AbilityCon-1   27           2        1        ****      ****      ****
32  AbilityCon-2   27           2        2        ****      ****      ****
33  AbilityInt-1   27           3        1        ****      ****      ****
34  AbilityInt-2   27           3        2        ****      ****      ****
35  AbilityWis-1   27           4        1        ****      ****      ****
36  AbilityWis-2   27           4        2        ****      ****      ****
37  AbilityCha-1   27           5        1        ****      ****      ****
38  AbilityCha-2   27           5        2        ****      ****      ****
39  DamRes_Fire5   23           10       1        ****      ****      ****
40  DamRes_Fire10  23           10       2        ****      ****      ****
41  DamRes_Fire15  23           10       3        ****      ****      ****
42  DamRes_Elec5   23           9        1        ****      ****      ****
43  DamRes_Elec10  23           9        2        ****      ****      ****
44  DamRes_Elec15  23           9        3        ****      ****      ****

Col:PropertyId - This column holds the id of an item property (ITEM_PROPERTY_* constant from 'nwscript.nss'). Example: 12 is ITEM_PROPERTY_BONUSFEAT


Col:Param1-5 - These columns hold all the parameters that would go into the appropriate item property function that would be called to create the item property defined in the PropertyId column.


Example:
If your ItemPropertyId is 12 (ITEM_PROPERTY_BONUSFEAT), you only need to put one parameter (the id from iprp_feats) into these columns, as the scripting function ItemPropertyBonusFeat(int nFeat) only has one parameter (nFeat).


Further Uses/Ideas


* This system could probably be used to make some kind of lycanthropy scripts.
* You can script conversations that change the players subrace and use the functions provided in the #include file (i.e. SubraceApplyPlayerSubrace) to easily implement those choices.
* You can add custom feats in HotU - allowing you to make even more convincing subraces (i.e. drows that can cast darkness).


Notes


Please note that if your server is running the Enforce Legal Character option (ELC), hides equipped on a player will be removed when the player enters (?). You would need to call the subrace check on every player login to avoid that.


What This Example Is Not


This example is not a subrace system that circumvents the existing limitations of NWScript, namely ELC compliant subraces or ability score adjustments on the actual character data. If you are looking for a way to break the barriers of NWScript, there are a couple of options for that available elsewere.


This example is also NOT trying to compete with any of the more complex community implementations of subraces, it is just an example to show how dynamic item properties could be used to remove the need for hardcoding in script.





 author: Georg Zoeller, editor: Grimlar