logo logo

Avlis Persistence System

Table of Contents:

I. Introduction - What does APS do?
II. Installing and updating APS
III. Setting up a database
IV. Customization
V. Speed comparison
VI. Troubleshooting

I. Introduction - What does APS/NWNX ODBC2 do?

APS is a set of scripts written for Neverwinter Nights that work with NWNX2 to produce reliable persistence in a module. At the heart of the APS is an include file that lists a number of custom made functions for governing persistence. These functions can be used as is in your module or changed to suit your needs.

Whenever a script work with persistent data, it calls our APS functions and the Extender pulls the query out of the memory of the NWN server. It then passes it to the database, and writes the result of the query back into the memory of the server. The database has been tested with MySQL, MS-SQL, PostgresSQL, Microsoft Access, and the internal SQL database engine so far. Conceivably, any database with a decent ODBC driver will work.

We have included a demo module that illustrates how to use APS/NWNX ODBC2 and makes creating the database tables easy, and a second module demonstrating how persistent containers could be implemented.

Licence
APS and NWNX2 ODBC2 are distributed unter the terms of the GNU GENERAL PUBLIC LICENSE included in licence.txt.

II. Installing and updating APS

A. Installing the plugin

Copy the file nwnx_odbc.dll to you NWN folder, and aps_demo.mod to your NWN module folder.

B. Importing the erf

In order to be able to use the APS functions, you will need to import the aps_include file into your module.

  1. Place the file "aps2.erf" into the C:\Neverwinternights\NWN\erf directory.
  2. In the toolset, open up the module into which you wish to install the scripts.
  3. Under the File Menu, click Import. A window will pop up.
  4. Make sure the contents of the C:\Neverwinternights\NWN\erf directory are showing in the window
  5. Select "aps2.erf" from the list
  6. Click Import and ignore any messages about missing resources (click Yes).

The following scripts should now be imported: aps_onload, aps_include.

C. Updating from previous versions

Setting up a database

The first choice you have to make is what database you are going to use. We strongly suggest starting out with the internal SQL database (SQLite), which is one of the fastest and easiest options. It also comes with a prepared datase that is ready to go. Read the section "Configuration for the internal database" on how to set this up.

If you are using MySQL, go to the section "Configuration for MySQL database". If you are using any other database with an ODBC driver, go to the section "Configuration for ODBC database".

3.a. Configuration for the internal database

The internal NWNX ODBC2 database is based on SQLite v3, which is a fast SQL compliant database that developers can package with their applications - and that is what we did ! The main benefits of SQLite for NWN users are twofold:
  1. It is fast.
  2. It is easy to setup (zero configuration).

Note: We recommend this type of database for single instance servers, meaning setups where only one NWN server is accessing the database. We recommend against using it in setups where multiple computers access the database simultaneously.

Edit the nwnx.ini file and set the source= parameter to SQLite, and the file= parameter to the path and file where you want your database file:

   [ODBC2]
   source = sqlite
   file = sqlite.db

Note: Omitting the path like in the example above will put the database file into your NWN folder. If you want it somewhere else, set the file parameter to e.g. c:\temp\nwn.db.

Hint: One of the tools for working with the internal database is SQLiteCC. It is not needed to follow the setup instructions in this document, but will come in handy later. A somewhat more advanced tool would be DBManager Pro, which is also available as a freeware version.

Advanced note: After opening the database file, an implicit transaction is started automatically, since SQLite is significantly faster when access to the database is happening inside a transaction. If you want to access the database concurrently, or if you want to handle transactions yourself, issue a COMMIT right after the call to SQLInit to commit the implicit transaction.

3.b. Configuration for MySQL database

Below the various parameters are listed which are required in the configuration file nwnx.ini:

An example configuration file is displayed below. In this configuration you tell the plugin to use the direct mysql connection to the database nwn. This database resides on the localhost (same machine as NWN runs) and should be connected with user 'your_user' and password 'your_pwd'.

   [ODBC2]
   source = mysql
   server = localhost
   user   = your_user
   pwd    = your_pwd
   db     = nwn

3.c. Configuration for ODBC database

If you have a different database system than MySQL you will use the ODBC connection method. In this section we will describe the steps needed to setup your system correctly. In the description below we asume that you are using Windows XP or a similar system.

Edit the nwnx.ini file and set the dsn= parameter to the name you have just entered, e.g.:
   [ODBC2]
   source = odbc
   dsn = nwn

3.c. Creating the database tables

Note: As there is an almost unlimited amount of different databases out there, we can not give detailed instructions for all of them. If you want to use a database server like MSSQL or PostgresSQL, try to follow the steps described below accordingly. If you are using the internal database, all tables have already been created for you (in the file sqlite.db).

Make sure your database is up and running and that you have a database that is accessible to the ODBC2 plugin. In order to store data in it, you have to create some tables in the database. The included module "aps_demo.mod" makes this easy for the internal database and MySQL. By default, it creates tables for the internal database. If you want to use MySQL instead, open the module with the toolset and edit the scripts "demo_createtable" and "demo_obj_create" accordingly.

Next connect to your server with the Neverwinter Nights client. On the left side, you will see several different signs in front of you:

Now click every sign once, starting with the one on the left (Create Tables).

If the last sign sends you the message "Retrieved variable from database: testValue" your setup is ok and you're ready to start using APS. Note: This is the most basic setup. We encourage you to use more sophisticated databases and data structures if you feel confident to do so (see below).

On the right side, you will see several different signs that do almost the same as the other signs, but they are dealing with object instead of strings storage.

If the variable is not retrieved correctly, check out the log file nwnx_odbc.txt for errors. Also check your database if the table really has been created. The SQL statement that is executed by default is suitable for MySQL. If you use a different database server, you should adjust that statement accordingly:

Create a table "pwdata" with the following fields player, tag, name, val, expire, last. Here is an example for MySQL (taken from aps_demo.mod, script demo_createtable):

    SQLExecDirect("CREATE TABLE pwdata (" +
        "player varchar(64) default NULL," +
        "tag varchar(64) default NULL," +
        "name varchar(64) default NULL," +
        "val text," +
        "expire int(11) default NULL," +
        "last timestamp(14) NOT NULL," +
        "KEY idx (player,tag,name)" +
        ")" );

E. Using the persistence functions in your module

  1. After installing according to the instructions above, go to Module Properties under the Edit menu.
  2. Select aps_onload for your module OnModuleLoad event.
      OR
    Open aps_onload in the script editor and paste the contents of it into your pre-existing module's OnModuleLoad script. We only recommend doing this if you are familiar with NWScript.
The functions below are now implemented. Here is a lexicon containing information on their purpose and use:

void SQLInit()

Setup placeholders for ODBC requests and responses. This functions reserves memory APS and NWNX use for communication. Call this function once in the module load event.

SetPersistentString(object oObject, string sVarName, string sValue, int iExpiration=0, string sTable="pwdata")

This sets a persistent string on an object. The object can be any valid object in the game. The command works the same way the usual SetLocalString function works, except you can optionally add a little more information:

SetPersistentInt(object oObject, string sVarName, int iValue, int iExpiration=0, string sTable="pwdata")

This sets a persistent integer value on an object. The object can be any valid object in the game. The command works the same way the usual SetLocalInt function works, except you can optionally add a little more information:

SetPersistentFloat(object oObject, string sVarName, float fValue, int iExpiration=0, string sTable="pwdata")

This sets a persistent float value on an object. The object can be any valid object in the game. The command works the same way the usual SetLocalFloat function works, except you can optionally add a little more information:

SetPersistentLocation(object oObject, string sVarName, location lLocation, int iExpiration=0, string sTable="pwdata")

This sets a persistent location on an object. The object can be any valid object in the game. The command works the same way the usual SetLocalLocation function works, except you can optionally add a little more information:

SetPersistentVector(object oObject, string sVarName, vector vVector, int iExpiration=0, string sTable="pwdata")

This sets a persistent vector on an object. The object can be any valid object in the game. The command works the same way the usual Set local variable functions work, except you can optionally add a little more information:

GetPersistentString(object oObject, string sVarName, string sTable="pwdata")

This function works in the same manner as GetLocalString. It gets the persistent string from object oObject.

GetPersistentInt(object oObject, string sVarName, string sTable="pwdata")

This function works in the same manner as GetLocalInt. It gets the persistent integer value from object oObject.

GetPersistentFloat(object oObject, string sVarName, string sTable="pwdata")

This function works in the same manner as GetLocalFloat. It gets the persistent float value from object oObject.

GetPersistentLocation(object oObject, string sVarName, string sTable="pwdata")

This function works in the same manner as GetLocalLocation. It gets the persistent location value from object oObject.

GetPersistentVector(object oObject, string sVarName, string sTable="pwdata")

This function works in the same manner as the other get local variable functions. It gets the persistent vector value from object oObject.

void DeletePersistentVariable(object oObject, string sVarName, string sTable="pwdata")

This function deletes a variable from the database.

void SQLExecDirect(string sSQL)

Executes a SQL statement. If the statement returns a result set, you can read the data with the next two functions.

int SQLFetch()

Position cursor on next row of the resultset. Call this function before using SQLGetData().
Returns

int SQLFirstRow() (*deprecated*)

Function is deprecated but still there for backward compability. Simply calls SQLFetch().

int SQLNextRow() (*deprecated*)

Function is deprecated but still there for backward compability. Simply calls SQLFetch().

string SQLGetData(int iCol)

Return value of column iCol in the current row of result set sResultSetName.

Comments

IV. Customization

The APS is merely a set of custom functions that were originally used for the Avlis persistent world. The names of the functions and their parameters can be set to whatever is convenient for your module. Below is a hypothetical example of how customization can be done.

The "PowerG I33t" persistent world maintains their persistence with the Joe Bloe Persistence system (JBPS). In that system, a persistent string is set with the following JBPS function:

SetStickySring(string sVariableName, string sVariableValue, object oTarget)

All 5000 scripts in PowerG's elite world are written with the SetStickyString function, and they wish to retrofit their world to use NWNX. They would follow these steps:

  1. Open up the file aps_include in the script editor.
  2. Change the name of the APS function called SetPersistentString to SetStickyString.
  3. Rearrange the parameters of:

    SetPersistentString(object oObject, string sVarName, string sVarValue, int iExpiration, string sTable = "pwdata")
      to:
    SetStickyString(string VarName, string VarValue, object oObject, int iExpiration = 0, string sTable = "pwdata")

  4. Build the module, i.e. recompile all scripts.

Once the module is restarted, all of PowerG I33t's old persistent string scripts should be running the new persistence system. All it took was changing one script.

The above example is a simplified one, and the rest of the functions in the JBPS would need to be changed in the same manner. In cases where the function parameters were completely not equivalent to those used by the APS, they may have to be changed throughout every script in the module. Many but not all of the persistence systems out there should be convertible by the above method. We encourage further modification of aps_include to tailor the variable handling to your needs.

For persistence systems that use tokens, conversion will not be as easy. In these systems, token items are used to represent information on a player character. These tokens are not lost because they are actually in the inventory of the character. Because these systems work with tokens and not actual variables it will be hard to convert them into a database format. The module will most likely have to be completely re-fitted.

On possible idea to do this scriptomatically would be to write a module OnEnter script that strips the character of their tokens and issues SetPersistent variable commands into the database before destroying them. That would preserve the information that is there, but handling the actual scripts throughout the module will have to be done separately.

Alternatively, you can have a look at the GetPersistentString() function in "aps_include". There are some comments in this functions that should give you ideas on how to convert persistent data from your current system to APS.

V. Speed comparison

To give you an idea what to expect from the various database options, we conducted a small test involving 500 writes and reads. Note that this test is very artificial, since many aspects like table fragmentation, concurrent access, database size, and more realistic queries are not factored in. All tests were done on a Athlon 64 3200+ with database server, NWServer, and NWClient running local (NWClient with reduced process priority).

Writes were done with the following code:

    for (i = 0; i < 500; i++)
    {
        SQLExecDirect("INSERT INTO pwdata (player, tag, name,val) values " +
            "('~', '~', 'iter_" + IntToString(i) + "', 'value')");
    }

Reads were done with the following code:

    SQLExecDirect("SELECT * from pwdata");
    while (SQLFetch() == SQL_SUCCESS) {}

Bioware DB reads and write were done with the following code:

    for (i = 0; i < 500; i++)
    {
        SetCampaignString("test", "iter_" + IntToString(i), "value");
      -- respectively --
        s = GetCampaignString("test", "iter_" + IntToString(i));
    }

Results:

Database Write Read
SQLite (1) 30 ms 20 ms
SQLite (2) 36 ms 20 ms
SQLite (3) 2800 ms 20 ms
MySQL via ODBC 71 ms 38 ms
MySQL direct 68 ms 22 ms
Bioware DB (4) 856 ms 10 ms

Comments:

VI. Troubleshooting

Starting out with NWNX ODBC2 can be a bit daunting at first, especially if you are on your own. We highly encourage you to visit us at www.nwnx.org to ask question and get help with setting this system up.