Browse Source

A fuckload of changes, need to rebase anyway

pull/8/head
Anton Tarasenko 2 years ago
parent
commit
ec567d51dc
  1. 4
      sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc
  2. 7
      sources/BaseRealm/Global.uc
  3. 4
      sources/ClientRealm/ClientAcediaAdapter.uc
  4. 5
      sources/ClientRealm/ClientGlobal.uc
  5. 4
      sources/CoreRealm/AcediaAdapter.uc
  6. 28
      sources/CoreRealm/CoreGlobal.uc
  7. 2
      sources/CoreRealm/Features/Commands/BuiltInCommands/ACommandHelp.uc
  8. 4
      sources/Data/Database/DBAPI.uc
  9. 16
      sources/Data/Database/Local/DBRecord.uc
  10. 8
      sources/Data/Database/Local/LocalDatabaseInstance.uc
  11. 10
      sources/Data/Database/Tests/TEST_DatabaseCommon.uc
  12. 42
      sources/Data/Database/Tests/TEST_LocalDatabase.uc
  13. 26
      sources/Players/EPlayer.uc
  14. 4
      sources/ServerRealm/ServerAcediaAdapter.uc
  15. 7
      sources/ServerRealm/ServerGlobal.uc
  16. 23
      sources/Types/AcediaActor.uc
  17. 23
      sources/Types/AcediaObject.uc
  18. 389
      sources/Users/ACommandUserGroups.uc
  19. 2
      sources/Users/User.uc
  20. 9
      sources/Users/UserAPI.uc
  21. 4
      sources/Users/UserID.uc
  22. 46
      sources/Users/Users_Feature.uc

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

7
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;

4
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<InteractionAPI> clientInteractionAPIClass;
defaultproperties
{
sideEffectAPIClass = class'KF1_SideEffectAPI'
timeAPIClass = class'KF1_TimeAPI'
dbAPIClass = class'DBAPI'
clientUnrealAPIClass = class'KF1_ClientUnrealAPI'
clientInteractionAPIClass = class'KF1_InteractionAPI'
}

5
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)

4
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<SideEffectAPI> sideEffectAPIClass;
var public const class<TimeAPI> timeAPIClass;
var public const class<DBAPI> dbAPIClass;
defaultproperties
{
sideEffectAPIClass = class'KF1_SideEffectAPI'
}

28
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<AcediaAdapter> 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.
*
@ -54,6 +79,7 @@ protected function Initialize()
sideEffects =
SideEffectAPI(api.Allocate(adapterClass.default.sideEffectAPIClass));
time = TimeAPI(api.Allocate(adapterClass.default.timeAPIClass));
db = DBAPI(api.Allocate(adapterClass.default.dbAPIClass));
}
/**

2
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);

4
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.

16
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;

8
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);
}
}

10
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");
}

42
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)

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

4
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<MutatorAPI> 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'

7
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)

23
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.

23
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.

389
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 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;
return true;
}
_.users.SetAnnotationForUserID(groupName, id, annotation);
_.memory.Free(id);
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,44 +229,160 @@ private function AddUser(
.WriteLine(annotation);
}
private function RemoveUser(BaseText groupName, BaseText userName)
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 UserID idFromName, idToRemove;
local array<Users_Feature.AnnotatedUserID> annotatedUsers;
local BaseText playerName, nextAnnotation;
local EPlayer nextPlayer;
local UserID nextID;
if (groupName == none) return;
if (userName == none) return;
if (players == none) return;
if (!ValidateGroupExistence(groupName)) return;
idFromName = UserID(_.memory.Allocate(class'UserID'));
idFromName.Initialize(userName);
annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName);
if (idFromName.IsInitialized())
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 (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)
{
if (idFromName.IsEqual(annotatedUsers[i].id))
local Text idAsText;
idAsText = idToRemove.GetUniqueID();
if (_.users.RemoveUserIDFromGroup(idToRemove, groupName))
{
idToRemove = annotatedUsers[i].id;
break;
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);
}
_.memory.Free(idFromName);
if (idToRemove == none)
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))
if (userName.Compare(annotatedUsers[i].annotation, SCASE_INSENSITIVE))
{
idToRemove = annotatedUsers[i].id;
break;
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);
}
if (idToRemove == none)
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 "))
@ -205,33 +394,47 @@ private function RemoveUser(BaseText groupName, BaseText userName)
.Write(groupName)
.WriteLine(P("!"));
}
else if (_.users.RemoveUserIDFromGroup(idToRemove, groupName))
}
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(F("{$TextNegative Removed} user "))
.Write(P("Player "))
.UseColorOnce(_.color.Gray)
.Write(userName)
.Write(P(" from the group "))
.Write(playerName)
.Write(F(" {$TextFailure doesn't belong} to 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 "))
.UseColorOnce(_.color.Gray)
.Write(userName)
.Write(P(" from the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P("."));
TryRemovingUserID(groupName, nextID, playerName);
}
for (i = 0; i < annotatedUsers.length; i += 1)
{
_.memory.Free(annotatedUsers[i].id);
_.memory.Free(annotatedUsers[i].annotation);
_.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<Text> availableGroups;
if (!ValidateUsersFeature()) {
@ -363,6 +593,9 @@ private function DisplayUserGroupsWithUsers()
}
for (i = 0; i < availableGroups.length; i += 1)
{
if (IsGroupSpecified(specifiedGroups, availableGroups[i]))
{
displayedGroup = true;
callerConsole
.Write(P("User group "))
.UseColorOnce(_.color.TextEmphasis)
@ -370,8 +603,12 @@ private function DisplayUserGroupsWithUsers()
.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)

2
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);

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

4
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) {

46
sources/Users/Users_Feature.uc

@ -42,10 +42,11 @@ struct IDAnnotationPair
// List of all available user groups for current config
var private array<Text> 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\".")
}
Loading…
Cancel
Save