diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 57aebf8..8b140e9 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -24,7 +24,8 @@ defaultproperties { features(0) = class'Aliases_Feature' features(1) = class'Commands_Feature' - features(2) = class'Avarice_Feature' + features(2) = class'Users_Feature' + features(3) = class'Avarice_Feature' testCases(0) = class'TEST_Base' testCases(1) = class'TEST_ActorService' testCases(2) = class'TEST_Boxes' diff --git a/sources/Users/User.uc b/sources/Users/User.uc index e5675d8..c5d2a76 100644 --- a/sources/Users/User.uc +++ b/sources/Users/User.uc @@ -77,11 +77,11 @@ public final function Initialize(UserID initID, int initKey) if (initID != none) { initID.NewRef(); } - LoadLocalGroups(); - groupsReadingTask = ReadPersistentData(P("Acedia"), P("UserGroups")); + //LoadLocalGroups(); + /*groupsReadingTask = ReadPersistentData(P("Acedia"), P("UserGroups")); if (groupsReadingTask != none) { groupsReadingTask.connect = LoadDBGroups; - } + }*/ } /** diff --git a/sources/Users/UserAPI.uc b/sources/Users/UserAPI.uc index 23faeaa..97a53b7 100644 --- a/sources/Users/UserAPI.uc +++ b/sources/Users/UserAPI.uc @@ -230,6 +230,260 @@ public final function User FetchByKey(int userKey) return result; } +/** + * Returns names of all groups available for the user with a SteamID given by + * `steamID`. + * + * In case active config of `Users_Feature` is set up 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 /*unreal*/ function array GetGroupsForSteamID( + BaseText steamID) +{ + local Users_Feature usersFeature; + local array result; + + usersFeature = Users_Feature(class'Users_Feature'.static + .GetEnabledInstance()); + if (usersFeature == none) { + return result; + } + result = usersFeature.GetGroupsForSteamID(steamID); + usersFeature.FreeSelf(); + return result; +} + +/** + * Returns names of all groups available for the user with a SteamID given by + * `steamID`. + * + * In case active config of `Users_Feature` is set up 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 Users_Feature usersFeature; + local array result; + + usersFeature = Users_Feature(class'Users_Feature'.static + .GetEnabledInstance()); + if (usersFeature == none) { + return result; + } + result = usersFeature.GetGroupsForSteamID_S(steamID); + usersFeature.FreeSelf(); + return result; +} + + +/** + * Returns names of all groups available for the user with an ID given by `id`. + * + * In case active config of `Users_Feature` is set up 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 Users_Feature usersFeature; + local array result; + + usersFeature = Users_Feature(class'Users_Feature'.static + .GetEnabledInstance()); + if (usersFeature == none) { + return result; + } + result = usersFeature.GetGroupsForUserID(id); + usersFeature.FreeSelf(); + return result; +} + +/** + * Returns names of all groups available for the user given by `user`. + * + * In case active config of `Users_Feature` is set up 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 Users_Feature usersFeature; + local array result; + + usersFeature = Users_Feature(class'Users_Feature'.static + .GetEnabledInstance()); + if (usersFeature == none) { + return result; + } + result = usersFeature.GetGroupsForUser(user); + usersFeature.FreeSelf(); + return result; +} + +/** + * Returns `UserID`s of all users that belong into the group named `groupName`. + * + * In case active config of `Users_Feature` is set up 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(Text groupName) +{ + local Users_Feature usersFeature; + local array result; + + usersFeature = Users_Feature(class'Users_Feature'.static + .GetEnabledInstance()); + if (usersFeature == none) { + return result; + } + result = usersFeature.GetGroupMembers(groupName); + usersFeature.FreeSelf(); + return result; +} + +/** + * Checks whether user with an ID given by `id` belongs to the group named + * `groupName`. + * + * In case active config of `Users_Feature` is set up 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. + * @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 Users_Feature usersFeature; + local bool result; + + usersFeature = Users_Feature(class'Users_Feature'.static + .GetEnabledInstance()); + if (usersFeature == none) { + return result; + } + result = usersFeature.IsUserIDInGroup(id, groupName); + usersFeature.FreeSelf(); + return result; +} + +/** + * Checks whether user given by `user` belongs to the group named `groupName`. + * + * In case active config of `Users_Feature` is set up 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 Users_Feature usersFeature; + local bool result; + + usersFeature = Users_Feature(class'Users_Feature'.static + .GetEnabledInstance()); + if (usersFeature == none) { + return result; + } + result = usersFeature.IsUserInGroup(user, groupName); + usersFeature.FreeSelf(); + 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() +{ + local Users_Feature usersFeature; + local bool result; + + usersFeature = Users_Feature(class'Users_Feature'.static + .GetEnabledInstance()); + if (usersFeature == none) { + return result; + } + result = usersFeature.IsUserGroupDataLoaded(); + usersFeature.FreeSelf(); + return result; +} + defaultproperties { userdataDBLink = "[local]database:/users" diff --git a/sources/Users/Users_Feature.uc b/sources/Users/Users_Feature.uc index 7b8513d..f73cb8e 100644 --- a/sources/Users/Users_Feature.uc +++ b/sources/Users/Users_Feature.uc @@ -1,5 +1,8 @@ /** - * ??? + * 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. @@ -21,9 +24,9 @@ class Users_Feature extends Feature; var private /*config*/ bool useDatabase; var private /*config*/ string databaseLink; -var private /*config*/ array userGroup; +var private /*config*/ array availableUserGroups; -// Defines order +// 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). @@ -37,9 +40,10 @@ protected function SwapConfig(FeatureConfig config) if (newConfig == none) { return; } - useDatabase = newConfig.useDatabase; - databaseLink = newConfig.databaseLink; - userGroup = newConfig.localUserGroup; + useDatabase = newConfig.useDatabase; + databaseLink = newConfig.databaseLink; + availableUserGroups = newConfig.localUserGroup; + LoadLocalData(); } private final function LoadLocalData() @@ -56,10 +60,10 @@ private final function LoadLocalGroupNames() _.memory.FreeMany(loadedUserGroups); loadedUserGroups.length = 0; - for (i = 0; i < userGroup.length; i += 1) + for (i = 0; i < availableUserGroups.length; i += 1) { isDuplicate = false; - nextUserGroup = _.text.FromString(userGroup[i]); + nextUserGroup = _.text.FromString(availableUserGroups[i]); for(j = 0; j < loadedUserGroups.length; j += 1) { if (loadedUserGroups[j].Compare(nextUserGroup, SCASE_INSENSITIVE)) @@ -123,14 +127,15 @@ private final function SaveLocalData() if (useDatabase) return; if (loadedGroupToUsersMap == none) return; - userGroup.length = 0; + availableUserGroups.length = 0; iter = HashTableIterator(loadedGroupToUsersMap.Iterate()); while (!iter.HasFinished()) { nextGroup = Text(iter.GetKey()); if (nextGroup != none) { - userGroup[userGroup.length] = nextGroup.ToString(); + availableUserGroups[availableUserGroups.length] = + nextGroup.ToString(); nextGroup.FreeSelf(); } iter.Next(); @@ -144,31 +149,46 @@ private final function SaveLocalData() } if (currentConfig != none) { - currentConfig.localUserGroup = userGroup; + currentConfig.localUserGroup = availableUserGroups; // !!! save config !!! } _.memory.Free(currentConfig); _.memory.Free(activeConfigName); } -public final function array GetGroupsForUserID(UserID user) -{ - return GetLocalGroupsForUserID(user); -} - -private final function array GetLocalGroupsForUserID(UserID id) +/** + * Returns names of all groups available for the user with a SteamID 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 /*unreal*/ function array GetGroupsForSteamID( + BaseText steamID) { - local Text steamID; + local Text immutableSteamID; local array result; local HashTableIterator iter; local Text nextGroup; local HashTable nextGroupUsers; if (loadedGroupToUsersMap == none) return result; - if (id == none) return result; - steamID = id.GetSteamID64String(); if (steamID == none) return result; + immutableSteamID = steamID.LowerCopy(); iter = HashTableIterator(loadedGroupToUsersMap.Iterate()); while (!iter.HasFinished()) { @@ -183,16 +203,91 @@ private final function array GetLocalGroupsForUserID(UserID id) _.memory.Free(nextGroup); _.memory.Free(nextGroupUsers); } - steamID.FreeSelf(); + immutableSteamID.FreeSelf(); return result; } -public final function array GetGroupsForUser(User user) +/** + * Returns names of all groups available for the user with a SteamID 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 data wasn't yet loaded - returns empty array. + */ +public final /*unreal*/ function array GetGroupsForSteamID_S( + string steamID) { - return GetLocalGroupsForUser(user); + local array result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(steamID); + result = GetGroupsForSteamID(wrapper); + wrapper.FreeSelf(); + return result; } -private final function array GetLocalGroupsForUser(User user) +/** + * Returns names of all groups available for the user with an ID given by `id`. + * + * 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 given by `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; @@ -201,22 +296,32 @@ private final function array GetLocalGroupsForUser(User user) return result; } id = user.GetID(); - result = GetLocalGroupsForUserID(id); + result = GetGroupsForUserID(id); _.memory.Free(id); return result; } -public final function array GetUserIDsInGroup(Text groupName) -{ - return GetUserIDsInLocalGroup(groupName); -} - -private final function array GetUserIDsInLocalGroup(Text groupName) +/** + * 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(Text groupName) { local int i; local Text lowerCaseGroupName; local HashTable groupUsers; - local array groupUserNames; + local array groupUsersNames; local UserID nextUserID; local array result; @@ -227,13 +332,13 @@ private final function array GetUserIDsInLocalGroup(Text groupName) groupUsers = loadedGroupToUsersMap.GetHashTable(lowerCaseGroupName); lowerCaseGroupName.FreeSelf(); if (groupUsers == none) { - groupUserNames = groupUsers.GetTextKeys(); + groupUsersNames = groupUsers.GetTextKeys(); } _.memory.Free(groupUsers); - for (i = 0; i < groupUserNames.length; i += 1) + for (i = 0; i < groupUsersNames.length; i += 1) { nextUserID = UserID(_.memory.Allocate(class'UserID')); - nextUserID.Initialize(groupUserNames[i]); + nextUserID.Initialize(groupUsersNames[i]); if (nextUserID.IsInitialized()) { result[result.length] = nextUserID; } @@ -241,10 +346,95 @@ private final function array GetUserIDsInLocalGroup(Text groupName) nextUserID.FreeSelf(); } } - _.memory.FreeMany(groupUserNames); + _.memory.FreeMany(groupUsersNames); + return result; +} + +/** + * Checks whether user with an ID given by `id` 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. + * @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 given by `user` 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 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'