|
|
|
/**
|
|
|
|
* This tool is for simplifying writing and reading persistent user data.
|
|
|
|
* All it requires is a setup of database + json pointer to data and it will
|
|
|
|
* take care of data caching and database connection.
|
|
|
|
* Copyright 2023 Anton Tarasenko
|
|
|
|
*------------------------------------------------------------------------------
|
|
|
|
* This file is part of Acedia.
|
|
|
|
*
|
|
|
|
* Acedia is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Acedia is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
class PersistentDataManager extends AcediaObject;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* # `PersistentDataManager`
|
|
|
|
*
|
|
|
|
* This tool is for simplifying writing and reading persistent user data.
|
|
|
|
* All it requires is a setup of database + json pointer to data and it will
|
|
|
|
* take care of data caching and database connection.
|
|
|
|
*
|
|
|
|
* ## Usage
|
|
|
|
*
|
|
|
|
* Create an instance and use `Setup()` to connect to the database with
|
|
|
|
* persistent data. You can use `Setup()` again on the same object to setup
|
|
|
|
* a different database as a source. All data will be automatically reloaded.
|
|
|
|
* After that you can use `GetPersistentData()`/`SetPersistentData()` to
|
|
|
|
* read/write persistent data for the particular user.
|
|
|
|
* Since loading data from the database takes time, you don't have an
|
|
|
|
* immediate access to it.
|
|
|
|
* But you can use `_.users.OnPersistentDataAvailable()` signal to track
|
|
|
|
* whenever new user data from database becomes available. However, you can
|
|
|
|
* start writing persistent data (and reading what you've wrote) at any time it
|
|
|
|
* - these changes will be reapplied whenever data is actually loaded from
|
|
|
|
* database.
|
|
|
|
*
|
|
|
|
* ## Implementation
|
|
|
|
*
|
|
|
|
* Implementation consists of simply creating `DBConnection` for every user
|
|
|
|
* and storing them in the `HashTable` that maps user IDs into those
|
|
|
|
* `DBConnection`s.
|
|
|
|
* We also maintain a reverse map to figure out what `DBConnection` belongs
|
|
|
|
* to what user when connection signals an update. We borrow the signal that
|
|
|
|
* `UsersAPI` provides to inform everyone interested about which users
|
|
|
|
* have updated.
|
|
|
|
*/
|
|
|
|
|
|
|
|
var private bool initialized;
|
|
|
|
var private Database database;
|
|
|
|
var private MutableJSONPointer rootPointer;
|
|
|
|
var private HashTable userToConnection, connectionToUser;
|
|
|
|
|
|
|
|
var private PersistentDataManager_OnPersistentDataReady_Signal onPersistentDataReadySignal;
|
|
|
|
|
|
|
|
protected function Constructor()
|
|
|
|
{
|
|
|
|
_.players.OnNewPlayer(self).connect = ConnectPersistentData;
|
|
|
|
onPersistentDataReadySignal = _.users._getOnReadySignal();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function Finalizer()
|
|
|
|
{
|
|
|
|
Reset();
|
|
|
|
_.players.OnNewPlayer(self).Disconnect();
|
|
|
|
}
|
|
|
|
|
|
|
|
private final function Reset()
|
|
|
|
{
|
|
|
|
_.memory.Free(database);
|
|
|
|
_.memory.Free(rootPointer);
|
|
|
|
_.memory.Free(userToConnection);
|
|
|
|
_.memory.Free(connectionToUser);
|
|
|
|
_.memory.Free(onPersistentDataReadySignal);
|
|
|
|
database = none;
|
|
|
|
rootPointer = none;
|
|
|
|
userToConnection = none;
|
|
|
|
connectionToUser = none;
|
|
|
|
onPersistentDataReadySignal = none;
|
|
|
|
initialized = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets up database and location inside it as a source of users' persistent
|
|
|
|
* data.
|
|
|
|
*
|
|
|
|
* Must be successfully called at least once for the caller
|
|
|
|
* `PersistentDataManager` to be usable.
|
|
|
|
*
|
|
|
|
* @param db Database inside which persistent data is stored.
|
|
|
|
* @param location Location inside specified database to the root of
|
|
|
|
* persistent data.
|
|
|
|
* @return `true` if setup was successful (requires both arguments to be not
|
|
|
|
* `none`) and `false` otherwise.
|
|
|
|
*/
|
|
|
|
public final function bool Setup(Database db, BaseJSONPointer location)
|
|
|
|
{
|
|
|
|
if (db == none) return false;
|
|
|
|
if (location == none) return false;
|
|
|
|
|
|
|
|
Reset();
|
|
|
|
database = db;
|
|
|
|
database.NewRef();
|
|
|
|
rootPointer = location.MutableCopy();
|
|
|
|
userToConnection = _.collections.EmptyHashTable();
|
|
|
|
connectionToUser = _.collections.EmptyHashTable();
|
|
|
|
// Using `userToConnection` as an empty hash table, not related to its
|
|
|
|
// actual meaning
|
|
|
|
database.IncrementData(location, userToConnection);
|
|
|
|
initialized = true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reads specified named persistent data for the specified group.
|
|
|
|
*
|
|
|
|
* @param id ID of the user to read persistent data from.
|
|
|
|
* @param groupName Group to which this persistent data belongs to.
|
|
|
|
* Groups are used as namespaces to avoid duplicate persistent variables
|
|
|
|
* between mods. If your mod needs several subgroups, its recommended to
|
|
|
|
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and
|
|
|
|
* "MyAwesomeMod.enemies".
|
|
|
|
* @param dataName Name of persistent data variable to read inside
|
|
|
|
* `groupName` persistent data group. Not `none` value must be provided.
|
|
|
|
* @param data Data to set as persistent value. Must be
|
|
|
|
* JSON-compatible. If `none` is passed, returns the all data for
|
|
|
|
* the given group.
|
|
|
|
* @return Data read from the persistent variable. `none` in case of any kind
|
|
|
|
* of failure.
|
|
|
|
*/
|
|
|
|
public final function AcediaObject GetPersistentData(
|
|
|
|
UserID id,
|
|
|
|
BaseText groupName,
|
|
|
|
optional BaseText dataName)
|
|
|
|
{
|
|
|
|
local AcediaObject result;
|
|
|
|
local Text textID;
|
|
|
|
local MutableJSONPointer location;
|
|
|
|
local DBConnection relevantConnection;
|
|
|
|
|
|
|
|
if (!initialized) return none;
|
|
|
|
if (id == none) return none;
|
|
|
|
if (groupName == none) return none;
|
|
|
|
|
|
|
|
textID = id.GetUniqueID();
|
|
|
|
relevantConnection = DBConnection(userToConnection.GetItem(textID));
|
|
|
|
textID.FreeSelf();
|
|
|
|
if (relevantConnection != none)
|
|
|
|
{
|
|
|
|
location = _.json.MutablePointer();
|
|
|
|
location.Push(groupName);
|
|
|
|
if (dataName != none) {
|
|
|
|
location.Push(dataName);
|
|
|
|
}
|
|
|
|
result = relevantConnection.ReadDataByJSON(location);
|
|
|
|
relevantConnection.FreeSelf();
|
|
|
|
location.FreeSelf();
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Writes specified named persistent data for the specified group.
|
|
|
|
*
|
|
|
|
* @param id ID of the user to change persistent data of.
|
|
|
|
* @param groupName Group to which this persistent data belongs to.
|
|
|
|
* Groups are used as namespaces to avoid duplicate persistent variables
|
|
|
|
* between mods. If your mod needs several subgroups, its recommended to
|
|
|
|
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and
|
|
|
|
* "MyAwesomeMod.enemies".
|
|
|
|
* @param dataName Name of persistent data variable to change inside
|
|
|
|
* `groupName` persistent data group.
|
|
|
|
* @param data Data to set as persistent value. Must be
|
|
|
|
* JSON-compatible.
|
|
|
|
* @return `true` if change succeeded in local cached version of database with
|
|
|
|
* persistent values and `false` otherwise. Such local changes can
|
|
|
|
* potentially be not applied to the actual database. But successful local
|
|
|
|
* changes should persist for the game session.
|
|
|
|
*/
|
|
|
|
public final function bool WritePersistentData(
|
|
|
|
UserID id,
|
|
|
|
BaseText groupName,
|
|
|
|
BaseText dataName,
|
|
|
|
AcediaObject data)
|
|
|
|
{
|
|
|
|
local bool result;
|
|
|
|
local Text textID;
|
|
|
|
local MutableJSONPointer location;
|
|
|
|
local DBConnection relevantConnection;
|
|
|
|
local HashTable emptyObject;
|
|
|
|
|
|
|
|
if (!initialized) return false;
|
|
|
|
if (id == none) return false;
|
|
|
|
if (groupName == none) return false;
|
|
|
|
if (dataName == none) return false;
|
|
|
|
|
|
|
|
textID = id.GetUniqueID();
|
|
|
|
relevantConnection = DBConnection(userToConnection.GetItem(textID));
|
|
|
|
textID.FreeSelf();
|
|
|
|
if (relevantConnection != none)
|
|
|
|
{
|
|
|
|
emptyObject = _.collections.EmptyHashTable();
|
|
|
|
location = _.json.MutablePointer();
|
|
|
|
location.Push(groupName);
|
|
|
|
relevantConnection.IncrementDataByJSON(location, emptyObject);
|
|
|
|
location.Push(dataName);
|
|
|
|
result = relevantConnection.WriteDataByJSON(location, data);
|
|
|
|
relevantConnection.FreeSelf();
|
|
|
|
location.FreeSelf();
|
|
|
|
emptyObject.FreeSelf();
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Increments specified named persistent data for the specified group.
|
|
|
|
*
|
|
|
|
* @param id ID of the user to change persistent data of.
|
|
|
|
* @param groupName Group to which this persistent data belongs to.
|
|
|
|
* Groups are used as namespaces to avoid duplicate persistent variables
|
|
|
|
* between mods. If your mod needs several subgroups, its recommended to
|
|
|
|
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and
|
|
|
|
* "MyAwesomeMod.enemies".
|
|
|
|
* @param dataName Name of persistent data variable to change inside
|
|
|
|
* `groupName` persistent data group.
|
|
|
|
* @param data Data by which to increment existing persistent value.
|
|
|
|
* Must be JSON-compatible.
|
|
|
|
* @return `true` if change succeeded in local cached version of database with
|
|
|
|
* persistent values and `false` otherwise. Such local changes can
|
|
|
|
* potentially be not applied to the actual database. But successful local
|
|
|
|
* changes should persist for the game session.
|
|
|
|
*/
|
|
|
|
public final function bool IncrementPersistentData(
|
|
|
|
UserID id,
|
|
|
|
BaseText groupName,
|
|
|
|
BaseText dataName,
|
|
|
|
AcediaObject data)
|
|
|
|
{
|
|
|
|
local bool result;
|
|
|
|
local Text textID;
|
|
|
|
local MutableJSONPointer location;
|
|
|
|
local DBConnection relevantConnection;
|
|
|
|
|
|
|
|
if (!initialized) return false;
|
|
|
|
if (id == none) return false;
|
|
|
|
if (groupName == none) return false;
|
|
|
|
if (dataName == none) return false;
|
|
|
|
|
|
|
|
textID = id.GetUniqueID();
|
|
|
|
relevantConnection = DBConnection(userToConnection.GetItem(textID));
|
|
|
|
textID.FreeSelf();
|
|
|
|
if (relevantConnection != none)
|
|
|
|
{
|
|
|
|
location = _.json.MutablePointer();
|
|
|
|
location.Push(groupName);
|
|
|
|
location.Push(dataName);
|
|
|
|
result = relevantConnection.IncrementDataByJSON(location, data);
|
|
|
|
relevantConnection.FreeSelf();
|
|
|
|
location.FreeSelf();
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes specified named persistent data for the specified group.
|
|
|
|
*
|
|
|
|
* @param id ID of the user to remove persistent data of.
|
|
|
|
* @param groupName Group to which this persistent data belongs to.
|
|
|
|
* Groups are used as namespaces to avoid duplicate persistent variables
|
|
|
|
* between mods. If your mod needs several subgroups, its recommended to
|
|
|
|
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and
|
|
|
|
* "MyAwesomeMod.enemies".
|
|
|
|
* @param dataName Name of persistent data variable to remove inside
|
|
|
|
* `groupName` persistent data group.
|
|
|
|
* @return `true` if removal succeeded in local cached version of database with
|
|
|
|
* persistent values and `false` otherwise. Such local changes can
|
|
|
|
* potentially be not applied to the actual database. But successful local
|
|
|
|
* changes should persist for the game session.
|
|
|
|
*/
|
|
|
|
public final function bool RemovePersistentData(
|
|
|
|
UserID id,
|
|
|
|
BaseText groupName,
|
|
|
|
BaseText dataName)
|
|
|
|
{
|
|
|
|
local bool result;
|
|
|
|
local Text textID;
|
|
|
|
local MutableJSONPointer location;
|
|
|
|
local DBConnection relevantConnection;
|
|
|
|
|
|
|
|
if (!initialized) return false;
|
|
|
|
if (id == none) return false;
|
|
|
|
if (groupName == none) return false;
|
|
|
|
if (dataName == none) return false;
|
|
|
|
|
|
|
|
textID = id.GetUniqueID();
|
|
|
|
relevantConnection = DBConnection(userToConnection.GetItem(textID));
|
|
|
|
textID.FreeSelf();
|
|
|
|
if (relevantConnection != none)
|
|
|
|
{
|
|
|
|
location = _.json.MutablePointer();
|
|
|
|
location.Push(groupName);
|
|
|
|
location.Push(dataName);
|
|
|
|
result = relevantConnection.RemoveDataByJSON(location);
|
|
|
|
relevantConnection.FreeSelf();
|
|
|
|
location.FreeSelf();
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Connects and starts synchronizing persistent data for the given player.
|
|
|
|
*
|
|
|
|
* @param player Player to synchronize persistent data for.
|
|
|
|
*/
|
|
|
|
public final function ConnectPersistentData(EPlayer player)
|
|
|
|
{
|
|
|
|
local UserID playerID;
|
|
|
|
|
|
|
|
if (initialized && player != none)
|
|
|
|
{
|
|
|
|
playerID = player.GetUserID();
|
|
|
|
ConnectPersistentDataByID(playerID);
|
|
|
|
_.memory.Free(playerID);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Connects and starts synchronizing persistent data for the player given by
|
|
|
|
* their ID.
|
|
|
|
*
|
|
|
|
* @param id User ID for which to synchronize persistent data from
|
|
|
|
* the database.
|
|
|
|
*/
|
|
|
|
public final function ConnectPersistentDataByID(UserID id)
|
|
|
|
{
|
|
|
|
local Text textID;
|
|
|
|
local DBConnection newConnection;
|
|
|
|
|
|
|
|
if (!initialized) return;
|
|
|
|
if (id == none) return;
|
|
|
|
|
|
|
|
textID = id.GetUniqueID();
|
|
|
|
if (userToConnection.HasKey(textID))
|
|
|
|
{
|
|
|
|
_.memory.Free(textID);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
rootPointer.Push(textID);
|
|
|
|
newConnection = DBConnection(_.memory.Allocate(class'DBConnection'));
|
|
|
|
newConnection.Initialize(database, rootPointer);
|
|
|
|
_.memory.Free(rootPointer.Pop());
|
|
|
|
newConnection.Connect();
|
|
|
|
userToConnection.SetItem(textID, newConnection);
|
|
|
|
connectionToUser.SetItem(newConnection, textID);
|
|
|
|
newConnection.OnStateChanged(self).connect = UserUpdated;
|
|
|
|
textID.FreeSelf();
|
|
|
|
newConnection.FreeSelf();
|
|
|
|
}
|
|
|
|
|
|
|
|
private final function UserUpdated(
|
|
|
|
DBConnection instance,
|
|
|
|
DBConnection.DBConnectionState oldState,
|
|
|
|
DBConnection.DBConnectionState newState)
|
|
|
|
{
|
|
|
|
local UserID id;
|
|
|
|
|
|
|
|
if (!initialized) return;
|
|
|
|
if (newState == DBCS_Connecting) return;
|
|
|
|
if (onPersistentDataReadySignal == none) return;
|
|
|
|
if (!onPersistentDataReadySignal.IsAllocated()) return;
|
|
|
|
|
|
|
|
id = UserID(connectionToUser.GetItem(instance));
|
|
|
|
if (id != none)
|
|
|
|
{
|
|
|
|
onPersistentDataReadySignal.Emit(id, newState == DBCS_Connected);
|
|
|
|
id.FreeSelf();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attempts to start persistent data synchronization for all players currently
|
|
|
|
* on the server.
|
|
|
|
*/
|
|
|
|
public final function LoadCurrentPlayers()
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local array<EPlayer> currentPlayers;
|
|
|
|
|
|
|
|
if (initialized)
|
|
|
|
{
|
|
|
|
currentPlayers = _.players.GetAll();
|
|
|
|
for (i = 0; i < currentPlayers.length; i += 1) {
|
|
|
|
ConnectPersistentData(currentPlayers[i]);
|
|
|
|
}
|
|
|
|
_.memory.FreeMany(currentPlayers);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
defaultproperties
|
|
|
|
{
|
|
|
|
}
|