/** * Feature for managing Acedia's user groups. Supports both config- and * database-defined information about group sources. An instance of this * feature is necessary for functioning of Acedia's `UserAPI` methods related * to user groups. * Copyright 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 Users_Feature extends Feature; var private /*config*/ bool useDatabase; var private /*config*/ string databaseLink; var private /*config*/ array availableUserGroups; // List of all available user groups for current config var private array loadedUserGroups; // `HashTable` (with group name keys) that stores `HashTable`s used as // a set data structure (has user id as keys and always `none` as a value). var private HashTable loadedGroupToUsersMap; protected function OnEnabled() { _.users._reloadFeature(); } protected function OnDisabled() { _.users._reloadFeature(); } protected function SwapConfig(FeatureConfig config) { local Users newConfig; newConfig = Users(config); if (newConfig == none) { return; } useDatabase = newConfig.useDatabase; databaseLink = newConfig.databaseLink; availableUserGroups = newConfig.localUserGroup; LoadLocalData(); } private final function LoadLocalData() { LoadLocalGroupNames(); LoadLocalGroupToUserMap(); } private final function LoadLocalGroupNames() { local int i, j; local bool isDuplicate; local Text nextUserGroup; _.memory.FreeMany(loadedUserGroups); loadedUserGroups.length = 0; for (i = 0; i < availableUserGroups.length; i += 1) { isDuplicate = false; nextUserGroup = _.text.FromString(availableUserGroups[i]); for(j = 0; j < loadedUserGroups.length; j += 1) { if (loadedUserGroups[j].Compare(nextUserGroup, SCASE_INSENSITIVE)) { isDuplicate = true; break; } } if (!isDuplicate) { loadedUserGroups[loadedUserGroups.length] = nextUserGroup.LowerCopy(); } nextUserGroup.FreeSelf(); } } private final function LoadLocalGroupToUserMap() { local int i, j; local Text newSteamID; local HashTable newPlayerSet; local UserGroup nextGroupConfig; local array nextGroupUserArray; _.memory.Free(loadedGroupToUsersMap); loadedGroupToUsersMap = _.collections.EmptyHashTable(); class'UserGroup'.static.Initialize(); // Go over every group for (i = 0; i < loadedUserGroups.length; i += 1) { nextGroupConfig = UserGroup( class'UserGroup'.static.GetConfigInstance(loadedUserGroups[i])); if (nextGroupConfig == none) { // !!! Log missing group continue; } // Copy player IDs from `string` array into `HashTable` // that is serving as a set data structure newPlayerSet = _.collections.EmptyHashTable(); nextGroupUserArray = nextGroupConfig.user; for (j = 0; j < nextGroupUserArray.length; j += 1) { newSteamID = _.text.FromString(nextGroupUserArray[j]); newPlayerSet.SetItem(newSteamID, none); newSteamID.FreeSelf(); } loadedGroupToUsersMap.SetItem(loadedUserGroups[i], newPlayerSet); newPlayerSet.FreeSelf(); nextGroupConfig.FreeSelf(); } } private final function SaveLocalData() { local Text nextGroup, activeConfigName; local Users currentConfig; local HashTableIterator iter; if (useDatabase) return; if (loadedGroupToUsersMap == none) return; availableUserGroups.length = 0; iter = HashTableIterator(loadedGroupToUsersMap.Iterate()); while (!iter.HasFinished()) { nextGroup = Text(iter.GetKey()); if (nextGroup != none) { availableUserGroups[availableUserGroups.length] = nextGroup.ToString(); nextGroup.FreeSelf(); } iter.Next(); } iter.FreeSelf(); activeConfigName = GetCurrentConfig(); if (activeConfigName != none) { currentConfig = Users(class'Users'.static .GetConfigInstance(activeConfigName)); } if (currentConfig != none) { currentConfig.localUserGroup = availableUserGroups; // !!! save config !!! } _.memory.Free(currentConfig); _.memory.Free(activeConfigName); } /** * Returns names of all available groups that users can belong to. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * * @return Array with names of all available groups. * All array elements are guaranteed to be not-`none`, unique and in * lower case. */ public final function array GetAvailableGroups() { local int i; local array result; for (i = 0; i < loadedUserGroups.length; i += 1) { result[i] = loadedUserGroups[i].Copy(); } return result; } /** * Adds a new user group. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * Changes will always persist for the duration of the match, but writing * them into the (non-config) source might fail, leading to changes being reset * after the level switch. For non-database (config) sources changes will * always be saved. * * @param groupName Name of the group to add. Case-insensitive. * @return `true` if group was added and `false` otherwise (including if it * already existed). */ public final function bool AddGroup(BaseText groupName) { local bool groupExists; local Text lowerCaseGroupName; local HashTable newUserSet; if (groupName == none) { return false; } lowerCaseGroupName = groupName.LowerCopy(); groupExists = loadedGroupToUsersMap.HasKey(lowerCaseGroupName); if (!groupExists) { lowerCaseGroupName.FreeSelf(); return false; } loadedUserGroups[loadedUserGroups.length] = lowerCaseGroupName; // Try loading local `UserGroup`? newUserSet = _.collections.EmptyHashTable(); loadedGroupToUsersMap.SetItem(lowerCaseGroupName, newUserSet); newUserSet.FreeSelf(); return true; } /** * Removes existing user group. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * Changes will always persist for the duration of the match, but writing * them into the (non-config) source might fail, leading to changes being reset * after the level switch. For non-database (config) sources changes will * always be saved. * * @param groupName Name of the group to remove. Case-insensitive. * @return `true` if group was removed and `false` otherwise (including if it * didn't exist in the first place). */ public final function bool RemoveGroup(BaseText groupName) { local int i; local bool groupExists; local Text lowerCaseGroupName; if (groupName == none) { return false; } lowerCaseGroupName = groupName.LowerCopy(); groupExists = loadedGroupToUsersMap.HasKey(lowerCaseGroupName); if (!groupExists) { lowerCaseGroupName.FreeSelf(); return false; } for (i = 0; i < loadedUserGroups.length; i += 1) { if (lowercaseGroupName.Compare(loadedUserGroups[i])) { loadedUserGroups.Remove(i, 1); break; } } // Try loading local `UserGroup`? loadedGroupToUsersMap.RemoveItem(lowerCaseGroupName); lowerCaseGroupName.FreeSelf(); return true; } /** * Checks whether group with specified name exists. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * * @param groupName Name of the group to check existence of. * Case-insensitive. * @return `true` if group exists and `false` otherwise. */ public final function bool IsGroupExisting(BaseText groupName) { local bool result; local Text lowerCaseGroupName; if (groupName == none) { return false; } lowerCaseGroupName = groupName.LowerCopy(); result = loadedGroupToUsersMap.HasKey(lowerCaseGroupName); lowerCaseGroupName.FreeSelf(); return result; } /** * Adds user with the given SteamID into the specified group. * * In case this feature is configured to load user groups from a database * (either local or remote), changes are guaranteed to be made to the locally * cached copy that will persist for the duration of the game. Method will also * attempt to change the database value, but that is not guaranteed to succeed, * meaning that changes might not be saved for later matches. * * @param steamID SteamID of the user to add to the group. * @param groupName Name of the group to add user to. Case-insensitive. * @return `true` if user was added to the group (including if her was already * added to it) and `false` in any other case. */ public final function bool AddSteamIDToGroup( BaseText steamID, BaseText groupName) { local Text lowercaseGroupName; local HashTable groupUsers; if (loadedGroupToUsersMap == none) return false; if (groupName == none) return false; lowercaseGroupName = groupName.LowerCopy(); groupUsers = loadedGroupToUsersMap.GetHashTable(lowercaseGroupName); lowercaseGroupName.FreeSelf(); // No specified group? Nothing to add! if (groupUsers == none) { return false; } groupUsers.SetItem(steamID, none); groupUsers.FreeSelf(); return true; } /** * Adds user with the given SteamID into the specified group. * * In case this feature is configured to load user groups from a database * (either local or remote), changes are guaranteed to be made to the locally * cached copy that will persist for the duration of the game. Method will also * attempt to change the database value, but that is not guaranteed to succeed, * meaning that changes might not be saved for later matches. * * @param steamID SteamID of the user to add to the group. * @param groupName Name of the group to add user to. Case-insensitive. * @return `true` if user was added to the group (including if her was already * added to it) and `false` in any other case. */ public final /*unreal*/ function bool AddSteamIDToGroup_S( string steamID, string groupName) { local bool result; local MutableText idWrapper, groupWrapper; idWrapper = _.text.FromStringM(steamID); groupWrapper = _.text.FromStringM(groupName); result = AddSteamIDToGroup(idWrapper, groupWrapper); idWrapper.FreeSelf(); groupWrapper.FreeSelf(); return result; } /** * Adds user (given by the `UserID`) into the specified group. * * In case this feature is configured to load user groups from a database * (either local or remote), changes are guaranteed to be made to the locally * cached copy that will persist for the duration of the game. Method will also * attempt to change the database value, but that is not guaranteed to succeed, * meaning that changes might not be saved for later matches. * * @param id `UserID` of the user to add to the group. * @param groupName Name of the group to add user to. Case-insensitive. * @return `true` if user was added to the group (including if her was already * added to it) and `false` in any other case. */ public final function bool AddUserIDToGroup( UserID id, BaseText groupName) { local bool result; local Text steamID; if (groupName == none) return false; if (id == none) return false; steamID = id.GetSteamID64String(); if (steamID == none) return false; result = AddSteamIDToGroup(steamID, groupName); steamID.FreeSelf(); return result; } /** * Adds given user into the specified group. * * In case this feature is configured to load user groups from a database * (either local or remote), changes are guaranteed to be made to the locally * cached copy that will persist for the duration of the game. Method will also * attempt to change the database value, but that is not guaranteed to succeed, * meaning that changes might not be saved for later matches. * * @param user User to add to the group. * @param groupName Name of the group to add user to. Case-insensitive. * @return `true` if user was added to the group (including if her was already * added to it) and `false` in any other case. */ public final function bool AddUserToGroup(User user, BaseText groupName) { local bool result; local UserID id; if (groupName == none) return false; if (user == none) return false; id = user.GetID(); result = AddUserIDToGroup(id, groupName); _.memory.Free(id); return result; } /** * Removes user with the given SteamID from the specified group. * * In case this feature is configured to load user groups from a database * (either local or remote), changes are guaranteed to be made to the locally * cached copy that will persist for the duration of the game. Method will also * attempt to change the database value, but that is not guaranteed to succeed, * meaning that changes might not be saved for later matches. * * @param steamID SteamID of the user to remove to the group. * @param groupName Name of the group to remove user to. Case-insensitive. * @return `true` if user was removed to the group (including if her was * already removed to it) and `false` in any other case. */ public final function bool RemoveSteamIDFromGroup( BaseText steamID, BaseText groupName) { local bool hadUser; local Text lowercaseGroupName; local HashTable groupUsers; if (groupName == none) return false; if (loadedGroupToUsersMap == none) return false; lowercaseGroupName = groupName.LowerCopy(); groupUsers = loadedGroupToUsersMap.GetHashTable(lowercaseGroupName); lowercaseGroupName.FreeSelf(); if (groupUsers == none) { return false; } hadUser = groupUsers.HasKey(steamID); groupUsers.RemoveItem(steamID); groupUsers.FreeSelf(); return hadUser; } /** * Removes user with the given SteamID from the specified group. * * In case this feature is configured to load user groups from a database * (either local or remote), changes are guaranteed to be made to the locally * cached copy that will persist for the duration of the game. Method will also * attempt to change the database value, but that is not guaranteed to succeed, * meaning that changes might not be saved for later matches. * * @param steamID SteamID of the user to remove to the group. * @param groupName Name of the group to remove user to. Case-insensitive. * @return `true` if user was removed to the group (including if her was * already removed to it) and `false` in any other case. */ public final /*unreal*/ function bool RemoveSteamIDFromGroup_S( string steamID, string groupName) { local bool result; local MutableText idWrapper, groupWrapper; idWrapper = _.text.FromStringM(steamID); groupWrapper = _.text.FromStringM(groupName); result = RemoveSteamIDFromGroup(idWrapper, groupWrapper); idWrapper.FreeSelf(); groupWrapper.FreeSelf(); return result; } /** * Removes user (given by the `UserID`) from the specified group. * * In case this feature is configured to load user groups from a database * (either local or remote), changes are guaranteed to be made to the locally * cached copy that will persist for the duration of the game. Method will also * attempt to change the database value, but that is not guaranteed to succeed, * meaning that changes might not be saved for later matches. * * @param id `UserID` of the user to remove to the group. * @param groupName Name of the group to remove user to. Case-insensitive. * @return `true` if user was removed to the group (including if her was * already removed to it) and `false` in any other case. */ public final function bool RemoveUserIDFromGroup( UserID id, BaseText groupName) { local bool result; local Text steamID; if (groupName == none) return false; if (id == none) return false; steamID = id.GetSteamID64String(); if (steamID == none) return false; result = RemoveSteamIDFromGroup(steamID, groupName); steamID.FreeSelf(); return result; } /** * Removes user from the specified group. * * In case this feature is configured to load user groups from a database * (either local or remote), changes are guaranteed to be made to the locally * cached copy that will persist for the duration of the game. Method will also * attempt to change the database value, but that is not guaranteed to succeed, * meaning that changes might not be saved for later matches. * * @param user User to remove to the group. * @param groupName Name of the group to remove user to. Case-insensitive. * @return `true` if user was removed to the group (including if her was * already removed to it) and `false` in any other case. */ public final function bool RemoveUserFromGroup(User user, BaseText groupName) { local bool result; local UserID id; if (groupName == none) return false; if (user == none) return false; id = user.GetID(); result = RemoveUserIDFromGroup(id, groupName); _.memory.Free(id); return result; } /** * Returns names of all groups available for the user given by SteamID. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * * @see `GetGroupsForUserID()` / `GetGroupsForUser()`. * * @param steamID SteamID of the user. * Must be specified in a SteamID64 format, e.g. "76561197960287930". * @return Array with names of the groups of the specified user. * All array elements are guaranteed to be not-`none`, unique and in * lower case. * If passed SteamID is `none` or data wasn't yet loaded - returns empty * array. */ public final function array GetGroupsForSteamID(BaseText steamID) { local Text immutableSteamID; local array result; local HashTableIterator iter; local Text nextGroup; local HashTable nextGroupUsers; if (loadedGroupToUsersMap == none) return result; if (steamID == none) return result; immutableSteamID = steamID.LowerCopy(); iter = HashTableIterator(loadedGroupToUsersMap.Iterate()); while (!iter.HasFinished()) { nextGroup = Text(iter.GetKey()); nextGroupUsers = HashTable(iter.Get()); if ( nextGroup != none && nextGroupUsers != none && nextGroupUsers.HasKey(steamID)) { result[result.length] = nextGroup.Copy(); } iter.Next(); _.memory.Free(nextGroup); _.memory.Free(nextGroupUsers); } immutableSteamID.FreeSelf(); return result; } /** * Returns names of all groups available for the user with a SteamID given by * `UserID`. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * * @see `GetGroupsForUserID()` / `GetGroupsForUser()`. * * @param steamID SteamID of the user. * Must be specified in a SteamID64 format, e.g. "76561197960287930". * @return Array with names of the groups of the specified user. * All array elements are guaranteed to be not-`none`, unique and in * lower case. * If data wasn't yet loaded - returns empty array. */ public final /*unreal*/ function array GetGroupsForSteamID_S( string steamID) { local array result; local MutableText wrapper; wrapper = _.text.FromStringM(steamID); result = GetGroupsForSteamID(wrapper); wrapper.FreeSelf(); return result; } /** * Returns names of all groups available for the user given by `UserID`. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * * @see `GetGroupsForSteamID()` / `GetGroupsForUser()`. * * @param id ID of the user. * @return Array with names of the groups of the specified user. * All array elements are guaranteed to be not-`none`, unique and in * lower case. * If data wasn't yet loaded - returns empty array. */ public final function array GetGroupsForUserID(UserID id) { local Text steamID; local array result; if (id == none) return result; steamID = id.GetSteamID64String(); if (steamID == none) return result; result = GetGroupsForSteamID(steamID); steamID.FreeSelf(); return result; } /** * Returns names of all groups available for the user. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * * @see `GetGroupsForSteamID()` / `GetGroupsForUserID()`. * * @param user Reference to `User` object that represent the user we are to * find groups for. * @return Array with names of the groups of the specified user. * All array elements are guaranteed to be not-`none`, unique and in * lower case. * If data wasn't yet loaded - returns empty array. */ public final function array GetGroupsForUser(User user) { local UserID id; local array result; if (user == none) { return result; } id = user.GetID(); result = GetGroupsForUserID(id); _.memory.Free(id); return result; } /** * Returns `UserID`s of all users that belong into the group named `groupName`. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * * @param groupName Name of the group. Case-insensitive. * @return Array with `UserID`s for every user in the user group named * `groupName`. All array elements are guaranteed to be not-`none` and * correspond to unique players. * If data wasn't yet loaded - returns empty array. */ public final function array GetGroupMembers(BaseText groupName) { local int i; local Text lowerCaseGroupName; local HashTable groupUsers; local array groupUsersNames; local UserID nextUserID; local array result; if (loadedGroupToUsersMap == none) return result; if (groupName == none) return result; lowerCaseGroupName = groupName.LowerCopy(); groupUsers = loadedGroupToUsersMap.GetHashTable(lowerCaseGroupName); lowerCaseGroupName.FreeSelf(); if (groupUsers == none) { groupUsersNames = groupUsers.GetTextKeys(); } _.memory.Free(groupUsers); for (i = 0; i < groupUsersNames.length; i += 1) { nextUserID = UserID(_.memory.Allocate(class'UserID')); nextUserID.Initialize(groupUsersNames[i]); if (nextUserID.IsInitialized()) { result[result.length] = nextUserID; } else { nextUserID.FreeSelf(); } } _.memory.FreeMany(groupUsersNames); return result; } /** * Checks whether user given by `UserID` belongs to the group named * `groupName`. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * * @param id ID of the user to check. * @param groupName Name of the group. Case-insensitive. * @return `true` if user with an ID given by `id` belongs to the group named * `groupName` and false if: it does not, either of the parameters is * invalid or group data wasn't yet properly loaded. */ public final function bool IsUserIDInGroup(UserID id, Text groupName) { local bool result; local Text steamID; local Text lowerGroupName; local HashTable nextGroupUsers; if (loadedGroupToUsersMap == none) return false; if (groupName == none) return false; if (id == none) return false; steamID = id.GetSteamID64String(); if (steamID == none) return false; lowerGroupName = groupName.LowerCopy(); nextGroupUsers = loadedGroupToUsersMap.GetHashTable(lowerGroupName); lowerGroupName.FreeSelf(); if (nextGroupUsers != none) { result = nextGroupUsers.HasKey(steamID); } _.memory.Free(nextGroupUsers); steamID.FreeSelf(); return result; } /** * Checks whether user belongs to the specified group. * * In case this feature is configured to load user groups from a database * (either local or remote), the returned value is a locally cached one. * This helps us avoid having to query database each time we want to check * something about user groups, but it also means we might have an outdated * information. * * @param user Reference to `User` object that represent the user we are to * check for belonging to the group. * @param groupName Name of the group. Case-insensitive. * @return `true` if user with an ID given by `id` belongs to the group named * `groupName` and false if: it does not, either of the parameters is * invalid or group data wasn't yet properly loaded. */ public final function bool IsUserInGroup(User user, Text groupName) { local UserID id; local bool result; if (user == none) { return false; } id = user.GetID(); result = IsUserIDInGroup(id, groupName); _.memory.Free(id); return result; } /** * Checks whether user groups' data was already loaded from the source * (either config file or local/remote database). * * Data loaded once is cached and this method returning `true` does not * guarantee that is isn't outdated. Additional, asynchronous queries must be * made to check for that. * * @return `true` if user groups' data was loaded and `false` otherwise. */ public final function bool IsUserGroupDataLoaded() { return true; } defaultproperties { configClass = class'Users' }