Migrating server to Neverwinter EE

Hi all,

I don’t know if there is any tutorial/help for the migration of a server to the Enhanced Edition but I found nothing. The lack of information is somewhat frustrating so I decided to share what I’ve already found just to help other people in the same situation.

(I’m Spanish, so please excuse my poor english)

Old server:

  • My good old server is running in a computer with Ubuntu 64 bits
  • NWNX plugins: chat, area, funcs, structs, weapon…
  • Chroot installed for the compilation of the NWNX plugins (32 bits)
  • Aurora toolset: I have a MacBook, so I’m running the toolset in a Parallels virtual machine (Windows XP)

New server, first decisions:

Since I want to first test the server on my laptop and then move it to the old PC, I decided to install it on a virtual machine: VMWare workstation (free on linux) So I can easily copy the machine from my Mac to my linux computer.

For the virtual machine I’ve installed Ubuntu Xenial (16.04 LTS) 32 bits, so less problems with the compilation of the new NWNX which is still 32 bits.

Aurora toolset:

The toolset of the EE, developed in Borland C++ if I remember well, it can only be run in windows and Beamdog is not planning to offer a new version for Linux or Mac. Then I will need another virtual machine. The questions are: ¿Windows 7? ¿Windows 10? ¿Parallels? ¿Vmware?

The choice between Parallels and VMWare is easy: OpenGL is very limited in Parallels, it only supports OpenGL 2.0. I tried to run the toolset on windows 7 and windows 10, and it crashes every time I tried to open an area.

So now I have a VMware virtual machine with Windows 10. However, there is still a big bug with the toolset: I can’t see the textures of the terrain :frowning: I don’t know yet if this is due to the haks, anyway… Now I’m focussed on the scripts migration and I’ve checked that the textures are shown when I load the module on my Mac client, I hope this bug will disappear in future versions of the toolset (or the VMWare player)

PS: I don’t have a lot of free time these days but the next post will be about the server install and the NWNX compilation.

3 Likes

Ok, so let’s start with server installation. I’m currently using the zip file you can found at: https://forums.beamdog.com/discussion/67157/server-download-packages-and-docker-support

It’s a 25MB zip with all the required files for running your server. You only have to uncompress it in your home directory, in my case in ~/server. Note that the server will use the files locate in the default directory ~/.local/share/Neverwinter Nights/ where the nwnplayer.ini file can be found with the server configuration.

The server can be started with the following command
~/server/bin/linux-x86/nwserver-linux -module “YourModuleName”

The module “YourModuleName.mod” must be located in the directory ~/.local/share/Neverwinter Nights/modules.

And next: NWNX

2 Likes

NWNX can be downloaded from here: https://github.com/nwnxee/unified . First install the git package on your linux server and then to clone the repository just type:

git clone https://github.com/nwnxee/unified.git

If your linux is the 32 bit version the compilation of NWNX is quite easy but it requires the gcc-6 package. In the case of Ubuntu 16.04 you’ll have to add the untrusted “Toolchain test builds PPA”:

sudo add-apt-repository ppa:ubuntu-toolchain-r/test
sudo apt-get update
sudo apt-get install gcc-6 g++-6

The cmake package is also required, and if like in my case you want to use MySQL databases for your persistent world, install also the MySQL dev package:

sudo apt-get install cmake libmysqlclient-dev

Now go to the Build directory of your NWNX repo and:

cmake -DCMAKE_BUILD_TYPE=Release ../
make -j 8

After that the compiled binaries are located in the Binaries directory of the NWNX repo. Now copy the binaries of core and the plugins you want to use to the same directory where the nwserver-linux is. In my case I copied NWNX_Core.so (the NWNX core) and the plugins NWNX_SQL.so , NWNX_Chat.so and NWNX_Creature.so to ~/server/bin/linux-x86/

IMPORTANT: At the time I’m writing this post, the NWNX log file (to debug NWNX) is not configurable. You have to create the directory logs.0 where you copied the NWNX_Core.so. So when you start your server, NWNX will create a file named nwnx.txt in it with all the debug information. If this directory does not exist, you will see nothing… I spent hours trying to find the damned log file! :sweat:

The default location where the neverwinter server store the logs is defined at the beginning of the ~/.local/share/Neverwinter Nights/nwn.ini file, since I don’t want to check the logs in two different directories I changed it to the previously created NWNX logs.0 directory.

Now I will suppose you have already created a user and a MySQL database for your server. For example: a user named “nwn”, with password “nwnpass” and a database named “mydb”.

The configuration of NWNX is done by shell environment variables, there is no config file. In order to configure the MySQL database you have to EXPORT (bash shell) some variables. In my case I use a script, nwnstartup.sh, in the same directory of the server binary, similar to this one:

#!/bin/sh
umask 002

export LD_PRELOAD=./NWNX_Core.so
export NWNX_SQL_USERNAME=nwn
export NWNX_SQL_PASSWORD=nwnpass
export NWNX_SQL_DATABASE=mydb

./nwserver-linux \
-module "MyModule" \
-quiet \
"$@"

If all is ok you will see the following:

Working Directory For Game Install Is: /home/nwn/server
Working Directory For Your Resources Is: /home/nwn/.local/share/Neverwinter Nights
Neverwinter Nights Server
Build:8162
Copyright BioWare Corp 1998-2004
Registering crash signal handlers.

Server: Loading...
I [20:49:48] NWNX_Core: Server is running version 8162.
I [20:49:48] NWNX_Core: Loading plugins from: .
I [20:49:48] NWNX_Core: Loaded plugin 0 (Chat) v1 by Liareth.
I [20:49:48] NWNX_Core: Loaded plugin 1 (Creature) v1 by various / sherincall.
I [20:49:48] NWNX_SQL: Connecting to type MYSQL
I [20:49:48] NWNX_SQL: Connection info:  host=localhost username=nwn
I [20:49:48] NWNX_Core: Loaded plugin 2 (SQL) v1 by Liareth.
Server: Running...

Server: Loading module "MyModule"...............................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
Server: Module loaded
2 Likes

Once the NWNX SQL is correctly loaded the database scripts of the module must be rewritten with the new syntax.

We started the server 12 years ago. At the beginning the server was running on Windows XP so the easy option for the database was the use of SQLite. For the toolset scripts I used the Avilis Persistent System (APS)… Hélas, the new NWNX does not support SQLITE :sob:

So I’ve changed the functions of the APS include file with the new NWNX_SQL syntax that can be found here: https://github.com/nwnxee/unified/blob/master/Plugins/SQL/Documentation/Migration_Guide.md

The result is here: https://drive.google.com/open?id=1qTTvbNTFbxXAPMiIWv7EZAuXAezHw4DK

If you want to use this file you will need to also download the file nwnx.nss: https://github.com/nwnxee/unified/blob/master/Core/NWScript/nwnx.nss

Please note that:

  1. This file replaces the original aps_include file
  2. I have only changed a small portion of the original file (only the things I needed)
  3. All the credits go to the original author of the APS (and the NWNX team for the new portion of the code)
  4. If somebody change the rest of the original include it would be nice to share the result to this community :wink:

If the changes are correct, in nwn.txt log file you will see something like:

I [13:59:36] NWNX_Core: Server is running version 8162.
I [13:59:36] NWNX_Core: Loading plugins from: .
I [13:59:36] NWNX_Core: Loaded plugin 0 (Chat) v1 by Liareth.
I [13:59:36] NWNX_Core: Loaded plugin 1 (Creature) v1 by various / sherincall.
I [13:59:36] NWNX_SQL: Connecting to type MYSQL
I [13:59:36] NWNX_SQL: Connection info:  host=localhost username=nwn
I [13:59:36] NWNX_Core: Loaded plugin 2 (SQL) v1 by Liareth.
I [14:00:02] NWNX_SQL: Successful SQL query. Query ID: '1', Query: 'SELECT val FROM sys WHERE player='~' AND tag='' AND name='MODULE_CALENDAR_DAY'', Results Count: '1'.
I [14:00:02] NWNX_SQL: Successful SQL query. Query ID: '2', Query: 'SELECT val FROM sys WHERE player='~' AND tag='' AND name='MODULE_CALENDAR_MONTH'', Results Count: '1'.
I [14:00:02] NWNX_SQL: Successful SQL query. Query ID: '3', Query: 'SELECT val FROM sys WHERE player='~' AND tag='' AND name='MODULE_CALENDAR_YEAR'', Results Count: '1'.
I [14:00:02] NWNX_SQL: Successful SQL query. Query ID: '4', Query: 'SELECT val FROM cnr WHERE player='~' AND tag='' AND name='CNR_MINING_SKILL_PCNAME'', Results Count: '0'.

In my case, the "on module load" script executes some SQL queries for the persistent “date” system of the module, these are the first queries that are displayed at the beginning of the log.

Once you have tested the SQL functions you can set the log level of the SQL plugin to display only warnings and errors adding this line to the nwnstartup.sh script:

export NWNX_SQL_LOG_LEVEL=4
3 Likes

Script g_i_dbsql.nss updated, the SQL WHERE clause where missing in the UPDATE :flushed:

2 Likes

Buenas Bhaal, podrías hacer el tutorial en español para los españoles? un abrazo!

@El_Vara ¿Has probado el Traductor de Google? No sé de inglés a español, pero de español a inglés parece funcionar muy bien en estos días. Como puede suponer, mi español no existe y esto fue realmente compuesto en Google Translate.

TR

Hi Bhaal, ¿que tal?

Ok, that’s my knowledge of Spanish exhausted as well as Tarot’s ( but at least I didn’t need Google . . .) :innocent:

I also know nothing of servers but I do know about Macs. Not sure if this is relevant but if you’re just looking for a way to use the Toolset on a Mac, probably the easiest is to use WINE which does away with the need for any virtual machines like those you mention. It works very well for me.

There is also a tutorial on YouTube to show how to install it.

@El_Vara este tutorial está obsoleto, es de marzo de 2018!!!

Ahora mismo si quieres usar nwnx lo más sencillo es usar docker, incluso en windows, está todo hecho. Nunca me he puesto con docker, pero hay gente que lo usa sin tener apenas conocimientos técnicos.

Un saludo

@jimdad55 My problems with the aurora toolset were 1 year ago but thank you :stuck_out_tongue:

EDIT: Nevermind, faking a solution to get past the error just hits another error. I need to sit down and see what I’m doing wrong from the start. I think I’m not adding teh correct wrapper functions…

I realize this might be convoluted, so feel free to say it’s too convoluted to assist with, just a quick check with someone more knowledgeable… I’m hoping for a quick fix where none might be available.

I’m trying to use your SQL script as an aps_include substitute, and everything compiles except the mod_load script which ends with this error:
1/26/2020 6:01:53 PM: Error. ‘mod_load’ did not compile.
mod_load.nss(14): ERROR: UNDEFINED IDENTIFIER (SQLInit)

The mod_load scripts starts with this:
#include “aps_include”
#include “nwnx_sql”
#include “_module”
////////////////////////////////////////////////////////////////////////////////
void main(){
object oModule = GetModule();
int iPlayers = GetPersistentInt(oModule,“Players”);
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
// Set up NWNX database
SQLInit();

if(GetPersistentInt(oModule,“Tables”)!=1)
{
SQLExecDirect(“CREATE TABLE pwdata (” + “player varchar(64) NOT NULL default ‘~’,” + “tag varchar(64) NOT NULL default ‘~’,” + “name varchar(64) NOT NULL default ‘~’,” + “val text,” + “expire int(11) default NULL,” + “last timestamp NOT NULL default CURRENT_TIMESTAMP,” + “PRIMARY KEY (player,tag,name)” + “)”);
SQLExecDirect(“CREATE TABLE pwobjdata (” + “player varchar(64) NOT NULL default ‘~’,” + “tag varchar(64) NOT NULL default ‘~’,” + “name varchar(64) NOT NULL default ‘~’,” + “val blob,” + “expire int(11) default NULL,” + “last timestamp NOT NULL default CURRENT_TIMESTAMP,” + “PRIMARY KEY (player,tag,name)” + “)”);
SetPersistentInt(oModule,“Tables”,1);
}

The error is pretty clear, no? :slight_smile: SQLInit is not in the headers you are using. It’s not used with the EE nwnx sql interface I believe so you can probably just remove that call.

Yeah, I think I got part of it working now, and it’s running on an existing DB dataset ok! Woo!

Unfortunately, it does NOT generate new worlds on Module load when no data is present… so while I can run my world on NWNXEE now (which IS awesome), I won’t be able to share the base version I’m using for people to set up on their own servers.

I could still share the original NWNX 1.69 version, though. So that’s something.

Where would I see the logs? Don’t seem to be any and the SQL text goes by so fast…

Ok, good ol’ ./startuoa > sqlerror.txt gives me 600 lines of:
W [22:27:32] [NWNX_SQL] [MySQL.cpp:90] Failed to prepare statement: Table ‘uoa.pwdata’ doesn’t exist

Then things start working, but that initial SQL work is supposed to do something, because my world appears to have no area to send my character to when I log in. It states there are no areas, and so while there are location values on my PC it has no place to send me except to leave me in limbo. Same for new characters.

Second server run gives no errors, but same result.

But that just depends on what data you want in your DB. I don’t use the old pwdata table at all. Just edit the table name in your Get/SetPersistent functions if needed.
Important thing is NWN:EE and NWNX is working with your MySQL DB, even if you don’t have a ‘uoa’ DB or a ‘pwdata’ table set.

Yeah, I got everything talking. The issue is that the mod_load feature of generating a new world (when starting a new game), which is the first step if no tables are present in the DB isn’t working. so I can only continue a game I’ve already started and won’t be able to start a new game… :frowning:.

Well, next part is figuring out what I’m doing wrong. I probably rewrote the function incorrectly, and since I am not a programmer it’ll take a while to figure out.

See the above code snippet. IT does nothing that I can tell, so I included the lower code block in the function and while it compiles it doesn’t generate the new world.

I’ll rewrite this part for you with NWNX:EE

if(GetPersistentInt(oModule,“Tables”)!=1) {
if (NWNX_SQL_PrepareQuery(“CREATE TABLE pwdata (player varchar(64) NOT NULL default ‘~’, tag varchar(64) NOT NULL default ‘~’, name varchar(64) NOT NULL default ‘~’, val text, expire int(11) default NULL, last timestamp NOT NULL default CURRENT_TIMESTAMP, PRIMARY KEY (player,tag,name))”)) {
NWNX_SQL_ExecutePreparedQuery();
}
if (NWNX_SQL_PrepareQuery(“CREATE TABLE pwobjdata (player varchar(64) NOT NULL default ‘~’, tag varchar(64) NOT NULL default ‘~’, name varchar(64) NOT NULL default ‘~’, val blob, expire int(11) default NULL, last timestamp NOT NULL default CURRENT_TIMESTAMP, PRIMARY KEY (player,tag,name))”)) {
NWNX_SQL_ExecutePreparedQuery();
}
SetPersistentInt(oModule,“Tables”,1);
}

That just creates 2 tables in MySQL if the persistent variable Tables does not exist in the Module.
That’s asumming you have the mysql connection right. You can use the lib_sql script I sent you if that helps.

Awesome, man, thanks! I’ll try it out tonight and let you know how it goes.

Edit:
1/27/2020 12:09:08 PM: Error. ‘mod_load’ did not compile.
mod_load.nss(25): ERROR: UNDEFINED IDENTIFIER (CREATE)

if (NWNX_SQL_PrepareQuery(“CREATE TABLE pwdata (player varchar(64) NOT NULL default ‘~’, tag varchar(64) NOT NULL default ‘~’, name varchar(64) NOT NULL default ‘~’, val text, expire int(11) default NULL, last timestamp NOT NULL default CURRENT_TIMESTAMP, PRIMARY KEY (player,tag,name))”)) {
NWNX_SQL_ExecutePreparedQuery();

I’ll have to research further, I’m not really understanding what’s going on, and this leads to confusion on my part.

I’m using aps_include as a wrapper:
#include “nwnx_sql”

void SQLInit()
{
/* nothing */
}

void SQLExecDirect(string sSQL)
{
    NWNX_SQL_ExecuteQuery(sSQL);
}

// Return a string value when given a location
string APSLocationToString(location lLocation);

// Return a location value when given the string form of the location
location APSStringToLocation(string sLocation);

// Set oObject's persistent string variable sVarName to sValue
// Optional parameters:
//   iExpiration: Number of days the persistent variable should be kept in database (default: 0=forever)
//   sTable: Name of the table where variable should be stored (default: pwdata)
void SetPersistentString(object oObject, string sVarName, string sValue, int iExpiration =
                         0, string sTable = "pwdata");

// Set oObject's persistent integer variable sVarName to iValue
// Optional parameters:
//   iExpiration: Number of days the persistent variable should be kept in database (default: 0=forever)
//   sTable: Name of the table where variable should be stored (default: pwdata)
void SetPersistentInt(object oObject, string sVarName, int iValue, int iExpiration =
                      0, string sTable = "pwdata");

// Get oObject's persistent integer variable sVarName
// Optional parameters:
//   sTable: Name of the table where variable is stored (default: pwdata)
// * Return value on error: 0
int GetPersistentInt(object oObject, string sVarName, string sTable = "pwdata");

// Get oObject's persistent string variable sVarName
// Optional parameters:
//   sTable: Name of the table where variable is stored (default: pwdata)
// * Return value on error: ""
string GetPersistentString(object oObject, string sVarName, string sTable = "pwdata");

// Delete persistent variable sVarName stored on oObject
// Optional parameters:
//   sTable: Name of the table where variable is stored (default: pwdata)
void DeletePersistentVariable(object oObject, string sVarName, string sTable = "pwdata");


// Set oObject's persistent location variable sVarName to lLocation
// Optional parameters:
//   iExpiration: Number of days the persistent variable should be kept in database (default: 0=forever)
//   sTable: Name of the table where variable should be stored (default: pwdata)
//   This function converts location to a string for storage in the database.
void SetPersistentLocation(object oObject, string sVarName, location lLocation, int iExpiration =
                           0, string sTable = "pwdata");

// Get oObject's persistent location variable sVarName
// Optional parameters:
//   sTable: Name of the table where variable is stored (default: pwdata)
// * Return value on error: 0
location GetPersistentLocation(object oObject, string sVarname, string sTable = "pwdata");


// (private function) Replace special character ' with ~
string SQLEncodeSpecialChars(string sString);

// (private function)Replace special character ' with ~
string SQLDecodeSpecialChars(string sString);

void SetPersistentInt(object oObject, string sVarName, int iValue, int iExpiration =
                      0, string sTable = "pwdata")
{
  SetPersistentString(oObject, sVarName, IntToString(iValue), iExpiration, sTable);
}

void SetPersistentString(object oObject, string sVarName, string sValue, int iExpiration =
                         0, string sTable = "pwdata")
{
  string sPlayer;
  string sTag;

  if (GetIsPC(oObject))
    {
      sPlayer = SQLEncodeSpecialChars(GetStringLowerCase(GetPCPlayerName(oObject)));
      sTag = SQLEncodeSpecialChars(GetName(oObject));
    }
  else
    {
      sPlayer = "~";
      sTag = GetTag(oObject);
    }

  sVarName = SQLEncodeSpecialChars(sVarName);
  sValue = SQLEncodeSpecialChars(sValue);

  string sSQL = "SELECT val FROM " + sTable + " WHERE player='" + sPlayer +
    "' AND tag='" + sTag + "' AND name='" + sVarName + "'";

  int ret=NWNX_SQL_ExecuteQuery(sSQL);
  if (ret)
    {
      if(NWNX_SQL_ReadyToReadNextRow())
        {

          sSQL = "UPDATE " + sTable + " SET val='" + sValue +
            "',expire=" + IntToString(iExpiration) + ",last=now() WHERE player='" + sPlayer +
            "' AND tag='" + sTag + "' AND name='" + sVarName + "'";

      NWNX_SQL_ExecuteQuery(sSQL);
    }
      else
    {
      // row doesn't exist
          sSQL = "INSERT INTO " + sTable + " (player,tag,name,val,expire,last) VALUES" +
            "('" + sPlayer + "','" + sTag + "','" + sVarName + "','" +
            sValue + "'," + IntToString(iExpiration) + ", now())";

      NWNX_SQL_ExecuteQuery(sSQL);
    }
    }
}

int GetPersistentInt(object oObject, string sVarName, string sTable = "pwdata")
{
  string sPlayer;
  string sTag;
  object oModule;

  if(GetLocalInt(GetModule(), "MOD_NO_NWNX")) return 0;

  if (GetIsPC(oObject))
    {
      sPlayer = SQLEncodeSpecialChars(GetStringLowerCase(GetPCPlayerName(oObject)));
      sTag = SQLEncodeSpecialChars(GetName(oObject));
    }
  else
    {
      sPlayer = "~";
      sTag = GetTag(oObject);
    }

  sVarName = SQLEncodeSpecialChars(sVarName);

  string sSQL = "SELECT val FROM " + sTable + " WHERE player='" + sPlayer +
    "' AND tag='" + sTag + "' AND name='" + sVarName + "'";
  int ret=NWNX_SQL_ExecuteQuery(sSQL);
  if (ret)
    {
      if(NWNX_SQL_ReadyToReadNextRow())
    {
      NWNX_SQL_ReadNextRow();
      // Note NWNX_SQL_ReadDataInActiveRow is zero based..
      //    0 is the first column, 1 is the second, etc.
      // Also, it returns a string representation by default.  Use StringToInt/Float as necessary.
      return StringToInt(NWNX_SQL_ReadDataInActiveRow(0));
    }
    }
  return 0;
}

string GetPersistentString(object oObject, string sVarName, string sTable = "pwdata")
{
  string sPlayer;
  string sTag;

  if(GetLocalInt(GetModule(), "MOD_NO_NWNX")) return "";

  if (GetIsPC(oObject))
    {
      sPlayer = SQLEncodeSpecialChars(GetStringLowerCase(GetPCPlayerName(oObject)));
      sTag = SQLEncodeSpecialChars(GetName(oObject));
    }
  else
    {
      sPlayer = "~";
      sTag = GetTag(oObject);
    }

  sVarName = SQLEncodeSpecialChars(sVarName);

  string sSQL = "SELECT val FROM " + sTable + " WHERE player='" + sPlayer +
    "' AND tag='" + sTag + "' AND name='" + sVarName + "'";
  int ret=NWNX_SQL_ExecuteQuery(sSQL);
  if (ret)
    {
      if(NWNX_SQL_ReadyToReadNextRow())
    {
      NWNX_SQL_ReadNextRow();
      // Note NWNX_SQL_ReadDataInActiveRow is zero based..
      //    0 is the first column, 1 is the second, etc.
      // Also, it returns a string representation by default.  Use StringToInt/Float as necessary.
      return SQLDecodeSpecialChars(NWNX_SQL_ReadDataInActiveRow(0));
    }
    }

  return "";
}

void DeletePersistentVariable(object oObject, string sVarName, string sTable = "pwdata")
{
  string sPlayer;
  string sTag;

  if (GetIsPC(oObject))
    {
      sPlayer = SQLEncodeSpecialChars(GetStringLowerCase(GetPCPlayerName(oObject)));
      sTag = SQLEncodeSpecialChars(GetName(oObject));
    }
  else
    {
      sPlayer = "~";
      sTag = GetTag(oObject);
    }

  sVarName = SQLEncodeSpecialChars(sVarName);
  string sSQL = "DELETE FROM " + sTable + " WHERE player='" + sPlayer +
    "' AND tag='" + sTag + "' AND name='" + sVarName + "'";
  NWNX_SQL_ExecuteQuery(sSQL);
}

void SetPersistentLocation(object oObject, string sVarName, location lLocation, int iExpiration =
                           0, string sTable = "pwdata")
{
  SetPersistentString(oObject, sVarName, APSLocationToString(lLocation), iExpiration, sTable);
}

location GetPersistentLocation(object oObject, string sVarName, string sTable = "pwdata")
{
  return APSStringToLocation(GetPersistentString(oObject, sVarName, sTable));
}

// Problems can arise with SQL commands if variables or values have single quotes
// in their names. These functions are a replace these quote with the tilde character

string SQLEncodeSpecialChars(string sString)
{
  if (FindSubString(sString, "'") == -1)      // not found
    return sString;

  int i;
  string sReturn = "";
  string sChar;

  // Loop over every character and replace special characters
  for (i = 0; i < GetStringLength(sString); i++)
    {
      sChar = GetSubString(sString, i, 1);
      if (sChar == "'")
    sReturn += "~";
      else
    sReturn += sChar;
    }
  return sReturn;
}

string SQLDecodeSpecialChars(string sString)
{
  if (FindSubString(sString, "~") == -1)      // not found
    return sString;

  int i;
  string sReturn = "";
  string sChar;

  // Loop over every character and replace special characters
  for (i = 0; i < GetStringLength(sString); i++)
    {
      sChar = GetSubString(sString, i, 1);
      if (sChar == "~")
    sReturn += "'";
      else
    sReturn += sChar;
    }
  return sReturn;
}

string APSLocationToString(location lLocation)
{
  object oArea = GetAreaFromLocation(lLocation);
  vector vPosition = GetPositionFromLocation(lLocation);
  float fOrientation = GetFacingFromLocation(lLocation);
  string sReturnValue;

  if (GetIsObjectValid(oArea))
    sReturnValue =
      "#AREA#" + GetTag(oArea) + "#POSITION_X#" + FloatToString(vPosition.x) +
      "#POSITION_Y#" + FloatToString(vPosition.y) + "#POSITION_Z#" +
      FloatToString(vPosition.z) + "#ORIENTATION#" + FloatToString(fOrientation) + "#END#";

  return sReturnValue;
}

location APSStringToLocation(string sLocation)
{
  location lReturnValue;
  object oArea;
  vector vPosition;
  float fOrientation, fX, fY, fZ;

  int iPos, iCount;
  int iLen = GetStringLength(sLocation);

  if (iLen > 0)
    {
      iPos = FindSubString(sLocation, "#AREA#") + 6;
      iCount = FindSubString(GetSubString(sLocation, iPos, iLen - iPos), "#");
      oArea = GetObjectByTag(GetSubString(sLocation, iPos, iCount));

      iPos = FindSubString(sLocation, "#POSITION_X#") + 12;
      iCount = FindSubString(GetSubString(sLocation, iPos, iLen - iPos), "#");
      fX = StringToFloat(GetSubString(sLocation, iPos, iCount));

      iPos = FindSubString(sLocation, "#POSITION_Y#") + 12;
      iCount = FindSubString(GetSubString(sLocation, iPos, iLen - iPos), "#");
      fY = StringToFloat(GetSubString(sLocation, iPos, iCount));

      iPos = FindSubString(sLocation, "#POSITION_Z#") + 12;
      iCount = FindSubString(GetSubString(sLocation, iPos, iLen - iPos), "#");
      fZ = StringToFloat(GetSubString(sLocation, iPos, iCount));

      vPosition = Vector(fX, fY, fZ);

      iPos = FindSubString(sLocation, "#ORIENTATION#") + 13;
      iCount = FindSubString(GetSubString(sLocation, iPos, iLen - iPos), "#");
      fOrientation = StringToFloat(GetSubString(sLocation, iPos, iCount));

      lReturnValue = Location(oArea, vPosition, fOrientation);
    }

  return lReturnValue;
}

I suggest you get rid of everything aps.
To help you it would take a while chatting in discord, maybe we can meet there.