Skip to content

Campaign Database and updating module in toolset

How does the campaign database persistense work if I choose to script a database, and then make updates into the module in toolset?


Will it be erased, or is there any way to keep player information?



And is the campaign database still "slow" as people claimed it was in 1.69?


Asking here because search engines didnt produce satisfactory results or I just didnt know what to look for


Thanks in advance.
«1

Comments

  • WilliamDracoWilliamDraco Member Posts: 175
    edited January 2021
    The campaign database backend system was changed to use sqlite (This is different to the full SQLite implementation, which was also added but is separate). As a result, the concerns regarding slowness or various tweaks and systems designed to work around the old system are no longer relevant. Some limitations do still apply (Limits on the length of variable names, for example, were maintained in order to preserve backwards compatibility)

    The Campaign Database is given a name. There are two lots of functions I think and one lot auto-names it the same as the module, where-as the other asks for the database name. This database is saved in the /database folder, and so long as it's not deleted (By script or manually) it remains there.

    Editing a module will only break this continuation if it changes the module name (in the auto-named case) or if you change the name which you gave to the database in the calling scripts.
  • ForSeriousForSerious Member Posts: 446
    Adding to that, SSDs make a huge difference in campaign database performance.

    I've been using a system made by Knat called Natural Bioware Database Extension (NBDE). Think I found it in the vault.
    Anyway, what it does is it stores all the variables that would be written to the database on objects. Then when called to, it saves those objects to the database. Essentially it can write thousands of entries with one entry. There are a bunch of other benefits it claims. Anyway it doesn't seems to work with locations, or vectors. And it also would not work with any SQL queries.
    Just sharing to give you an idea about it.
  • WilliamDracoWilliamDraco Member Posts: 175
    Since the change to sqlite which I mention, NBDE is strictly unnecessary and should be avoided. It was a substantial upgrade from the old codebase backend, but the new sqlite backend is much better and NBDE adds complexity without any performance increase (and in fact possibly a performance decrease, although locals have also been sped up recent).

    If an old module uses it, it's fine to keep using - but highly recommended to avoid in new setups.
  • ForSeriousForSerious Member Posts: 446
    With that being said. The only advantage it offers is a work-around for the limits on variable names. Otherwise, yes. It's probably just adds needless complexity and another potential point of failure. I think I just really want it to be good, but doing a reality check, it's failed me and messed up my database at least three times.
  • WilliamDracoWilliamDraco Member Posts: 175
    Yeah, NBDE was fantastic and very needed at the time, but is now simply obsolete and a relic of past patches.

    If variable name length really ends up being unsolvable by just using smaller names my recommendation is to make use of the full sqlite implementation. It does require acquiring a familiarity with sqlite commands but it can fill in far more gaps/additional functionality than NBDE :)
  • ForgeForge Member Posts: 32
    Interesting. Thank you for your replies. I will ponder upon these.
  • ForgeForge Member Posts: 32
    Back with new questions.

    So I should Get persistence even on a virtual server currently?

    Shutting down the host, and rehosting will preserve player locations etc if I create functions for these?


    This is new for me and im trying to get into this because its very interesting, but I havent found a good tool to learn about neverwinter's database so im asking these dumb questions here
  • ForSeriousForSerious Member Posts: 446
    The CampaignVariable functions create files in your current user documents location. (~\Documents\Neverwinter Nights\database[On Windows])
    Calling SetCampaignInt("Dude", "Thing", 1); in a script, will create a file in that folder named 'dude.sqlite3'.
    It is that file that stores the data. So long as that file is being persisted, (You did say virtual, throwing doubt into my understanding of how you are doing things...) your data—like locations—should be in it.
  • ForgeForge Member Posts: 32
    With virtual I meant that Im not running nwserver.exe from a physical server machine with a dedicated server, but hosting a server from the DM Client -> Host Internet game.



    Im currently trying to get the module to save player location into a database OnClientExit but having no luck:


    I made an include file:



    //Database Include file:


    //Variables:


    const string DB_NAME = "THIS_DATABASE";//Test Database

    const string DB_PC_L_LEAVE = "DB_PC_L_PLAYER_LEAVE"; //String into database for last place of exit




    //Interface:


    location GetPCStartLocation(object oPC, string sLocationID);

    void SetPCStartLocation(object oPC, string sLocationID);



    //Implementation:

    location GetPCStartLocation(object oPC, string sLocationID){

    return GetCampaignLocation(DB_NAME, sLocationID, oPC);

    }



    void SetPCStartLocation(object oPC, string sLocation){
    {
    SetCampaignLocation(DB_NAME, sLocationID , GetLocation(oPC), oPC);

    }

    }


    And then OnClientLeave:




    #include "dbmodule_test"

    void main()
    {



    object oPC = GetExitingObject();





    SetPCStartLocation(oPC, DB_PC_L_LEAVE);





    }



    Getting errors here while compiling.


    Could you lend me a hand what I am doing wrong here.


    I am not new to neverscript, but I am new to these databases.
  • ForSeriousForSerious Member Posts: 446
    Those little things get me all the time.
    Change
    SetCampaignLocation(DB_NAME, sLocationID, GetLocation(oPC), oPC);
    
    to
    SetCampaignLocation(DB_NAME, sLocation, GetLocation(oPC), oPC);
    
  • ForSeriousForSerious Member Posts: 446
    Hey, if that still isn't working for you, check out what is said about campaign locations in this post.
  • ForgeForge Member Posts: 32
    When I host server from DM Client -> host internet game, It wont load player locations when I turn down server and rehost it.

    Is this "resetting" the server ? The players start from the editor set start location after this.
  • ForgeForge Member Posts: 32
    What do you think ForSerious, could I test this out by making a little test quest into a light test module utilizing the campaign databases instead of SetLocalInt(); ?

    Shouldnt variables placed into a campaign database persist and save "quest status" even after driving server down?
  • ForSeriousForSerious Member Posts: 446
    Huh. I've been thinking about doing something similar for awhile now. I'll report back with my findings.
  • ForSeriousForSerious Member Posts: 446
    edited February 2021
    SetLocalInt(); only ever lasts for the duration of the game session. It will be saved in a saved game, but will always be lost when a server resets.

    Alrighty. So, I was able to save and get locations through server resets using Get/SetCampaignLocation();
    The only thing I did different was I did not use oPC as the last parameter:
    SetCampaignLocation(DB_NAME, sLocation, GetLocation(oPC));
    
    I set DB_NAME to be a database that only stores locations. I use sLocation as the unique identifier for the player. So in this database each player gets one, and only one, location.
    This worked calling SetCampaignLocation() in the OnClientLeave script, and it also worked using test module from the toolset.

    (I use GetTag(); to get the unique identifier, but this only works if you have a tagging system setup. Check out this post for details of how to set that up.)
  • ForgeForge Member Posts: 32

    This code of yours, from that playerhouse thread. Is it an include file you wrote?

    // Give the player a unique tag.
    // sIsNew — The name of the database
    string MakePlayerTag(object oPC, string sIsNew);
    string MakePlayerTag(object oPC, string sIsNew)
    {
        string sTag = "tag";
        // Get the last number that was used to make a tag.
        int iTag = GetCampaignInt(sIsNew, sTag);
        iTag = iTag + 1;
        // Mark that number has been used now.
        SetCampaignInt(sIsNew, sTag, iTag);
        string sParsed = IntToString(iTag);
        // Pad the tag with some zeros.
        int iLen;
        for(iLen = GetStringLength(sParsed); iLen <= 6; iLen++)
        {
            sParsed = "0" + sParsed;
        }
        sParsed = "ZZ" + sParsed;
        SetTag(oPC, sParsed);
        return sParsed;
    }
    
  • ForgeForge Member Posts: 32
    And when you assign a Tag onto a player, do you place a variable onto a player OnClientEnter which checks if player has already received a Tag and has logged in before?

    I mean that a player wont get Tagged everytime he enters?
  • ForSeriousForSerious Member Posts: 446
    edited February 2021
    Correct. That code can be made into its own include file, or you can add it to an existing one.
    And then yes, you need to add the if statement from that same post in your OnClientEnter script.
    By default GetTag(oPC); will return "". That will have a length of 0.
  • ForgeForge Member Posts: 32
    So it creates it's own database?

    So its possible for a module to have more databases than one?

    I thought that ppl generally made an #include file where they implemented the database name as a string constant, and implemented all database functions into there.
  • ForSeriousForSerious Member Posts: 446
    edited February 2021
    Well in a classic database you only need one because you can have tables inside it—and in those, columns. You can indeed have that now in NWN, but need to know SQL and how to use the NWN SQLite functions.

    When I made all my database functions, that wasn't really an option. (And I still don't love SQL)
    I made one database for each variable I wanted to keep track of. For example, I made a kill tracker that tracks kills by challenge rating. That means I have 7 databases just for all the different challenge ratings, and it works fine.
    Now, I could have made just one database, and added a suffix or prefix to the unique identifier for each rating, but I didn't learn that until much later.
  • ForgeForge Member Posts: 32
    Ok. Sounds complicated for a beginner
  • ForgeForge Member Posts: 32
    Cant get it to work, so I am doing something wrong.

    Posting my OnClientEnter, OnClientLeave, and Include files from my test module


    OnClientEnter:
    #include "x3_inc_horse"
    #include "dbmodule_test"
    
    
    string MakePlayerTag(object oPC, string sIsNew);
    
    
    void main()
    {
        object oPC=GetEnteringObject();
    
        ExecuteScript("x3_mod_pre_enter",OBJECT_SELF); // Override for other skin systems
        if ((GetIsPC(oPC)||GetIsDM(oPC))&&!GetHasFeat(FEAT_HORSE_MENU,oPC))
        { // add horse menu
            HorseAddHorseMenu(oPC);
            if (GetLocalInt(GetModule(),"X3_ENABLE_MOUNT_DB"))
            { // restore PC horse status from database
                DelayCommand(2.0,HorseReloadFromDatabase(oPC,X3_HORSE_DATABASE));
            } // restore PC horse status from database
        } // add horse menu
        if (GetIsPC(oPC))
        { // more details
            // restore appearance in case you export your character in mounted form, etc.
            if (!GetSkinInt(oPC,"bX3_IS_MOUNTED")) HorseIfNotDefaultAppearanceChange(oPC);
            // pre-cache horse animations for player as attaching a tail to the model
            HorsePreloadAnimations(oPC);
            DelayCommand(3.0,HorseRestoreHenchmenLocations(oPC));
        } // more details
    
    
    
    
    
           string sName = GetTag(oPC);
    
        if(GetStringLength(sName) <= 0 || GetSubString(sName, 0, 2) != "ZZ")
        {
            MakePlayerTag(oPC, "THIS_DATABASE");
        }
    
        AssignCommand(oPC, ActionJumpToLocation(GetCampaignLocation(PW_DB_NAME, DB_PC_L_LEAVE, oPC)));
    
    
            }
    
    
    
    
    
    string MakePlayerTag(object oPC, string sIsNew)
    {
        string sTag = "tag";
        // Get the last number that was used to make a tag.
        int iTag = GetCampaignInt(sIsNew, sTag);
        iTag = iTag + 1;
        // Mark that number has been used now.
        SetCampaignInt(sIsNew, sTag, iTag);
        string sParsed = IntToString(iTag);
        // Pad the tag with some zeros.
        int iLen;
        for(iLen = GetStringLength(sParsed); iLen <= 6; iLen++)
        {
            sParsed = "0" + sParsed;
        }
        sParsed = "ZZ" + sParsed;
        SetTag(oPC, sParsed);
        return sParsed;
    }
    


    OnClientLeave:
    #include "dbmodule_test"
    #include "pctag"
    
    void main()
    {
    
    
    
    object oPC = GetExitingObject();
    
    
    
    SetCampaignLocation(PW_DB_NAME, DB_PC_L_LEAVE, GetLocation(oPC));
    
    
    
    
    
    
    
    
    
    
    
    }
    


    Include Files:
    //Database Include file
    
    
    //Variables:
    
    
    const string PW_DB_NAME = "THIS_DATABASE";
    
    const string DB_PC_L_LEAVE = "DB_PC_L_PLAYER_LEAVE";
    
    
    
    
    //Interface:
    
    location GetPCStartLocation(object oPC, string sLocation);
    
    void SetPCStartLocation(object oPC, string sLocation);
    
    
    
    //Implementation:
    
    location GetPCStartLocation(object oPC, string sLocation){
    
        return GetCampaignLocation(PW_DB_NAME, sLocation, oPC);
    
        }
    
    
    
    void SetPCStartLocation(object oPC, string sLocation){
    {
           SetCampaignLocation(PW_DB_NAME, sLocation, GetLocation(oPC));
    
                }
    
            }
    
    

    // Give the player a unique tag.
    // sIsNew — The name of the database
    string MakePlayerTag(object oPC, string sIsNew);
    string MakePlayerTag(object oPC, string sIsNew)
    {
        string sTag = "tag";
        // Get the last number that was used to make a tag.
        int iTag = GetCampaignInt(sIsNew, sTag);
        iTag = iTag + 1;
        // Mark that number has been used now.
        SetCampaignInt(sIsNew, sTag, iTag);
        string sParsed = IntToString(iTag);
        // Pad the tag with some zeros.
        int iLen;
        for(iLen = GetStringLength(sParsed); iLen <= 6; iLen++)
        {
            sParsed = "0" + sParsed;
        }
        sParsed = "ZZ" + sParsed;
        SetTag(oPC, sParsed);
        return sParsed;
    }
    
  • ForSeriousForSerious Member Posts: 446
    You have some things called in the wrong order. And you left some of the old functions in.
    Here I'll add loads of comments.
    The database include file. I joined the two include files:
    //Database Include file
    //Variables:
    const string PW_DB_NAME = "THIS_DATABASE";
    const string DB_PC_L_LEAVE_PREFIX = "PLAYER_LEAVE_";
    //Interface:
    
    // Gets the last saved location of oPC from the database.
    location GetPCStartLocation(object oPC);
    // Saves the current location of oPC to the database.
    void SetPCStartLocation(object oPC);
    // Give the player a unique tag.
    // sIsNew — The name of the database
    string MakePlayerTag(object oPC, string sIsNew = PW_DB_NAME);
    //Implementation:
    location GetPCStartLocation(object oPC)
    {
        // The prefix plus the tag of oPC is the unique identifier for the location stored in the database.
        return GetCampaignLocation(PW_DB_NAME, (DB_PC_L_LEAVE_PREFIX + GetTag(oPC)));
    }
    void SetPCStartLocation(object oPC)
    {
        SetCampaignLocation(PW_DB_NAME, (DB_PC_L_LEAVE_PREFIX + GetTag(oPC)), GetLocation(oPC));
    }
    string MakePlayerTag(object oPC, string sIsNew)
    {
        string sTag = "tag";
        // Get the last number that was used to make a tag.
        int iTag = GetCampaignInt(sIsNew, sTag);
        iTag = iTag + 1;
        // Mark that number has been used now.
        SetCampaignInt(sIsNew, sTag, iTag);
        string sParsed = IntToString(iTag);
        // Pad the tag with some zeros.
        int iLen;
        for(iLen = GetStringLength(sParsed); iLen <= 6; iLen++)
        {
            sParsed = "0" + sParsed;
        }
        // Add a prefix to the zeros
        sParsed = "ZZ" + sParsed;
        SetTag(oPC, sParsed);
        return sParsed;
    }
    

    The OnClientEnter script:
    #include "x3_inc_horse"
    #include "dbmodule_test"
    void main()
    {
        // Define the entering object.
        object oPC = GetEnteringObject();
        // Check if oPC is a player and not a DM
        if(GetIsPC(oPC) && GetIsDM(oPC) == FALSE)
        {
            // Get the tag of oPC.
            string sName = GetTag(oPC);
            // Check if the player already has a tag.
            // A string length less than or equal to zero means they do not.
            // A string starting with something that is not 'ZZ' means they got a tag from some other server. That's sus.
            if(GetStringLength(sName) <= 0 || GetSubString(sName, 0, 2) != "ZZ")
            {
                // Give the player a new tag and replace sName with it.
                sName = MakePlayerTag(oPC, PW_DB_NAME);
            }
            // Get the saved location from the database.
            location lLoc = GetPCStartLocation(oPC);
            // Check if the area from the location is valid.
            object oCheck = GetAreaFromLocation(lLoc);
            // A new character will not have a saved location.
            if(GetIsObjectValid(oCheck))
            {
                // Send them on their way.
                AssignCommand(oPC, ClearAllActions());
                AssignCommand(oPC, JumpToLocation(lLoc));
            }
            // All the default horse stuff
            ExecuteScript("x3_mod_pre_enter",OBJECT_SELF); // Override for other skin systems
            if ((GetIsPC(oPC)||GetIsDM(oPC))&&!GetHasFeat(FEAT_HORSE_MENU,oPC))
            { // add horse menu
                HorseAddHorseMenu(oPC);
                if (GetLocalInt(GetModule(),"X3_ENABLE_MOUNT_DB"))
                { // restore PC horse status from database
                    DelayCommand(2.0,HorseReloadFromDatabase(oPC,X3_HORSE_DATABASE));
                } // restore PC horse status from database
                // restore appearance in case you export your character in mounted form, etc.
            if (!GetSkinInt(oPC,"bX3_IS_MOUNTED"))
            {
                HorseIfNotDefaultAppearanceChange(oPC);
            }
            // pre-cache horse animations for player as attaching a tail to the model
            HorsePreloadAnimations(oPC);
            DelayCommand(3.0,HorseRestoreHenchmenLocations(oPC));
            } // add horse menu
        }
    }
    


    OnClientLeave:
    #include "dbmodule_test"
    void main()
    {
        // Call the function made in the include file.
        SetPCStartLocation(GetExitingObject());
    }
    
  • ForSeriousForSerious Member Posts: 446
    Oh I probably messed up the horse stuff a little bit. Sorry if you had plans to use it.
  • ForgeForge Member Posts: 32
    No I didnt. It was there by Default and I was afraid to touch it if it corrups my module :cold_sweat:
  • ForgeForge Member Posts: 32
    Thankyou. I will check these out.

    Good to know that you can create a database for each of the identifiers you want to store in that certain database itself.
  • ForSeriousForSerious Member Posts: 446
    I decided to use a prefix this time. That way, if you want, you can do that too. One benefit to that is then you only have to backup one database file, instead of 27, like I have.
  • ForgeForge Member Posts: 32
    They're compiling ok, but I cant get it to work.

    I think im still doing something wrong here.

    Are you running this on a dedicated server?
  • ForSeriousForSerious Member Posts: 446
    Yes I am, but I was able to get something similar to work while running test module from the toolset.
    The difference is I do not teleport the player until they choose to leave the server lobby area.
    You can try putting the teleport lines in delay commands.
  • ForSeriousForSerious Member Posts: 446
    I think I remember reading somewhere that other servers will use a trigger around the start location. In the OnEnter script for that trigger they'll do things like teleporting players to where they need to be.
Sign In or Register to comment.