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
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 |
|
{ |
|
} |