Browse Source

Refactor `UserAPI`

core_refactor
Anton Tarasenko 2 years ago
parent
commit
78aff18cad
  1. 35
      config/AcediaUsers.ini
  2. 3
      sources/Manifest.uc
  3. 31
      sources/Players/EPlayer.uc
  4. 655
      sources/Users/ACommandUserGroups.uc
  5. 40
      sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Signal.uc
  6. 41
      sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Slot.uc
  7. 407
      sources/Users/PersistentData/PersistentDataManager.uc
  8. 4
      sources/Users/Tests/TEST_User.uc
  9. 211
      sources/Users/User.uc
  10. 1397
      sources/Users/UserAPI.uc
  11. 16
      sources/Users/UserDatabase.uc
  12. 70
      sources/Users/UserGroup.uc
  13. 37
      sources/Users/UserID.uc
  14. 99
      sources/Users/Users.uc
  15. 2158
      sources/Users/Users_Feature.uc

35
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=

3
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'

31
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.
*

655
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 <https://www.gnu.org/licenses/>.
*/
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<Users_Feature.AnnotatedUserID> 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<Text> 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<Text> 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<Users_Feature.AnnotatedUserID> 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
{
}

40
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 <https://www.gnu.org/licenses/>.
*/
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'
}

41
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 <https://www.gnu.org/licenses/>.
*/
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
{
}

407
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 <https://www.gnu.org/licenses/>.
*/
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<EPlayer> currentPlayers;
if (initialized)
{
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
ConnectPersistentData(currentPlayers[i]);
}
_.memory.FreeMany(currentPlayers);
}
}
defaultproperties
{
}

4
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();

211
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\".")
}

1397
sources/Users/UserAPI.uc

File diff suppressed because it is too large Load Diff

16
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<User> 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];
}
}

70
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 <https://www.gnu.org/licenses/>.
*/
class UserGroup extends AcediaConfig
perobjectconfig
config(AcediaUsers);
var public config array<string> 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
}

37
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`.

99
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 <https://www.gnu.org/licenses/>.
*/
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<string> 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"
}

2158
sources/Users/Users_Feature.uc

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save