/**
* Object that is supposed to store a persistent data about the
* certain player. That is data that will be remembered even after player
* reconnects or server changes map/restarts.
* Copyright 2020-2022 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 .
*/
class User extends AcediaObject;
// Unique identifier for which this `User` stores it's data
var private UserID id;
// A numeric "key" assigned to this user for a session that can serve as
// an easy reference in console commands
var private int key;
// If we failed to create user database skeleton - set this to `true`,
// this will prevent us from making changes that might mess up database due to
// misconfiguration
var private bool failedToCreateDatabaseSkeleton;
// Database where user's persistent data is stored
var private Database persistentDatabase;
// Pointer to this user's "settings" data in particular
var private JSONPointer persistentSettingsPointer;
// Groups to which caller `User` belongs to.
// Every user always belongs to group "everyone", so it is never listed
// here.
// Local user groups are not available for modification and are only loaded
// from configs, so `userGroups` might duplicate groups from `localUserGroup`,
// allowing to add them to editable sources (database).
// Group names are stored in the lower register.
var private array userGroups; // user groups loaded from database
var private array localUserGroups; // user groups loaded from local files
var private LoggerAPI.Definition warnNoPersistentDatabase;
var private LoggerAPI.Definition infoPersistentDatabaseLoaded;
var private LoggerAPI.Definition errCannotCreateSkeletonFor;
var private LoggerAPI.Definition errCannotReadDB, errInvalidUserGroups;
protected function Finalizer()
{
if (id != none) {
id.FreeSelf();
}
if (persistentSettingsPointer != none) {
persistentSettingsPointer.FreeSelf();
}
id = none;
persistentSettingsPointer = none;
}
/**
* Initializes caller `User` with id and it's session key.
*
* Initialization should (and can) only be done once.
* Before a `Initialize()` call, any other method calls on such `User`
* must be considerate to have undefined behavior.
* DO NOT CALL THIS METHOD MANUALLY.
*/
public final function Initialize(UserID initID, int initKey)
{
local DBReadTask groupsReadingTask;
id = initID;
key = initKey;
if (initID != none) {
initID.NewRef();
}
//LoadLocalGroups();
/*groupsReadingTask = ReadPersistentData(P("Acedia"), P("UserGroups"));
if (groupsReadingTask != none) {
groupsReadingTask.connect = LoadDBGroups;
}*/
}
/**
* Return id for which caller `User` stores data.
*
* @return `UserID` that caller `User` was initialized with.
*/
public final function UserID GetID()
{
if (id != none) {
id.NewRef();
}
return id;
}
/**
* Return session key of the caller `User`.
*
* @return Session key of the caller `User`.
*/
public final function int GetKey()
{
return key;
}
// Loads locally defined groups from the "AcediaUserGroups.ini" config
private final function LoadLocalGroups()
{
local int i, j;
local string mySteamID;
local UserGroup nextGroupConfig;
local array nextUserArray;
local array availableGroups;
if (id == none) {
return;
}
class'UserGroup'.static.Initialize();
mySteamID = _.text.IntoString(id.GetSteamID64String());
availableGroups = class'UserGroup'.static.AvailableConfigs();
// Go over every group
for (i = 0; i < availableGroups.length; i += 1)
{
nextGroupConfig = UserGroup(
class'UserGroup'.static.GetConfigInstance(availableGroups[i]));
// Add group as local if it has our ID recorded
nextUserArray = nextGroupConfig.user;
for (j = 0; j < nextUserArray.length; j += 1)
{
if (nextUserArray[j] == mySteamID)
{
localUserGroups[localUserGroups.length] =
availableGroups[i].LowerCopy();
}
}
_.memory.Free(nextGroupConfig);
}
_.memory.FreeMany(availableGroups);
}
// Loads groups defined in database with user data
private final function LoadDBGroups(
Database.DBQueryResult result,
AcediaObject data,
Database source)
{
local int i;
local MutableText nextGroup;
local ArrayList dbGroups;
if (result != DBR_Success)
{
_.logger.Auto(errCannotReadDB);
return;
}
_.memory.FreeMany(userGroups);
userGroups.length = 0;
dbGroups = ArrayList(data);
if (dbGroups == none)
{
if (data != none)
{
_.logger.Auto(errInvalidUserGroups);
_.memory.Free(data);
}
return;
}
for (i = 0; i < dbGroups.GetLength(); i += 1)
{
nextGroup = dbGroups.GetMutableText(i);
if (nextGroup == none) {
continue;
}
if (!class'UserGroup'.static.Exists(nextGroup))
{
nextGroup.FreeSelf();
continue;
}
userGroups[userGroups.length] = nextGroup.IntoText();
}
dbGroups.FreeSelf();
}
// Save current user groups into the user data database
private final function UpdateDBGroups()
{
local ArrayList newDBData;
newDBData = _.collections.NewArrayList(userGroups);
WritePersistentData(P("Acedia"), P("UserGroups"), newDBData);
newDBData.FreeSelf();
}
/**
* Adds caller user into new group, specified by `newGroup`.
* This group must exist for the method to succeed.
*
* @param newGroup Name of the group to add caller `User` into.
*/
public final function AddGroup(Text newGroup)
{
local int i;
if (newGroup == none) return;
if (class'UserGroup'.static.Exists(newGroup)) return;
for (i = 0; i < userGroups.length; i += 1)
{
if (newGroup.Compare(userGroups[i], SCASE_INSENSITIVE)) {
return;
}
}
userGroups[userGroups.length] = newGroup.LowerCopy();
UpdateDBGroups();
}
/**
* Removes caller user from the given group `groupToRemove`.
*
* @param groupToRemove Name of the group to remove caller `User` from.
* @return `true` if user was actually removed from the group and `false`
* otherwise (group doesn't exist or user didn't belong to it).
*/
public final function bool RemoveGroup(Text groupToRemove)
{
local int i;
if (groupToRemove == none) {
return false;
}
for (i = 0; i < userGroups.length; i += 1)
{
if (groupToRemove.Compare(userGroups[i], SCASE_INSENSITIVE))
{
userGroups[i].FreeSelf();
userGroups.Remove(i, 1);
UpdateDBGroups();
return true;
}
}
return false;
}
/**
* Checks whether caller `User` belongs to the group specified by
* `groupToCheck`.
*
* @param groupToCheck Name of the group to check for whether caller `User`
* belongs to it.
* @return `true` if caller `User` belongs to the group `groupToCheck` and
* `false` otherwise.
*/
public final function bool IsInGroup(Text groupToCheck)
{
local int i;
if (groupToCheck == none) {
return false;
}
for (i = 0; i < userGroups.length; i += 1)
{
if (groupToCheck.Compare(userGroups[i], SCASE_INSENSITIVE)) {
return true;
}
}
return false;
}
/**
* Returns array with names of all groups to which caller user belongs to.
*
* @return Array of names of the groups that caller user belongs to.
* Guaranteed to not contain duplicates or `none` values.
*/
public final function array GetGroups()
{
local int i, j;
local bool duplicate;
local array result;
for (i = 0; i < localUserGroups.length; i += 1) {
result[result.length] = localUserGroups[i].Copy();
}
for (i = 0; i < userGroups.length; i += 1)
{
duplicate = false;
// Check `userGroups[i]` for being a duplicate from `localUserGroups`
for (j = 0; j < localUserGroups.length; j += 1)
{
// No need for `SCASE_INSENSITIVE`, since user group names
// are stored in lower case
if (userGroups[i].Compare(localUserGroups[j]))
{
duplicate = true;
break;
}
}
if (!duplicate) {
result[result.length] = userGroups[i].Copy();
}
}
return result;
}
/**
* Reads user's persistent data saved inside group `groupName`, saving it into
* a collection using mutable data types.
* Only should be used if `_.users.PersistentStorageExists()` returns `true`.
*
* @param groupName Name of the group these settings belong to.
* This exists to help reduce name collisions between different mods.
* Acedia stores all its settings under "Acedia" group. We suggest that you
* pick at least one name to use for your own mods.
* It should be unique enough to not get picked by others - "weapons" is
* a bad name, while "CoolModMastah79" is actually a good pick.
* @return Task object for reading specified persistent data from the database.
* For more info see `Database.ReadData()` method.
* Guaranteed to not be `none` iff
* `_.users.PersistentStorageExists() == true`.
*/
public final function DBReadTask ReadGroupOfPersistentData(BaseText groupName)
{
local DBReadTask task;
if (groupName == none) return none;
if (!SetupDatabaseVariables()) return none;
persistentSettingsPointer.Push(groupName);
task = persistentDatabase.ReadData(persistentSettingsPointer, true);
_.memory.Free(persistentSettingsPointer.Pop());
return task;
}
/**
* Reads user's persistent data saved under name `dataName`, saving it into
* a collection using mutable data types.
* Only should be used if `_.users.PersistentStorageExists()` returns `true`.
*
* @param groupName Name of the group these settings belong to.
* This exists to help reduce name collisions between different mods.
* Acedia stores all its settings under "Acedia" group. We suggest that you
* pick at least one name to use for your own mods.
* It should be unique enough to not get picked by others - "weapons" is
* a bad name, while "CoolModMastah79" is actually a good pick.
* @param dataName Any name, from under which settings you are interested
* (inside `groupName` group) should be read.
* @return Task object for reading specified persistent data from the database.
* For more info see `Database.ReadData()` method.
* Guaranteed to not be `none` iff
* `_.users.PersistentStorageExists() == true`.
*/
public final function DBReadTask ReadPersistentData(
BaseText groupName,
BaseText dataName)
{
local DBReadTask task;
if (groupName == none) return none;
if (dataName == none) return none;
if (!SetupDatabaseVariables()) return none;
persistentSettingsPointer.Push(groupName).Push(dataName);
task = persistentDatabase.ReadData(persistentSettingsPointer, true);
_.memory.Free(persistentSettingsPointer.Pop());
_.memory.Free(persistentSettingsPointer.Pop());
return task;
}
/**
* Writes user's persistent data under name `dataName`.
* Only should be used if `_.users.PersistentStorageExists()` returns `true`.
*
* @param groupName Name of the group these settings belong to.
* This exists to help reduce name collisions between different mods.
* Acedia stores all its settings under "Acedia" group. We suggest that you
* pick at least one name to use for your own mods.
* It should be unique enough to not get picked by others - "weapons" is
* a bad name, while "CoolModMastah79" is actually a good pick.
* @param dataName Any name, under which settings you are interested
* (inside `groupName` group) should be written.
* @param data JSON-compatible (see `_.json.IsCompatible()`) data that
* should be written into database.
* @return Task object for writing specified persistent data into the database.
* For more info see `Database.WriteData()` method.
* Guarantee to not be `none` iff
* `_.users.PersistentStorageExists() == true`.
*/
public final function DBWriteTask WritePersistentData(
BaseText groupName,
BaseText dataName,
AcediaObject data)
{
local DBWriteTask task;
local HashTable emptyObject;
if (groupName == none) return none;
if (dataName == none) return none;
if (!SetupDatabaseVariables()) return none;
emptyObject = _.collections.EmptyHashTable();
persistentSettingsPointer.Push(groupName);
persistentDatabase.IncrementData(persistentSettingsPointer, emptyObject);
persistentSettingsPointer.Push(dataName);
task = persistentDatabase.WriteData(persistentSettingsPointer, data);
_.memory.Free(persistentSettingsPointer.Pop());
_.memory.Free(persistentSettingsPointer.Pop());
_.memory.Free(emptyObject);
return task;
}
// Setup database `persistentDatabase` and pointer to this user's data
// `persistentSettingsPointer`.
// Return `true` if these variables were setup (during this call or before)
// and `false` otherwise.
private function bool SetupDatabaseVariables()
{
local Text userDataLink;
local Text userTextID;
if (failedToCreateDatabaseSkeleton) return false;
if (persistentDatabase != none) return true;
if (id == none || !id.IsInitialized()) return false;
// Check if database was even specified
persistentDatabase = _.users.GetPersistentDatabase();
if (persistentDatabase == none)
{
_.logger.Auto(warnNoPersistentDatabase);
return false;
}
// Try making skeleton database
userTextID = id.GetSteamID64String();
userDataLink = _.users.GetPersistentDataLink();
persistentSettingsPointer = _.db.GetPointer(userDataLink);
persistentSettingsPointer.Push(P("PerUserData"));
persistentSettingsPointer.Push(userTextID);
MakeSkeletonUserDatabase(userTextID, persistentSettingsPointer);
persistentSettingsPointer.Push(P("settings"));
userTextID.FreeSelf();
_.memory.Free(userDataLink);
return true;
}
private function MakeSkeletonUserDatabase(
Text userTextID,
JSONPointer userDataPointer)
{
local HashTable skeleton, emptyObject;
// Construct skeleton object
skeleton = _.collections.EmptyHashTable();
emptyObject = _.collections.EmptyHashTable();
skeleton.SetItem(P("Settings"), emptyObject);
skeleton.SetItem(P("Statistics"), emptyObject);
// Try adding the skeleton object
persistentDatabase
.IncrementData(userDataPointer, skeleton)
.connect = ReportSkeletonCreationResult;
// Release skeleton objects
skeleton.FreeSelf();
emptyObject.FreeSelf();
}
private function ReportSkeletonCreationResult(
Database.DBQueryResult result,
Database source)
{
local Text userTextID;
local Text userDataLink;
userTextID = id.GetSteamID64String();
userDataLink = _.users.GetPersistentDataLink();
if (result == DBR_Success)
{
_.logger.Auto(infoPersistentDatabaseLoaded)
.Arg(userTextID)
.Arg(userDataLink);
}
else
{
_.logger.Auto(errCannotCreateSkeletonFor)
.Arg(userTextID)
.Arg(userDataLink);
failedToCreateDatabaseSkeleton = true;
_.memory.Free(persistentDatabase);
_.memory.Free(persistentSettingsPointer);
persistentDatabase = none;
persistentSettingsPointer = none;
}
_.memory.Free(userTextID);
_.memory.Free(userDataLink);
}
// Load groups from db data only, inside the `UserAPI`
// Get rid of the "AcediaUserGroups.ini"
// Make command for editing user groups
defaultproperties
{
warnNoPersistentDatabase = (l=LOG_Error,m="No persistent user database available.")
infoPersistentDatabaseLoaded = (l=LOG_Info,m="Persistent user database was setup for user \"%1\" (using database link \"%2\").")
errCannotCreateSkeletonFor = (l=LOG_Error,m="Failed to create persistent user database skeleton for user \"%1\" (using database link \"%2\"). User data functionality won't function properly.")
errCannotReadDB = (l=LOG_Error,m="Failed to read user groups from persistent user database.")
errInvalidUserGroups = (l=LOG_Error,m="Invalid data is written as user groups array inside persistent user database.")
}