From 78aff18cad8dc6de0ee2dd6880631a28b2c37db3 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Mon, 6 Mar 2023 03:33:21 +0700 Subject: [PATCH] Refactor `UserAPI` --- config/AcediaUsers.ini | 35 + sources/Manifest.uc | 3 +- sources/Players/EPlayer.uc | 31 +- sources/Users/ACommandUserGroups.uc | 655 +++++ ...ataManager_OnPersistentDataReady_Signal.uc | 40 + ...tDataManager_OnPersistentDataReady_Slot.uc | 41 + .../PersistentData/PersistentDataManager.uc | 407 ++++ sources/Users/Tests/TEST_User.uc | 4 +- sources/Users/User.uc | 211 +- sources/Users/UserAPI.uc | 1397 ++++++++++- sources/Users/UserDatabase.uc | 16 +- sources/Users/UserGroup.uc | 70 + sources/Users/UserID.uc | 37 +- sources/Users/Users.uc | 99 + sources/Users/Users_Feature.uc | 2158 +++++++++++++++++ 15 files changed, 5020 insertions(+), 184 deletions(-) create mode 100644 config/AcediaUsers.ini create mode 100644 sources/Users/ACommandUserGroups.uc create mode 100644 sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Signal.uc create mode 100644 sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Slot.uc create mode 100644 sources/Users/PersistentData/PersistentDataManager.uc create mode 100644 sources/Users/UserGroup.uc create mode 100644 sources/Users/Users.uc create mode 100644 sources/Users/Users_Feature.uc diff --git a/config/AcediaUsers.ini b/config/AcediaUsers.ini new file mode 100644 index 0000000..17353ff --- /dev/null +++ b/config/AcediaUsers.ini @@ -0,0 +1,35 @@ +; Acedia requires adding its own `GameRules` to listen to many different +; game events. + +; In this config you can setup Acedia's user groups and persistent data +; storage. Enabling this feature automatically enables user group support, +; while persistent data is optional. +; Databases can be configured in `AcediaDB.ini`. +[default Users] +; Configures whether to use database (and which) for storing user groups. +; Set `useDatabaseForGroupsData` to `false` if you want to define which users +; belong to what groups inside this config. +useDatabaseForGroupsData=true +groupsDatabaseLink=[local]Database:/group_data +; Configures whether persistent data should be additionally used. +; It can only be stored inside a database. +usePersistentData=true +persistentDataDatabaseLink=[local]Database:/user_data +; Available groups. Only used if `useDatabaseForGroupsData` is set to `false`. +localUserGroup=admin +localUserGroup=moderator +localUserGroup=trusted + +; These groups definitions only work in case you're using a config with +; `useDatabaseForGroupsData` set to `false`. Simply add new `user=` record, +; specifying SteamIDs of the players, e.g. `user=76561197960287930`. +; You can also optionally specify a human-readable lable for the SteamID after +; slash "/", e.g. `user=76561197960287930/gabe`. +[admin UserGroup] +;user= + +[moderator UserGroup] +;user= + +[trusted UserGroup] +;user= \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index ecdae1c..4e35b39 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/Players/EPlayer.uc b/sources/Players/EPlayer.uc index ee53dd4..40dabce 100644 --- a/sources/Players/EPlayer.uc +++ b/sources/Players/EPlayer.uc @@ -1,6 +1,6 @@ /** * Provides a common interface to a connected player connection. - * Copyright 2021 - 2022 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -57,6 +57,7 @@ protected function Finalizer() { _.memory.Free(controller); _.memory.Free(consoleInstance); + _.memory.Free(identity); controller = none; consoleInstance = none; // No need to deallocate `User` objects, since they are all have unique @@ -96,7 +97,6 @@ public final /* unreal */ function bool Initialize( idHash = _.text.FromString(initController.GetPlayerIDHash()); identity = _.users.FetchByIDHash(idHash); idHash.FreeSelf(); - idHash = none; } signalsReferences = playerSignals; controller = _server.unreal.ActorRef(initController); @@ -121,6 +121,9 @@ public function EInterface Copy() // not initialized return playerCopy; } + if (identity != none) { + identity.NewRef(); + } playerCopy.identity = identity; playerCopy.Initialize( PlayerController(controller.Get()), signalsReferences); @@ -231,15 +234,33 @@ public final /* unreal */ function PlayerController GetController() /** * Returns `User` object that is corresponding to the caller `EPlayer`. * - * @return `User` corresponding to the caller `EPlayer`. Guarantee to be - * not `none` for correctly initialized `EPlayer` (it remembers `User` - * record even if player has disconnected). + * @return `User` corresponding to the caller `EPlayer`. Guaranteed to not be + * `none` for correctly initialized `EPlayer` (it remembers `User` record + * even if player has disconnected). */ public final function User GetIdentity() { + if (identity != none) { + identity.NewRef(); + } return identity; } +/** + * Returns `UserID` object that describes ID of the caller `EPlayer`. + * + * @return `UserID` corresponding to the caller `EPlayer`. Guaranteed to not be + * `none` for correctly initialized `EPlayer` (it remembers `User` record + * even if player has disconnected). + */ +public final function UserID GetUserID() +{ + if (identity == none) { + return none; + } + return identity.GetID(); +} + /** * Returns player's original name - the one he joined the game with. * diff --git a/sources/Users/ACommandUserGroups.uc b/sources/Users/ACommandUserGroups.uc new file mode 100644 index 0000000..b58f706 --- /dev/null +++ b/sources/Users/ACommandUserGroups.uc @@ -0,0 +1,655 @@ +/** + * Command for displaying help information about registered Acedia's commands. + * Copyright 2022-2023 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 ACommandUserGroups extends Command + dependson(Users_Feature); + +protected function BuildData(CommandDataBuilder builder) +{ + builder.Name(P("usergroups")) + .Group(P("admin")) + .Summary(P("User groups management.")) + .Describe(P("Allows to add/remove user groups and users to these:" + @ "groups. Changes made by it will always affect current session," + @ "but might fail to be saved in case user groups are stored in" + @ "a database that is either corrupted or in read-only mode.")); + builder.SubCommand(P("list")) + .Describe(P("Lists specified groups along with users that belong to" + @ "them. If no groups were specified at all - lists all available" + @ "groups.")) + .OptionalParams() + .ParamTextList(P("groups")); + builder.SubCommand(P("add")) + .Describe(P("Adds a new group")) + .ParamText(P("group_name")); + builder.SubCommand(P("remove")) + .Describe(P("Removes a group")) + .ParamText(P("group_name")); + builder.SubCommand(P("addplayer")) + .Describe(F("Adds new user to the group, specified by the player" + @ "selector. Can add several players at once." + @ "Allows to also optionally specify annotation" + @ "(human-readable name) that can be thought of as" + @ "a {$TextEmphasis comment}. If annotation isn't specified" + @ "current nickname will be used as one.")) + .ParamText(P("group_name")) + .ParamPlayers(P("player_selector")) + .OptionalParams() + .ParamText(P("annotation")); + builder.SubCommand(P("removeplayer")) + .Describe(P("Removes user from the group, specified by player selector." + @ "Can remove several players at once.")) + .ParamText(P("group_name")) + .ParamPlayers(P("player_selector")); + builder.SubCommand(P("adduser")) + .Describe(F("Adds new user to the group. Allows to also optionally" + @ "specify annotation (human-readable name) that can be thought of" + @ "as a {$TextEmphasis comment}.")) + .ParamText(P("group_name")) + .ParamText(P("user_id")) + .OptionalParams() + .ParamText(P("annotation")); + builder.SubCommand(P("removeuser")) + .Describe(P("Removes user from the group. User can be specified by both" + @ "user's id or annotation, with id taking priority.")) + .ParamText(P("group_name")) + .ParamText(P("user_name")); + builder.Option(P("force")) + .Describe(P("Allows to force usage of invalid user IDs.")); +} + +protected function Executed(CallData arguments, EPlayer instigator) +{ + local bool forceOption; + local Text groupName, userID, userName, annotation; + local ArrayList players, groups; + + groupName = arguments.parameters.GetText(P("group_name")); + // For parameters named `user_id`, can only be ID + userID = arguments.parameters.GetText(P("user_id")); + // For parameters named `user_id`, can be either ID or annotation + userName = arguments.parameters.GetText(P("user_name")); + annotation = arguments.parameters.GetText(P("annotation")); + // An array of players that can be specified for some commands + players = arguments.parameters.GetArrayList(P("player_selector")); + groups = arguments.parameters.GetArrayList(P("groups")); + forceOption = arguments.options.HasKey(P("force")); + if (arguments.subCommandName.IsEmpty()) { + DisplayUserGroups(); + } + else if (arguments.subCommandName.Compare(P("list"), SCASE_SENSITIVE)) { + DisplayUserGroupsWithUsers(groups); + } + else if (arguments.subCommandName.Compare(P("add"), SCASE_SENSITIVE)) { + AddGroup(groupName); + } + else if (arguments.subCommandName.Compare(P("remove"), SCASE_SENSITIVE)) { + RemoveGroup(groupName); + } + else if (arguments.subCommandName.Compare(P("adduser"), SCASE_SENSITIVE)) { + AddOrAnnotateUser(groupName, userID, annotation, forceOption); + } + else if (arguments.subCommandName.Compare(P("removeuser"), SCASE_SENSITIVE)) + { + RemoveUser(groupName, userName); + } + else if (arguments.subCommandName.Compare(P("addplayer"), SCASE_SENSITIVE)) { + AddOrAnnotatePlayers(groupName, players, annotation); + } + else if (arguments.subCommandName + .Compare(P("removeplayer"), SCASE_SENSITIVE)) + { + RemovePlayers(groupName, players); + } + _.memory.Free(groupName); + _.memory.Free(userID); + _.memory.Free(userName); + _.memory.Free(annotation); + _.memory.Free(players); + _.memory.Free(groups); +} + +private function bool ValidateGroupExistence(BaseText groupName) +{ + if (_.users.IsGroupExisting(groupName)) { + return true; + } + callerConsole + .Write(P("Group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .UseColorOnce(_.color.TextFailure) + .Write(P(" doesn't exists")) + .WriteLine(P("!")); + return false; +} + +private function bool ValidateUserID(BaseText textUserID) +{ + local int i; + + if (textUserID == none) { + return false; + } + if (textUserID.IsEmpty()) + { + callerConsole.WriteLine(F("Valid User ID" + @ "{$TextFailure shouldn't be empty}," + @ "use {$TextEmphasis --force} flag if you want to enforce" + @ "using it.")); + return false; + } + for (i = 0; i < textUserID.GetLength(); i += 1) + { + if (!_.text.IsDigit(textUserID.GetCharacter(i))) + { + callerConsole.WriteLine(F("Valid User ID" + @ "{$TextFailure should consist only of digits}," + @ "use {$TextEmphasis --force} flag if you want" + @ "to enforce using it.")); + return false; + } + } + return true; +} + +private function bool TryAddingUserID( + BaseText groupName, + UserID userID, + BaseText userSpecifiedID) +{ + if (_.users.IsUserIDInGroup(userID, groupName)) + { + callerConsole + .Write(P("User id specified as ")) + .UseColorOnce(_.color.Gray) + .Write(userSpecifiedID) + .UseColorOnce(_.color.TextFailure) + .Write(P(" is already in the group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .WriteLine(P("!")); + } + else if (_.users.AddUserIDToGroup(userID, groupName)) + { + callerConsole + .Write(F("{$TextPositive Added} user id specified as ")) + .UseColorOnce(_.color.Gray) + .Write(userSpecifiedID) + .Write(P(" to the group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .WriteLine(P("!")); + } + else + { + callerConsole + .UseColorOnce(_.color.TextFailure) + .Write(P("Failed (for unknown reason)")) + .Write(P(" to add user id ")) + .UseColorOnce(_.color.Gray).Write(userSpecifiedID) + .Write(P(" to the group ")) + .UseColorOnce(_.color.TextEmphasis).Write(groupName) + .WriteLine(P("!")); + return false; + } + return true; +} + +private function DisplayAnnotation( + BaseText userSpecifiedName, + BaseText groupName, + BaseText annotation) +{ + callerConsole + .Write(P("Annotation for user id specified as ")) + .UseColorOnce(_.color.Gray) + .Write(userSpecifiedName) + .UseColorOnce(_.color.TextPositive) + .Write(P(" in the group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .Write(P(" is set to ")) + .UseColorOnce(_.color.TextNeutral) + .WriteLine(annotation); +} + +private function AddOrAnnotateUser( + BaseText groupName, + BaseText textUserID, + BaseText annotation, + bool forceOption) +{ + local UserID id; + + if (groupName == none) return; + if (textUserID == none) return; + if (!ValidateGroupExistence(groupName)) return; + if (!forceOption && !ValidateUserID(textUserID)) return; + + id = UserID(_.memory.Allocate(class'UserID')); + id.Initialize(textUserID); + if (!TryAddingUserID(groupName, id, textUserID) || annotation == none) + { + _.memory.Free(id); + return; + } + _.users.SetAnnotationForUserID(groupName, id, annotation); + _.memory.Free(id); + DisplayAnnotation(textUserID, groupName, annotation); +} + +private function AddOrAnnotatePlayers( + BaseText groupName, + ArrayList players, + BaseText annotation) +{ + local int i; + local BaseText playerName, nextAnnotation; + local EPlayer nextPlayer; + local UserID nextID; + + if (groupName == none) return; + if (players == none) return; + if (!ValidateGroupExistence(groupName)) return; + + for (i = 0; i < players.GetLength(); i += 1) + { + nextPlayer = EPlayer(players.GetItem(i)); + if (nextPlayer == none) { + continue; + } + playerName = nextPlayer.GetName(); + nextID = nextPlayer.GetUserID(); + if (TryAddingUserID(groupName, nextID, playerName)) + { + if (annotation == none) { + nextAnnotation = playerName; + } + else { + nextAnnotation = annotation; + } + _.users.SetAnnotationForUserID(groupName, nextID, nextAnnotation); + DisplayAnnotation(playerName, groupName, nextAnnotation); + _.memory.Free(nextID); + nextAnnotation = none; + } + _.memory.Free(nextPlayer); + _.memory.Free(playerName); + _.memory.Free(nextID); + nextPlayer = none; + playerName = none; + nextID = none; + } +} + +private function TryRemovingUserID( + BaseText groupName, + UserID idToRemove, + BaseText userSpecifiedName) +{ + local Text idAsText; + + idAsText = idToRemove.GetUniqueID(); + if (_.users.RemoveUserIDFromGroup(idToRemove, groupName)) + { + callerConsole + .Write(F("{$TextNegative Removed} user ")) + .UseColorOnce(_.color.Gray) + .Write(userSpecifiedName) + .Write(P(" (with id ")) + .UseColorOnce(_.color.Gray) + .Write(idAsText) + .Write(P(") from the group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .WriteLine(P("!")); + } + else + { + callerConsole + .UseColorOnce(_.color.TextFailure) + .Write(P("Failed (for unknown reason)")) + .Write(P("to remove user with id ")) + .UseColorOnce(_.color.Gray) + .Write(idAsText) + .Write(P(" from the group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .WriteLine(P(".")); + } + _.memory.Free(idAsText); +} + +private function bool RemoveUsersByAnnotation( + BaseText groupName, + BaseText userName) +{ + local int i; + local bool removedUser; + local array annotatedUsers; + + annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName); + for (i = 0; i < annotatedUsers.length; i += 1) + { + if (userName.Compare(annotatedUsers[i].annotation, SCASE_INSENSITIVE)) + { + TryRemovingUserID(groupName, annotatedUsers[i].id, userName); + removedUser = true; + } + } + for (i = 0; i < annotatedUsers.length; i += 1) + { + _.memory.Free(annotatedUsers[i].id); + _.memory.Free(annotatedUsers[i].annotation); + } + return removedUser; +} + +private function RemoveUser(BaseText groupName, BaseText userName) +{ + local bool matchedUserName; + local UserID idFromName; + + if (groupName == none) return; + if (userName == none) return; + if (!ValidateGroupExistence(groupName)) return; + + idFromName = UserID(_.memory.Allocate(class'UserID')); + idFromName.Initialize(userName); + if ( idFromName.IsInitialized() + && _.users.IsUserIDInGroup(idFromName, groupName)) + { + TryRemovingUserID(groupName, idFromName, userName); + matchedUserName = true; + } + else { + matchedUserName = RemoveUsersByAnnotation(groupName, userName); + } + _.memory.Free(idFromName); + if (!matchedUserName) + { + callerConsole + .Write(P("User ")) + .UseColorOnce(_.color.Gray) + .Write(userName) + .UseColorOnce(_.color.TextFailure) + .Write(P(" doesn't belong to the group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .WriteLine(P("!")); + } +} + +private function RemovePlayers(BaseText groupName, ArrayList players) +{ + local int i; + local Text playerName; + local EPlayer nextPlayer; + local UserID nextID; + + if (groupName == none) return; + if (players == none) return; + if (!ValidateGroupExistence(groupName)) return; + + for (i = 0; i < players.GetLength(); i += 1) + { + nextPlayer = EPlayer(players.GetItem(i)); + if (nextPlayer == none) { + continue; + } + playerName = nextPlayer.GetName(); + nextID = nextPlayer.GetUserID(); + if (!_.users.IsUserIDInGroup(nextID, groupName)) + { + callerConsole + .Write(P("Player ")) + .UseColorOnce(_.color.Gray) + .Write(playerName) + .Write(F(" {$TextFailure doesn't belong} to the group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .WriteLine(P("!")); + } + else { + TryRemovingUserID(groupName, nextID, playerName); + } + _.memory.Free(nextPlayer); + _.memory.Free(playerName); + _.memory.Free(nextID); + nextPlayer = none; + playerName = none; + nextID = none; + } +} + +private function AddGroup(BaseText groupName) +{ + if (_.users.IsGroupExisting(groupName)) + { + callerConsole + .Write(P("Group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .UseColorOnce(_.color.TextNegative) + .Write(P(" already exists")) + .WriteLine(P("!")); + return; + } + if (_.users.AddGroup(groupName)) + { + callerConsole + .Write(P("Group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .UseColorOnce(_.color.TextPositive) + .Write(P(" was added")) + .WriteLine(P("!")); + } + else + { + callerConsole + .UseColorOnce(_.color.TextFailure) + .Write(P("Cannot add")) + .Write(P(" group with a name ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .WriteLine(P(" for unknown reason.")); + } +} + +private function RemoveGroup(BaseText groupName) +{ + if (!_.users.IsGroupExisting(groupName)) + { + callerConsole + .Write(P("Group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .UseColorOnce(_.color.TextNegative) + .Write(P(" doesn't exists")) + .WriteLine(P("!")); + return; + } + if (_.users.RemoveGroup(groupName)) + { + callerConsole + .Write(P("Group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .UseColorOnce(_.color.TextPositive) + .Write(P(" was removed")) + .WriteLine(P("!")); + } + else + { + callerConsole + .UseColorOnce(_.color.TextFailure) + .Write(P("Cannot remove")) + .Write(P(" group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(groupName) + .WriteLine(P(" for unknown reason.")); + } +} + +private function DisplayUserGroups() +{ + local int i; + local array availableGroups; + + if (!ValidateUsersFeature()) { + return; + } + availableGroups = _.users.GetAvailableGroups(); + if (availableGroups.length <= 0) + { + callerConsole.WriteLine(F("{$TextNegative No user groups}" + @ "currently available.")); + return; + } + callerConsole + .UseColorOnce(_.color.TextEmphasis) + .Write(P("Available user groups")) + .Write(P(": ")); + for (i = 0; i < availableGroups.length; i += 1) + { + if (i > 0) { + callerConsole.Write(P(", ")); + } + callerConsole.Write(availableGroups[i]); + } + callerConsole.Flush(); + _.memory.FreeMany(availableGroups); +} + +private function bool ValidateUsersFeature() +{ + if (class'Users_Feature'.static.IsEnabled()) { + return true; + } + callerConsole + .UseColorOnce(_.color.TextFailure) + .WriteLine(P("`Users_Feature` is currently disabled.")); + return false; +} + +private function bool IsGroupSpecified( + ArrayList specifiedGroups, + BaseText groupToCheck) +{ + local int i; + local int length; + local Text nextGroup; + + if (groupToCheck == none) return false; + if (specifiedGroups == none) return true; + length = groupToCheck.GetLength(); + if (length <= 0) return true; + + for (i = 0; i < length; i += 1) + { + nextGroup = specifiedGroups.GetText(i); + if (groupToCheck.Compare(nextGroup, SCASE_INSENSITIVE)) + { + nextGroup.FreeSelf(); + return true; + } + _.memory.Free(nextGroup); + } + return false; +} + +private function DisplayUserGroupsWithUsers(ArrayList specifiedGroups) +{ + local int i; + local bool displayedGroup; + local array availableGroups; + + if (!ValidateUsersFeature()) { + return; + } + availableGroups = _.users.GetAvailableGroups(); + if (availableGroups.length <= 0) + { + callerConsole.WriteLine(F("{$TextNegative No user groups}" + @ "currently available.")); + return; + } + for (i = 0; i < availableGroups.length; i += 1) + { + if (IsGroupSpecified(specifiedGroups, availableGroups[i])) + { + displayedGroup = true; + callerConsole + .Write(P("User group ")) + .UseColorOnce(_.color.TextEmphasis) + .Write(availableGroups[i]) + .WriteLine(P(":")); + DisplayUsersFor(availableGroups[i]); + } + } + callerConsole.Flush(); + _.memory.FreeMany(availableGroups); + if (!displayedGroup && specifiedGroups != none) { + callerConsole.WriteLine(F("{$TextFailure No valid groups} specified!")); + } +} + +private function DisplayUsersFor(Text groupName) +{ + local int i; + local Text nextID; + local array annotatedUsers; + + annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName); + if (annotatedUsers.length <= 0) + { + callerConsole.WriteBlock(P("No users")); + return; + } + for (i = 0; i < annotatedUsers.length; i += 1) + { + if (annotatedUsers[i].id == none) { + continue; + } + nextID = annotatedUsers[i].id.GetUniqueID(); + if (annotatedUsers[i].annotation != none) + { + callerConsole + .Write(nextID) + .UseColorOnce(_.color.TextNeutral) + .Write(P(" aka ")) + .WriteBlock(annotatedUsers[i].annotation); + } + else { + callerConsole.WriteBlock(nextID); + } + _.memory.Free(nextID); + } + for (i = 0; i < annotatedUsers.length; i += 1) + { + _.memory.Free(annotatedUsers[i].id); + _.memory.Free(annotatedUsers[i].annotation); + } +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Signal.uc b/sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Signal.uc new file mode 100644 index 0000000..8b98050 --- /dev/null +++ b/sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Signal.uc @@ -0,0 +1,40 @@ +/** + * Signal class for `PersistentDataManager`'s `OnPersistentDataReady()` signal. + * Copyright 2023 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 PersistentDataManager_OnPersistentDataReady_Signal extends Signal + dependson(DBConnection); + +public final function Emit(UserID id, bool online) +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + PersistentDataManager_OnPersistentDataReady_Slot(nextSlot) + .connect(id, online); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties +{ + relatedSlotClass = class'PersistentDataManager_OnPersistentDataReady_Slot' +} \ No newline at end of file diff --git a/sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Slot.uc b/sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Slot.uc new file mode 100644 index 0000000..2e9ddc1 --- /dev/null +++ b/sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Slot.uc @@ -0,0 +1,41 @@ +/** + * Slot class for `PersistentDataManager`'s `OnPersistentDataReady()` signal. + * Copyright 2023 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 PersistentDataManager_OnPersistentDataReady_Slot extends Slot + dependson(DBConnection); + +delegate connect(UserID id, bool online) +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Users/PersistentData/PersistentDataManager.uc b/sources/Users/PersistentData/PersistentDataManager.uc new file mode 100644 index 0000000..c6ec518 --- /dev/null +++ b/sources/Users/PersistentData/PersistentDataManager.uc @@ -0,0 +1,407 @@ +/** + * This tool is for simplifying writing and reading persistent user data. + * All it requires is a setup of database + json pointer to data and it will + * take care of data caching and database connection. + * Copyright 2023 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 PersistentDataManager extends AcediaObject; + +/** + * # `PersistentDataManager` + * + * This tool is for simplifying writing and reading persistent user data. + * All it requires is a setup of database + json pointer to data and it will + * take care of data caching and database connection. + * + * ## Usage + * + * Create an instance and use `Setup()` to connect to the database with + * persistent data. You can use `Setup()` again on the same object to setup + * a different database as a source. All data will be automatically reloaded. + * After that you can use `GetPersistentData()`/`SetPersistentData()` to + * read/write persistent data for the particular user. + * Since loading data from the database takes time, you don't have an + * immediate access to it. + * But you can use `_.users.OnPersistentDataAvailable()` signal to track + * whenever new user data from database becomes available. However, you can + * start writing persistent data (and reading what you've wrote) at any time it + * - these changes will be reapplied whenever data is actually loaded from + * database. + * + * ## Implementation + * + * Implementation consists of simply creating `DBConnection` for every user + * and storing them in the `HashTable` that maps user IDs into those + * `DBConnection`s. + * We also maintain a reverse map to figure out what `DBConnection` belongs + * to what user when connection signals an update. We borrow the signal that + * `UsersAPI` provides to inform everyone interested about which users + * have updated. + */ + +var private bool initialized; +var private Database database; +var private JSONPointer rootPointer; +var private HashTable userToConnection, connectionToUser; + +var private PersistentDataManager_OnPersistentDataReady_Signal onPersistentDataReadySignal; + +protected function Constructor() +{ + _.players.OnNewPlayer(self).connect = ConnectPersistentData; + onPersistentDataReadySignal = _.users._getOnReadySignal(); +} + +protected function Finalizer() +{ + Reset(); + _.players.OnNewPlayer(self).Disconnect(); +} + +private final function Reset() +{ + _.memory.Free(database); + _.memory.Free(rootPointer); + _.memory.Free(userToConnection); + _.memory.Free(connectionToUser); + _.memory.Free(onPersistentDataReadySignal); + database = none; + rootPointer = none; + userToConnection = none; + connectionToUser = none; + onPersistentDataReadySignal = none; + initialized = false; +} + +/** + * Sets up database and location inside it as a source of users' persistent + * data. + * + * Must be successfully called at least once for the caller + * `PersistentDataManager` to be usable. + * + * @param db Database inside which persistent data is stored. + * @param location Location inside specified database to the root of + * persistent data. + * @return `true` if setup was successful (requires both arguments to be not + * `none`) and `false` otherwise. + */ +public final function bool Setup(Database db, JSONPointer location) +{ + if (db == none) return false; + if (location == none) return false; + + Reset(); + database = db; + database.NewRef(); + rootPointer = location.Copy(); + userToConnection = _.collections.EmptyHashTable(); + connectionToUser = _.collections.EmptyHashTable(); + // Using `userToConnection` as an empty hash table, not related to its + // actual meaning + database.IncrementData(location, userToConnection); + initialized = true; + return true; +} + +/** + * Reads specified named persistent data for the specified group. + * + * @param id ID of the user to read persistent data from. + * @param groupName Group to which this persistent data belongs to. + * Groups are used as namespaces to avoid duplicate persistent variables + * between mods. If your mod needs several subgroups, its recommended to + * use the same prefix for them, e.g. "MyAwesomeMod.economy" and + * "MyAwesomeMod.enemies". + * @param dataName Name of persistent data variable to read inside + * `groupName` persistent data group. Not `none` value must be provided. + * @param data Data to set as persistent value. Must be + * JSON-compatible. If `none` is passed, returns the all data for + * the given group. + * @return Data read from the persistent variable. `none` in case of any kind + * of failure. + */ +public final function AcediaObject GetPersistentData( + UserID id, + BaseText groupName, + optional BaseText dataName) +{ + local AcediaObject result; + local Text textID; + local JSONPointer location; + local DBConnection relevantConnection; + + if (!initialized) return none; + if (id == none) return none; + if (groupName == none) return none; + + textID = id.GetUniqueID(); + relevantConnection = DBConnection(userToConnection.GetItem(textID)); + textID.FreeSelf(); + if (relevantConnection != none) + { + location = _.json.Pointer(); + location.Push(groupName); + if (dataName != none) { + location.Push(dataName); + } + result = relevantConnection.ReadDataByJSON(location); + relevantConnection.FreeSelf(); + location.FreeSelf(); + } + return result; +} + +/** + * Writes specified named persistent data for the specified group. + * + * @param id ID of the user to change persistent data of. + * @param groupName Group to which this persistent data belongs to. + * Groups are used as namespaces to avoid duplicate persistent variables + * between mods. If your mod needs several subgroups, its recommended to + * use the same prefix for them, e.g. "MyAwesomeMod.economy" and + * "MyAwesomeMod.enemies". + * @param dataName Name of persistent data variable to change inside + * `groupName` persistent data group. + * @param data Data to set as persistent value. Must be + * JSON-compatible. + * @return `true` if change succeeded in local cached version of database with + * persistent values and `false` otherwise. Such local changes can + * potentially be not applied to the actual database. But successful local + * changes should persist for the game session. + */ +public final function bool WritePersistentData( + UserID id, + BaseText groupName, + BaseText dataName, + AcediaObject data) +{ + local bool result; + local Text textID; + local JSONPointer location; + local DBConnection relevantConnection; + local HashTable emptyObject; + + if (!initialized) return false; + if (id == none) return false; + if (groupName == none) return false; + if (dataName == none) return false; + + textID = id.GetUniqueID(); + relevantConnection = DBConnection(userToConnection.GetItem(textID)); + textID.FreeSelf(); + if (relevantConnection != none) + { + emptyObject = _.collections.EmptyHashTable(); + location = _.json.Pointer(); + location.Push(groupName); + relevantConnection.IncrementDataByJSON(location, emptyObject); + location.Push(dataName); + result = relevantConnection.WriteDataByJSON(location, data); + relevantConnection.FreeSelf(); + location.FreeSelf(); + emptyObject.FreeSelf(); + } + return result; +} + +/** + * Increments specified named persistent data for the specified group. + * + * @param id ID of the user to change persistent data of. + * @param groupName Group to which this persistent data belongs to. + * Groups are used as namespaces to avoid duplicate persistent variables + * between mods. If your mod needs several subgroups, its recommended to + * use the same prefix for them, e.g. "MyAwesomeMod.economy" and + * "MyAwesomeMod.enemies". + * @param dataName Name of persistent data variable to change inside + * `groupName` persistent data group. + * @param data Data by which to increment existing persistent value. + * Must be JSON-compatible. + * @return `true` if change succeeded in local cached version of database with + * persistent values and `false` otherwise. Such local changes can + * potentially be not applied to the actual database. But successful local + * changes should persist for the game session. + */ +public final function bool IncrementPersistentData( + UserID id, + BaseText groupName, + BaseText dataName, + AcediaObject data) +{ + local bool result; + local Text textID; + local JSONPointer location; + local DBConnection relevantConnection; + + if (!initialized) return false; + if (id == none) return false; + if (groupName == none) return false; + if (dataName == none) return false; + + textID = id.GetUniqueID(); + relevantConnection = DBConnection(userToConnection.GetItem(textID)); + textID.FreeSelf(); + if (relevantConnection != none) + { + location = _.json.Pointer(); + location.Push(groupName).Push(dataName); + result = relevantConnection.IncrementDataByJSON(location, data); + relevantConnection.FreeSelf(); + location.FreeSelf(); + } + return result; +} + +/** + * Removes specified named persistent data for the specified group. + * + * @param id ID of the user to remove persistent data of. + * @param groupName Group to which this persistent data belongs to. + * Groups are used as namespaces to avoid duplicate persistent variables + * between mods. If your mod needs several subgroups, its recommended to + * use the same prefix for them, e.g. "MyAwesomeMod.economy" and + * "MyAwesomeMod.enemies". + * @param dataName Name of persistent data variable to remove inside + * `groupName` persistent data group. + * @return `true` if removal succeeded in local cached version of database with + * persistent values and `false` otherwise. Such local changes can + * potentially be not applied to the actual database. But successful local + * changes should persist for the game session. + */ +public final function bool RemovePersistentData( + UserID id, + BaseText groupName, + BaseText dataName) +{ + local bool result; + local Text textID; + local JSONPointer location; + local DBConnection relevantConnection; + + if (!initialized) return false; + if (id == none) return false; + if (groupName == none) return false; + if (dataName == none) return false; + + textID = id.GetUniqueID(); + relevantConnection = DBConnection(userToConnection.GetItem(textID)); + textID.FreeSelf(); + if (relevantConnection != none) + { + location = _.json.Pointer(); + location.Push(groupName).Push(dataName); + result = relevantConnection.RemoveDataByJSON(location); + relevantConnection.FreeSelf(); + location.FreeSelf(); + } + return result; +} + +/** + * Connects and starts synchronizing persistent data for the given player. + * + * @param player Player to synchronize persistent data for. + */ +public final function ConnectPersistentData(EPlayer player) +{ + local UserID playerID; + + if (initialized && player != none) + { + playerID = player.GetUserID(); + ConnectPersistentDataByID(playerID); + _.memory.Free(playerID); + } +} + +/** + * Connects and starts synchronizing persistent data for the player given by + * their ID. + * + * @param id User ID for which to synchronize persistent data from + * the database. + */ +public final function ConnectPersistentDataByID(UserID id) +{ + local Text textID; + local DBConnection newConnection; + + if (!initialized) return; + if (id == none) return; + + textID = id.GetUniqueID(); + if (userToConnection.HasKey(textID)) + { + _.memory.Free(textID); + return; + } + rootPointer.Push(textID); + newConnection = DBConnection(_.memory.Allocate(class'DBConnection')); + newConnection.Initialize(database, rootPointer); + _.memory.Free(rootPointer.Pop()); + newConnection.Connect(); + userToConnection.SetItem(textID, newConnection); + connectionToUser.SetItem(newConnection, textID); + newConnection.OnStateChanged(self).connect = UserUpdated; + textID.FreeSelf(); + newConnection.FreeSelf(); +} + +private final function UserUpdated( + DBConnection instance, + DBConnection.DBConnectionState oldState, + DBConnection.DBConnectionState newState) +{ + local UserID id; + + if (!initialized) return; + if (newState == DBCS_Connecting) return; + if (onPersistentDataReadySignal == none) return; + if (!onPersistentDataReadySignal.IsAllocated()) return; + + id = UserID(connectionToUser.GetItem(instance)); + if (id != none) + { + onPersistentDataReadySignal.Emit(id, newState == DBCS_Connected); + id.FreeSelf(); + } +} + +/** + * Attempts to start persistent data synchronization for all players currently + * on the server. + */ +public final function LoadCurrentPlayers() +{ + local int i; + local array currentPlayers; + + if (initialized) + { + currentPlayers = _.players.GetAll(); + for (i = 0; i < currentPlayers.length; i += 1) { + ConnectPersistentData(currentPlayers[i]); + } + _.memory.FreeMany(currentPlayers); + } +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Users/Tests/TEST_User.uc b/sources/Users/Tests/TEST_User.uc index 6467052..86cd7fb 100644 --- a/sources/Users/Tests/TEST_User.uc +++ b/sources/Users/Tests/TEST_User.uc @@ -55,9 +55,9 @@ protected static function Test_UserID() testID3 = UserID(__().memory.Allocate(class'UserID')); testID2.Initialize(P("76561198025127722")); testID3.Initialize(P("76561198044316328")); - TEST_ExpectTrue(testID.IsEqualTo(testID2)); + TEST_ExpectTrue(testID.IsEqual(testID2)); TEST_ExpectTrue(testID.IsEqualToSteamID(testID2.GetSteamID())); - TEST_ExpectFalse(testID3.IsEqualTo(testID)); + TEST_ExpectFalse(testID3.IsEqual(testID)); Issue("Steam data returned by `UserID` is incorrect."); SteamID = testID3.GetSteamID(); diff --git a/sources/Users/User.uc b/sources/Users/User.uc index db9ba36..246a825 100644 --- a/sources/Users/User.uc +++ b/sources/Users/User.uc @@ -2,7 +2,7 @@ * 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 - 2021 Anton Tarasenko + * Copyright 2020-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -27,26 +27,57 @@ var private UserID id; // an easy reference in console commands var private int key; -// 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; +var private HashTable sessionData; -var private LoggerAPI.Definition errNoUserDataDatabase; +var private int persistentDataLifeVersion; +var private PersistentDataManager persistentData; + +protected function Finalizer() +{ + if (id != none) { + id.FreeSelf(); + } + id = none; +} + +private final function UpdatePersistentDataManager() +{ + local Users_Feature feature; + + if ( persistentData != none + && persistentData.GetLifeVersion() != persistentDataLifeVersion) + { + persistentData = none; + } + if (persistentData == none) + { + feature = + Users_Feature(class'Users_Feature'.static.GetEnabledInstance()); + if (feature != none) { + persistentData = feature.BorrowPersistentDataManager(); + } + if (persistentData != none) { + persistentDataLifeVersion = persistentData.GetLifeVersion(); + } + _.memory.Free(feature); + } +} -// TODO: redo this comment /** - * Initializes caller `User` with id and it's session key. Should be called - * right after `EPlayer` was created. + * 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) { id = initID; key = initKey; + if (initID != none) { + initID.NewRef(); + } } /** @@ -56,6 +87,9 @@ public final function Initialize(UserID initID, int initKey) */ public final function UserID GetID() { + if (id != none) { + id.NewRef(); + } return id; } @@ -70,152 +104,59 @@ public final function int GetKey() } /** - * 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`. + * Returns persistent data for the caller user. Data is specified by the its + * name along with the name of the data group it is stored in. * - * @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`. + * @param groupName Name of the group to get data from. Cannot be `none`. + * @param dataName Name of the data to return. If `none` value is provided, + * all the data in specified group will be returned. + * @return Requested data, `none` in case of failure (i.e. data is missing). */ -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( +public final function AcediaObject GetPersistentData( BaseText groupName, BaseText dataName) { - local DBReadTask task; - if (groupName == none) return none; - if (dataName == none) return none; - if (!SetupDatabaseVariables()) return none; + local AcediaObject result; + local UserID myID; - persistentSettingsPointer.Push(groupName).Push(dataName); - task = persistentDatabase.ReadData(persistentSettingsPointer, true); - _.memory.Free(persistentSettingsPointer.Pop()); - _.memory.Free(persistentSettingsPointer.Pop()); - return task; + UpdatePersistentDataManager(); + if (persistentData == none) { + return none; + } + myID = GetID(); + result = persistentData.GetPersistentData(myID, groupname, dataName); + _.memory.Free(myID); + return result; } /** - * Writes user's persistent data under name `dataName`. - * Only should be used if `_.users.PersistentStorageExists()` returns `true`. + * Changes persistent data for the caller user. Data to change is specified by + * the its name along with the name of the data group it is stored in. * - * @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`. + * @param groupName Name of the group to get data from. Cannot be `none`. + * @param dataName Name of the data to return. Cannot be `none`. + * @param data New data to record. + * @return `true` in case operation was successful and `false` otherwise. */ -public final function DBWriteTask WritePersistentData( +public final function bool SetPersistentData( 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; -} + local bool result; + local UserID myID; -// 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; - local HashTable emptyObject, skeletonObject; - - if ( persistentDatabase != none && persistentSettingsPointer != none - && persistentDatabase.IsAllocated()) - { - return true; - } - if (id == none || !id.IsInitialized()) { - return false; - } - _.memory.Free(persistentSettingsPointer); - userDataLink = _.users.GetUserDataLink(); - persistentDatabase = _.db.Load(userDataLink); - if (persistentDatabase == none) - { - _.logger.Auto(errNoUserDataDatabase).Arg(userDataLink); + UpdatePersistentDataManager(); + if (persistentData == none) { return false; } - persistentSettingsPointer = _.db.GetPointer(userDataLink); - userTextID = id.GetSteamID64String(); - skeletonObject = _.collections.EmptyHashTable(); - skeletonObject.SetItem(P("statistics"), _.collections.EmptyHashTable()); - skeletonObject.SetItem(P("settings"), _.collections.EmptyHashTable()); - emptyObject = _.collections.EmptyHashTable(); - persistentDatabase.IncrementData(persistentSettingsPointer, emptyObject); - persistentSettingsPointer.Push(userTextID); - persistentDatabase.IncrementData(persistentSettingsPointer, skeletonObject); - persistentSettingsPointer.Push(P("settings")); - _.memory.Free(userTextID); - _.memory.Free(userDataLink); - _.memory.Free(skeletonObject); - _.memory.Free(emptyObject); - return true; + myID = GetID(); + result = persistentData + .WritePersistentData(myID, groupname, dataName, data); + _.memory.Free(myID); + return result; } defaultproperties { - errNoUserDataDatabase = (l=LOG_Error,m="Failed to load persistent user database instance given by link \"%1\".") } \ No newline at end of file diff --git a/sources/Users/UserAPI.uc b/sources/Users/UserAPI.uc index d6d7ec6..d879f4c 100644 --- a/sources/Users/UserAPI.uc +++ b/sources/Users/UserAPI.uc @@ -1,6 +1,6 @@ /** * API that allows easy access to `User` persistent data and `UserID`s. - * Copyright 2020 - 2021 Anton Tarasenko + * Copyright 2020-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -18,20 +18,90 @@ * along with Acedia. If not, see . */ class UserAPI extends AcediaObject + dependson(Users_Feature) config(AcediaSystem); -var private config string userDataDBLink; +// Active `Users_Feature`, remember it along with life version to avoid +// taking up a reference +var private int usersFeatureLifeVersion; +var private Users_Feature usersFeature; + +var private int persistentDataLifeVersion; +var private PersistentDataManager persistentData; + +var private PersistentDataManager_OnPersistentDataReady_Signal onPersistentDataReadySignal; /** - * Returns reference to the database of user records that Acedia was - * set up to use. + * Signal that will be emitted whenever we get an update on connection status + * to the database, where persistent data for UserID is stored. This can + * be updated several times in cases like `Users_Feature` being rebooted or + * losing connection to the database. + * + * [Signature] + * void (UserID id, bool online) * - * @return Main `UserDatabase` that Acedia currently uses to load and - * store user information. Guaranteed to be a valid non-`none` reference. + * @param id ID of the user, for whom status of persistent data got + * updated. + * @param online Is connection to the database online? If this flag is set to + * `false` - an local, session-only storage will be used instead. */ -public final function UserDatabase GetDatabase() +/* SIGNAL */ +public final function PersistentDataManager_OnPersistentDataReady_Slot OnEditResult( + AcediaObject receiver) +{ + return PersistentDataManager_OnPersistentDataReady_Slot( + onPersistentDataReadySignal.NewSlot(receiver)); +} + +// DO NOT CALL MANUALLY +public function PersistentDataManager_OnPersistentDataReady_Signal _getOnReadySignal() +{ + return onPersistentDataReadySignal; +} + +// DO NOT CALL MANUALLY +public function _reloadFeature() +{ + if ( usersFeature != none + && usersFeature.GetLifeVersion() == usersFeatureLifeVersion) + { + usersFeature.FreeSelf(); + usersFeature = none; + } + usersFeature = + Users_Feature(class'Users_Feature'.static.GetEnabledInstance()); + if (usersFeature != none) { + usersFeatureLifeVersion = usersFeature.GetLifeVersion(); + } + _.memory.Free(usersFeature); +} + +protected function Constructor() { - return class'UserDatabase'.static.GetInstance(); + onPersistentDataReadySignal = + PersistentDataManager_OnPersistentDataReady_Signal( + _.memory.Allocate( + class'PersistentDataManager_OnPersistentDataReady_Signal') + ); +} + +/** + * Checks whether database setup to store users' persistent data was configured + * and actually exists. + * + * This does not check for whether that database is properly configured. + * If sub-object set to store users' data was not created inside it, then + * Acedia will not be able to make use of users' persistent storage. + * + * @return `true` if database for users' persistent data storage exists and + * `false` otherwise. + */ +public final function bool IsPersistentStorageActive() +{ + if (usersFeature != none) { + return (usersFeature.BorrowPersistentDataManager() != none); + } + return false; } /** @@ -44,7 +114,16 @@ public final function UserDatabase GetDatabase() */ public final function User Fetch(UserID userID) { - return class'UserDatabase'.static.GetInstance().FetchUser(userID); + local User result; + local UserDatabase userDB; + + userDB = class'UserDatabase'.static.GetInstance(); + if (userDB == none) { + return none; + } + result = userDB.FetchUser(userID); + userDB.FreeSelf(); + return result; } /** @@ -58,9 +137,20 @@ public final function User FetchByIDHash(BaseText idHash) { local UserID userID; local UserDatabase userDB; + local User result; + userDB = class'UserDatabase'.static.GetInstance(); + if (userDB == none) { + return none; + } userID = userDB.FetchUserID(idHash); - return userDB.FetchUser(userID); + userDB.FreeSelf(); + if (userID == none) { + return none; + } + result = userDB.FetchUser(userID); + userID.FreeSelf(); + return result; } /** @@ -73,38 +163,1291 @@ public final function User FetchByIDHash(BaseText idHash) */ public final function User FetchByKey(int userKey) { - return class'UserDatabase'.static.GetInstance().FetchUserByKey(userKey); + local User result; + local UserDatabase userDB; + + userDB = class'UserDatabase'.static.GetInstance(); + if (userDB != none) { + return none; + } + result = userDB.FetchUserByKey(userKey); + userDB.FreeSelf(); + return result; } /** - * Returns configured database link to the JSON object in which users' data - * is stored. + * Returns names of all available groups that users can belong to. * - * @return Database link to the JSON object in which users' data is stored. - * Guaranteed to not be `none`. + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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 Text GetUserDataLink() +public final function array GetAvailableGroups() { - return P(userDataDBLink).Copy(); + local array emptyResult; + + if (usersFeature != none) { + return usersFeature.GetAvailableGroups(); + } + return emptyResult; } /** - * Checks whether database setup to store users' persistent data was configured - * and actually exists. + * Returns names of all available groups that users can belong to. * - * This does not check for whether that database is properly configured. - * If sub-object set to store users' data was not created inside it, then - * Acedia will not be able to make use of users' persistent storage. + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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 `true` if database for users' persistent data storage exists and - * `false` otherwise. + * @return Array with names of all available groups. All array elements are + * guaranteed to be unique and in lower case. + */ +public final /*unreal*/ function array GetAvailableGroups_S() +{ + local array emptyResult; + + if (usersFeature != none) { + return usersFeature.GetAvailableGroups_S(); + } + return emptyResult; +} + +/** + * Adds a new user group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.AddGroup(groupName); + } + return false; +} + +/** + * Adds a new user group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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 /*unreal*/ function bool AddGroup_S(string groupName) +{ + if (usersFeature != none) { + return usersFeature.AddGroup_S(groupName); + } + return false; +} + +/** + * Removes existing user group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.RemoveGroup(groupName); + } + return false; +} + +/** + * Removes existing user group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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 /*unreal*/ function bool RemoveGroup_S(string groupName) +{ + if (usersFeature != none) { + return usersFeature.RemoveGroup_S(groupName); + } + return false; +} + +/** + * Checks whether group with specified name exists. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.IsGroupExisting(groupName); + } + return false; +} + +/** + * Checks whether group with specified name exists. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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 /*unreal*/ function bool IsGroupExisting_S(string groupName) +{ + if (usersFeature != none) { + return usersFeature.IsGroupExisting_S(groupName); + } + return false; +} + +/** + * Adds user with the given SteamID into the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.AddSteamIDToGroup(steamID, groupName); + } + return false; +} + +/** + * Adds user with the given SteamID into the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.AddSteamIDToGroup_S(steamID, groupName); + } + return false; +} + +/** + * Adds user (given by the `UserID`) into the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.AddUserIDToGroup(id, groupName); + } + return false; +} + +/** + * Adds user (given by the `UserID`) into the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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 /*unreal*/ function bool AddUserIDToGroup_S( + UserID id, + string groupName) +{ + if (usersFeature != none) { + return usersFeature.AddUserIDToGroup_S(id, groupName); + } + return false; +} + +/** + * Adds given user into the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.AddUserToGroup(user, groupName); + } + return false; +} + +/** + * Adds given user into the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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 /*unreal*/ function bool AddUserToGroup_S( + User user, + string groupName) +{ + if (usersFeature != none) { + return usersFeature.AddUserToGroup_S(user, groupName); + } + return false; +} + +/** + * Removes user with the given SteamID from the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.RemoveSteamIDFromGroup(steamID, groupName); + } + return false; +} + +/** + * Removes user with the given SteamID from the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.RemoveSteamIDFromGroup_S(steamID, groupName); + } + return false; +} + +/** + * Removes user (given by the `UserID`) from the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.RemoveUserIDFromGroup(id, groupName); + } + return false; +} + +/** + * Removes user (given by the `UserID`) from the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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 /*unreal*/ function bool RemoveUserIDFromGroup_S( + UserID id, + string groupName) +{ + if (usersFeature != none) { + return usersFeature.RemoveUserIDFromGroup_S(id, groupName); + } + return false; +} + +/** + * Removes user from the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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) +{ + if (usersFeature != none) { + return usersFeature.RemoveUserFromGroup(user, groupName); + } + return false; +} + +/** + * Removes user from the specified group. + * + * Will only work if `Users_Feature` is active. + * In case active config of `Users_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 /*unreal*/ function bool RemoveUserFromGroup_S( + User user, + string groupName) +{ + if (usersFeature != none) { + return usersFeature.RemoveUserFromGroup_S(user, groupName); + } + return false; +} + +/** + * Returns names of all groups available for the user given by the SteamID. + * + * Will only work if `Users_Feature` is active. + * 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 function array GetGroupsForSteamID( + BaseText steamID) +{ + local array emptyResult; + + if (usersFeature != none) { + return usersFeature.GetGroupsForSteamID(steamID); + } + return emptyResult; +} + +/** + * Returns names of all groups available for the user given by the SteamID. + * + * Will only work if `Users_Feature` is active. + * 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 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 emptyResult; + + if (usersFeature != none) { + return usersFeature.GetGroupsForSteamID_S(steamID); + } + return emptyResult; +} + + +/** + * Returns names of all groups available for the user given by the `UserID`. + * + * Will only work if `Users_Feature` is active. + * 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 array emptyResult; + + if (usersFeature != none) { + return usersFeature.GetGroupsForUserID(id); + } + return emptyResult; +} + +/** + * Returns names of all groups available for the user given by the `UserID`. + * + * Will only work if `Users_Feature` is active. + * 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 unique and in lower case. + * If data wasn't yet loaded - returns empty array. + */ +public final /*unreal*/ function array GetGroupsForUserID_S(UserID id) +{ + local array emptyResult; + + if (usersFeature != none) { + return usersFeature.GetGroupsForUserID_S(id); + } + return emptyResult; +} + +/** + * Returns names of all groups available for the user. + * + * Will only work if `Users_Feature` is active. + * 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 array emptyResult; + + if (usersFeature != none) { + return usersFeature.GetGroupsForUser(user); + } + return emptyResult; +} + +/** + * Returns names of all groups available for the user. + * + * Will only work if `Users_Feature` is active. + * 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 unique and in lower case. + * If data wasn't yet loaded - returns empty array. + */ +public final /*unreal*/ function array GetGroupsForUser_S(User user) +{ + local array emptyResult; + + if (usersFeature != none) { + return usersFeature.GetGroupsForUser_S(user); + } + return emptyResult; +} + +/** + * Returns `UserID`s of all users that belong into the group named `groupName`. + * + * @see For more information alongside `UserID`s use + * `GetAnnotatedGroupMembers()`. + * + * Will only work if `Users_Feature` is active. + * 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(BaseText groupName) +{ + local array emptyResult; + + if (usersFeature != none) { + return usersFeature.GetGroupMembers(groupName); + } + return emptyResult; +} + +/** + * Returns annotated `UserID`s of all users that belong into the group named + * `groupName`. `UserID`s aren't necessarily human-readable (e.g. SteamID) + * and to help organize configs they can be annotated with a `Text` name. + * This method returns `UserID` alongside such annotation, if it exists. + * NOTE: Same user can have different annotations in different groups. + * + * @see For just `UserID`s use `GetGroupMembers()`. + * + * Will only work if `Users_Feature` is active. + * 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. + * WARNING: References in fields of the returned `struct`s must be freed. + */ +public final function array GetAnnotatedGroupMembers( + BaseText groupName) +{ + local array emptyResult; + + if (usersFeature != none) { + return usersFeature.GetAnnotatedGroupMembers(groupName); + } + return emptyResult; +} + +/** + * Returns annotation for user given by SteamID inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * Will only work if `Users_Feature` is active. + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * `none` if either group doesn't exist, user doesn't belong to it or it is + * not annotated. + * If data wasn't yet loaded - returns `none`. + */ +public final function Text GetAnnotationForSteamID( + BaseText groupName, + BaseText steamID) +{ + if (usersFeature != none) { + return usersFeature.GetAnnotationForSteamID(groupName, steamID); + } + return none; +} + +/** + * Returns annotation for user given by SteamID inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * Will only work if `Users_Feature` is active. + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * Empty `string` if either group doesn't exist, user doesn't belong to it + * or it is not annotated. + * If data wasn't yet loaded - returns empty `string`. + */ +public final /*unreal*/ function string GetAnnotationForSteamID_S( + string groupName, + string steamID) +{ + if (usersFeature != none) { + return usersFeature.GetAnnotationForSteamID_S(groupName, steamID); + } + return ""; +} + +/** + * Returns annotation for user given by `UserID` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * Will only work if `Users_Feature` is active. + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * `none` if either group doesn't exist, user doesn't belong to it or it is + * not annotated. + * If data wasn't yet loaded - returns `none`. + */ +public final function Text GetAnnotationForUserID(BaseText groupName, UserID id) +{ + if (usersFeature != none) { + return usersFeature.GetAnnotationForUserID(groupName, id); + } + return none; +} + +/** + * Returns annotation for user given by `UserID` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * Will only work if `Users_Feature` is active. + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * Empty `string` if either group doesn't exist, user doesn't belong to it + * or it is not annotated. + * If data wasn't yet loaded - returns empty `string`. + */ +public final /*unreal*/ function string GetAnnotationForUserID_S( + string groupName, + UserID id) +{ + if (usersFeature != none) { + return usersFeature.GetAnnotationForUserID_S(groupName, id); + } + return ""; +} + +/** + * Returns annotation for user given by `User` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * Will only work if `Users_Feature` is active. + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * `none` if either group doesn't exist, user doesn't belong to it or it is + * not annotated. + * If data wasn't yet loaded - returns `none`. + */ +public final function Text GetAnnotationForUser(BaseText groupName, User user) +{ + if (usersFeature != none) { + return usersFeature.GetAnnotationForUser(groupName, user); + } + return none; +} + +/** + * Returns annotation for user given by `User` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * Will only work if `Users_Feature` is active. + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * Empty `string` if either group doesn't exist, user doesn't belong to it + * or it is not annotated. + * If data wasn't yet loaded - returns empty `string`. + */ +public final /*unreal*/ function string GetAnnotationForUser_S( + string groupName, + User user) +{ + if (usersFeature != none) { + return usersFeature.GetAnnotationForUser_S(groupName, user); + } + return ""; +} + +/** + * Changes annotation for user given by SteamID inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. + */ +public final function SetAnnotationForSteamID( + BaseText groupName, + BaseText steamID, + BaseText annotation) +{ + if (usersFeature != none) { + usersFeature.SetAnnotationForSteamID(groupName, steamID, annotation); + } +} + +/** + * Changes annotation for user given by SteamID inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. Empty annotation + * means simply removing any existing annotation. + */ +public final /*unreal*/ function SetAnnotationForSteamID_S( + string groupName, + string steamID, + string annotation) +{ + if (usersFeature != none) { + usersFeature.SetAnnotationForSteamID_S(groupName, steamID, annotation); + } +} + +/** + * Changes annotation for user given by `UserID` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. + */ +public final function SetAnnotationForUserID( + BaseText groupName, + UserID id, + BaseText annotation) +{ + if (usersFeature != none) { + usersFeature.SetAnnotationForUserID(groupName, id, annotation); + } +} + +/** + * Changes annotation for user given by `UserID` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. Empty annotation + * means simply removing any existing annotation. + */ +public final /*unreal*/ function SetAnnotationForUserID_S( + string groupName, + UserID id, + string annotation) +{ + if (usersFeature != none) { + usersFeature.SetAnnotationForUserID_S(groupName, id, annotation); + } +} + +/** + * Changes annotation for user given by `User` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. + */ +public final function SetAnnotationForUser( + BaseText groupName, + User user, + BaseText annotation) +{ + if (usersFeature != none) { + usersFeature.SetAnnotationForUser(groupName, user, annotation); + } +} + +/** + * Changes annotation for user given by `User` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. Empty annotation + * means simply removing any existing annotation. + */ +public final /*unreal*/ function SetAnnotationForUser_S( + string groupName, + User user, + string annotation) +{ + if (usersFeature != none) { + usersFeature.SetAnnotationForUser_S(groupName, user, annotation); + } +} + +/** + * Checks whether user given by SteamID belongs to the group named `groupName`. + * + * Will only work if `Users_Feature` is active. + * 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 steamID 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 IsSteamIDInGroup( + BaseText steamID, + BaseText groupName) +{ + if (usersFeature != none) { + return usersFeature.IsSteamIDInGroup(steamID, groupName); + } + return false; +} + +/** + * Checks whether user given by SteamID belongs to the group named `groupName`. + * + * Will only work if `Users_Feature` is active. + * 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 steamID 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 /*unreal*/ function bool IsSteamIDInGroup_S( + string id, + string groupName) +{ + if (usersFeature != none) { + return usersFeature.IsSteamIDInGroup_S(id, groupName); + } + return false; +} + +/** + * Checks whether user given by the `UserID` belongs to the group named + * `groupName`. + * + * Will only work if `Users_Feature` is active. + * 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 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, BaseText groupName) +{ + if (usersFeature != none) { + return usersFeature.IsUserIDInGroup(id, groupName); + } + return false; +} + +/** + * Checks whether user given by the `UserID` belongs to the group named + * `groupName`. + * + * Will only work if `Users_Feature` is active. + * 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 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 /*unreal*/ function bool IsUserIDInGroup_S( + UserID id, + string groupName) +{ + if (usersFeature != none) { + return usersFeature.IsUserIDInGroup_S(id, groupName); + } + return false; +} + +/** + * Checks whether user belongs to the given group. + * + * Will only work if `Users_Feature` is active. + * 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, BaseText groupName) +{ + if (usersFeature != none) { + return usersFeature.IsUserInGroup(user, groupName); + } + return false; +} + +/** + * Checks whether user belongs to the given group. + * + * Will only work if `Users_Feature` is active. + * 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 /*unreal*/ function bool IsUserInGroup_S( + User user, + string groupName) +{ + if (usersFeature != none) { + return usersFeature.IsUserInGroup_S(user, groupName); + } + return false; +} + +/** + * Checks whether user groups' data was already loaded from the source + * (either config file or local/remote database). + * + * Will only work if `Users_Feature` is active. + * 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 PersistentStorageExists() +public final function bool IsUserGroupDataLoaded() { - return (_.db.Load(P(userDataDBLink)) != none); + if (usersFeature != none) { + return usersFeature.IsUserGroupDataLoaded(); + } + return false; } defaultproperties { - userDataDBLink = "[local]database:/users" } \ No newline at end of file diff --git a/sources/Users/UserDatabase.uc b/sources/Users/UserDatabase.uc index eee877a..4fc6735 100644 --- a/sources/Users/UserDatabase.uc +++ b/sources/Users/UserDatabase.uc @@ -1,7 +1,7 @@ /** * Simple user database for Acedia. * Only stores data for a session, map or server restarts will clear it. - * Copyright 2020 Anton Tarasenko + * Copyright 2020-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -23,7 +23,7 @@ class UserDatabase extends AcediaObject // This is used as a global variable only (`default.activeDatabase`) to store // a reference to main database for persistent data, used by Acedia. -var public UserDatabase activeDatabase; +var public UserDatabase activeDatabase; // `User` records that were stored this session var private array sessionUsers; // `UserID`s generated during this session. @@ -52,6 +52,7 @@ public final static function UserDatabase GetInstance() default.activeDatabase = UserDatabase(__().memory.Allocate(class'UserDatabase')); } + default.activeDatabase.NewRef(); return default.activeDatabase; } @@ -80,6 +81,7 @@ public final function UserID FetchUserID(BaseText idHash) if (storedUserIDs[i].IsEqualToSteamID(steamID)) { _.memory.Free(steamID.steamID64); + storedUserIDs[i].NewRef(); return storedUserIDs[i]; } } @@ -88,6 +90,7 @@ public final function UserID FetchUserID(BaseText idHash) if (newUserID.IsInitialized()) { storedUserIDs[storedUserIDs.length] = newUserID; + newUserID.NewRef(); return newUserID; } _.memory.Free(steamID.steamID64); @@ -109,13 +112,16 @@ public final function User FetchUser(UserID userID) local User newUser; for (i = 0; i < sessionUsers.length; i += 1) { - if (sessionUsers[i].GetID().IsEqualTo(userID)) { + if (sessionUsers[i].GetID().IsEqual(userID)) + { + sessionUsers[i].NewRef(); return sessionUsers[i]; } } newUser = User(__().memory.Allocate(class'User')); newUser.Initialize(userID, sessionUsers.length + 1); sessionUsers[sessionUsers.length] = newUser; + newUser.NewRef(); return newUser; } @@ -132,7 +138,9 @@ public final function User FetchUserByKey(int userKey) local int i; for (i = 0; i < sessionUsers.length; i += 1) { - if (sessionUsers[i].GetKey() == userKey) { + if (sessionUsers[i].GetKey() == userKey) + { + sessionUsers[i].NewRef(); return sessionUsers[i]; } } diff --git a/sources/Users/UserGroup.uc b/sources/Users/UserGroup.uc new file mode 100644 index 0000000..ff7bc7d --- /dev/null +++ b/sources/Users/UserGroup.uc @@ -0,0 +1,70 @@ +/** + * Acedia's class for defining user group in config files. + * 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 UserGroup extends AcediaConfig + perobjectconfig + config(AcediaUsers); + +var public config array user; + +protected function HashTable ToData() +{ + local int i; + local HashTable data; + local ArrayList wrappedUserArray; + + data = __().collections.EmptyHashTable(); + wrappedUserArray = __().collections.EmptyArrayList(); + for (i = 0; i < user.length; i += 1) { + wrappedUserArray.AddString(user[i]); + } + data.SetItem(P("user"), wrappedUserArray); + wrappedUserArray.FreeSelf(); + return data; +} + +protected function FromData(HashTable source) +{ + local int i; + local ArrayList wrappedUserArray; + + DefaultIt(); + if (source == none) { + return; + } + wrappedUserArray = source.GetArrayList(P("user")); + if (wrappedUserArray == none) { + return; + } + for (i = 0; i < wrappedUserArray.GetLength(); i += 1) { + user[user.length] = wrappedUserArray.GetString(i); + } + wrappedUserArray.FreeSelf(); +} + +protected function DefaultIt() +{ + user.length = 0; +} + +defaultproperties +{ + configName = "AcediaUsers" + supportsDataConversion = true +} \ No newline at end of file diff --git a/sources/Users/UserID.uc b/sources/Users/UserID.uc index 7d7ca42..d0b3322 100644 --- a/sources/Users/UserID.uc +++ b/sources/Users/UserID.uc @@ -1,6 +1,6 @@ /** * Acedia's class for storing user's ID. - * Copyright 2020 - 2021 Anton Tarasenko + * Copyright 2020-2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -45,6 +45,17 @@ var protected SteamID initializedData; // after `initialized` is set to `true`. var protected bool initialized; +protected function Finalizer() +{ + initialized = false; + _.memory.Free(initializedData.steamID64); + initializedData.steamID64 = none; + initializedData.accountType = 0; + initializedData.universe = 0; + initializedData.instance = 0; + initializedData.steamID32 = 0; +} + // Given a number in form of array (`digits`) of it's digits // (425327 <-> [4, 2, 5, 3, 2, 7]) // return given number mod 2 and @@ -82,6 +93,10 @@ private static final function int ReadBitsFromDigitArray( local int i; local int result; local int binaryPadding; + + if (digits.length <= 0) { + return 0; + } result = 0; binaryPadding = 1; for (i = 0; i < bitsToRead; i += 1) { @@ -233,21 +248,23 @@ public final function SteamID GetSteamID() return initializedData; } -/** - * Checks if two `UserID`s are the same. - * - * @param otherID `UserID` to compare caller object to. - * @return `true` if caller `UserID` is identical to `otherID` and - * `false` otherwise. If at least one of the `UserID`s being compared is - * uninitialized, the result will be `false`. - */ -public final function bool IsEqualTo(UserID otherID) +public function bool IsEqual(Object other) { + local UserID otherID; + if (!IsInitialized()) return false; + otherID = UserID(other); + if (otherID == none) return false; if (!otherID.IsInitialized()) return false; + return (initializedData.steamID32 == otherID.initializedData.steamID32); } +protected function int CalculateHashCode() +{ + return initializedData.steamID32; +} + /** * Checks if caller `UserID`s is the same as what's described by * given `SteamID`. diff --git a/sources/Users/Users.uc b/sources/Users/Users.uc new file mode 100644 index 0000000..28282f9 --- /dev/null +++ b/sources/Users/Users.uc @@ -0,0 +1,99 @@ +/** + * Config object for `Users_Feature`. + * 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 extends FeatureConfig + perobjectconfig + config(AcediaUsers); + +var public config bool usePersistentData; +var public config string persistentDataDatabaseLink; +var public config bool useDatabaseForGroupsData; +var public config string groupsDatabaseLink; +var public config array localUserGroup; + +protected function HashTable ToData() +{ + local int i; + local HashTable data; + local ArrayList userGroupList; + + data = __().collections.EmptyHashTable(); + data.SetBool(P("usePersistentData"), usePersistentData); + data.SetString(P("persistentDataDatabaseLink"), persistentDataDatabaseLink); + data.SetBool(P("useDatabaseForGroupsData"), useDatabaseForGroupsData); + data.SetString(P("groupsDatabaseLink"), groupsDatabaseLink); + userGroupList = _.collections.EmptyArrayList(); + for (i = 0; i < localUserGroup.length; i += 1) { + userGroupList.AddString(localUserGroup[i]); + } + data.SetItem(P("userGroups"), userGroupList); + userGroupList.FreeSelf(); + return data; +} + +protected function FromData(HashTable source) +{ + local int i; + local ArrayList userGroupList; + + if (source == none) { + return; + } + usePersistentData = source.GetBool(P("usePersistentData")); + persistentDataDatabaseLink = source.GetString( + P("persistentDataDatabaseLink"), + "[local]database:/persistent_data"); + useDatabaseForGroupsData = source.GetBool(P("useDatabaseForGroupsData")); + groupsDatabaseLink = source.GetString( + P("groupsDatabaseLink"), + "[local]database:/groups_data"); + userGroupList = source.GetArrayList(P("userGroups")); + localUserGroup.length = 0; + if (userGroupList == none) { + return; + } + for (i = 0; i < userGroupList.GetLength(); i += 1) { + localUserGroup[localUserGroup.length] = userGroupList.GetString(i); + } + userGroupList.FreeSelf(); +} + +protected function DefaultIt() +{ + usePersistentData = false; + persistentDataDatabaseLink = "[local]database:/persistent_data"; + useDatabaseForGroupsData = false; + groupsDatabaseLink = "[local]database:/groups_data"; + localUserGroup.length = 0; + localUserGroup[0] = "admin"; + localUserGroup[1] = "moderator"; + localUserGroup[2] = "trusted"; +} + +defaultproperties +{ + configName = "AcediaUsers" + usePersistentData = false + persistentDataDatabaseLink = "[local]database:/persistent_data" + useDatabaseForGroupsData = false + groupsDatabaseLink = "[local]database:/groups_data" + localUserGroup(0) = "admin" + localUserGroup(1) = "moderator" + localUserGroup(2) = "trusted" +} \ No newline at end of file diff --git a/sources/Users/Users_Feature.uc b/sources/Users/Users_Feature.uc new file mode 100644 index 0000000..e785eb5 --- /dev/null +++ b/sources/Users/Users_Feature.uc @@ -0,0 +1,2158 @@ +/** + * 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-2023 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 + dependson(Database); + +/** + * This feature is responsible for managing users: their groups and persistent + * data. Group information can be stored in both configs and databases, while + * persistent data can only be stored in databases. + */ + +var private /*config*/ bool usePersistentData; +var private /*config*/ string persistentDataDatabaseLink; +var private /*config*/ bool useDatabaseForGroupsData; +var private /*config*/ string groupsDatabaseLink; +var private /*config*/ array availableUserGroups; + +var private bool diskSaveScheduled; + +struct AnnotatedUserID +{ + var public UserID id; + var public Text annotation; +}; + +struct IDAnnotationPair +{ + var Text id, annotation; +}; + +var private bool userGroupsDataLoaded; +var private Database usersGroupsDatabase; +var private JSONPointer userGroupsRootPointer; +var private int stackedDBReadingRequests; + +var private PersistentDataManager currentPersistentDataManager; + +// 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 annotation as a value). +var private HashTable loadedGroupToUsersMap; + +var private LoggerAPI.Definition warnNoLocalGroup, warnDuplicateIDs; +var private LoggerAPI.Definition errCannotCreateLocalGroup; +var private LoggerAPI.Definition errCannotOpenDatabase, infoUserGroupDataLoaded; +var private LoggerAPI.Definition errDBBadRootUserGroupData, errDBBadLinkPointer; +var private LoggerAPI.Definition errDBDamaged, errNoServerCore; +var private LoggerAPI.Definition errDBContainsNonLowerRegister; + +protected function OnEnabled() +{ + local Commands_Feature feature; + + _.users._reloadFeature(); + feature = + Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + if (feature != none) + { + feature.RegisterCommand(class'ACommandUserGroups'); + feature.FreeSelf(); + } + LoadUserData(); +} + +protected function OnDisabled() +{ + local Commands_Feature feature; + + _.users._reloadFeature(); + feature = + Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance()); + if (feature != none) + { + feature.RemoveCommand(class'ACommandUserGroups'); + feature.FreeSelf(); + } + ResetUploadedUserGroups(); + _.memory.Free(currentPersistentDataManager); + currentPersistentDataManager = none; +} + +protected function SwapConfig(FeatureConfig config) +{ + local Users newConfig; + + newConfig = Users(config); + if (newConfig == none) { + return; + } + usePersistentData = newConfig.usePersistentData; + persistentDataDatabaseLink = newConfig.persistentDataDatabaseLink; + useDatabaseForGroupsData = newConfig.useDatabaseForGroupsData; + groupsDatabaseLink = newConfig.groupsDatabaseLink; + availableUserGroups = newConfig.localUserGroup; + ResetUploadedUserGroups(); + if (IsEnabled()) + { + if (!_server.IsAvailable()) + { + _.logger.Auto(errNoServerCore); + return; + } + LoadUserData(); + SetupPersistentData(usePersistentData); + } +} + +/** + * Borrows active `PersistentDataManager` (if one is setup for this config). + * + * @return Borrowed reference to active `PersistentDataManager`. Can be `none` + * if persistent data isn't setup for the current config. + */ +public final function PersistentDataManager BorrowPersistentDataManager() +{ + return currentPersistentDataManager; +} + +private final function SetupPersistentData(bool doUsePersistentData) +{ + local Text databaseLinkAsText; + local Database persistentDatabase; + local JSONPointer persistentRootPointer; + + if (!doUsePersistentData) + { + _.memory.Free(currentPersistentDataManager); + currentPersistentDataManager = none; + return; + } + databaseLinkAsText = _.text.FromString(persistentDataDatabaseLink); + persistentDatabase = _server.db.Load(databaseLinkAsText); + if (persistentDatabase != none) { + persistentRootPointer = _server.db.GetPointer(databaseLinkAsText); + } + else { + _.logger.Auto(errCannotOpenDatabase).Arg(databaseLinkAsText); + } + if (persistentRootPointer != none) + { + if (currentPersistentDataManager == none) + { + currentPersistentDataManager = PersistentDataManager( + _.memory.Allocate(class'PersistentDataManager')); + } + currentPersistentDataManager.Setup( + persistentDatabase, + persistentRootPointer); + currentPersistentDataManager.LoadCurrentPlayers(); + } + _.memory.Free(persistentRootPointer); + _.memory.Free(persistentDatabase); + _.memory.Free(databaseLinkAsText); +} + +private final function LoadUserData() +{ + local Text databaseLinkAsText; + local HashTable emptyHashTable; + + if (userGroupsDataLoaded) { + return; + } + if (useDatabaseForGroupsData) + { + databaseLinkAsText = _.text.FromString(groupsDatabaseLink); + usersGroupsDatabase = _server.db.Load(databaseLinkAsText); + if (usersGroupsDatabase == none) { + _.logger.Auto(errCannotOpenDatabase).Arg(databaseLinkAsText); + } + else + { + userGroupsRootPointer = _server.db.GetPointer(databaseLinkAsText); + emptyHashTable = _.collections.EmptyHashTable(); + usersGroupsDatabase.IncrementData( + userGroupsRootPointer, + emptyHashTable); + emptyHashTable.FreeSelf(); + usersGroupsDatabase.ReadData(userGroupsRootPointer).connect = + HandleInitialUserGroupsDataLoading; + stackedDBReadingRequests += 1; + databaseLinkAsText.FreeSelf(); + } + } + else + { + class'UserGroup'.static.Initialize(); + LoadLocalData(); + userGroupsDataLoaded = true; + } +} + +private final function HandleInitialUserGroupsDataLoading( + Database.DBQueryResult result, + AcediaObject data, + Database source, + int requestID) +{ + local Text databaseLinkAsText; + local HashTable newGroupData; + + stackedDBReadingRequests -= 1; + // If this counter remains above zero, that means several requests were + // made and this response is to the outdated one + if (stackedDBReadingRequests > 0) return; + if (!IsEnabled()) return; + + newGroupData = HashTable(data); + databaseLinkAsText = _.text.FromString(groupsDatabaseLink); + if (result == DBR_Success) + { + if (newGroupData == none) + { + _.logger.Auto(errDBBadRootUserGroupData).Arg(databaseLinkAsText); + return; + } + userGroupsDataLoaded = true; + _.memory.Free(loadedGroupToUsersMap); + _.memory.FreeMany(loadedUserGroups); + loadedGroupToUsersMap = FilterDBData(newGroupData); + loadedUserGroups = loadedGroupToUsersMap.GetTextKeys(); + newGroupData.FreeSelf(); + _.logger.Auto(infoUserGroupDataLoaded).Arg(databaseLinkAsText); + } + else if (result == DBR_InvalidPointer) { + _.logger.Auto(errDBBadLinkPointer).Arg(databaseLinkAsText); + } + else + { + // Any other error indicates that database is somehow damaged and + // unusable for our purpose + _.logger.Auto(errDBDamaged).Arg(databaseLinkAsText); + } +} + +private final function HashTable FilterDBData(HashTable received) +{ + local int i; + local array allKeys; + local AcediaObject nextItem; + local HashTable result; + + if (received == none) { + return none; + } + result = _.collections.EmptyHashTable(); + allKeys = received.GetTextKeys(); + for (i = 0; i < allKeys.length; i += 1) + { + if (allKeys[i].IsLowerCase()) + { + nextItem = received.GetItem(allKeys[i]); + result.SetItem(allKeys[i], nextItem); + _.memory.Free(nextItem); + } + else + { + allKeys[i].NewRef(); + _.logger.Auto(errDBContainsNonLowerRegister) + .Arg(_.text.FromString(groupsDatabaseLink)) + .Arg(allKeys[i]); + } + } + _.memory.FreeMany(allKeys); + return result; +} + +private final function ResetUploadedUserGroups() +{ + _.memory.Free(userGroupsRootPointer); + _.memory.Free(usersGroupsDatabase); + _.memory.Free(loadedGroupToUsersMap); + _.memory.FreeMany(loadedUserGroups); + userGroupsRootPointer = none; + usersGroupsDatabase = none; + loadedGroupToUsersMap = none; + loadedUserGroups.length = 0; + userGroupsDataLoaded = false; +} + +private final function LoadLocalData() +{ + 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(); + } + LoadLocalGroupToUserMap(); +} + +private final function LoadLocalGroupToUserMap() +{ + local int i; + + _.memory.Free(loadedGroupToUsersMap); + loadedGroupToUsersMap = _.collections.EmptyHashTable(); + class'UserGroup'.static.Initialize(); + // Go over every group + for (i = 0; i < loadedUserGroups.length; i += 1) { + LoadLocalGroup(loadedUserGroups[i], true); + } +} + +private final function bool LoadLocalGroup( + BaseText groupName, + optional bool localGroupIsExpected) +{ + local int i; + local Text lowerCaseGroupName; + local HashTable newPlayerSet; + local UserGroup groupConfig; + local IDAnnotationPair nextUserPair; + local array groupUserArray; + + if (groupName == none) { + return false; + } + groupConfig = UserGroup( + class'UserGroup'.static.GetConfigInstance(groupName)); + if (groupConfig == none) + { + if (localGroupIsExpected) + { + _.logger.Auto(warnNoLocalGroup).Arg(groupName.Copy()); + return false; + } + class'UserGroup'.static.NewConfig(groupName); + groupConfig = UserGroup( + class'UserGroup'.static.GetConfigInstance(groupName)); + if (groupConfig == none) + { + _.logger.Auto(errCannotCreateLocalGroup).Arg(groupName.Copy()); + return false; + } + } + // Copy player IDs from `string` array into `HashTable` + // that is serving as a set data structure + newPlayerSet = _.collections.EmptyHashTable(); + groupUserArray = groupConfig.user; + for (i = 0; i < groupUserArray.length; i += 1) + { + nextUserPair = ParseConfigUserName(groupUserArray[i]); + if (newPlayerSet.HasKey(nextUserPair.id)) + { + _.logger.Auto(warnDuplicateIDs) + .Arg(nextUserPair.id.Copy()) + .Arg(groupName.Copy()); + continue; + } + newPlayerSet.SetItem(nextUserPair.id, nextUserPair.annotation); + _.memory.Free(nextUserPair.id); + _.memory.Free(nextUserPair.annotation); + } + lowerCaseGroupName = groupName.LowerCopy(); + loadedGroupToUsersMap.SetItem(lowerCaseGroupName, newPlayerSet); + lowerCaseGroupName.FreeSelf(); + newPlayerSet.FreeSelf(); + groupConfig.FreeSelf(); + return true; +} + +private final function IDAnnotationPair ParseConfigUserName( + string configUserName) +{ + local int lastSlashIndex; + local Text userAnnotation; + local MutableText userNameAsText; + local IDAnnotationPair result; + + userNameAsText = _.text.FromStringM(configUserName); + lastSlashIndex = userNameAsText.IndexOf(P("/")); + if (lastSlashIndex >= 0 && lastSlashIndex + 1 < userNameAsText.GetLength()) + { + userAnnotation = userNameAsText.Copy(lastSlashIndex + 1); + if (!userAnnotation.IsEmpty()) { + result.annotation = userAnnotation; + } + else { + userAnnotation.FreeSelf(); + } + } + if (lastSlashIndex != 0) { + result.id = userNameAsText.Copy(, lastSlashIndex); + } + else { + result.id = P("").Copy(); + } + userNameAsText.FreeSelf(); + return result; +} + +private final function SaveLocalData() +{ + local Text nextGroup, activeConfigName; + local Users currentConfig; + local HashTableIterator iter; + + if (useDatabaseForGroupsData) 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(); + SaveLocalGroup(nextGroup); + nextGroup.FreeSelf(); + } + iter.Next(); + } + iter.FreeSelf(); + activeConfigName = GetCurrentConfig(); + if (activeConfigName != none) + { + currentConfig = Users(class'Users'.static + .GetConfigInstance(activeConfigName)); + } + if (currentConfig != none) + { + currentConfig.localUserGroup = availableUserGroups; + currentConfig.SaveConfig(); + } + _.memory.Free(currentConfig); + _.memory.Free(activeConfigName); +} + +private final function bool SaveLocalGroup(BaseText groupName) +{ + local string nextUserLine; + local int userLinesAdded; + local Text nextID, nextAnnotation; + local Text lowerCaseGroupName; + local UserGroup configEntry; + local HashTable playersSet; + local HashTableIterator iter; + + if (loadedGroupToUsersMap == none) return false; + if (groupName == none) return false; + + // Create group if missing + class'UserGroup'.static.NewConfig(groupName); + configEntry = + UserGroup(class'UserGroup'.static.GetConfigInstance(groupName)); + if (configEntry == none) + { + // Also add log for loading it + // Move these logs and checks into a separate method? + _.logger.Auto(errCannotCreateLocalGroup).Arg(groupName.Copy()); + return false; + } + configEntry.user.length = 0; + lowerCaseGroupName = groupName.LowerCopy(); + playersSet = loadedGroupToUsersMap.GetHashTable(lowerCaseGroupName); + lowerCaseGroupName.FreeSelf(); + + iter = HashTableIterator(playersSet.Iterate()); + while(!iter.HasFinished()) + { + nextID = Text(iter.GetKey()); + nextAnnotation = Text(iter.Get()); + nextUserLine = nextID.ToString(); + if (nextAnnotation != none) { + nextUserLine = nextUserLine $ "/" $ nextAnnotation.ToString(); + } + configEntry.user[userLinesAdded] = nextUserLine; + userLinesAdded += 1; + iter.Next(); + _.memory.Free(nextID); + _.memory.Free(nextAnnotation); + } + iter.FreeSelf(); + configEntry.SyncSave(); +} + +private final function ScheduleConfigSave() +{ + if (diskSaveScheduled) { + return; + } + _.scheduler.RequestDiskAccess(self).connect = SaveLocalData; + diskSaveScheduled = false; +} + +/** + * 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; +} + +/** + * 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 unique and in lower case. + */ +public final /*unreal*/ function array GetAvailableGroups_S() +{ + local int i; + local array result; + + for (i = 0; i < loadedUserGroups.length; i += 1) { + result[i] = loadedUserGroups[i].ToString(); + } + 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 Text lowerCaseGroupName; + local HashTable emptyHashTable; + + if (groupName == none) { + return false; + } + lowerCaseGroupName = groupName.LowerCopy(); + if (loadedGroupToUsersMap.HasKey(lowerCaseGroupName)) + { + lowerCaseGroupName.FreeSelf(); + return false; + } + if (useDatabaseForGroupsData) + { + emptyHashTable = _.collections.EmptyHashTable(); + userGroupsRootPointer.Push(lowerCaseGroupName); + usersGroupsDatabase.IncrementData( + userGroupsRootPointer, + emptyHashTable); + _.memory.Free(userGroupsRootPointer.Pop()); + loadedUserGroups[loadedUserGroups.length] = lowerCaseGroupName; + loadedGroupToUsersMap.SetItem(lowerCaseGroupName, emptyHashTable); + emptyHashTable.FreeSelf(); + return true; + } + else if (LoadLocalGroup(lowerCaseGroupName)) + { + // Move `lowerCaseGroupName` here, do NOT release the reference + loadedUserGroups[loadedUserGroups.length] = lowerCaseGroupName; + ScheduleConfigSave(); + return true; + } + // In case we couldn't load local group - we don't need it and + // can release the reference + lowerCaseGroupName.FreeSelf(); + return false; +} + +/** + * 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 /*unreal*/ function bool AddGroup_S(string groupName) +{ + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(groupName); + result = AddGroup(wrapper); + wrapper.FreeSelf(); + return result; +} + +/** + * 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; + } + } + loadedGroupToUsersMap.RemoveItem(lowerCaseGroupName); + if (useDatabaseForGroupsData) + { + userGroupsRootPointer.Push(lowerCaseGroupName); + usersGroupsDatabase.RemoveData(userGroupsRootPointer); + _.memory.Free(userGroupsRootPointer.Pop()); + } + else { + ScheduleConfigSave(); + } + lowerCaseGroupName.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 /*unreal*/ function bool RemoveGroup_S(string groupName) +{ + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(groupName); + result = RemoveGroup(wrapper); + wrapper.FreeSelf(); + return result; +} + +/** + * 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; +} + +/** + * 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 /*unreal*/ function bool IsGroupExisting_S(string groupName) +{ + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(groupName); + result = IsGroupExisting(wrapper); + wrapper.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 (steamID == none) return false; + if (loadedGroupToUsersMap == none) return false; + if (groupName == none) return false; + + lowercaseGroupName = groupName.LowerCopy(); + groupUsers = loadedGroupToUsersMap.GetHashTable(lowercaseGroupName); + if (groupUsers != none && !groupUsers.HasKey(steamID)) + { + groupUsers.SetItem(steamID, none); + if (useDatabaseForGroupsData) + { + userGroupsRootPointer.Push(lowerCaseGroupName); + userGroupsRootPointer.Push(steamID); + usersGroupsDatabase.IncrementData(userGroupsRootPointer, none); + _.memory.Free(userGroupsRootPointer.Pop()); + _.memory.Free(userGroupsRootPointer.Pop()); + } + else { + ScheduleConfigSave(); + } + } + lowercaseGroupName.FreeSelf(); + _.memory.Free(groupUsers); + return (groupUsers != none); +} + +/** + * 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.GetUniqueID(); + if (steamID == none) return false; + + result = AddSteamIDToGroup(steamID, groupName); + steamID.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 /*unreal*/ function bool AddUserIDToGroup_S( + UserID id, + string groupName) +{ + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(groupName); + result = AddUserIDToGroup(id, wrapper); + wrapper.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; +} + +/** + * 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 /*unreal*/ function bool AddUserToGroup_S( + User user, + string groupName) +{ + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(groupName); + result = AddUserToGroup(user, wrapper); + wrapper.FreeSelf(); + 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 (steamID == none) return false; + if (groupName == none) return false; + if (loadedGroupToUsersMap == none) return false; + + lowercaseGroupName = groupName.LowerCopy(); + groupUsers = loadedGroupToUsersMap.GetHashTable(lowercaseGroupName); + if (groupUsers != none) + { + hadUser = groupUsers.HasKey(steamID); + groupUsers.RemoveItem(steamID); + if (useDatabaseForGroupsData) + { + userGroupsRootPointer.Push(lowerCaseGroupName); + userGroupsRootPointer.Push(steamID); + usersGroupsDatabase.RemoveData(userGroupsRootPointer); + _.memory.Free(userGroupsRootPointer.Pop()); + _.memory.Free(userGroupsRootPointer.Pop()); + } + else { + ScheduleConfigSave(); + } + } + _.memory.Free(groupUsers); + lowercaseGroupName.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.GetUniqueID(); + if (steamID == none) return false; + + result = RemoveSteamIDFromGroup(steamID, groupName); + steamID.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 /*unreal*/ function bool RemoveUserIDFromGroup_S( + UserID id, + string groupName) +{ + local bool result; + local MutableText groupWrapper; + + groupWrapper = _.text.FromStringM(groupName); + result = RemoveUserIDFromGroup(id, groupWrapper); + groupWrapper.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; +} + +/** + * 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 /*unreal*/ function bool RemoveUserFromGroup_S( + User user, + string groupName) +{ + local bool result; + local MutableText groupWrapper; + + groupWrapper = _.text.FromStringM(groupName); + result = RemoveUserFromGroup(user, groupWrapper); + groupWrapper.FreeSelf(); + 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); + } + iter.FreeSelf(); + immutableSteamID.FreeSelf(); + return result; +} + +/** + * Returns names of all groups available for the user with a given 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 unique and in lower case. + * If data wasn't yet loaded - returns empty array. + */ +public final /*unreal*/ function array GetGroupsForSteamID_S( + string steamID) +{ + local int i; + local array wrapperResult; + local array result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(steamID); + wrapperResult = GetGroupsForSteamID(wrapper); + wrapper.FreeSelf(); + for (i = 0; i < wrapperResult.length; i += 1) { + result[i] = _.text.IntoString(wrapperResult[i]); + } + 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.GetUniqueID(); + if (steamID == none) return result; + + result = GetGroupsForSteamID(steamID); + steamID.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 unique and in lower case. + * If data wasn't yet loaded - returns empty array. + */ +public final /*unreal*/ function array GetGroupsForUserID_S(UserID id) +{ + local int i; + local array wrapperResult; + local array result; + + wrapperResult = GetGroupsForUserID(id); + for (i = 0; i < wrapperResult.length; i += 1) { + result[i] = _.text.IntoString(wrapperResult[i]); + } + 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 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 unique and in lower case. + * If data wasn't yet loaded - returns empty array. + */ +public final /*unreal*/ function array GetGroupsForUser_S(User user) +{ + local int i; + local array wrapperResult; + local array result; + + wrapperResult = GetGroupsForUser(user); + for (i = 0; i < wrapperResult.length; i += 1) { + result[i] = _.text.IntoString(wrapperResult[i]); + } + return result; +} + +/** + * Returns `UserID`s of all users that belong into the group named `groupName`. + * + * @see For more information alongside `UserID`s use + * `GetAnnotatedGroupMembers()`. + * + * 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; +} + +/** + * Returns annotated `UserID`s of all users that belong into the group named + * `groupName`. `UserID`s aren't necessarily human-readable (e.g. SteamID) + * and to help organize configs they can be annotated with a `Text` name. + * This method returns `UserID` alongside such annotation, if it exists. + * NOTE: Same user can have different annotations in different groups. + * + * @see For just `UserID`s use `GetGroupMembers()`. + * + * 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. + * WARNING: References in fields of the returned `struct`s must be freed. + */ +public final function array GetAnnotatedGroupMembers( + BaseText groupName) +{ + local int i; + local Text lowerCaseGroupName; + local HashTable groupUsers; + local array groupUsersNames; + local AnnotatedUserID nextRecord; + 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(); + } + for (i = 0; i < groupUsersNames.length; i += 1) + { + nextRecord.id = UserID(_.memory.Allocate(class'UserID')); + nextRecord.id.Initialize(groupUsersNames[i]); + if (nextRecord.id.IsInitialized()) + { + nextRecord.annotation = groupUsers.GetText(groupUsersNames[i]); + result[result.length] = nextRecord; + } + else + { + nextRecord.id.FreeSelf(); + nextRecord.id = none; + } + } + _.memory.FreeMany(groupUsersNames); + _.memory.Free(groupUsers); + return result; +} + +/** + * Returns annotation for user given by SteamID inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * `none` if either group doesn't exist, user doesn't belong to it or it is + * not annotated. + * If data wasn't yet loaded - returns `none`. + */ +public final function Text GetAnnotationForSteamID( + BaseText groupName, + BaseText steamID) +{ + local Text result; + local Text lowerCaseGroupName; + local HashTable groupUsers; + + if (loadedGroupToUsersMap == none) return result; + if (groupName == none) return result; + if (steamID == none) return result; + + lowerCaseGroupName = groupName.LowerCopy(); + groupUsers = loadedGroupToUsersMap.GetHashTable(lowerCaseGroupName); + lowerCaseGroupName.FreeSelf(); + if (groupUsers != none) { + result = groupUsers.GetText(steamID); + } + _.memory.Free(groupUsers); + return result; +} + +/** + * Returns annotation for user given by SteamID inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * Empty `string` if either group doesn't exist, user doesn't belong to it + * or it is not annotated. + * If data wasn't yet loaded - returns empty `string`. + */ +public final /*unreal*/ function string GetAnnotationForSteamID_S( + string groupName, + string steamID) +{ + local Text result; + local MutableText groupWrapper, idWrapper; + + groupWrapper = _.text.FromStringM(steamID); + idWrapper = _.text.FromStringM(steamID); + result = GetAnnotationForSteamID(groupWrapper, idWrapper); + groupWrapper.FreeSelf(); + idWrapper.FreeSelf(); + return _.text.IntoString(result); +} + +/** + * Returns annotation for user given by `UserID` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * `none` if either group doesn't exist, user doesn't belong to it or it is + * not annotated. + * If data wasn't yet loaded - returns `none`. + */ +public final function Text GetAnnotationForUserID(BaseText groupName, UserID id) +{ + local Text steamID; + local Text result; + + if (id == none) return result; + steamID = id.GetUniqueID(); + if (steamID == none) return result; + + result = GetAnnotationForSteamID(groupName, steamID); + steamID.FreeSelf(); + return result; +} + +/** + * Returns annotation for user given by `UserID` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * Empty `string` if either group doesn't exist, user doesn't belong to it + * or it is not annotated. + * If data wasn't yet loaded - returns empty `string`. + */ +public final /*unreal*/ function string GetAnnotationForUserID_S( + string groupName, + UserID id) +{ + local Text result; + local MutableText wrapper; + + if (id == none) { + return ""; + } + wrapper = _.text.FromStringM(groupName); + result = GetAnnotationForUserID(wrapper, id); + wrapper.FreeSelf(); + return _.text.IntoString(result); +} + +/** + * Returns annotation for user given by `User` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * `none` if either group doesn't exist, user doesn't belong to it or it is + * not annotated. + * If data wasn't yet loaded - returns `none`. + */ +public final function Text GetAnnotationForUser(BaseText groupName, User user) +{ + local UserID id; + local Text result; + + if (user == none) { + return result; + } + id = user.GetID(); + result = GetAnnotationForUserID(groupName, id); + _.memory.Free(id); + return result; +} + +/** + * Returns annotation for user given by `User` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method returns these annotations, if they exists. + * NOTE: Same user can have different annotations in different groups. + * + * 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. + * @param steamID ID of the user, in whose annotation we are interested. + * @return Annotation for the specified user inside the specified group. + * Empty `string` if either group doesn't exist, user doesn't belong to it + * or it is not annotated. + * If data wasn't yet loaded - returns empty `string`. + */ +public final /*unreal*/ function string GetAnnotationForUser_S( + string groupName, + User user) +{ + local Text result; + local MutableText wrapper; + + if (user == none) { + return ""; + } + wrapper = _.text.FromStringM(groupName); + result = GetAnnotationForUser(wrapper, user); + wrapper.FreeSelf(); + return _.text.IntoString(result); +} + +/** + * Changes annotation for user given by SteamID inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. + */ +public final function SetAnnotationForSteamID( + BaseText groupName, + BaseText steamID, + BaseText annotation) +{ + local Text lowerCaseGroupName; + local HashTable groupUsers; + + if (loadedGroupToUsersMap == none) return; + if (groupName == none) return; + if (steamID == none) return; + + lowerCaseGroupName = groupName.LowerCopy(); + groupUsers = loadedGroupToUsersMap.GetHashTable(lowerCaseGroupName); + if (groupUsers != none && groupUsers.HasKey(steamID)) + { + groupUsers.SetItem(steamID, annotation); + if (useDatabaseForGroupsData) + { + userGroupsRootPointer.Push(lowerCaseGroupName); + userGroupsRootPointer.Push(steamID); + usersGroupsDatabase.WriteData(userGroupsRootPointer, annotation); + _.memory.Free(userGroupsRootPointer.Pop()); + _.memory.Free(userGroupsRootPointer.Pop()); + } + else { + ScheduleConfigSave(); + } + } + _.memory.Free(groupUsers); + lowerCaseGroupName.FreeSelf(); +} + +/** + * Changes annotation for user given by SteamID inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. Empty annotation + * means simply removing any existing annotation. + */ +public final /*unreal*/ function SetAnnotationForSteamID_S( + string groupName, + string steamID, + string annotation) +{ + local MutableText groupWrapper; + local MutableText idWrapper; + local MutableText annotationWrapper; + + groupWrapper = _.text.FromStringM(groupName); + idWrapper = _.text.FromStringM(steamID); + // Leave `annotationWrapper` as `none` for empty annotations + if (annotation != "") { + annotationWrapper = _.text.FromStringM(annotation); + } + SetAnnotationForSteamID(groupWrapper, idWrapper, annotationWrapper); + groupWrapper.FreeSelf(); + idWrapper.FreeSelf(); + _.memory.Free(annotationWrapper); +} + +/** + * Changes annotation for user given by `UserID` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. + */ +public final function SetAnnotationForUserID( + BaseText groupName, + UserID id, + BaseText annotation) +{ + local Text steamID; + + if (id == none) return; + steamID = id.GetUniqueID(); + if (steamID == none) return; + + SetAnnotationForSteamID(groupName, steamID, annotation); + steamID.FreeSelf(); +} + +/** + * Changes annotation for user given by `UserID` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. Empty annotation + * means simply removing any existing annotation. + */ +public final /*unreal*/ function SetAnnotationForUserID_S( + string groupName, + UserID id, + string annotation) +{ + local MutableText groupWrapper; + local MutableText annotationWrapper; + + groupWrapper = _.text.FromStringM(groupName); + // Leave `annotationWrapper` as `none` for empty annotations + if (annotation != "") { + annotationWrapper = _.text.FromStringM(annotation); + } + SetAnnotationForUserID(groupWrapper, id, annotationWrapper); + groupWrapper.FreeSelf(); + _.memory.Free(annotationWrapper); +} + +/** + * Changes annotation for user given by `User` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. + */ +public final function SetAnnotationForUser( + BaseText groupName, + User user, + BaseText annotation) +{ + local UserID id; + + if (user == none) { + return; + } + id = user.GetID(); + SetAnnotationForUserID(groupName, id, annotation); + _.memory.Free(id); +} + +/** + * Changes annotation for user given by `User` inside the group named + * `groupName`. `UserID`s that are used to store belonging users into groups + * aren't necessarily human-readable (e.g. SteamID) and to help organize + * configs they can be annotated with a `Text` name. + * This method allows to change these annotations. + * NOTE: Same user can have different annotations in different groups. + * + * 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 groupName Name of the group. Case-insensitive. + * @param steamID ID of the user, whose annotation we want to change. + * @param annotation New annotation for the specified user. Empty annotation + * means simply removing any existing annotation. + */ +public final /*unreal*/ function SetAnnotationForUser_S( + string groupName, + User user, + string annotation) +{ + local MutableText groupWrapper; + local MutableText annotationWrapper; + + groupWrapper = _.text.FromStringM(groupName); + // Leave `annotationWrapper` as `none` for empty annotations + if (annotation != "") { + annotationWrapper = _.text.FromStringM(annotation); + } + SetAnnotationForUser(groupWrapper, user, annotationWrapper); + groupWrapper.FreeSelf(); + _.memory.Free(annotationWrapper); +} + +/** + * Checks whether user given by SteamID 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 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 IsSteamIDInGroup( + BaseText steamID, + BaseText groupName) +{ + local bool result; + local Text lowerGroupName; + local HashTable nextGroupUsers; + + if (loadedGroupToUsersMap == none) return false; + if (groupName == none) return false; + if (steamID == none) return false; + + lowerGroupName = groupName.LowerCopy(); + nextGroupUsers = loadedGroupToUsersMap.GetHashTable(lowerGroupName); + lowerGroupName.FreeSelf(); + if (nextGroupUsers != none) { + result = nextGroupUsers.HasKey(steamID); + } + _.memory.Free(nextGroupUsers); + return result; +} + +/** + * Checks whether user given by SteamID 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 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 IsSteamIDInGroup_S( + string steamID, + string groupName) +{ + local bool result; + local MutableText idWrapper, groupWrapper; + + idWrapper = _.text.FromStringM(steamID); + groupWrapper = _.text.FromStringM(groupName); + result = IsSteamIDInGroup(idWrapper, groupWrapper); + idWrapper.FreeSelf(); + groupWrapper.FreeSelf(); + 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, BaseText groupName) +{ + local bool result; + local Text steamID; + + if (groupName == none) return false; + if (id == none) return false; + steamID = id.GetUniqueID(); + if (steamID == none) return false; + + result = IsSteamIDInGroup(steamID, groupName); + steamID.FreeSelf(); + 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 /*unreal*/ function bool IsUserIDInGroup_S( + UserID id, + string groupName) +{ + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(groupName); + result = IsUserIDInGroup(id, wrapper); + wrapper.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, BaseText 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 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 /*unreal*/ function bool IsUserInGroup_S( + User user, + string groupName) +{ + local bool result; + local MutableText wrapper; + + wrapper = _.text.FromStringM(groupName); + result = IsUserInGroup(user, wrapper); + wrapper.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() +{ + return userGroupsDataLoaded; +} + +defaultproperties +{ + configClass = class'Users' + warnNoLocalGroup = (l=LOG_Warning,m="Expected config to contain `UserGroup` named \"%1\", but it is missing. \"AcediaUsers.ini\" might be misconfigured.") + warnDuplicateIDs = (l=LOG_Warning,m="Duplicate record for user id \"%1\" is found in `UserGroup` named \"%2\". \"AcediaUsers.ini\" is misconfigured and needs to be fixed.") + errCannotCreateLocalGroup = (l=LOG_Error,m="Failed to create config section for `UserGroup` named \"%1\".") + errCannotOpenDatabase = (l=LOG_Error,m="\"Users_Feature\" has failed to open database given by the link \"%1\".") + infoUserGroupDataLoaded = (l=LOG_Info,m="Successfully loaded user data from the database link \"%1\".") + errDBBadRootUserGroupData = (l=LOG_Error,m="Database link \"%1\" (configured to load user group data in \"AcediaUsers.ini\") contains incompatible data.") + errDBBadLinkPointer = (l=LOG_Error,m="Path inside database link \"%1\" (configured inside \"AcediaUsers.ini\") is invalid.") + errDBDamaged = (l=LOG_Error,m="Database given by the link \"%1\" (configured inside \"AcediaUsers.ini\") seems to be damaged.") + errNoServerCore = (l=LOG_Error,m="Cannot start \"Users_Feature\", because no `ServerCore` was created.") + errDBContainsNonLowerRegister = (l=LOG_Error,m="Database given by the link \"%1\" contains non-lower case key \"%2\". This shouldn't happen, unless someone manually edited database.") +} \ No newline at end of file