/** * 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.") }