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