diff --git a/sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc b/sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc index 0eaf13c..dbbec75 100644 --- a/sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc +++ b/sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc @@ -1,6 +1,6 @@ /** * Container for the information about available resources from other packages. - * Copyright 2022 Anton Tarasenko + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -520,7 +520,7 @@ defaultproperties manifestSuffix = ".Manifest" infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".") infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.") - errNotRegistered = (l=LOG_Error,m="Package \"%2\" has failed to be registered.") + errNotRegistered = (l=LOG_Error,m="Package \"%1\" has failed to be registered.") warnFeatureAlreadyEnabled = (l=LOG_Warning,m="Same instance of `Feature` class `%1` is already enabled.") errFeatureClassAlreadyEnabled = (l=LOG_Error,m="Different instance of the same `Feature` class `%1` is already enabled.") } \ No newline at end of file diff --git a/sources/BaseRealm/Global.uc b/sources/BaseRealm/Global.uc index f26f92f..596e580 100644 --- a/sources/BaseRealm/Global.uc +++ b/sources/BaseRealm/Global.uc @@ -2,7 +2,7 @@ * Class for an object that will provide an access to a Acedia's functionality * that is common for both clients and servers by giving a reference to this * object to all Acedia's objects and actors, emulating a global API namespace. - * Copyright 2020-2022 Anton Tarasenko + * Copyright 2020-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -39,7 +39,6 @@ var public ColorAPI color; var public UserAPI users; var public PlayersAPI players; var public JSONAPI json; -var public DBAPI db; var public SchedulerAPI scheduler; var public AvariceAPI avarice; @@ -68,6 +67,7 @@ protected function Initialize() text = TextAPI(memory.Allocate(class'TextAPI')); math = MathAPI(memory.Allocate(class'MathAPI')); collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI')); + json = JSONAPI(memory.Allocate(class'JSONAPI')); logger = LoggerAPI(memory.Allocate(class'LoggerAPI')); color = ColorAPI(memory.Allocate(class'ColorAPI')); alias = AliasesAPI(memory.Allocate(class'AliasesAPI')); @@ -75,8 +75,6 @@ protected function Initialize() chat = ChatAPI(memory.Allocate(class'ChatAPI')); users = UserAPI(memory.Allocate(class'UserAPI')); players = PlayersAPI(memory.Allocate(class'PlayersAPI')); - json = JSONAPI(memory.Allocate(class'JSONAPI')); - db = DBAPI(memory.Allocate(class'DBAPI')); scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI')); avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); @@ -97,7 +95,6 @@ public function DropCoreAPI() users = none; players = none; json = none; - db = none; scheduler = none; avarice = none; default.myself = none; diff --git a/sources/ClientRealm/ClientAcediaAdapter.uc b/sources/ClientRealm/ClientAcediaAdapter.uc index 4000ea0..f01b15a 100644 --- a/sources/ClientRealm/ClientAcediaAdapter.uc +++ b/sources/ClientRealm/ClientAcediaAdapter.uc @@ -2,7 +2,7 @@ * Base class for objects that will provide an access to a Acedia's client- and * server-specific functionality by giving a reference to this object to all * Acedia's objects and actors, emulating a global API namespace. - * Copyright 2022 Anton Tarasenko + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -27,7 +27,9 @@ var public const class clientInteractionAPIClass; defaultproperties { + sideEffectAPIClass = class'KF1_SideEffectAPI' timeAPIClass = class'KF1_TimeAPI' + dbAPIClass = class'DBAPI' clientUnrealAPIClass = class'KF1_ClientUnrealAPI' clientInteractionAPIClass = class'KF1_InteractionAPI' } \ No newline at end of file diff --git a/sources/ClientRealm/ClientGlobal.uc b/sources/ClientRealm/ClientGlobal.uc index bdc9314..9ab6fc8 100644 --- a/sources/ClientRealm/ClientGlobal.uc +++ b/sources/ClientRealm/ClientGlobal.uc @@ -29,6 +29,11 @@ var public ClientUnrealAPI unreal; var private LoggerAPI.Definition fatBadAdapterClass, errNoInteraction; +public function UnrealAPI unreal_api() +{ + return unreal; +} + public final static function ClientGlobal GetInstance() { if (default.myself == none) diff --git a/sources/CoreRealm/AcediaAdapter.uc b/sources/CoreRealm/AcediaAdapter.uc index b7cc9d1..1b3ae94 100644 --- a/sources/CoreRealm/AcediaAdapter.uc +++ b/sources/CoreRealm/AcediaAdapter.uc @@ -1,7 +1,7 @@ /** * Base class for describing what API Acedia should load into its client- and * server- `...Global`s objects. - * Copyright 2022 Anton Tarasenko + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -37,8 +37,8 @@ class AcediaAdapter extends AcediaObject var public const class sideEffectAPIClass; var public const class timeAPIClass; +var public const class dbAPIClass; defaultproperties { - sideEffectAPIClass = class'KF1_SideEffectAPI' } \ No newline at end of file diff --git a/sources/CoreRealm/CoreGlobal.uc b/sources/CoreRealm/CoreGlobal.uc index 0ed413b..a4aa24f 100644 --- a/sources/CoreRealm/CoreGlobal.uc +++ b/sources/CoreRealm/CoreGlobal.uc @@ -2,7 +2,7 @@ * Base class for objects that will provide an access to a Acedia's client- and * server-specific functionality by giving a reference to this object to all * Acedia's objects and actors, emulating a global API namespace. - * Copyright 2022 Anton Tarasenko + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -26,9 +26,34 @@ var protected class adapterClass; var public SideEffectAPI sideEffects; var public TimeAPI time; +var public DBAPI db; var private LoggerAPI.Definition fatNoAdapterClass; +/** + * Accessor to the generic `UnrealAPI`. + */ +public function UnrealAPI unreal_api() +{ + return none; +} + +public final static function CoreGlobal GetGenericInstance() +{ + local ServerGlobal serverAPI; + local ClientGlobal clientAPI; + + serverAPI = class'ServerGlobal'.static.GetInstance(); + if (serverAPI != none && serverAPI.IsAvailable()) { + return serverAPI; + } + clientAPI = class'ClientGlobal'.static.GetInstance(); + if (clientAPI != none && clientAPI.IsAvailable()) { + return clientAPI; + } + return none; +} + /** * This method must perform initialization of the caller `...Global` instance. * @@ -50,10 +75,11 @@ protected function Initialize() .ArgClass(self.class); return; } - api = class'Global'.static.GetInstance().memory; + api = class'Global'.static.GetInstance().memory; sideEffects = SideEffectAPI(api.Allocate(adapterClass.default.sideEffectAPIClass)); - time = TimeAPI(api.Allocate(adapterClass.default.timeAPIClass)); + time = TimeAPI(api.Allocate(adapterClass.default.timeAPIClass)); + db = DBAPI(api.Allocate(adapterClass.default.dbAPIClass)); } /** diff --git a/sources/CoreRealm/Features/Commands/BuiltInCommands/ACommandHelp.uc b/sources/CoreRealm/Features/Commands/BuiltInCommands/ACommandHelp.uc index 2c2b7ba..a4ec0f9 100644 --- a/sources/CoreRealm/Features/Commands/BuiltInCommands/ACommandHelp.uc +++ b/sources/CoreRealm/Features/Commands/BuiltInCommands/ACommandHelp.uc @@ -178,6 +178,8 @@ private final function FillCommandToAliasesMap(Feature enabledFeature) InsertIntoAliasesMap(commandName, subcommandName, availableAliases[i]); commandName.FreeSelf(); subcommandName.FreeSelf(); + commandName = none; + subcommandName = none; } // Clean up _.memory.FreeMany(availableAliases); diff --git a/sources/Data/Database/DBAPI.uc b/sources/Data/Database/DBAPI.uc index 0e121f7..9f750e8 100644 --- a/sources/Data/Database/DBAPI.uc +++ b/sources/Data/Database/DBAPI.uc @@ -1,7 +1,7 @@ /** * API that provides methods for creating/destroying and managing available * databases. - * Copyright 2021-2022 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -279,7 +279,7 @@ private function EraseAllPackageData(BaseText packageToErase) if (packageName == "") { return; } - game = _server.unreal.GetGameType(); + game = __core().unreal_api().GetGameType(); game.DeletePackage(packageName); // Delete any leftover objects. This has to be done *after* // `DeletePackage()` call, otherwise removed garbage can reappear. diff --git a/sources/Data/Database/Local/DBRecord.uc b/sources/Data/Database/Local/DBRecord.uc index 9f343f0..a2730fd 100644 --- a/sources/Data/Database/Local/DBRecord.uc +++ b/sources/Data/Database/Local/DBRecord.uc @@ -7,7 +7,7 @@ * Auxiliary data object that can store either a JSON array or an object in * the local Acedia database. It is supposed to be saved and loaded * to / from packages. - * Copyright 2021-2022 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -264,9 +264,9 @@ public static final function Global __() return class'Global'.static.GetInstance(); } -public static final function ServerGlobal __server() +public static final function CoreGlobal __core() { - return class'ServerGlobal'.static.GetInstance(); + return class'CoreGlobal'.static.GetGenericInstance(); } /** @@ -304,7 +304,7 @@ private final static function DBRecord NewRecordFor(string dbPackageName) if (recordCandidate != none) { continue; } - recordCandidate = __server().unreal.GetGameType() + recordCandidate = __core().unreal_api().GetGameType() .CreateDataObject(class'DBRecord', nextName, dbPackageName); recordCandidate.package = dbPackageName; return recordCandidate; @@ -330,7 +330,7 @@ private final static function DBRecord LoadRecordFor( string name, string package) { - return __server().unreal.GetGameType() + return __core().unreal_api().GetGameType() .LoadDataObject(class'DBRecord', name, package); } @@ -689,7 +689,7 @@ private final function SetItem( if (oldRecord != none) { oldRecord.EmptySelf(); } - __server().unreal.GetGameType() + __core().unreal_api().GetGameType() .DeleteDataObject(class'DBRecord', oldItem.s, package); } } @@ -723,7 +723,7 @@ private final function RemoveItem(int index) if (oldRecord != none) { oldRecord.EmptySelf(); } - __server().unreal.GetGameType() + __core().unreal_api().GetGameType() .DeleteDataObject(class'DBRecord', oldItem.s, package); } storage.Remove(index, 1); @@ -872,7 +872,7 @@ public final function EmptySelf() return; } lockEraseSelf = true; - game = __server().unreal.GetGameType(); + game = __core().unreal_api().GetGameType(); for (i = 0; i < storage.length; i += 1) { if (storage[i].t != DBAT_Reference) continue; diff --git a/sources/Data/Database/Local/LocalDatabaseInstance.uc b/sources/Data/Database/Local/LocalDatabaseInstance.uc index 07aafb0..1ff544a 100644 --- a/sources/Data/Database/Local/LocalDatabaseInstance.uc +++ b/sources/Data/Database/Local/LocalDatabaseInstance.uc @@ -4,7 +4,7 @@ * This class SHOULD NOT be deallocated manually. * This name was chosen so that more readable `LocalDatabase` could be * used in config for defining local databases through per-object-config. - * Copyright 2021-2022 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -83,7 +83,7 @@ var private int lastTaskLifeVersion; protected function Constructor() { - _server.unreal.OnTick(self).connect = CompleteAllTasks; + __core().unreal_api().OnTick(self).connect = CompleteAllTasks; } protected function Finalizer() @@ -93,7 +93,7 @@ protected function Finalizer() CompleteAllTasks(); WriteToDisk(); rootRecord = none; - _server.unreal.OnTick(self).Disconnect(); + __core().unreal_api().OnTick(self).Disconnect(); configEntry = none; } @@ -130,7 +130,7 @@ public final function WriteToDisk() packageName = _.text.IntoString(configEntry.GetPackageName()); } if (packageName != "") { - _server.unreal.GetGameType().SavePackage(packageName); + __core().unreal_api().GetGameType().SavePackage(packageName); } } diff --git a/sources/Data/Database/Tests/TEST_DatabaseCommon.uc b/sources/Data/Database/Tests/TEST_DatabaseCommon.uc index 63b7992..22f7014 100644 --- a/sources/Data/Database/Tests/TEST_DatabaseCommon.uc +++ b/sources/Data/Database/Tests/TEST_DatabaseCommon.uc @@ -25,20 +25,20 @@ protected static function TESTS() local JSONPointer pointer; Context("Testing extracting `JSONPointer` from database link."); Issue("`JSONPointer` is incorrectly extracted."); - pointer = __().db.GetPointer( + pointer = __core().db.GetPointer( __().text.FromString("[local]default:/huh/what/is/")); TEST_ExpectNotNone(pointer); TEST_ExpectTrue(pointer.ToText().ToString() == "/huh/what/is/"); - pointer = __().db.GetPointer(__().text.FromString("[remote]db:")); + pointer = __core().db.GetPointer(__().text.FromString("[remote]db:")); TEST_ExpectNotNone(pointer); TEST_ExpectTrue(pointer.ToText().ToString() == ""); - pointer = __().db.GetPointer(__().text.FromString("[remote]:")); + pointer = __core().db.GetPointer(__().text.FromString("[remote]:")); TEST_ExpectNotNone(pointer); TEST_ExpectTrue(pointer.ToText().ToString() == ""); - pointer = __().db.GetPointer(__().text.FromString("db:/just/a/pointer")); + pointer = __core().db.GetPointer(__().text.FromString("db:/just/a/pointer")); TEST_ExpectNotNone(pointer); TEST_ExpectTrue(pointer.ToText().ToString() == "/just/a/pointer"); - pointer = __().db.GetPointer(__().text.FromString(":/just/a/pointer")); + pointer = __core().db.GetPointer(__().text.FromString(":/just/a/pointer")); TEST_ExpectNotNone(pointer); TEST_ExpectTrue(pointer.ToText().ToString() == "/just/a/pointer"); } diff --git a/sources/Data/Database/Tests/TEST_LocalDatabase.uc b/sources/Data/Database/Tests/TEST_LocalDatabase.uc index fd4200a..bceb211 100644 --- a/sources/Data/Database/Tests/TEST_LocalDatabase.uc +++ b/sources/Data/Database/Tests/TEST_LocalDatabase.uc @@ -117,7 +117,7 @@ local LocalDatabaseInstance db; source = GetJSONTemplateString(); parser = __().text.ParseString(source); root = HashTable(__().json.ParseWith(parser)); -db = __().db.NewLocal(P("TEST_ReadOnly")); +db = __core().db.NewLocal(P("TEST_ReadOnly")); db.WriteData(__().json.Pointer(), root); */ protected static function string GetJSONTemplateString() @@ -225,14 +225,14 @@ protected static function TESTS() protected static function Test_LoadingPrepared() { local LocalDatabaseInstance db; - db = __().db.LoadLocal(P("TEST_ReadOnly")); + db = __core().db.LoadLocal(P("TEST_ReadOnly")); Context("Testing reading prepared data from the local database."); Issue("Existing database reported as missing."); - TEST_ExpectTrue(__().db.ExistsLocal(P("TEST_ReadOnly"))); + TEST_ExpectTrue(__core().db.ExistsLocal(P("TEST_ReadOnly"))); Issue("Loading same database several times produces different" @ "`LocalDatabaseInstance` objects."); - TEST_ExpectTrue(__().db.LoadLocal(P("TEST_ReadOnly")) == db); + TEST_ExpectTrue(__core().db.LoadLocal(P("TEST_ReadOnly")) == db); // Groups of read-only tests SubTest_LoadingPreparedSuccessRoot(db); SubTest_LoadingPreparedSuccessSubValues(db); @@ -471,18 +471,18 @@ protected static function SubTest_LoadingPreparedGetKeysFail( protected static function Test_Writing() { local LocalDatabaseInstance db; - db = __().db.NewLocal(P("TEST_DB")); + db = __core().db.NewLocal(P("TEST_DB")); Context("Testing (re-)creating and writing into a new local database."); Issue("Cannot create a new database."); TEST_ExpectNotNone(db); - TEST_ExpectTrue(__().db.ExistsLocal(P("TEST_DB"))); + TEST_ExpectTrue(__core().db.ExistsLocal(P("TEST_DB"))); Issue("Freshly created database is not empty."); TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 1); // 1 root object Issue("Loading just created database produces different" @ "`LocalDatabaseInstance` object."); - TEST_ExpectTrue(__().db.LoadLocal(P("TEST_DB")) == db); + TEST_ExpectTrue(__core().db.LoadLocal(P("TEST_DB")) == db); // This set of tests fills our test database with objects SubTest_WritingSuccess(db); SubTest_WritingDataCheck(db); @@ -495,33 +495,33 @@ protected static function Test_Writing() @ "local database."); __().memory.Free(db); // For `NewLocal()` call __().memory.Free(db); // For `LoadLocal()` call - TEST_ExpectTrue(__().db.DeleteLocal(P("TEST_DB"))); + TEST_ExpectTrue(__core().db.DeleteLocal(P("TEST_DB"))); Issue("Newly created database is reported to still exist after deletion."); - TEST_ExpectFalse(__().db.ExistsLocal(P("TEST_DB"))); + TEST_ExpectFalse(__core().db.ExistsLocal(P("TEST_DB"))); TEST_ExpectFalse(db.IsAllocated()); Issue("`DeleteLocal()` does not return `false` after trying to delete" @ "non-existing local database."); - TEST_ExpectFalse(__().db.DeleteLocal(P("TEST_DB"))); + TEST_ExpectFalse(__core().db.DeleteLocal(P("TEST_DB"))); } protected static function Test_Recreate() { local LocalDatabaseInstance db; Issue("Freshly created database is not empty."); - db = __().db.NewLocal(P("TEST_DB")); + db = __core().db.NewLocal(P("TEST_DB")); TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 1); Issue("Cannot create a database after database with the same name was" @ "just deleted."); TEST_ExpectNotNone(db); - TEST_ExpectTrue(__().db.ExistsLocal(P("TEST_DB"))); + TEST_ExpectTrue(__core().db.ExistsLocal(P("TEST_DB"))); SubTest_WritingArrayIndicies(db); - __().db.DeleteLocal(P("TEST_DB")); + __core().db.DeleteLocal(P("TEST_DB")); Issue("Newly created database is reported to still exist after deletion."); __().memory.Free(db); - TEST_ExpectFalse(__().db.ExistsLocal(P("TEST_DB"))); + TEST_ExpectFalse(__core().db.ExistsLocal(P("TEST_DB"))); TEST_ExpectFalse(db.IsAllocated()); } @@ -530,15 +530,15 @@ protected static function Test_TaskChaining() local LocalDatabaseInstance db; Context("Testing (re-)creating and writing into a new local database."); Issue("Freshly created database is not empty."); - db = __().db.NewLocal(P("TEST_DB")); + db = __core().db.NewLocal(P("TEST_DB")); TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 1); Issue("Cannot create a database after database with the same name was" @ "just deleted."); TEST_ExpectNotNone(db); - TEST_ExpectTrue(__().db.ExistsLocal(P("TEST_DB"))); + TEST_ExpectTrue(__core().db.ExistsLocal(P("TEST_DB"))); SubTest_TaskChaining(db); - __().db.DeleteLocal(P("TEST_DB")); + __core().db.DeleteLocal(P("TEST_DB")); } protected static function HashTable GetJSONSubTemplateObject() @@ -776,7 +776,7 @@ protected static function Test_Removal() local HashTable templateObject; templateObject = GetJSONSubTemplateObject(); templateArray = GetJSONSubTemplateArray(); - db = __().db.NewLocal(P("TEST_DB")); + db = __core().db.NewLocal(P("TEST_DB")); db.WriteData(__().json.Pointer(P("")), templateObject); db.WriteData(__().json.Pointer(P("/B")), templateObject); db.WriteData(__().json.Pointer(P("/B/A")), templateArray); @@ -787,7 +787,7 @@ protected static function Test_Removal() SubTest_RemovalResult(db); SubTest_RemovalCheckValuesAfter(db); SubTest_RemovalRoot(db); - __().db.DeleteLocal(P("TEST_DB")); + __core().db.DeleteLocal(P("TEST_DB")); } protected static function SubTest_RemovalResult(LocalDatabaseInstance db) @@ -861,7 +861,7 @@ protected static function Test_Increment() local HashTable templateObject; templateObject = GetJSONSubTemplateObject(); templateArray = GetJSONSubTemplateArray(); - db = __().db.NewLocal(P("TEST_DB")); + db = __core().db.NewLocal(P("TEST_DB")); db.WriteData(__().json.Pointer(P("")), templateObject); db.WriteData(__().json.Pointer(P("/B")), templateObject); db.WriteData(__().json.Pointer(P("/C")), __().box.int(-5)); @@ -904,7 +904,7 @@ protected static function Test_Increment() Issue("Incrementing database values has created garbage objects."); // 5 initial records + 1 made for a new array in `SubTest_IncrementNull()` TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 6); - __().db.DeleteLocal(P("TEST_DB")); + __core().db.DeleteLocal(P("TEST_DB")); } protected static function SubTest_IncrementNull(LocalDatabaseInstance db) diff --git a/sources/Players/EPlayer.uc b/sources/Players/EPlayer.uc index ee53dd4..210e709 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. * @@ -231,15 +231,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/ServerRealm/ServerAcediaAdapter.uc b/sources/ServerRealm/ServerAcediaAdapter.uc index 5eb3290..4bcac4a 100644 --- a/sources/ServerRealm/ServerAcediaAdapter.uc +++ b/sources/ServerRealm/ServerAcediaAdapter.uc @@ -2,7 +2,7 @@ * Base class for objects that will provide an access to a Acedia's client- and * server-specific functionality by giving a reference to this object to all * Acedia's objects and actors, emulating a global API namespace. - * Copyright 2022 Anton Tarasenko + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -30,7 +30,9 @@ var public const class serverMutatorAPIClass; defaultproperties { + sideEffectAPIClass = class'KF1_SideEffectAPI' timeAPIClass = class'KF1_TimeAPI' + dbAPIClass = class'DBAPI' serverUnrealAPIClass = class'KF1_ServerUnrealAPI' serverBroadcastAPIClass = class'KF1_BroadcastAPI' serverGameRulesAPIClass = class'KF1_GameRulesAPI' diff --git a/sources/ServerRealm/ServerGlobal.uc b/sources/ServerRealm/ServerGlobal.uc index 85c436e..67fefcd 100644 --- a/sources/ServerRealm/ServerGlobal.uc +++ b/sources/ServerRealm/ServerGlobal.uc @@ -2,7 +2,7 @@ * Class for an object that will provide an access to a Acedia's * server-specific functionality by giving a reference to this object to all * Acedia's objects and actors, emulating a global API namespace. - * Copyright 2022 Anton Tarasenko + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -30,6 +30,11 @@ var public ServerUnrealAPI unreal; var private LoggerAPI.Definition fatBadAdapterClass; +public function UnrealAPI unreal_api() +{ + return unreal; +} + public final static function ServerGlobal GetInstance() { if (default.myself == none) diff --git a/sources/Types/AcediaActor.uc b/sources/Types/AcediaActor.uc index 06578e4..c7d2cb1 100644 --- a/sources/Types/AcediaActor.uc +++ b/sources/Types/AcediaActor.uc @@ -3,7 +3,7 @@ * `AcediaActor` provides access to Acedia's APIs through an accessor to * a `Global` object, built-in mechanism for storing unneeded references in * an object pool and constructor/finalizer. - * Copyright 2021 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -416,6 +416,27 @@ public simulated static final function Global __() return class'Global'.static.GetInstance(); } +/** + * Static method accessor to the generic core API namespace (either server or + * client one, depending on which is available), necessary for Acedia's + * implementation. + */ +public static final function CoreGlobal __core() +{ + local ServerGlobal serverAPI; + local ClientGlobal clientAPI; + + serverAPI = class'ServerGlobal'.static.GetInstance(); + if (serverAPI != none && serverAPI.IsAvailable()) { + return serverAPI; + } + clientAPI = class'ClientGlobal'.static.GetInstance(); + if (clientAPI != none && clientAPI.IsAvailable()) { + return clientAPI; + } + return none; +} + /** * Static method accessor to server API namespace, necessary for Acedia's * implementation. diff --git a/sources/Types/AcediaObject.uc b/sources/Types/AcediaObject.uc index ab1473a..3e0a4df 100644 --- a/sources/Types/AcediaObject.uc +++ b/sources/Types/AcediaObject.uc @@ -3,7 +3,7 @@ * `AcediaObject` provides access to Acedia's APIs through an accessor to * a `Global` object, built-in mechanism for storing unneeded references in * an object pool and constructor/finalizer. - * Copyright 2021 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -489,6 +489,27 @@ public static final function Global __() return class'Global'.static.GetInstance(); } +/** + * Static method accessor to the generic core API namespace (either server or + * client one, depending on which is available), necessary for Acedia's + * implementation. + */ +public static final function CoreGlobal __core() +{ + local ServerGlobal serverAPI; + local ClientGlobal clientAPI; + + serverAPI = class'ServerGlobal'.static.GetInstance(); + if (serverAPI != none && serverAPI.IsAvailable()) { + return serverAPI; + } + clientAPI = class'ClientGlobal'.static.GetInstance(); + if (clientAPI != none && clientAPI.IsAvailable()) { + return clientAPI; + } + return none; +} + /** * Static method accessor to server API namespace, necessary for Acedia's * implementation. diff --git a/sources/Users/ACommandUserGroups.uc b/sources/Users/ACommandUserGroups.uc index 54990d2..f1d5478 100644 --- a/sources/Users/ACommandUserGroups.uc +++ b/sources/Users/ACommandUserGroups.uc @@ -1,6 +1,6 @@ /** * Command for displaying help information about registered Acedia's commands. - * Copyright 2022 Anton Tarasenko + * Copyright 2022-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -29,8 +29,12 @@ protected function BuildData(CommandDataBuilder builder) @ "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("show")) - .Describe(P("Shows all groups along with users that belong to them.")); + 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")); @@ -50,11 +54,31 @@ protected function BuildData(CommandDataBuilder builder) @ "user's id or annotation, with id taking priority.")) .ParamText(P("group_name")) .ParamText(P("user_name")); + builder.SubCommand(P("addplayer")) + .Describe(P("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.Option(P("force")) + .Describe(P("Allows to force usage of invalid user IDs.")); } protected function Executed(CallData arguments, EPlayer instigator) { - local Text groupName, userID, userName, annotation; + 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 @@ -62,11 +86,14 @@ protected function Executed(CallData arguments, EPlayer instigator) // 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")); if (arguments.subCommandName.IsEmpty()) { DisplayUserGroups(); } - else if (arguments.subCommandName.Compare(P("show"), SCASE_SENSITIVE)) { - DisplayUserGroupsWithUsers(); + else if (arguments.subCommandName.Compare(P("list"), SCASE_SENSITIVE)) { + DisplayUserGroupsWithUsers(groups); } else if (arguments.subCommandName.Compare(P("add"), SCASE_SENSITIVE)) { AddGroup(groupName); @@ -75,78 +102,124 @@ protected function Executed(CallData arguments, EPlayer instigator) RemoveGroup(groupName); } else if (arguments.subCommandName.Compare(P("adduser"), SCASE_SENSITIVE)) { - AddUser(groupName, userID, annotation); + 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 AddUser( - BaseText groupName, - BaseText textUserID, - BaseText annotation) +private function bool ValidateGroupExistence(BaseText groupName) { - local bool userInGroup; - local UserID id; + 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; +} - if (groupName == none) return; - if (textUserID == none) return; +private function bool ValidateUserID(BaseText textUserID) +{ + local int i; - id = UserID(_.memory.Allocate(class'UserID')); - id.Initialize(textUserID); - if (_.users.IsUserIDInGroup(id, groupName)) + 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)) { - userInGroup = true; callerConsole - .Write(P("User ")) + .Write(P("User id specified as ")) .UseColorOnce(_.color.Gray) - .Write(textUserID) + .Write(userSpecifiedID) .UseColorOnce(_.color.TextFailure) .Write(P(" is already in the group ")) .UseColorOnce(_.color.TextEmphasis) .Write(groupName) .WriteLine(P("!")); } - else if (_.users.AddUserIDToGroup(id, groupName)) + else if (_.users.AddUserIDToGroup(userID, groupName)) { - userInGroup = true; callerConsole - .Write(F("{$TextPositive Added} user ")) + .Write(F("{$TextPositive Added} user id specified as ")) .UseColorOnce(_.color.Gray) - .Write(textUserID) + .Write(userSpecifiedID) .Write(P(" to the group ")) .UseColorOnce(_.color.TextEmphasis) .Write(groupName) .WriteLine(P("!")); } - else { - // One of the reasons - NO GROUP + else + { callerConsole .UseColorOnce(_.color.TextFailure) .Write(P("Failed (for unknown reason)")) - .Write(P(" to add user ")) - .UseColorOnce(_.color.Gray) - .Write(textUserID) + .Write(P(" to add user id ")) + .UseColorOnce(_.color.Gray).Write(userSpecifiedID) .Write(P(" to the group ")) - .UseColorOnce(_.color.TextEmphasis) - .Write(groupName) + .UseColorOnce(_.color.TextEmphasis).Write(groupName) .WriteLine(P("!")); + return false; } - if (!userInGroup || annotation == none) { - return; - } - _.users.SetAnnotationForUserID(groupName, id, annotation); - _.memory.Free(id); + return true; +} + +private function DisplayAnnotation( + BaseText userSpecifiedName, + BaseText groupName, + BaseText annotation) +{ callerConsole - .Write(P("Annotation for user ")) + .Write(P("Annotation for user id specified as ")) .UseColorOnce(_.color.Gray) - .Write(textUserID) + .Write(userSpecifiedName) .UseColorOnce(_.color.TextPositive) .Write(P(" in the group ")) .UseColorOnce(_.color.TextEmphasis) @@ -156,82 +229,212 @@ private function AddUser( .WriteLine(annotation); } -private function RemoveUser(BaseText groupName, BaseText userName) +private function AddOrAnnotateUser( + BaseText groupName, + BaseText textUserID, + BaseText annotation, + bool forceOption) { - local int i; - local UserID idFromName, idToRemove; - local array annotatedUsers; + local UserID id; - if (groupName == none) return; - if (userName == none) return; + if (groupName == none) return; + if (textUserID == none) return; + if (!ValidateGroupExistence(groupName)) return; + if (!forceOption && !ValidateUserID(textUserID)) return; - idFromName = UserID(_.memory.Allocate(class'UserID')); - idFromName.Initialize(userName); - annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName); - if (idFromName.IsInitialized()) + id = UserID(_.memory.Allocate(class'UserID')); + id.Initialize(textUserID); + if (!TryAddingUserID(groupName, id, textUserID) || annotation == none) { - for (i = 0; i < annotatedUsers.length; i += 1) - { - if (idFromName.IsEqual(annotatedUsers[i].id)) - { - idToRemove = annotatedUsers[i].id; - break; - } - } + _.memory.Free(id); + return; } - _.memory.Free(idFromName); - if (idToRemove == none) + _.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) { - for (i = 0; i < annotatedUsers.length; i += 1) + nextPlayer = EPlayer(players.GetItem(i)); + if (nextPlayer == none) { + continue; + } + playerName = nextPlayer.GetName(); + nextID = nextPlayer.GetUserID(); + if (TryAddingUserID(groupName, nextID, playerName)) { - if (userName.Compare( - annotatedUsers[i].annotation, - SCASE_INSENSITIVE)) - { - idToRemove = annotatedUsers[i].id; - break; + 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; } - if (idToRemove == none) +} + +private function TryRemovingUserID( + BaseText groupName, + UserID idToRemove, + BaseText userSpecifiedName) +{ + local Text idAsText; + + idAsText = idToRemove.GetUniqueID(); + if (_.users.RemoveUserIDFromGroup(idToRemove, groupName)) { callerConsole - .Write(P("User ")) + .Write(F("{$TextNegative Removed} user ")) .UseColorOnce(_.color.Gray) - .Write(userName) - .UseColorOnce(_.color.TextFailure) - .Write(P(" doesn't belong to the group ")) + .Write(userSpecifiedName) + .Write(P(" (with id ")) + .UseColorOnce(_.color.Gray) + .Write(idAsText) + .Write(P(") from the group ")) .UseColorOnce(_.color.TextEmphasis) .Write(groupName) .WriteLine(P("!")); } - else if (_.users.RemoveUserIDFromGroup(idToRemove, groupName)) + else { callerConsole - .Write(F("{$TextNegative Removed} user ")) + .UseColorOnce(_.color.TextFailure) + .Write(P("Failed (for unknown reason)")) + .Write(P("to remove user with id ")) .UseColorOnce(_.color.Gray) - .Write(userName) + .Write(idAsText) .Write(P(" from the group ")) .UseColorOnce(_.color.TextEmphasis) .Write(groupName) - .WriteLine(P("!")); + .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 - .UseColorOnce(_.color.TextFailure) - .Write(P("Failed (for unknown reason)")) - .Write(P("to remove user ")) + .Write(P("User ")) .UseColorOnce(_.color.Gray) .Write(userName) - .Write(P(" from the group ")) + .UseColorOnce(_.color.TextFailure) + .Write(P(" doesn't belong to the group ")) .UseColorOnce(_.color.TextEmphasis) .Write(groupName) - .WriteLine(P(".")); + .WriteLine(P("!")); } - for (i = 0; i < annotatedUsers.length; i += 1) +} + +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) { - _.memory.Free(annotatedUsers[i].id); - _.memory.Free(annotatedUsers[i].annotation); + 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; } } @@ -346,9 +549,36 @@ private function bool ValidateUsersFeature() return false; } -private function DisplayUserGroupsWithUsers() +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()) { @@ -363,15 +593,22 @@ private function DisplayUserGroupsWithUsers() } for (i = 0; i < availableGroups.length; i += 1) { - callerConsole - .Write(P("User group ")) - .UseColorOnce(_.color.TextEmphasis) - .Write(availableGroups[i]) - .WriteLine(P(":")); - DisplayUsersFor(availableGroups[i]); + 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) diff --git a/sources/Users/User.uc b/sources/Users/User.uc index c5d2a76..6a9b1ed 100644 --- a/sources/Users/User.uc +++ b/sources/Users/User.uc @@ -435,7 +435,7 @@ private function bool SetupDatabaseVariables() // Try making skeleton database userTextID = id.GetSteamID64String(); userDataLink = _.users.GetPersistentDataLink(); - persistentSettingsPointer = _.db.GetPointer(userDataLink); + persistentSettingsPointer = __core().db.GetPointer(userDataLink); persistentSettingsPointer.Push(P("PerUserData")); persistentSettingsPointer.Push(userTextID); MakeSkeletonUserDatabase(userTextID, persistentSettingsPointer); diff --git a/sources/Users/UserAPI.uc b/sources/Users/UserAPI.uc index fbfdd92..978896a 100644 --- a/sources/Users/UserAPI.uc +++ b/sources/Users/UserAPI.uc @@ -38,7 +38,7 @@ var private LoggerAPI.Definition infoPersistentDatabaseLoaded; protected function Constructor() { - SetupUserDataDatabase(); + //SetupUserDataDatabase(); } // DO NOT CALL MANUALLY @@ -78,11 +78,10 @@ private function SetupUserDataDatabase() return; } // If link was specified - try loading database from it - persistentDatabase = _.db.Load(persistentDataLink); + persistentDatabase = __core().db.Load(persistentDataLink); if (persistentDatabase == none) { _.logger.Auto(errNoPersistentDatabase).Arg(persistentDataLink); - persistentDataLink.FreeSelf(); return; } // Write skeleton database's skeleton @@ -90,7 +89,7 @@ private function SetupUserDataDatabase() emptyObject = _.collections.EmptyHashTable(); skeleton.SetItem(P("Groups"), emptyObject); skeleton.SetItem(P("PerUserData"), emptyObject); - persistentDataPointer = _.db.GetPointer(persistentDataLink); + persistentDataPointer = __core().db.GetPointer(persistentDataLink); persistentDatabase .IncrementData(persistentDataPointer, skeleton) .connect = ReportSkeletonCreationResult; @@ -1531,6 +1530,6 @@ defaultproperties userdataDBLink = "[local]database:/users" warnNoPersistentDatabaseLink = (l=LOG_Warning,m="No persistent user database link is setup. No persistent user data or user groups will be available. Setup `userDataDBLink` inside \"AcediaSystem.ini\".") errCannotCreateSkeletonFor = (l=LOG_Error,m="Failed to create persistent database skeleton for connected database with link \"%1\". User data functionality won't function properly.") - errNoPersistentDatabase = (l=LOG_Error,m="Failed to connect to persistent user database with link \"%1\").") + errNoPersistentDatabase = (l=LOG_Error,m="Failed to connect to persistent user database with link \"%1\".") infoPersistentDatabaseLoaded = (l=LOG_Info,m="Connected to persistent user database with link \"%1\".") } \ No newline at end of file diff --git a/sources/Users/UserID.uc b/sources/Users/UserID.uc index e89e903..d0b3322 100644 --- a/sources/Users/UserID.uc +++ b/sources/Users/UserID.uc @@ -93,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) { diff --git a/sources/Users/Users_Feature.uc b/sources/Users/Users_Feature.uc index 27fe3fd..7f398c6 100644 --- a/sources/Users/Users_Feature.uc +++ b/sources/Users/Users_Feature.uc @@ -42,10 +42,11 @@ struct IDAnnotationPair // List of all available user groups for current config var private array loadedUserGroups; // `HashTable` (with group name keys) that stores `HashTable`s used as -// a set data structure (has user id as keys and always `none` as a value). +// a set data structure (has user id as keys and annotation as a value). var private HashTable loadedGroupToUsersMap; -var private LoggerAPI.Definition warnNoLocalGroup, errCannotCreateLocalGroup; +var private LoggerAPI.Definition warnNoLocalGroup, warnDuplicateIDs; +var private LoggerAPI.Definition errCannotCreateLocalGroup; protected function OnEnabled() { @@ -172,6 +173,13 @@ private final function bool LoadLocalGroup( 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); @@ -187,23 +195,30 @@ private final function bool LoadLocalGroup( private final function IDAnnotationPair ParseConfigUserName( string configUserName) { - local Parser parser; - local MutableText parsingResult; + local int lastSlashIndex; + local Text userAnnotation; + local MutableText userNameAsText; local IDAnnotationPair result; - local Text.Character slashSeparator; - parser = _.text.ParseString(configUserName); - slashSeparator = _.text.GetCharacter("/"); - if (parser.MUntil(parsingResult, slashSeparator).Match(P("/")).Ok()) { - result.annotation = parser.GetRemainderM().IntoText(); - } - result.id = parsingResult.IntoText(); - if (result.annotation != none && result.annotation.IsEmpty()) + userNameAsText = _.text.FromStringM(configUserName); + lastSlashIndex = userNameAsText.IndexOf(P("/")); + if (lastSlashIndex >= 0 && lastSlashIndex + 1 < userNameAsText.GetLength()) { - result.annotation.FreeSelf(); - result.annotation = none; + 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(); } - parser.FreeSelf(); + userNameAsText.FreeSelf(); return result; } @@ -1870,5 +1885,6 @@ 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\".") } \ No newline at end of file