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.
508 lines
17 KiB
508 lines
17 KiB
/** |
|
* 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 <https://www.gnu.org/licenses/>. |
|
*/ |
|
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<Text> userGroups; // user groups loaded from database |
|
var private array<Text> 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<string> nextUserArray; |
|
local array<Text> 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<Text> GetGroups() |
|
{ |
|
local int i, j; |
|
local bool duplicate; |
|
local array<Text> 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.") |
|
} |