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. 393
      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. * 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. * This file is part of Acedia.
* *
@ -520,7 +520,7 @@ defaultproperties
manifestSuffix = ".Manifest" manifestSuffix = ".Manifest"
infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".") infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".")
infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.") 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.") 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.") 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 * 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 * 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. * 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. * This file is part of Acedia.
* *
@ -39,7 +39,6 @@ var public ColorAPI color;
var public UserAPI users; var public UserAPI users;
var public PlayersAPI players; var public PlayersAPI players;
var public JSONAPI json; var public JSONAPI json;
var public DBAPI db;
var public SchedulerAPI scheduler; var public SchedulerAPI scheduler;
var public AvariceAPI avarice; var public AvariceAPI avarice;
@ -68,6 +67,7 @@ protected function Initialize()
text = TextAPI(memory.Allocate(class'TextAPI')); text = TextAPI(memory.Allocate(class'TextAPI'));
math = MathAPI(memory.Allocate(class'MathAPI')); math = MathAPI(memory.Allocate(class'MathAPI'));
collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI')); collections = CollectionsAPI(memory.Allocate(class'CollectionsAPI'));
json = JSONAPI(memory.Allocate(class'JSONAPI'));
logger = LoggerAPI(memory.Allocate(class'LoggerAPI')); logger = LoggerAPI(memory.Allocate(class'LoggerAPI'));
color = ColorAPI(memory.Allocate(class'ColorAPI')); color = ColorAPI(memory.Allocate(class'ColorAPI'));
alias = AliasesAPI(memory.Allocate(class'AliasesAPI')); alias = AliasesAPI(memory.Allocate(class'AliasesAPI'));
@ -75,8 +75,6 @@ protected function Initialize()
chat = ChatAPI(memory.Allocate(class'ChatAPI')); chat = ChatAPI(memory.Allocate(class'ChatAPI'));
users = UserAPI(memory.Allocate(class'UserAPI')); users = UserAPI(memory.Allocate(class'UserAPI'));
players = PlayersAPI(memory.Allocate(class'PlayersAPI')); players = PlayersAPI(memory.Allocate(class'PlayersAPI'));
json = JSONAPI(memory.Allocate(class'JSONAPI'));
db = DBAPI(memory.Allocate(class'DBAPI'));
scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI')); scheduler = SchedulerAPI(memory.Allocate(class'SchedulerAPI'));
avarice = AvariceAPI(memory.Allocate(class'AvariceAPI')); avarice = AvariceAPI(memory.Allocate(class'AvariceAPI'));
environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment')); environment = AcediaEnvironment(memory.Allocate(class'AcediaEnvironment'));
@ -97,7 +95,6 @@ public function DropCoreAPI()
users = none; users = none;
players = none; players = none;
json = none; json = none;
db = none;
scheduler = none; scheduler = none;
avarice = none; avarice = none;
default.myself = 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 * 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 * server-specific functionality by giving a reference to this object to all
* Acedia's objects and actors, emulating a global API namespace. * 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. * This file is part of Acedia.
* *
@ -27,7 +27,9 @@ var public const class<InteractionAPI> clientInteractionAPIClass;
defaultproperties defaultproperties
{ {
sideEffectAPIClass = class'KF1_SideEffectAPI'
timeAPIClass = class'KF1_TimeAPI' timeAPIClass = class'KF1_TimeAPI'
dbAPIClass = class'DBAPI'
clientUnrealAPIClass = class'KF1_ClientUnrealAPI' clientUnrealAPIClass = class'KF1_ClientUnrealAPI'
clientInteractionAPIClass = class'KF1_InteractionAPI' clientInteractionAPIClass = class'KF1_InteractionAPI'
} }

5
sources/ClientRealm/ClientGlobal.uc

@ -29,6 +29,11 @@ var public ClientUnrealAPI unreal;
var private LoggerAPI.Definition fatBadAdapterClass, errNoInteraction; var private LoggerAPI.Definition fatBadAdapterClass, errNoInteraction;
public function UnrealAPI unreal_api()
{
return unreal;
}
public final static function ClientGlobal GetInstance() public final static function ClientGlobal GetInstance()
{ {
if (default.myself == none) 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 * Base class for describing what API Acedia should load into its client- and
* server- `...Global`s objects. * server- `...Global`s objects.
* Copyright 2022 Anton Tarasenko * Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -37,8 +37,8 @@ class AcediaAdapter extends AcediaObject
var public const class<SideEffectAPI> sideEffectAPIClass; var public const class<SideEffectAPI> sideEffectAPIClass;
var public const class<TimeAPI> timeAPIClass; var public const class<TimeAPI> timeAPIClass;
var public const class<DBAPI> dbAPIClass;
defaultproperties 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 * 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 * server-specific functionality by giving a reference to this object to all
* Acedia's objects and actors, emulating a global API namespace. * 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. * This file is part of Acedia.
* *
@ -26,9 +26,34 @@ var protected class<AcediaAdapter> adapterClass;
var public SideEffectAPI sideEffects; var public SideEffectAPI sideEffects;
var public TimeAPI time; var public TimeAPI time;
var public DBAPI db;
var private LoggerAPI.Definition fatNoAdapterClass; 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. * This method must perform initialization of the caller `...Global` instance.
* *
@ -54,6 +79,7 @@ protected function Initialize()
sideEffects = sideEffects =
SideEffectAPI(api.Allocate(adapterClass.default.sideEffectAPIClass)); 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));
} }
/** /**

2
sources/CoreRealm/Features/Commands/BuiltInCommands/ACommandHelp.uc

@ -178,6 +178,8 @@ private final function FillCommandToAliasesMap(Feature enabledFeature)
InsertIntoAliasesMap(commandName, subcommandName, availableAliases[i]); InsertIntoAliasesMap(commandName, subcommandName, availableAliases[i]);
commandName.FreeSelf(); commandName.FreeSelf();
subcommandName.FreeSelf(); subcommandName.FreeSelf();
commandName = none;
subcommandName = none;
} }
// Clean up // Clean up
_.memory.FreeMany(availableAliases); _.memory.FreeMany(availableAliases);

4
sources/Data/Database/DBAPI.uc

@ -1,7 +1,7 @@
/** /**
* API that provides methods for creating/destroying and managing available * API that provides methods for creating/destroying and managing available
* databases. * databases.
* Copyright 2021-2022 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -279,7 +279,7 @@ private function EraseAllPackageData(BaseText packageToErase)
if (packageName == "") { if (packageName == "") {
return; return;
} }
game = _server.unreal.GetGameType(); game = __core().unreal_api().GetGameType();
game.DeletePackage(packageName); game.DeletePackage(packageName);
// Delete any leftover objects. This has to be done *after* // Delete any leftover objects. This has to be done *after*
// `DeletePackage()` call, otherwise removed garbage can reappear. // `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 * 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 * the local Acedia database. It is supposed to be saved and loaded
* to / from packages. * to / from packages.
* Copyright 2021-2022 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -264,9 +264,9 @@ public static final function Global __()
return class'Global'.static.GetInstance(); 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) { if (recordCandidate != none) {
continue; continue;
} }
recordCandidate = __server().unreal.GetGameType() recordCandidate = __core().unreal_api().GetGameType()
.CreateDataObject(class'DBRecord', nextName, dbPackageName); .CreateDataObject(class'DBRecord', nextName, dbPackageName);
recordCandidate.package = dbPackageName; recordCandidate.package = dbPackageName;
return recordCandidate; return recordCandidate;
@ -330,7 +330,7 @@ private final static function DBRecord LoadRecordFor(
string name, string name,
string package) string package)
{ {
return __server().unreal.GetGameType() return __core().unreal_api().GetGameType()
.LoadDataObject(class'DBRecord', name, package); .LoadDataObject(class'DBRecord', name, package);
} }
@ -689,7 +689,7 @@ private final function SetItem(
if (oldRecord != none) { if (oldRecord != none) {
oldRecord.EmptySelf(); oldRecord.EmptySelf();
} }
__server().unreal.GetGameType() __core().unreal_api().GetGameType()
.DeleteDataObject(class'DBRecord', oldItem.s, package); .DeleteDataObject(class'DBRecord', oldItem.s, package);
} }
} }
@ -723,7 +723,7 @@ private final function RemoveItem(int index)
if (oldRecord != none) { if (oldRecord != none) {
oldRecord.EmptySelf(); oldRecord.EmptySelf();
} }
__server().unreal.GetGameType() __core().unreal_api().GetGameType()
.DeleteDataObject(class'DBRecord', oldItem.s, package); .DeleteDataObject(class'DBRecord', oldItem.s, package);
} }
storage.Remove(index, 1); storage.Remove(index, 1);
@ -872,7 +872,7 @@ public final function EmptySelf()
return; return;
} }
lockEraseSelf = true; lockEraseSelf = true;
game = __server().unreal.GetGameType(); game = __core().unreal_api().GetGameType();
for (i = 0; i < storage.length; i += 1) for (i = 0; i < storage.length; i += 1)
{ {
if (storage[i].t != DBAT_Reference) continue; 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 class SHOULD NOT be deallocated manually.
* This name was chosen so that more readable `LocalDatabase` could be * This name was chosen so that more readable `LocalDatabase` could be
* used in config for defining local databases through per-object-config. * 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. * This file is part of Acedia.
* *
@ -83,7 +83,7 @@ var private int lastTaskLifeVersion;
protected function Constructor() protected function Constructor()
{ {
_server.unreal.OnTick(self).connect = CompleteAllTasks; __core().unreal_api().OnTick(self).connect = CompleteAllTasks;
} }
protected function Finalizer() protected function Finalizer()
@ -93,7 +93,7 @@ protected function Finalizer()
CompleteAllTasks(); CompleteAllTasks();
WriteToDisk(); WriteToDisk();
rootRecord = none; rootRecord = none;
_server.unreal.OnTick(self).Disconnect(); __core().unreal_api().OnTick(self).Disconnect();
configEntry = none; configEntry = none;
} }
@ -130,7 +130,7 @@ public final function WriteToDisk()
packageName = _.text.IntoString(configEntry.GetPackageName()); packageName = _.text.IntoString(configEntry.GetPackageName());
} }
if (packageName != "") { 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; local JSONPointer pointer;
Context("Testing extracting `JSONPointer` from database link."); Context("Testing extracting `JSONPointer` from database link.");
Issue("`JSONPointer` is incorrectly extracted."); Issue("`JSONPointer` is incorrectly extracted.");
pointer = __().db.GetPointer( pointer = __core().db.GetPointer(
__().text.FromString("[local]default:/huh/what/is/")); __().text.FromString("[local]default:/huh/what/is/"));
TEST_ExpectNotNone(pointer); TEST_ExpectNotNone(pointer);
TEST_ExpectTrue(pointer.ToText().ToString() == "/huh/what/is/"); 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_ExpectNotNone(pointer);
TEST_ExpectTrue(pointer.ToText().ToString() == ""); TEST_ExpectTrue(pointer.ToText().ToString() == "");
pointer = __().db.GetPointer(__().text.FromString("[remote]:")); pointer = __core().db.GetPointer(__().text.FromString("[remote]:"));
TEST_ExpectNotNone(pointer); TEST_ExpectNotNone(pointer);
TEST_ExpectTrue(pointer.ToText().ToString() == ""); 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_ExpectNotNone(pointer);
TEST_ExpectTrue(pointer.ToText().ToString() == "/just/a/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_ExpectNotNone(pointer);
TEST_ExpectTrue(pointer.ToText().ToString() == "/just/a/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(); source = GetJSONTemplateString();
parser = __().text.ParseString(source); parser = __().text.ParseString(source);
root = HashTable(__().json.ParseWith(parser)); root = HashTable(__().json.ParseWith(parser));
db = __().db.NewLocal(P("TEST_ReadOnly")); db = __core().db.NewLocal(P("TEST_ReadOnly"));
db.WriteData(__().json.Pointer(), root); db.WriteData(__().json.Pointer(), root);
*/ */
protected static function string GetJSONTemplateString() protected static function string GetJSONTemplateString()
@ -225,14 +225,14 @@ protected static function TESTS()
protected static function Test_LoadingPrepared() protected static function Test_LoadingPrepared()
{ {
local LocalDatabaseInstance db; 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."); Context("Testing reading prepared data from the local database.");
Issue("Existing database reported as missing."); 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" Issue("Loading same database several times produces different"
@ "`LocalDatabaseInstance` objects."); @ "`LocalDatabaseInstance` objects.");
TEST_ExpectTrue(__().db.LoadLocal(P("TEST_ReadOnly")) == db); TEST_ExpectTrue(__core().db.LoadLocal(P("TEST_ReadOnly")) == db);
// Groups of read-only tests // Groups of read-only tests
SubTest_LoadingPreparedSuccessRoot(db); SubTest_LoadingPreparedSuccessRoot(db);
SubTest_LoadingPreparedSuccessSubValues(db); SubTest_LoadingPreparedSuccessSubValues(db);
@ -471,18 +471,18 @@ protected static function SubTest_LoadingPreparedGetKeysFail(
protected static function Test_Writing() protected static function Test_Writing()
{ {
local LocalDatabaseInstance db; 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."); Context("Testing (re-)creating and writing into a new local database.");
Issue("Cannot create a new database."); Issue("Cannot create a new database.");
TEST_ExpectNotNone(db); 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."); Issue("Freshly created database is not empty.");
TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 1); // 1 root object TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 1); // 1 root object
Issue("Loading just created database produces different" Issue("Loading just created database produces different"
@ "`LocalDatabaseInstance` object."); @ "`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 // This set of tests fills our test database with objects
SubTest_WritingSuccess(db); SubTest_WritingSuccess(db);
SubTest_WritingDataCheck(db); SubTest_WritingDataCheck(db);
@ -495,33 +495,33 @@ protected static function Test_Writing()
@ "local database."); @ "local database.");
__().memory.Free(db); // For `NewLocal()` call __().memory.Free(db); // For `NewLocal()` call
__().memory.Free(db); // For `LoadLocal()` 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."); 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()); TEST_ExpectFalse(db.IsAllocated());
Issue("`DeleteLocal()` does not return `false` after trying to delete" Issue("`DeleteLocal()` does not return `false` after trying to delete"
@ "non-existing local database."); @ "non-existing local database.");
TEST_ExpectFalse(__().db.DeleteLocal(P("TEST_DB"))); TEST_ExpectFalse(__core().db.DeleteLocal(P("TEST_DB")));
} }
protected static function Test_Recreate() protected static function Test_Recreate()
{ {
local LocalDatabaseInstance db; local LocalDatabaseInstance db;
Issue("Freshly created database is not empty."); 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); TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 1);
Issue("Cannot create a database after database with the same name was" Issue("Cannot create a database after database with the same name was"
@ "just deleted."); @ "just deleted.");
TEST_ExpectNotNone(db); TEST_ExpectNotNone(db);
TEST_ExpectTrue(__().db.ExistsLocal(P("TEST_DB"))); TEST_ExpectTrue(__core().db.ExistsLocal(P("TEST_DB")));
SubTest_WritingArrayIndicies(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."); Issue("Newly created database is reported to still exist after deletion.");
__().memory.Free(db); __().memory.Free(db);
TEST_ExpectFalse(__().db.ExistsLocal(P("TEST_DB"))); TEST_ExpectFalse(__core().db.ExistsLocal(P("TEST_DB")));
TEST_ExpectFalse(db.IsAllocated()); TEST_ExpectFalse(db.IsAllocated());
} }
@ -530,15 +530,15 @@ protected static function Test_TaskChaining()
local LocalDatabaseInstance db; local LocalDatabaseInstance db;
Context("Testing (re-)creating and writing into a new local database."); Context("Testing (re-)creating and writing into a new local database.");
Issue("Freshly created database is not empty."); 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); TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 1);
Issue("Cannot create a database after database with the same name was" Issue("Cannot create a database after database with the same name was"
@ "just deleted."); @ "just deleted.");
TEST_ExpectNotNone(db); TEST_ExpectNotNone(db);
TEST_ExpectTrue(__().db.ExistsLocal(P("TEST_DB"))); TEST_ExpectTrue(__core().db.ExistsLocal(P("TEST_DB")));
SubTest_TaskChaining(db); SubTest_TaskChaining(db);
__().db.DeleteLocal(P("TEST_DB")); __core().db.DeleteLocal(P("TEST_DB"));
} }
protected static function HashTable GetJSONSubTemplateObject() protected static function HashTable GetJSONSubTemplateObject()
@ -776,7 +776,7 @@ protected static function Test_Removal()
local HashTable templateObject; local HashTable templateObject;
templateObject = GetJSONSubTemplateObject(); templateObject = GetJSONSubTemplateObject();
templateArray = GetJSONSubTemplateArray(); 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("")), templateObject);
db.WriteData(__().json.Pointer(P("/B")), templateObject); db.WriteData(__().json.Pointer(P("/B")), templateObject);
db.WriteData(__().json.Pointer(P("/B/A")), templateArray); db.WriteData(__().json.Pointer(P("/B/A")), templateArray);
@ -787,7 +787,7 @@ protected static function Test_Removal()
SubTest_RemovalResult(db); SubTest_RemovalResult(db);
SubTest_RemovalCheckValuesAfter(db); SubTest_RemovalCheckValuesAfter(db);
SubTest_RemovalRoot(db); SubTest_RemovalRoot(db);
__().db.DeleteLocal(P("TEST_DB")); __core().db.DeleteLocal(P("TEST_DB"));
} }
protected static function SubTest_RemovalResult(LocalDatabaseInstance db) protected static function SubTest_RemovalResult(LocalDatabaseInstance db)
@ -861,7 +861,7 @@ protected static function Test_Increment()
local HashTable templateObject; local HashTable templateObject;
templateObject = GetJSONSubTemplateObject(); templateObject = GetJSONSubTemplateObject();
templateArray = GetJSONSubTemplateArray(); 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("")), templateObject);
db.WriteData(__().json.Pointer(P("/B")), templateObject); db.WriteData(__().json.Pointer(P("/B")), templateObject);
db.WriteData(__().json.Pointer(P("/C")), __().box.int(-5)); 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."); Issue("Incrementing database values has created garbage objects.");
// 5 initial records + 1 made for a new array in `SubTest_IncrementNull()` // 5 initial records + 1 made for a new array in `SubTest_IncrementNull()`
TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 6); TEST_ExpectTrue(CountRecordsInPackage("TEST_DB") == 6);
__().db.DeleteLocal(P("TEST_DB")); __core().db.DeleteLocal(P("TEST_DB"));
} }
protected static function SubTest_IncrementNull(LocalDatabaseInstance 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. * 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. * 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`. * Returns `User` object that is corresponding to the caller `EPlayer`.
* *
* @return `User` corresponding to the caller `EPlayer`. Guarantee to be * @return `User` corresponding to the caller `EPlayer`. Guaranteed to not be
* not `none` for correctly initialized `EPlayer` (it remembers `User` * `none` for correctly initialized `EPlayer` (it remembers `User` record
* record even if player has disconnected). * even if player has disconnected).
*/ */
public final function User GetIdentity() public final function User GetIdentity()
{ {
if (identity != none) {
identity.NewRef();
}
return identity; 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. * 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 * 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 * server-specific functionality by giving a reference to this object to all
* Acedia's objects and actors, emulating a global API namespace. * 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. * This file is part of Acedia.
* *
@ -30,7 +30,9 @@ var public const class<MutatorAPI> serverMutatorAPIClass;
defaultproperties defaultproperties
{ {
sideEffectAPIClass = class'KF1_SideEffectAPI'
timeAPIClass = class'KF1_TimeAPI' timeAPIClass = class'KF1_TimeAPI'
dbAPIClass = class'DBAPI'
serverUnrealAPIClass = class'KF1_ServerUnrealAPI' serverUnrealAPIClass = class'KF1_ServerUnrealAPI'
serverBroadcastAPIClass = class'KF1_BroadcastAPI' serverBroadcastAPIClass = class'KF1_BroadcastAPI'
serverGameRulesAPIClass = class'KF1_GameRulesAPI' 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 * 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 * server-specific functionality by giving a reference to this object to all
* Acedia's objects and actors, emulating a global API namespace. * 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. * This file is part of Acedia.
* *
@ -30,6 +30,11 @@ var public ServerUnrealAPI unreal;
var private LoggerAPI.Definition fatBadAdapterClass; var private LoggerAPI.Definition fatBadAdapterClass;
public function UnrealAPI unreal_api()
{
return unreal;
}
public final static function ServerGlobal GetInstance() public final static function ServerGlobal GetInstance()
{ {
if (default.myself == none) if (default.myself == none)

23
sources/Types/AcediaActor.uc

@ -3,7 +3,7 @@
* `AcediaActor` provides access to Acedia's APIs through an accessor to * `AcediaActor` provides access to Acedia's APIs through an accessor to
* a `Global` object, built-in mechanism for storing unneeded references in * a `Global` object, built-in mechanism for storing unneeded references in
* an object pool and constructor/finalizer. * an object pool and constructor/finalizer.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -416,6 +416,27 @@ public simulated static final function Global __()
return class'Global'.static.GetInstance(); 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 * Static method accessor to server API namespace, necessary for Acedia's
* implementation. * implementation.

23
sources/Types/AcediaObject.uc

@ -3,7 +3,7 @@
* `AcediaObject` provides access to Acedia's APIs through an accessor to * `AcediaObject` provides access to Acedia's APIs through an accessor to
* a `Global` object, built-in mechanism for storing unneeded references in * a `Global` object, built-in mechanism for storing unneeded references in
* an object pool and constructor/finalizer. * an object pool and constructor/finalizer.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -489,6 +489,27 @@ public static final function Global __()
return class'Global'.static.GetInstance(); 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 * Static method accessor to server API namespace, necessary for Acedia's
* implementation. * implementation.

393
sources/Users/ACommandUserGroups.uc

@ -1,6 +1,6 @@
/** /**
* Command for displaying help information about registered Acedia's commands. * 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. * 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," @ "groups. Changes made by it will always affect current session,"
@ "but might fail to be saved in case user groups are stored in" @ "but might fail to be saved in case user groups are stored in"
@ "a database that is either corrupted or in read-only mode.")); @ "a database that is either corrupted or in read-only mode."));
builder.SubCommand(P("show")) builder.SubCommand(P("list"))
.Describe(P("Shows all groups along with users that belong to them.")); .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")) builder.SubCommand(P("add"))
.Describe(P("Adds a new group")) .Describe(P("Adds a new group"))
.ParamText(P("group_name")); .ParamText(P("group_name"));
@ -50,11 +54,31 @@ protected function BuildData(CommandDataBuilder builder)
@ "user's id or annotation, with id taking priority.")) @ "user's id or annotation, with id taking priority."))
.ParamText(P("group_name")) .ParamText(P("group_name"))
.ParamText(P("user_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) protected function Executed(CallData arguments, EPlayer instigator)
{ {
local bool forceOption;
local Text groupName, userID, userName, annotation; local Text groupName, userID, userName, annotation;
local ArrayList players, groups;
groupName = arguments.parameters.GetText(P("group_name")); groupName = arguments.parameters.GetText(P("group_name"));
// For parameters named `user_id`, can only be ID // 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 // For parameters named `user_id`, can be either ID or annotation
userName = arguments.parameters.GetText(P("user_name")); userName = arguments.parameters.GetText(P("user_name"));
annotation = arguments.parameters.GetText(P("annotation")); 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()) { if (arguments.subCommandName.IsEmpty()) {
DisplayUserGroups(); DisplayUserGroups();
} }
else if (arguments.subCommandName.Compare(P("show"), SCASE_SENSITIVE)) { else if (arguments.subCommandName.Compare(P("list"), SCASE_SENSITIVE)) {
DisplayUserGroupsWithUsers(); DisplayUserGroupsWithUsers(groups);
} }
else if (arguments.subCommandName.Compare(P("add"), SCASE_SENSITIVE)) { else if (arguments.subCommandName.Compare(P("add"), SCASE_SENSITIVE)) {
AddGroup(groupName); AddGroup(groupName);
@ -75,78 +102,124 @@ protected function Executed(CallData arguments, EPlayer instigator)
RemoveGroup(groupName); RemoveGroup(groupName);
} }
else if (arguments.subCommandName.Compare(P("adduser"), SCASE_SENSITIVE)) { 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)) else if (arguments.subCommandName.Compare(P("removeuser"), SCASE_SENSITIVE))
{ {
RemoveUser(groupName, userName); 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(groupName);
_.memory.Free(userID); _.memory.Free(userID);
_.memory.Free(userName); _.memory.Free(userName);
_.memory.Free(annotation); _.memory.Free(annotation);
_.memory.Free(players);
_.memory.Free(groups);
} }
private function AddUser( private function bool ValidateGroupExistence(BaseText groupName)
BaseText groupName,
BaseText textUserID,
BaseText annotation)
{ {
local bool userInGroup; if (_.users.IsGroupExisting(groupName)) {
local UserID id; 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; private function bool ValidateUserID(BaseText textUserID)
if (textUserID == none) return; {
local int i;
id = UserID(_.memory.Allocate(class'UserID')); if (textUserID == none) {
id.Initialize(textUserID); return false;
if (_.users.IsUserIDInGroup(id, groupName)) }
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 callerConsole
.Write(P("User ")) .Write(P("User id specified as "))
.UseColorOnce(_.color.Gray) .UseColorOnce(_.color.Gray)
.Write(textUserID) .Write(userSpecifiedID)
.UseColorOnce(_.color.TextFailure) .UseColorOnce(_.color.TextFailure)
.Write(P(" is already in the group ")) .Write(P(" is already in the group "))
.UseColorOnce(_.color.TextEmphasis) .UseColorOnce(_.color.TextEmphasis)
.Write(groupName) .Write(groupName)
.WriteLine(P("!")); .WriteLine(P("!"));
} }
else if (_.users.AddUserIDToGroup(id, groupName)) else if (_.users.AddUserIDToGroup(userID, groupName))
{ {
userInGroup = true;
callerConsole callerConsole
.Write(F("{$TextPositive Added} user ")) .Write(F("{$TextPositive Added} user id specified as "))
.UseColorOnce(_.color.Gray) .UseColorOnce(_.color.Gray)
.Write(textUserID) .Write(userSpecifiedID)
.Write(P(" to the group ")) .Write(P(" to the group "))
.UseColorOnce(_.color.TextEmphasis) .UseColorOnce(_.color.TextEmphasis)
.Write(groupName) .Write(groupName)
.WriteLine(P("!")); .WriteLine(P("!"));
} }
else { else
// One of the reasons - NO GROUP {
callerConsole callerConsole
.UseColorOnce(_.color.TextFailure) .UseColorOnce(_.color.TextFailure)
.Write(P("Failed (for unknown reason)")) .Write(P("Failed (for unknown reason)"))
.Write(P(" to add user ")) .Write(P(" to add user id "))
.UseColorOnce(_.color.Gray) .UseColorOnce(_.color.Gray).Write(userSpecifiedID)
.Write(textUserID)
.Write(P(" to the group ")) .Write(P(" to the group "))
.UseColorOnce(_.color.TextEmphasis) .UseColorOnce(_.color.TextEmphasis).Write(groupName)
.Write(groupName)
.WriteLine(P("!")); .WriteLine(P("!"));
return false;
} }
if (!userInGroup || annotation == none) { return true;
return; }
}
_.users.SetAnnotationForUserID(groupName, id, annotation); private function DisplayAnnotation(
_.memory.Free(id); BaseText userSpecifiedName,
BaseText groupName,
BaseText annotation)
{
callerConsole callerConsole
.Write(P("Annotation for user ")) .Write(P("Annotation for user id specified as "))
.UseColorOnce(_.color.Gray) .UseColorOnce(_.color.Gray)
.Write(textUserID) .Write(userSpecifiedName)
.UseColorOnce(_.color.TextPositive) .UseColorOnce(_.color.TextPositive)
.Write(P(" in the group ")) .Write(P(" in the group "))
.UseColorOnce(_.color.TextEmphasis) .UseColorOnce(_.color.TextEmphasis)
@ -156,44 +229,160 @@ private function AddUser(
.WriteLine(annotation); .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 id;
local UserID idFromName, idToRemove;
local array<Users_Feature.AnnotatedUserID> annotatedUsers;
if (groupName == none) return; if (groupName == none) return;
if (userName == none) return; if (textUserID == none) return;
if (!ValidateGroupExistence(groupName)) return;
if (!forceOption && !ValidateUserID(textUserID)) return;
idFromName = UserID(_.memory.Allocate(class'UserID')); id = UserID(_.memory.Allocate(class'UserID'));
idFromName.Initialize(userName); id.Initialize(textUserID);
annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName); if (!TryAddingUserID(groupName, id, textUserID) || annotation == none)
if (idFromName.IsInitialized())
{ {
for (i = 0; i < annotatedUsers.length; i += 1) _.memory.Free(id);
return;
}
_.users.SetAnnotationForUserID(groupName, id, annotation);
_.memory.Free(id);
DisplayAnnotation(textUserID, groupName, annotation);
}
private function AddOrAnnotatePlayers(
BaseText groupName,
ArrayList players,
BaseText annotation)
{
local int i;
local BaseText playerName, nextAnnotation;
local EPlayer nextPlayer;
local UserID nextID;
if (groupName == none) return;
if (players == none) return;
if (!ValidateGroupExistence(groupName)) return;
for (i = 0; i < players.GetLength(); i += 1)
{ {
if (idFromName.IsEqual(annotatedUsers[i].id)) nextPlayer = EPlayer(players.GetItem(i));
if (nextPlayer == none) {
continue;
}
playerName = nextPlayer.GetName();
nextID = nextPlayer.GetUserID();
if (TryAddingUserID(groupName, nextID, playerName))
{ {
idToRemove = annotatedUsers[i].id; if (annotation == none) {
break; nextAnnotation = playerName;
} }
else {
nextAnnotation = annotation;
} }
_.users.SetAnnotationForUserID(groupName, nextID, nextAnnotation);
DisplayAnnotation(playerName, groupName, nextAnnotation);
_.memory.Free(nextID);
nextAnnotation = none;
} }
_.memory.Free(idFromName); _.memory.Free(nextPlayer);
if (idToRemove == none) _.memory.Free(playerName);
_.memory.Free(nextID);
nextPlayer = none;
playerName = none;
nextID = none;
}
}
private function TryRemovingUserID(
BaseText groupName,
UserID idToRemove,
BaseText userSpecifiedName)
{
local Text idAsText;
idAsText = idToRemove.GetUniqueID();
if (_.users.RemoveUserIDFromGroup(idToRemove, groupName))
{
callerConsole
.Write(F("{$TextNegative Removed} user "))
.UseColorOnce(_.color.Gray)
.Write(userSpecifiedName)
.Write(P(" (with id "))
.UseColorOnce(_.color.Gray)
.Write(idAsText)
.Write(P(") from the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P("!"));
}
else
{ {
callerConsole
.UseColorOnce(_.color.TextFailure)
.Write(P("Failed (for unknown reason)"))
.Write(P("to remove user with id "))
.UseColorOnce(_.color.Gray)
.Write(idAsText)
.Write(P(" from the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P("."));
}
_.memory.Free(idAsText);
}
private function bool RemoveUsersByAnnotation(
BaseText groupName,
BaseText userName)
{
local int i;
local bool removedUser;
local array<Users_Feature.AnnotatedUserID> annotatedUsers;
annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName);
for (i = 0; i < annotatedUsers.length; i += 1) for (i = 0; i < annotatedUsers.length; i += 1)
{ {
if (userName.Compare( if (userName.Compare(annotatedUsers[i].annotation, SCASE_INSENSITIVE))
annotatedUsers[i].annotation,
SCASE_INSENSITIVE))
{ {
idToRemove = annotatedUsers[i].id; TryRemovingUserID(groupName, annotatedUsers[i].id, userName);
break; 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 callerConsole
.Write(P("User ")) .Write(P("User "))
@ -205,33 +394,47 @@ private function RemoveUser(BaseText groupName, BaseText userName)
.Write(groupName) .Write(groupName)
.WriteLine(P("!")); .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 callerConsole
.Write(F("{$TextNegative Removed} user ")) .Write(P("Player "))
.UseColorOnce(_.color.Gray) .UseColorOnce(_.color.Gray)
.Write(userName) .Write(playerName)
.Write(P(" from the group ")) .Write(F(" {$TextFailure doesn't belong} to the group "))
.UseColorOnce(_.color.TextEmphasis) .UseColorOnce(_.color.TextEmphasis)
.Write(groupName) .Write(groupName)
.WriteLine(P("!")); .WriteLine(P("!"));
} }
else { else {
callerConsole TryRemovingUserID(groupName, nextID, playerName);
.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("."));
} }
for (i = 0; i < annotatedUsers.length; i += 1) _.memory.Free(nextPlayer);
{ _.memory.Free(playerName);
_.memory.Free(annotatedUsers[i].id); _.memory.Free(nextID);
_.memory.Free(annotatedUsers[i].annotation); nextPlayer = none;
playerName = none;
nextID = none;
} }
} }
@ -346,9 +549,36 @@ private function bool ValidateUsersFeature()
return false; return false;
} }
private function DisplayUserGroupsWithUsers() private function bool IsGroupSpecified(
ArrayList specifiedGroups,
BaseText groupToCheck)
{ {
local int i; 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; local array<Text> availableGroups;
if (!ValidateUsersFeature()) { if (!ValidateUsersFeature()) {
@ -363,6 +593,9 @@ private function DisplayUserGroupsWithUsers()
} }
for (i = 0; i < availableGroups.length; i += 1) for (i = 0; i < availableGroups.length; i += 1)
{ {
if (IsGroupSpecified(specifiedGroups, availableGroups[i]))
{
displayedGroup = true;
callerConsole callerConsole
.Write(P("User group ")) .Write(P("User group "))
.UseColorOnce(_.color.TextEmphasis) .UseColorOnce(_.color.TextEmphasis)
@ -370,8 +603,12 @@ private function DisplayUserGroupsWithUsers()
.WriteLine(P(":")); .WriteLine(P(":"));
DisplayUsersFor(availableGroups[i]); DisplayUsersFor(availableGroups[i]);
} }
}
callerConsole.Flush(); callerConsole.Flush();
_.memory.FreeMany(availableGroups); _.memory.FreeMany(availableGroups);
if (!displayedGroup && specifiedGroups != none) {
callerConsole.WriteLine(F("{$TextFailure No valid groups} specified!"));
}
} }
private function DisplayUsersFor(Text groupName) private function DisplayUsersFor(Text groupName)

2
sources/Users/User.uc

@ -435,7 +435,7 @@ private function bool SetupDatabaseVariables()
// Try making skeleton database // Try making skeleton database
userTextID = id.GetSteamID64String(); userTextID = id.GetSteamID64String();
userDataLink = _.users.GetPersistentDataLink(); userDataLink = _.users.GetPersistentDataLink();
persistentSettingsPointer = _.db.GetPointer(userDataLink); persistentSettingsPointer = __core().db.GetPointer(userDataLink);
persistentSettingsPointer.Push(P("PerUserData")); persistentSettingsPointer.Push(P("PerUserData"));
persistentSettingsPointer.Push(userTextID); persistentSettingsPointer.Push(userTextID);
MakeSkeletonUserDatabase(userTextID, persistentSettingsPointer); MakeSkeletonUserDatabase(userTextID, persistentSettingsPointer);

9
sources/Users/UserAPI.uc

@ -38,7 +38,7 @@ var private LoggerAPI.Definition infoPersistentDatabaseLoaded;
protected function Constructor() protected function Constructor()
{ {
SetupUserDataDatabase(); //SetupUserDataDatabase();
} }
// DO NOT CALL MANUALLY // DO NOT CALL MANUALLY
@ -78,11 +78,10 @@ private function SetupUserDataDatabase()
return; return;
} }
// If link was specified - try loading database from it // If link was specified - try loading database from it
persistentDatabase = _.db.Load(persistentDataLink); persistentDatabase = __core().db.Load(persistentDataLink);
if (persistentDatabase == none) if (persistentDatabase == none)
{ {
_.logger.Auto(errNoPersistentDatabase).Arg(persistentDataLink); _.logger.Auto(errNoPersistentDatabase).Arg(persistentDataLink);
persistentDataLink.FreeSelf();
return; return;
} }
// Write skeleton database's skeleton // Write skeleton database's skeleton
@ -90,7 +89,7 @@ private function SetupUserDataDatabase()
emptyObject = _.collections.EmptyHashTable(); emptyObject = _.collections.EmptyHashTable();
skeleton.SetItem(P("Groups"), emptyObject); skeleton.SetItem(P("Groups"), emptyObject);
skeleton.SetItem(P("PerUserData"), emptyObject); skeleton.SetItem(P("PerUserData"), emptyObject);
persistentDataPointer = _.db.GetPointer(persistentDataLink); persistentDataPointer = __core().db.GetPointer(persistentDataLink);
persistentDatabase persistentDatabase
.IncrementData(persistentDataPointer, skeleton) .IncrementData(persistentDataPointer, skeleton)
.connect = ReportSkeletonCreationResult; .connect = ReportSkeletonCreationResult;
@ -1531,6 +1530,6 @@ defaultproperties
userdataDBLink = "[local]database:/users" 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\".") 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.") 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\".") 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 i;
local int result; local int result;
local int binaryPadding; local int binaryPadding;
if (digits.length <= 0) {
return 0;
}
result = 0; result = 0;
binaryPadding = 1; binaryPadding = 1;
for (i = 0; i < bitsToRead; i += 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 // List of all available user groups for current config
var private array<Text> loadedUserGroups; var private array<Text> loadedUserGroups;
// `HashTable` (with group name keys) that stores `HashTable`s used as // `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 HashTable loadedGroupToUsersMap;
var private LoggerAPI.Definition warnNoLocalGroup, errCannotCreateLocalGroup; var private LoggerAPI.Definition warnNoLocalGroup, warnDuplicateIDs;
var private LoggerAPI.Definition errCannotCreateLocalGroup;
protected function OnEnabled() protected function OnEnabled()
{ {
@ -172,6 +173,13 @@ private final function bool LoadLocalGroup(
for (i = 0; i < groupUserArray.length; i += 1) for (i = 0; i < groupUserArray.length; i += 1)
{ {
nextUserPair = ParseConfigUserName(groupUserArray[i]); 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); newPlayerSet.SetItem(nextUserPair.id, nextUserPair.annotation);
_.memory.Free(nextUserPair.id); _.memory.Free(nextUserPair.id);
_.memory.Free(nextUserPair.annotation); _.memory.Free(nextUserPair.annotation);
@ -187,23 +195,30 @@ private final function bool LoadLocalGroup(
private final function IDAnnotationPair ParseConfigUserName( private final function IDAnnotationPair ParseConfigUserName(
string configUserName) string configUserName)
{ {
local Parser parser; local int lastSlashIndex;
local MutableText parsingResult; local Text userAnnotation;
local MutableText userNameAsText;
local IDAnnotationPair result; local IDAnnotationPair result;
local Text.Character slashSeparator;
parser = _.text.ParseString(configUserName); userNameAsText = _.text.FromStringM(configUserName);
slashSeparator = _.text.GetCharacter("/"); lastSlashIndex = userNameAsText.IndexOf(P("/"));
if (parser.MUntil(parsingResult, slashSeparator).Match(P("/")).Ok()) { if (lastSlashIndex >= 0 && lastSlashIndex + 1 < userNameAsText.GetLength())
result.annotation = parser.GetRemainderM().IntoText();
}
result.id = parsingResult.IntoText();
if (result.annotation != none && result.annotation.IsEmpty())
{ {
result.annotation.FreeSelf(); userAnnotation = userNameAsText.Copy(lastSlashIndex + 1);
result.annotation = none; 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; return result;
} }
@ -1870,5 +1885,6 @@ defaultproperties
{ {
configClass = class'Users' configClass = class'Users'
warnNoLocalGroup = (l=LOG_Warning,m="Expected config to contain `UserGroup` named \"%1\", but it is missing. \"AcediaUsers.ini\" might be misconfigured.") 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\".") errCannotCreateLocalGroup = (l=LOG_Error,m="Failed to create config section for `UserGroup` named \"%1\".")
} }
Loading…
Cancel
Save