UnrealScript library and basis for all Acedia Framework mods
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

407 lines
14 KiB

/**
* 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 JSONPointer 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, JSONPointer location)
{
if (db == none) return false;
if (location == none) return false;
Reset();
database = db;
database.NewRef();
rootPointer = location.Copy();
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 JSONPointer 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.Pointer();
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 JSONPointer 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.Pointer();
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 JSONPointer 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.Pointer();
location.Push(groupName).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 JSONPointer 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.Pointer();
location.Push(groupName).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
{
}