Add user groups and data API #8

Merged
dkanus merged 20 commits from feature_user_groups into develop 2 years ago
  1. 5
      config/AcediaDB.ini
  2. 35
      config/AcediaUsers.ini
  3. 1
      sources/BaseRealm/API/Scheduler/SchedulerAPI.uc
  4. 5
      sources/BaseRealm/AcediaEnvironment/AcediaEnvironment.uc
  5. 7
      sources/BaseRealm/Global.uc
  6. 4
      sources/ClientRealm/ClientAcediaAdapter.uc
  7. 5
      sources/ClientRealm/ClientGlobal.uc
  8. 49
      sources/Config/AcediaConfig.uc
  9. 4
      sources/CoreRealm/AcediaAdapter.uc
  10. 32
      sources/CoreRealm/CoreGlobal.uc
  11. 2
      sources/CoreRealm/Features/Commands/BuiltInCommands/ACommandHelp.uc
  12. 84
      sources/Data/Collections/ArrayList.uc
  13. 99
      sources/Data/Collections/Collection.uc
  14. 46
      sources/Data/Collections/HashTable.uc
  15. 52
      sources/Data/Collections/Tests/TEST_ArrayList.uc
  16. 82
      sources/Data/Collections/Tests/TEST_HashTable.uc
  17. 1099
      sources/Data/Database/Connection/DBCache.uc
  18. 791
      sources/Data/Database/Connection/DBConnection.uc
  19. 40
      sources/Data/Database/Connection/Events/DBConnection_EditResult_Signal.uc
  20. 41
      sources/Data/Database/Connection/Events/DBConnection_EditResult_Slot.uc
  21. 43
      sources/Data/Database/Connection/Events/DBConnection_StateChanged_Signal.uc
  22. 44
      sources/Data/Database/Connection/Events/DBConnection_StateChanged_Slot.uc
  23. 389
      sources/Data/Database/Connection/Tests/TEST_DBConnection.uc
  24. 64
      sources/Data/Database/DBAPI.uc
  25. 24
      sources/Data/Database/DBTask.uc
  26. 129
      sources/Data/Database/Database.uc
  27. 19
      sources/Data/Database/Local/DBRecord.uc
  28. 107
      sources/Data/Database/Local/LocalDatabaseInstance.uc
  29. 7
      sources/Data/Database/Tasks/DBCheckTask.uc
  30. 9
      sources/Data/Database/Tasks/DBIncrementTask.uc
  31. 9
      sources/Data/Database/Tasks/DBKeysTask.uc
  32. 9
      sources/Data/Database/Tasks/DBReadTask.uc
  33. 9
      sources/Data/Database/Tasks/DBRemoveTask.uc
  34. 10
      sources/Data/Database/Tasks/DBSizeTask.uc
  35. 9
      sources/Data/Database/Tasks/DBWriteTask.uc
  36. 10
      sources/Data/Database/Tests/TEST_DatabaseCommon.uc
  37. 252
      sources/Data/Database/Tests/TEST_LocalDatabase.uc
  38. 10
      sources/Manifest.uc
  39. 31
      sources/Players/EPlayer.uc
  40. 4
      sources/ServerRealm/ServerAcediaAdapter.uc
  41. 7
      sources/ServerRealm/ServerGlobal.uc
  42. 48
      sources/Text/BaseText.uc
  43. 311
      sources/Text/JSON/JSONAPI.uc
  44. 143
      sources/Text/JSON/JSONPointer.uc
  45. 514
      sources/Text/Tests/TEST_JSON.uc
  46. 23
      sources/Types/AcediaActor.uc
  47. 23
      sources/Types/AcediaObject.uc
  48. 655
      sources/Users/ACommandUserGroups.uc
  49. 40
      sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Signal.uc
  50. 41
      sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Slot.uc
  51. 407
      sources/Users/PersistentData/PersistentDataManager.uc
  52. 4
      sources/Users/Tests/TEST_User.uc
  53. 211
      sources/Users/User.uc
  54. 1397
      sources/Users/UserAPI.uc
  55. 16
      sources/Users/UserDatabase.uc
  56. 70
      sources/Users/UserGroup.uc
  57. 37
      sources/Users/UserID.uc
  58. 99
      sources/Users/Users.uc
  59. 2158
      sources/Users/Users_Feature.uc

5
config/AcediaDB.ini

@ -0,0 +1,5 @@
; Define all databases you want Acedia to use here.
; For simply making default Acedia configs work, set `createIfMissing` below
; to `true`.
[Database LocalDatabase]
createIfMissing=false

35
config/AcediaUsers.ini

@ -0,0 +1,35 @@
; Acedia requires adding its own `GameRules` to listen to many different
; game events.
; In this config you can setup Acedia's user groups and persistent data
; storage. Enabling this feature automatically enables user group support,
; while persistent data is optional.
; Databases can be configured in `AcediaDB.ini`.
[default Users]
; Configures whether to use database (and which) for storing user groups.
; Set `useDatabaseForGroupsData` to `false` if you want to define which users
; belong to what groups inside this config.
useDatabaseForGroupsData=true
groupsDatabaseLink=[local]Database:/group_data
; Configures whether persistent data should be additionally used.
; It can only be stored inside a database.
usePersistentData=true
persistentDataDatabaseLink=[local]Database:/user_data
; Available groups. Only used if `useDatabaseForGroupsData` is set to `false`.
localUserGroup=admin
localUserGroup=moderator
localUserGroup=trusted
; These groups definitions only work in case you're using a config with
; `useDatabaseForGroupsData` set to `false`. Simply add new `user=` record,
; specifying SteamIDs of the players, e.g. `user=76561197960287930`.
; You can also optionally specify a human-readable lable for the SteamID after
; slash "/", e.g. `user=76561197960287930/gabe`.
[admin UserGroup]
;user=
[moderator UserGroup]
;user=
[trusted UserGroup]
;user=

1
sources/BaseRealm/API/Scheduler/SchedulerAPI.uc

@ -182,7 +182,6 @@ public function SchedulerDiskRequest RequestDiskAccess(AcediaObject receiver)
if (receiver == none) return none; if (receiver == none) return none;
if (!receiver.IsAllocated()) return none; if (!receiver.IsAllocated()) return none;
newRequest = newRequest =
SchedulerDiskRequest(_.memory.Allocate(class'SchedulerDiskRequest')); SchedulerDiskRequest(_.memory.Allocate(class'SchedulerDiskRequest'));
diskQueue[diskQueue.length] = newRequest; diskQueue[diskQueue.length] = newRequest;

5
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.
* *
@ -414,6 +414,7 @@ public final function Feature GetEnabledFeature(class<Feature> featureClass)
/** /**
* Enables given `Feature` instance `newEnabledFeature` with a given config. * Enables given `Feature` instance `newEnabledFeature` with a given config.
* Does not change a config for already enabled feature, failing instead.
* *
* @see `Feature::EnableMe()`. * @see `Feature::EnableMe()`.
* *
@ -520,7 +521,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)

49
sources/Config/AcediaConfig.uc

@ -67,6 +67,9 @@ class AcediaConfig extends AcediaObject
// was detected in config, but not yet loaded. // was detected in config, but not yet loaded.
// Only its default value is ever used. // Only its default value is ever used.
var private HashTable existingConfigs; var private HashTable existingConfigs;
// TODO: comment and add static cleanup
var private array<AcediaConfig> clearQueue;
var private bool syncScheduled;
// Stores name of the config where settings are to be stored. // Stores name of the config where settings are to be stored.
// Must correspond to value in `config(...)` modifier in class definition. // Must correspond to value in `config(...)` modifier in class definition.
@ -174,7 +177,7 @@ public final static function bool NewConfig(BaseText name)
new(none, NameToStorageVersion(name.ToString())) default.class; new(none, NameToStorageVersion(name.ToString())) default.class;
newConfig._ = __(); newConfig._ = __();
newConfig.DefaultIt(); newConfig.DefaultIt();
newConfig.SaveConfig(); newConfig.SyncSave();
default.existingConfigs.SetItem(name, newConfig); default.existingConfigs.SetItem(name, newConfig);
name.FreeSelf(); name.FreeSelf();
return true; return true;
@ -212,14 +215,18 @@ public final static function bool Exists(BaseText name)
*/ */
public final static function DeleteConfig(BaseText name) public final static function DeleteConfig(BaseText name)
{ {
local AcediaObject value; local AcediaConfig value;
if (name == none) return; if (name == none) return;
if (default.existingConfigs == none) return; if (default.existingConfigs == none) return;
name = name.LowerCopy(); name = name.LowerCopy();
value = default.existingConfigs.TakeItem(name); value = AcediaConfig(default.existingConfigs.TakeItem(name));
if (value != none) { if (value != none)
value.ClearConfig(); {
__().scheduler.RequestDiskAccess(default.existingConfigs).connect =
HandleClearQueue;
default.clearQueue[default.clearQueue.length] = value;
} }
__().memory.Free(name); __().memory.Free(name);
} }
@ -317,8 +324,38 @@ public final static function SaveData(BaseText name, HashTable data)
if (requiredConfig != none) if (requiredConfig != none)
{ {
requiredConfig.FromData(data); requiredConfig.FromData(data);
requiredConfig.SaveConfig(); requiredConfig.SyncSave();
}
}
/**
* Synchronizes current in-memory data by saving it onto the disk (into
* the config file). Can be performed asynchronously (actual saving can be
* postponed for performance reasons).
*/
public final function SyncSave()
{
if (syncScheduled) {
return;
}
syncScheduled = true;
__().scheduler.RequestDiskAccess(default.existingConfigs).connect = DoSync;
}
// Does actual saving
private final function DoSync()
{
syncScheduled = false;
SaveConfig();
}
private final static function HandleClearQueue()
{
if (default.clearQueue.length <= 0) {
return;
} }
default.clearQueue[0].ClearConfig();
default.clearQueue.Remove(0, 1);
} }
defaultproperties defaultproperties

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

32
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.
* *
@ -50,10 +75,11 @@ protected function Initialize()
.ArgClass(self.class); .ArgClass(self.class);
return; return;
} }
api = class'Global'.static.GetInstance().memory; api = class'Global'.static.GetInstance().memory;
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);

84
sources/Data/Collections/ArrayList.uc

@ -5,7 +5,7 @@
* `AcediaObject`s. * `AcediaObject`s.
* Appropriate classes and APIs for their construction are provided for * Appropriate classes and APIs for their construction are provided for
* main primitive types and can be extended to any custom `struct`. * main primitive types and can be extended to any custom `struct`.
* Copyright 2022 Anton Tarasenko * Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -24,6 +24,7 @@
*/ */
class ArrayList extends Collection; class ArrayList extends Collection;
var bool FLAG;
// Actual storage of all our data. // Actual storage of all our data.
var private array<AcediaObject> storedObjects; var private array<AcediaObject> storedObjects;
@ -54,6 +55,34 @@ public final function int GetLength()
return storedObjects.length; return storedObjects.length;
} }
/**
* Appends objects from another `ArrayList` to the caller one.
*
* @param other Array to append objects from. `none` means nothing will be
* added.
* @return Reference to the caller `ArrayList` to allow for method chaining.
*/
public final function ArrayList Append(ArrayList other)
{
local int i, shift;
local array<AcediaObject> otherObjects;
if (other == none) return self;
if (other.GetLength() <= 0) return self;
shift = storedObjects.length;
otherObjects = other.storedObjects;
SetLength(storedObjects.length + otherObjects.length);
for (i = 0; i < otherObjects.length; i += 1)
{
if (otherObjects[i] != none) {
otherObjects[i].NewRef();
}
storedObjects[i + shift] = otherObjects[i];
}
return self;
}
/** /**
* Changes length of the caller `ArrayList`. * Changes length of the caller `ArrayList`.
* If `ArrayList` size is increased as a result - added items will be * If `ArrayList` size is increased as a result - added items will be
@ -67,12 +96,15 @@ public final function ArrayList SetLength(int newLength)
{ {
local int i; local int i;
if (newLength < 0) { if (newLength < 0) return self;
return self; if (storedObjects.length == newLength) return self;
}
for (i = newLength; i < storedObjects.length; i += 1) { for (i = newLength; i < storedObjects.length; i += 1) {
FreeItem(i); FreeItem(i);
} }
if (storedObjects.length <= 0) {
storedObjects[0] = none;
}
storedObjects.length = newLength; storedObjects.length = newLength;
return self; return self;
} }
@ -218,20 +250,15 @@ public final function ArrayList RemoveIndex(int index)
} }
/** /**
* Validates item at `index`: in case it was erroneously deallocated while * Validates item at `index`: whether it fits in current array bounds.
* being stored in caller `ArrayList` - forgets stored `AcediaObject`
* reference.
* *
* @param index Index of an item to validate/ * @param index Index of an item to validate.
* @return `true` if `index` is valid for `storedObjects` array. * @return `true` if `index` is valid for `storedObjects` array.
*/ */
private final function bool ValidateIndex(int index) private final function bool ValidateIndex(int index)
{ {
local AcediaObject item;
if (index < 0) return false; if (index < 0) return false;
if (index >= storedObjects.length) return false; if (index >= storedObjects.length) return false;
item = storedObjects[index];
return true; return true;
} }
@ -429,7 +456,40 @@ public final function int Find(AcediaObject item)
return -1; return -1;
} }
protected function AcediaObject GetByText(BaseText key) /**
* `ArrayList` only support `IntBox` and `IntRef` indices for this method.
*/
public function bool HasKey(AcediaObject key)
{
if (key == none) {
return false;
}
else if (key.class == class'IntBox') {
return ValidateIndex(IntBox(key).Get());
}
else if (key.class == class'IntRef') {
return ValidateIndex(IntRef(key).Get());
}
return false;
}
public function bool HasKeyByText(Text key)
{
local int index, consumed;
local Parser parser;
parser = _.text.Parse(key);
parser.MUnsignedInteger(index,,, consumed);
if (!parser.Ok())
{
parser.FreeSelf();
return false;
}
parser.FreeSelf();
return ValidateIndex(index);
}
public function AcediaObject GetByText(Text key)
{ {
local int index, consumed; local int index, consumed;
local Parser parser; local Parser parser;

99
sources/Data/Collections/Collection.uc

@ -2,7 +2,7 @@
* Acedia provides a small set of collections for easier data storage. * Acedia provides a small set of collections for easier data storage.
* This is their base class that provides a simple interface for * This is their base class that provides a simple interface for
* common methods. * common methods.
* Copyright 2020 - 2022 Anton Tarasenko * Copyright 2020-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -25,18 +25,46 @@ class Collection extends AcediaObject
var protected class<CollectionIterator> iteratorClass; var protected class<CollectionIterator> iteratorClass;
/** /**
* Method that must be overloaded for `GetItemByPointer()` to properly work. * Checks if caller `Collection` has value recorded with a given `key`.
* *
* This method must return an item that `key` refers to with it's * Not all collections must support all possible types of keys.
* textual content (not as an object itself). * Key equality is determined by `AcediaObject::IsEqual()` method.
*
* @return `true` if caller `Collection` has value recorded with
* a given `key` and `false` otherwise.
*/
public function bool HasKey(AcediaObject key);
/**
* Checks if caller `Collection` has value that a given `key` refers to with
* its textual content (not as an object itself).
* For example, `ArrayList` parses it into unsigned number, while * For example, `ArrayList` parses it into unsigned number, while
* `HashTable` uses it as a key directly. * `HashTable` uses it as a key directly.
* There is no requirement that all stored values must be reachable by
* this method (i.e. `HashTable` only lets you access values with
* `Text` keys).
* *
* @return `true` if caller `Collection` has value recorded with
* a given `key` (understood within its textual content) and `false`
* otherwise.
*/
public function bool HasKeyByText(Text key);
/**
* This method must return an item that `key` refers to with its
* textual content (not as an object itself).
* For example, `ArrayList` parses it into unsigned number, while
* `HashTable` uses it as a key directly.
* There is no requirement that all stored values must be reachable by * There is no requirement that all stored values must be reachable by
* this method (i.e. `HashTable` only lets you access values with * this method (i.e. `HashTable` only lets you access values with
* `Text` keys). * `Text` keys).
*
* To check whether such value even exists in the collection @see HasKeyByText.
*
* @param key Key that refers to the value to return.
* @return Value that `key` refers to with its textual content.
*/ */
protected function AcediaObject GetByText(BaseText key); public function AcediaObject GetByText(Text key);
/** /**
* Creates an `Iterator` instance to iterate over stored items. * Creates an `Iterator` instance to iterate over stored items.
@ -88,7 +116,7 @@ public function Empty() {}
* this method (i.e. `HashTable` only lets you access values with `Text` keys). * this method (i.e. `HashTable` only lets you access values with `Text` keys).
* *
* @param jsonPointer Path, given by a JSON pointer. * @param jsonPointer Path, given by a JSON pointer.
* @return An item `jsonPointerAsText` is referring to (according to the above * @return An item `jsonPointer` is referring to (according to the above
* stated rules). `none` if such item does not exist. * stated rules). `none` if such item does not exist.
*/ */
public final function AcediaObject GetItemByJSON(JSONPointer jsonPointer) public final function AcediaObject GetItemByJSON(JSONPointer jsonPointer)
@ -522,7 +550,35 @@ public final function Text GetTextBy(BaseText jsonPointerAsText)
} }
/** /**
* Returns an `HashTable` value (stored in the caller `Collection` or * Returns a generic `Collection` value (stored in the caller `Collection` or
* one of it's sub-collections) pointed by
* [JSON pointer](https://tools.ietf.org/html/rfc6901).
* See `GetItemBy()` for more information.
*
* Referred value must be stored as `Collection`
* (or one of it's sub-classes) for this method to work.
*
* @param jsonPointerAsText Description of a path to the `Collection` value.
* @return `Collection` value, stored at `jsonPointerAsText` or
* `none` if it is missing or has a different type.
*/
public final function Collection GetCollectionBy(
BaseText jsonPointerAsText)
{
local Collection asCollection;
local AcediaObject result;
result = GetItemBy(jsonPointerAsText);
asCollection = Collection(result);
if (asCollection != none) {
return asCollection;
}
_.memory.Free(result);
return none;
}
/**
* Returns a `HashTable` value (stored in the caller `Collection` or
* one of it's sub-collections) pointed by * one of it's sub-collections) pointed by
* [JSON pointer](https://tools.ietf.org/html/rfc6901). * [JSON pointer](https://tools.ietf.org/html/rfc6901).
* See `GetItemBy()` for more information. * See `GetItemBy()` for more information.
@ -904,7 +960,34 @@ public final function Text GetTextByJSON(JSONPointer jsonPointer)
} }
/** /**
* Returns an `HashTable` value (stored in the caller `Collection` or * Returns a generic `Collection` value (stored in the caller `Collection` or
* one of it's sub-collections) pointed by JSON pointer.
* See `GetItemByJSON()` for more information.
*
* Referred value must be stored as `Collection`
* (or one of it's sub-classes) for this method to work.
*
* @param jsonPointer JSON path to the `Collection` value.
* @return `Collection` value, stored at `jsonPointerAsText` or
* `none` if it is missing or has a different type.
*/
public final function Collection GetCollectionByJSON(
JSONPointer jsonPointer)
{
local AcediaObject result;
local Collection asCollection;
result = GetItemByJSON(jsonPointer);
asCollection = Collection(result);
if (asCollection != none) {
return asCollection;
}
_.memory.Free(result);
return none;
}
/**
* Returns a `HashTable` value (stored in the caller `Collection` or
* one of it's sub-collections) pointed by JSON pointer. * one of it's sub-collections) pointed by JSON pointer.
* See `GetItemByJSON()` for more information. * See `GetItemByJSON()` for more information.
* *

46
sources/Data/Collections/HashTable.uc

@ -197,6 +197,37 @@ private final function ResizeHashTable(int newSize)
} }
} }
/**
* Appends objects from another `HashTable` to the caller one.
*
* @param other Array to append objects from. `none` means nothing will be
* added.
* @return Reference to the caller `HashTable` to allow for method chaining.
*/
public final function HashTable Append(HashTable other)
{
local AcediaObject nextKey, nextValue;
local HashTableIterator iter;
if (other == none) return self;
if (other.GetLength() <= 0) return self;
iter = HashTableIterator(other.Iterate());
while (!iter.HasFinished())
{
nextKey = iter.GetKey();
nextValue = iter.Get();
if (!HasKey(nextKey)) {
SetItem(nextKey, nextValue);
}
_.memory.Free(nextKey);
_.memory.Free(nextValue);
iter.Next();
}
_.memory.Free(iter);
return self;
}
/** /**
* Returns minimal capacity of the caller associative array. * Returns minimal capacity of the caller associative array.
* *
@ -234,19 +265,18 @@ public final function SetMinimalCapacity(int newMinimalCapacity)
UpdateHashTableSize(); UpdateHashTableSize();
} }
/** public function bool HasKey(AcediaObject key)
* Checks if caller `HashTable` has value recorded with a given `key`.
*
* @return `true` if caller `HashTable` has value recorded with
* a given `key` and `false` otherwise.
*/
public final function bool HasKey(AcediaObject key)
{ {
local int bucketIndex, entryIndex; local int bucketIndex, entryIndex;
return FindEntryIndices(key, bucketIndex, entryIndex); return FindEntryIndices(key, bucketIndex, entryIndex);
} }
public function bool HasKeyByText(Text key)
{
return HasKey(key);
}
/** /**
* Returns borrowed value recorded by a given key `key` in the caller * Returns borrowed value recorded by a given key `key` in the caller
* `HashTable`. * `HashTable`.
@ -629,7 +659,7 @@ public final function AcediaObject GetKeyByIndex(Index index)
return key.NewRef(); return key.NewRef();
} }
protected function AcediaObject GetByText(BaseText key) public function AcediaObject GetByText(Text key)
{ {
return GetItem(key); return GetItem(key);
} }

52
sources/Data/Collections/Tests/TEST_ArrayList.uc

@ -31,6 +31,7 @@ protected static function TESTS()
Test_Find(); Test_Find();
Test_ReferenceManagementGet(); Test_ReferenceManagementGet();
Test_Take(); Test_Take();
Test_Append();
} }
protected static function Test_GetSet() protected static function Test_GetSet()
@ -289,7 +290,7 @@ protected static function Test_Take()
local array<MockItem> allocatedItems; local array<MockItem> allocatedItems;
array = NewMockArray(20, allocatedItems); array = NewMockArray(20, allocatedItems);
Context("Testing how well `ArrayList`'s `TakeItem()` command"); Context("Testing how well `ArrayList`'s `TakeItem()` command.");
Issue("`TakeItem()` return wrongs item."); Issue("`TakeItem()` return wrongs item.");
for (i = 0; i < allocatedItems.length; i += 1) for (i = 0; i < allocatedItems.length; i += 1)
{ {
@ -312,6 +313,55 @@ protected static function Test_Take()
} }
} }
protected static function Test_Append()
{
local int i;
local ArrayList main, additive, empty;
main = __().collections.EmptyArrayList();
additive = __().collections.EmptyArrayList();
empty = __().collections.EmptyArrayList();
// Ref counter = 2, from creation and adding to collection
main.AddItem(__().box.int(76)).AddItem(__().text.FromString("yoyoyo"));
main.AddItem(none).AddItem(__().ref.float(34.3));
additive.AddItem(none).AddItem(__().ref.bool(true));
Context("Testing appending `ArrayList`'s.");
Issue("`Append(none)` changes caller `ArrayList`.");
main.Append(none);
TEST_ExpectTrue(__().json.Print(main).ToString()
== "[76,\"yoyoyo\",null,34.3]");
Issue("`Append()` doesn't properly work on empty `ArrayList`");
// main ref = 3, +1 from copying
// additive ref = 2 (still)
empty.Append(main);
TEST_ExpectTrue(__().json.Print(main).ToString()
== "[76,\"yoyoyo\",null,34.3]");
Issue("`Append()` doesn't properly append `ArrayList`s.");
// main ref = 3
// additive ref = 3, +1 from copying
main.Append(additive);
TEST_ExpectTrue(__().json.Print(main).ToString()
== "[76,\"yoyoyo\",null,34.3,null,true]");
Issue("`Append()` changes appended `ArrayList`");
TEST_ExpectTrue(__().json.Print(additive).ToString() == "[null,true]");
Issue("`Append()` incorrectly changes reference count of stored objects.");
// Ref counter = 3, but will be visible as 4 from getters
for (i = 0; i < main.GetLength(); i += 1)
{
if (i == 2 || i == 4) {
TEST_ExpectNone(main.GetItem(i));
}
else {
TEST_ExpectTrue(main.GetItem(i)._getRefCount() == 4);
}
}
}
defaultproperties defaultproperties
{ {
caseGroup = "Collections" caseGroup = "Collections"

82
sources/Data/Collections/Tests/TEST_HashTable.uc

@ -37,6 +37,7 @@ protected static function TESTS()
Test_ReferenceManagement(); Test_ReferenceManagement();
Test_Take(); Test_Take();
Test_LargeArray(); Test_LargeArray();
Test_Append();
} }
protected static function AcediaObject NewKey(int value) protected static function AcediaObject NewKey(int value)
@ -570,6 +571,87 @@ protected static function Test_LargeArray()
} }
} }
protected static function Test_Append()
{
local HashTable main, additive, empty;
main = __().collections.EmptyHashTable();
additive = __().collections.EmptyHashTable();
empty = __().collections.EmptyHashTable();
// Ref count in main = 2, creation and copy into collection
main.SetItem(P("A"), __().text.FromString("value of A"));
main.SetItem(P("B"), __().text.FromString("value of B"));
main.SetItem(P("C"), __().text.FromString("value of C"));
main.SetItem(P("D"), none);
additive.SetItem(P("C"), __().text.FromString("other value of C!"));
additive.SetItem(P("D"), __().text.FromString("value of D"));
additive.SetItem(P("E"), __().text.FromString("value of E"));
Context("Testing appending `HashTable`'s.");
SubTest_EmptyCopies(main, additive, empty);
SubTest_ProperCopies(main, additive, empty);
}
protected static function SubTest_EmptyCopies(
HashTable main,
HashTable additive,
HashTable empty)
{
Issue("`Append(none)` changes caller `HashTable`.");
main.Append(none);
TEST_ExpectTrue(main.GetLength() == 4);
TEST_ExpectTrue(main.GetString(P("A")) == "value of A");
TEST_ExpectTrue(main.GetString(P("B")) == "value of B");
TEST_ExpectTrue(main.GetString(P("C")) == "value of C");
TEST_ExpectNone(main.GetItem(P("D")));
Issue("`Append()` for empty argument changes caller `HashTable`.");
main.Append(empty);
TEST_ExpectTrue(main.GetLength() == 4);
TEST_ExpectTrue(main.GetString(P("A")) == "value of A");
TEST_ExpectTrue(main.GetString(P("B")) == "value of B");
TEST_ExpectTrue(main.GetString(P("C")) == "value of C");
TEST_ExpectNone(main.GetItem(P("D")));
Issue("`Append()` doesn't properly work on empty `HashTable`");
// Ref count in main = 3, +1 for appending
empty.Append(main);
TEST_ExpectTrue(empty.GetLength() == 4);
TEST_ExpectTrue(empty.GetString(P("A")) == "value of A");
TEST_ExpectTrue(empty.GetString(P("B")) == "value of B");
TEST_ExpectTrue(empty.GetString(P("C")) == "value of C");
TEST_ExpectNone(empty.GetItem(P("D")));
}
protected static function SubTest_ProperCopies(
HashTable main,
HashTable additive,
HashTable empty)
{
Issue("`Append()` doesn't properly append `HashTable`s.");
main.Append(additive);
TEST_ExpectTrue(main.GetLength() == 5);
TEST_ExpectTrue(main.GetString(P("A")) == "value of A");
TEST_ExpectTrue(main.GetString(P("B")) == "value of B");
TEST_ExpectTrue(main.GetString(P("C")) == "value of C");
TEST_ExpectNone(main.GetItem(P("D")));
TEST_ExpectTrue(main.GetString(P("E")) == "value of E");
Issue("`Append()` changes appended `HashTable`");
TEST_ExpectTrue(additive.GetLength() == 3);
TEST_ExpectTrue(additive.GetString(P("C")) == "other value of C!");
TEST_ExpectTrue(additive.GetString(P("D")) == "value of D");
TEST_ExpectTrue(additive.GetString(P("E")) == "value of E");
Issue("`Append()` incorrectly changes reference counts of items inside"
@ "`HashTable`");
// Ref count in main = 3, so 4 after getter
TEST_ExpectTrue(main.GetItem(P("A"))._getRefCount() == 4);
TEST_ExpectTrue(main.GetItem(P("B"))._getRefCount() == 4);
TEST_ExpectTrue(main.GetItem(P("C"))._getRefCount() == 4);
TEST_ExpectTrue(main.GetItem(P("E"))._getRefCount() == 4);
}
defaultproperties defaultproperties
{ {
caseGroup = "Collections" caseGroup = "Collections"

1099
sources/Data/Database/Connection/DBCache.uc

File diff suppressed because it is too large Load Diff

791
sources/Data/Database/Connection/DBConnection.uc

@ -0,0 +1,791 @@
/**
* Auxiliary object for simplifying working with databases.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection extends AcediaObject
dependson(Database)
dependson(DBCache);
/**
* # `DBConnection`
*
* Auxiliary object for simplifying working with databases.
* `Database` class has a rather simple interface and there are several issues
* that constantly arise when trying to use it:
*
* 1. If one tries to read/write data from/to specific location in
* the database, then `JSONPointer` has to be kept in addition to
* the `Database` reference at all times;
* 2. One has to perform initial checks about whether database can even be
* connected to, if at desired location there is a proper data
* structure, etc.. If one also wants to start using data before
* database's response or after its failure, then the same work of
* duplication that data locally must be performed.
* 3. Instead of immediate operations, database operations are delayed and
* user has to handle their results asynchronously in separate methods.
*
* `DBConnection` takes care of these issues by providing you synchronous
* methods for accessing cached version of the data at the given location that
* is duplicated to the database as soon as possible (and even if its no longer
* possible in the case of a failure).
* `DBConnection` makes immediate changes on the local cache and reports
* about possible failures with database later through signals `OnEditResult()`
* (reports about success of writing operations) and `OnStateChanged()`
* (reports about state changes of connected database, including complete
* failures).
* The only reading of database's values occurs at the moment of connecting
* to it, after that all the data is read from the local cache.
* Possible `DBConnection` states include:
*
* * `DBCS_Idle` - database was created, but not yet connected;
* * `DBCS_Connecting` - `Connect()` method was successfully called, but
* its result is still unknown;
* * `DBCS_Connected` - database is connected and properly working;
* * `DBCS_Disconnected` - database was manually disconnected and now
* operates solely on the local cache. Once disconnected `DBConnection`
* cannot be reconnected - create a new one instead.
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
* can be found out with `GetError()` method:
*
* * `FDE_None` - no error has yet occurred;
* * `FDE_CannotReadRootData` - root data couldn't be read from
* the database, most likely because of the invalid `JSONPointer`
* for the root value;
* * `FDE_UnexpectedRootData` - root data was read from the database,
* but has an unexpected format. `DBConnection` expects either
* JSON object or array (can be specified which one) and this error
* occurs if its not found at specified database's location;
* * `FDE_Unknown` - database returned `DBR_InvalidDatabase` result
* for one of the queries. This is likely to happen when database
* is damaged. More precise details depend on the implementation,
* but result boils down to database being unusable.
*
* ## Usage
*
* Usage is straightforward:
*
* 1. Initialize with appropriate database by calling `Initialize()`;
* 2. Start connecting to it by calling `Connect()`;
* 3. Use `ReadDataByJSON()`/`WriteDataByJSON()` to read/write into
* the connected database;
*
* You can use it transparently even if database connection fails, but if you
* need to handle such failure - connect to the `OnStateChanged()` signal for
* tracking `DBCS_FaultyDatabase` state and to `OnEditResult()` for tracking
* success of writing operations.
*
* ## Implementation
*
* The brunt of work is done by `DBCache` and most of the logic in this
* class is for tracking state of the connection to the database and then
* reporting these changes through its own signals.
* The most notable hidden functionality is tracking requests by ID -
* `DBConnection` makes each request with unique ID and then stores them inside
* `requestIDs` (and in case of write requests - along with corresponding
* `JSONPointer` inside `queuedPointers`). This is necessary because:
*
* 1. Even if `DBConnection` gets reallocated - as far as UnrealScript is
* concerned it is still the same object, so the responses we no longer
* care about will still arrive. Keeping track of the IDs that interest
* us inside `requestIDs` allows us to filter out responses we no
* longer care about;
* 2. Tracking corresponding (to the IDs) `queuedPointers` also allow us
* to know responses to which writing requests we've received.
*
* ## Remarks
*
* Currently `DBConnection` doesn't support important feature of *incrementing*
* data that allows several sources to safely change the same value
* asynchronously. We're skipping on it right now to save time as its not
* really currently needed, however it will be added in the future.
*/
enum DBConnectionState
{
// `DBConnection` was created, but didn't yet attempt to connect
// to database
DBCS_Idle,
// `DBConnection` is currently connecting
DBCS_Connecting,
// `DBConnection` has already connected without errors
DBCS_Connected,
// `DBConnection` was manually disconnected
DBCS_Disconnected,
// `DBConnection` was disconnected because of the database error,
// @see `FaultyDatabaseError` for more.
DBCS_FaultyDatabase
};
// Current connection state
var private DBConnectionState currentState;
enum FaultyDatabaseError
{
// No error has occurred yet
FDE_None,
// Root data isn't available
FDE_CannotReadRootData,
// Root data has incorrect format
FDE_UnexpectedRootData,
// Some internal error in database has occurred
FDE_Unknown
};
// Reason for why current state is in `DBCS_FaultyDatabase` state;
// `FDE_None` if `DBConnection` is in any other state.
var private FaultyDatabaseError dbFailureReason;
// Keeps track whether root value read from the database was of the correct
// type. Only relevant after database tried connecting (i.e. it is in states
// `DBCS_Connected`, `DBCS_Disconnected` or `DBCS_FaultyDatabase`).
// This variable helps us determine whether error should be
// `FDE_CannotReadRootData` or `FDE_UnexpectedRootData`.
var private bool rootIsOfExpectedType;
// `Database` + `JSONPointer` combo that point at the data we want to
// connect to
var private Database dbInstance;
var private JSONPointer rootPointer;
// Local, cached version of that data
var private DBCache localCache;
// This is basically an array of (`int`, `JSONPointer`) pairs for tracking
// database requests of interest
var private array<int> requestIDs;
var private array<JSONPointer> queuedPointers;
// Next usable ID. `DBConnection` is expected to always use unique IDs.
var private int nextRequestID;
var private DBConnection_StateChanged_Signal onStateChangedSignal;
var private DBConnection_EditResult_Signal onEditResultSignal;
var private LoggerAPI.Definition errDoubleInitialization;
/**
* Signal that will be emitted whenever `DBConnection` changes state.
* List of the available states:
*
* * `DBCS_Idle` - database was created, but not yet connected;
* * `DBCS_Connecting` - `Connect()` method was successfully called, but
* its result is still unknown;
* * `DBCS_Connected` - database is connected and properly working;
* * `DBCS_Disconnected` - database was manually disconnected and now
* operates solely on the local cache. Once disconnected `DBConnection`
* cannot be reconnected - create a new one instead.
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
* can be found out with `GetError()` method.
*
* This method *is not* called when `DBConnection` is deallocated.
*
* [Signature]
* void <slot>(
* DBConnection instance,
* DBConnectionState oldState,
* DBConnectionState newState)
*
* @param instance Instance of the `DBConnection` that has changed state.
* @param oldState State it was previously in.
* @param oldState New state.
*/
/* SIGNAL */
public final function DBConnection_StateChanged_Slot OnStateChanged(
AcediaObject receiver)
{
return DBConnection_StateChanged_Slot(onStateChangedSignal
.NewSlot(receiver));
}
/**
* Signal that will be emitted whenever `DBConnection` receives response from
* connected database about success of writing operation.
*
* Responses to old requests can still be received even if database got
* disconnected.
*
* Any emissions of this signal when the database's state is `DBCS_Connecting`
* correspond to reapplying edits made prior connection was established.
*
* [Signature]
* void <slot>(JSONPointer editLocation, bool isSuccessful)
*
* @param editLocation Location of the writing operation this is
* a response to.
* @param isSuccessful Whether writing operation ended in the success.
*/
/* SIGNAL */
public final function DBConnection_EditResult_Slot OnEditResult(
AcediaObject receiver)
{
return DBConnection_EditResult_Slot(onEditResultSignal.NewSlot(receiver));
}
protected function Constructor()
{
localCache = DBCache(_.memory.Allocate(class'DBCache'));
onStateChangedSignal = DBConnection_StateChanged_Signal(
_.memory.Allocate(class'DBConnection_StateChanged_Signal'));
onEditResultSignal = DBConnection_EditResult_Signal(
_.memory.Allocate(class'DBConnection_EditResult_Signal'));
}
protected function Finalizer()
{
rootIsOfExpectedType = false;
currentState = DBCS_Idle;
_.memory.Free(dbInstance);
_.memory.Free(rootPointer);
_.memory.Free(localCache);
dbInstance = none;
rootPointer = none;
localCache = none;
_.memory.FreeMany(queuedPointers);
queuedPointers.length = 0;
requestIDs.length = 0;
// Free signals
_.memory.Free(onStateChangedSignal);
_.memory.Free(onEditResultSignal);
onStateChangedSignal = none;
onEditResultSignal = none;
}
/**
* Initializes `DBConnection` with database and location to which it must be
* connected.
*
* For the initialization to be successful `DBConnection` must not yet be
* initialized and `initDatabase` be not `none`.
*
* To check whether caller `DBConnection` is initialized
* @see `IsInitialized()`.
*
* @param initDatabase Database with data we want to connect to.
* @param initRootPointer Location of said data in the given database.
* If `none` is specified, uses root object of the database.
* @return `true` if initialization was successful and `false` otherwise.
*/
public final function bool Initialize(
Database initDatabase,
optional JSONPointer initRootPointer)
{
if (IsInitialized()) return false;
if (initDatabase == none) return false;
if (!initDatabase.IsAllocated()) return false;
dbInstance = initDatabase;
dbInstance.NewRef();
if (initRootPointer != none) {
rootPointer = initRootPointer.Copy();
}
else {
rootPointer = _.json.Pointer();
}
return true;
}
/**
* Reads data from the `DBConnection` at the location defined by the given
* `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual returned data's location in the database is defined by appending
* given `pointer` to that root pointer.
*
* Data is actually always read from the local cache and, therefore, we can
* read data we've written via `DBConnection` even without actually connecting
* to the database.
*
* @param pointer Location from which to read the data.
* @return Data recorded for the given `JSONPointer`. `none` if it is missing.
*/
public final function AcediaObject ReadDataByJSON(JSONPointer pointer)
{
return localCache.Read(pointer);
}
/**
* Writes given data into the `DBConnection` at the location defined by
* the given `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual location for writing data in the database is defined by appending
* given `pointer` to that root pointer.
*
* Data is actually always also written into the local cache, even when
* there is no connection to the database. Once connection is made - all valid
* changes will be duplicated into it.
* Success of failure of actually making changes into the database can be
* tracked with `OnEditResult()` signal.
*
* This operation also returns immediate indication of whether it has
* failed *locally*. This can happen when trying to perform operation
* impossible for the local cache. For example, we cannot write any data at
* location "/a/b/c" for the JSON object "{"a":45.6}".
* If operation ended in failure locally, then change to database won't
* even be attempted.
*
* @param pointer Location into which to write the data.
* @param data Data to write into the connection.
* @return `true` on success and `false` on failure. `true` is required for
* the writing database request to be made.
*/
public final function bool WriteDataByJSON(
JSONPointer pointer,
AcediaObject data)
{
if (pointer == none) {
return false;
}
if (localCache.Write(pointer, data))
{
ModifyDataInDatabase(pointer, data, false);
return true;
}
return false;
}
/**
* Increments given data into the `DBConnection` at the location defined by
* the given `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual location for incrementing data in the database is defined by
* appending given `pointer` to that root pointer.
*
* Data is actually always also incremented into the local cache, even when
* there is no connection to the database. Once connection is made - all valid
* changes will be duplicated into it.
* Success of failure of actually making changes into the database can be
* tracked with `OnEditResult()` signal.
*
* This operation also returns immediate indication of whether it has
* failed *locally*. This can happen when trying to perform operation
* impossible for the local cache. For example, we cannot increment any data at
* location "/a/b/c" for the JSON object "{"a":45.6}".
* If operation ended in failure locally, then change to database won't
* even be attempted.
*
* @param pointer Location at which to increment the data.
* @param data Data with which to increment value inside the connection.
* @return `true` on success and `false` on failure. `true` is required for
* the incrementing database request to be made.
*/
public final function bool IncrementDataByJSON(
JSONPointer pointer,
AcediaObject data)
{
if (pointer == none) {
return false;
}
if (localCache.Increment(pointer, data))
{
ModifyDataInDatabase(pointer, data, true);
return true;
}
return false;
}
/**
* Removes data from the `DBConnection` at the location defined by the given
* `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual location at which to remove data in the database is defined by
* appending given `pointer` to that root pointer.
*
* Data is actually always also removed from the local cache, even when
* there is no connection to the database. Once connection is made - all valid
* changes will be duplicated into it.
* Success of failure of actually making changes into the database can be
* tracked with `OnEditResult()` signal.
*
* This operation also returns immediate indication of whether it has
* failed *locally*.
* If operation ended in failure locally, then change to database won't
* even be attempted.
*
* @param pointer Location at which to remove data.
* @return `true` on success and `false` on failure. `true` is required for
* the removal database request to be made.
*/
public final function bool RemoveDataByJSON(JSONPointer pointer)
{
if (pointer == none) {
return false;
}
if (localCache.Remove(pointer))
{
RemoveDataInDatabase(pointer);
return true;
}
return false;
}
private final function ModifyDataInDatabase(
JSONPointer pointer,
AcediaObject data,
bool increment)
{
local JSONPointer dataPointer;
if (currentState != DBCS_Connected) {
return;
}
dataPointer = rootPointer.Copy();
dataPointer.Append(pointer);
// `dataPointer` is consumed by `RegisterNextRequestID()` method
if (increment)
{
dbInstance
.IncrementData(
dataPointer,
data,
RegisterNextRequestID(dataPointer))
.connect = EditDataHandler;
}
else
{
dbInstance
.WriteData(dataPointer, data, RegisterNextRequestID(dataPointer))
.connect = EditDataHandler;
}
}
private final function RemoveDataInDatabase(JSONPointer pointer)
{
local JSONPointer dataPointer;
if (currentState != DBCS_Connected) {
return;
}
dataPointer = rootPointer.Copy();
dataPointer.Append(pointer);
// `dataPointer` is consumed by `RegisterNextRequestID()` method
dbInstance
.RemoveData(dataPointer, RegisterNextRequestID(dataPointer))
.connect = EditDataHandler;
}
/**
* Checks caller `DBConnection` was successfully initialized.
*
* @return `true` if caller `DBConnection` was initialized and `false`
* otherwise.
*/
public final function bool IsInitialized()
{
return (dbInstance != none);
}
/**
* Returns current state of the connection of `DBConnection` to the database
* it was initialized with.
*
* @see `OnStateChanged()` for more information about connection states.
* @return Current connection state.
*/
public final function DBConnectionState GetConnectionState()
{
return currentState;
}
/**
* Checks whether caller `DBConnection` is currently connected without errors
* to the database it was initialized with.
*
* @return `true` if caller `DBConnection` is connected to the database and
* `false` otherwise.
*/
public final function bool IsConnected()
{
return (currentState == DBCS_Connected);
}
/**
* Checks whether an error has occurred with connection to the database.
*
* `DBConnection` can get disconnected from database manually and without
* any errors, so, if you simply want to check whether connection exists,
* @see `IsConnected()` or @see `GetConnectionState()`.
* To obtain more detailed information @see `GetError()`.
*
* @return `true` if there were no error thus far and `false` otherwise.
*/
public final function bool IsOk()
{
return (dbFailureReason == FDE_None);
}
/**
* Returns error that has occurred during connection.
*
* @return Error that has occurred during connection to the database,
* `FDE_None` if there was no errors.
*/
public final function FaultyDatabaseError GetError()
{
return dbFailureReason;
}
private final function ChangeState(DBConnectionState newState)
{
local DBConnectionState oldState;
oldState = currentState;
currentState = newState;
onStateChangedSignal.Emit(self, oldState, newState);
}
/**
* Attempts connection to the database caller `DBConnection` was initialized
* with. Result isn't immediate and can be tracked with `OnStateChanged()`
* signal.
*
* Connection checks whether data by the initialization address can be read and
* has proper type (by default JSON object, but JSON array can be used
* instead).
*
* Whether connection is successfully established isn't known at the moment
* this function returns. User `OnStateChanged()` to track that.
*
* @param expectArray Set this to `true` if the expected root value is
* JSON array.
*/
public final function Connect(optional bool expectArray)
{
local Collection incrementObject;
if (!IsInitialized()) return;
if (currentState != DBCS_Idle) return;
if (expectArray) {
incrementObject = _.collections.EmptyArrayList();
}
else {
incrementObject = _.collections.EmptyHashTable();
}
dbInstance.IncrementData(
rootPointer,
incrementObject,
RegisterNextRequestID()).connect = IncrementCheckHandler;
incrementObject.FreeSelf();
// Copy of the `rootPointer` is consumed by `RegisterNextRequestID()`
// method
dbInstance.ReadData(rootPointer,, RegisterNextRequestID(rootPointer.Copy()))
.connect = InitialLoadingHandler;
ChangeState(DBCS_Connecting);
}
/**
* Disconnects `DBConnection` from its database, preventing its further
* updates.
*
* Database can only be disconnected if connection was at least initialized
* (state isn't `DBCS_Idle`) and no error has yet occurred (state isn't
* `DBCS_FaultyDatabase`).
*
* @return `true` if `DBConnection` was disconnected from the database and
* `false` otherwise (including if it already was disconnected).
*/
public final function bool Disconnect()
{
if ( currentState != DBCS_FaultyDatabase
&& currentState != DBCS_Idle
&& currentState != DBCS_Disconnected)
{
ChangeState(DBCS_Disconnected);
return true;
}
return false;
}
private final function int RegisterNextRequestID(
optional /*take*/ JSONPointer relativePointer)
{
if (relativePointer != none) {
queuedPointers[queuedPointers.length] = relativePointer;
}
else {
queuedPointers[queuedPointers.length] = _.json.Pointer();
}
requestIDs[requestIDs.length] = nextRequestID;
nextRequestID += 1;
return (nextRequestID - 1);
}
private final function JSONPointer FetchRequestPointer(int requestID)
{
local int i;
local JSONPointer result;
while (i < requestIDs.length)
{
if (requestIDs[i] < requestID)
{
// We receive all requests in order, so if `requestID` is higher
// than IDs of some other requests - it means that they are older,
// lost requests
_.memory.Free(queuedPointers[i]);
queuedPointers.Remove(i, 1);
requestIDs.Remove(i, 1);
}
if (requestIDs[i] == requestID)
{
result = queuedPointers[i];
queuedPointers.Remove(i, 1);
requestIDs.Remove(i, 1);
return result;
}
i += 1;
}
return none;
}
private final function bool FetchIfRequestStillValid(int requestID)
{
local JSONPointer result;
result = FetchRequestPointer(requestID);
if (result != none)
{
_.memory.Free(result);
return true;
}
return false;
}
private final function IncrementCheckHandler(
Database.DBQueryResult result,
Database source,
int requestID)
{
if (!FetchIfRequestStillValid(requestID)) {
return;
}
// If we could successfully increment value with appropriate JSON value,
// then its type is correct
rootIsOfExpectedType = (result == DBR_Success);
}
private final function InitialLoadingHandler(
Database.DBQueryResult result,
/*take*/ AcediaObject data,
Database source,
int requestID)
{
local int i;
local array<DBCache.PendingEdit> completedEdits;
if (!FetchIfRequestStillValid(requestID))
{
_.memory.Free(data);
return;
}
if (HandleInitializationError(result))
{
_.memory.Free(data);
return;
}
completedEdits = localCache.SetRealData(data);
for (i = 0; i < completedEdits.length; i += 1)
{
if (completedEdits[i].successful)
{
if (completedEdits[i].type == DBCET_Remove) {
RemoveDataInDatabase(completedEdits[i].location);
}
else
{
ModifyDataInDatabase(
completedEdits[i].location,
completedEdits[i].data,
completedEdits[i].type == DBCET_Increment);
}
}
else {
onEditResultSignal.Emit(completedEdits[i].location, false);
}
_.memory.Free(completedEdits[i].location);
_.memory.Free(completedEdits[i].data);
}
_.memory.Free(data);
ChangeState(DBCS_Connected);
}
// Return `true` if further initialization must be stopped.
private final function bool HandleInitializationError(
Database.DBQueryResult result)
{
// Get disconnected before even response has even arrived
if (currentState == DBCS_Disconnected) {
return true;
}
if (currentState == DBCS_Connected)
{
_.logger.Auto(errDoubleInitialization).Arg(rootPointer.ToText());
return true;
}
if (result == DBR_InvalidDatabase)
{
dbFailureReason = FDE_Unknown;
ChangeState(DBCS_FaultyDatabase);
return true;
}
if (result != DBR_Success)
{
dbFailureReason = FDE_CannotReadRootData;
ChangeState(DBCS_FaultyDatabase);
return true;
}
if (!rootIsOfExpectedType)
{
dbFailureReason = FDE_UnexpectedRootData;
ChangeState(DBCS_FaultyDatabase);
return true;
}
return false;
}
private final function EditDataHandler(
Database.DBQueryResult result,
Database source,
int requestID)
{
local JSONPointer relatedPointer;
relatedPointer = FetchRequestPointer(requestID);
if (relatedPointer == none) {
return;
}
if (result == DBR_InvalidDatabase)
{
dbFailureReason = FDE_Unknown;
ChangeState(DBCS_FaultyDatabase);
relatedPointer.FreeSelf();
return;
}
if (result == DBR_Success) {
onEditResultSignal.Emit(relatedPointer, true);
}
else {
onEditResultSignal.Emit(relatedPointer, false);
}
relatedPointer.FreeSelf();
}
defaultproperties
{
errDoubleInitialization = (l=LOG_Error,m="`DBConnection` connected to \"%1\" was double-initialized. This SHOULD NOT happen. Please report this bug.")
}

40
sources/Data/Database/Connection/Events/DBConnection_EditResult_Signal.uc

@ -0,0 +1,40 @@
/**
* Signal class for `DBConnections`'s `OnEditResult()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_EditResult_Signal extends Signal
dependson(DBConnection);
public final function Emit(JSONPointer editLocation, bool isSuccessful)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
DBConnection_EditResult_Slot(nextSlot)
.connect(editLocation, isSuccessful);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'DBConnection_EditResult_Slot'
}

41
sources/Data/Database/Connection/Events/DBConnection_EditResult_Slot.uc

@ -0,0 +1,41 @@
/**
* Slot class for `DBConnections`'s `OnEditResult()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_EditResult_Slot extends Slot
dependson(DBConnection);
delegate connect(JSONPointer editLocation, bool isSuccessful)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

43
sources/Data/Database/Connection/Events/DBConnection_StateChanged_Signal.uc

@ -0,0 +1,43 @@
/**
* Signal class for `DBConnections`'s `OnStatusChanged()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_StateChanged_Signal extends Signal
dependson(DBConnection);
public final function Emit(
DBConnection instance,
DBConnection.DBConnectionState oldState,
DBConnection.DBConnectionState newState)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
DBConnection_StateChanged_Slot(nextSlot)
.connect(instance, oldState, newState);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'DBConnection_StateChanged_Slot'
}

44
sources/Data/Database/Connection/Events/DBConnection_StateChanged_Slot.uc

@ -0,0 +1,44 @@
/**
* Slot class for `DBConnections`'s `OnStatusChanged()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_StateChanged_Slot extends Slot
dependson(DBConnection);
delegate connect(
DBConnection instance,
DBConnection.DBConnectionState oldState,
DBConnection.DBConnectionState newState)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

389
sources/Data/Database/Connection/Tests/TEST_DBConnection.uc

@ -0,0 +1,389 @@
/**
* Set of tests for `DBConnection` and `DBCache` classes.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_DBConnection extends TestCase
abstract;
var DBCache cache;
protected static function TESTS()
{
Context("Testing how `DBCache` handles data writes and reads when"
@ "the actual database's data is yet to be downloaded.");
Test_DBCache_BasicWriteRead_DuringLoading();
Test_DBCache_AllOperations_DuringLoading();
Context("Testing how `DBCache` handles applying pending writes after real"
@ "data was set.");
Test_DBCache_ApplyingPendingEdits();
Test_DBCache_ApplyingPendingEdit_AllOperations();
Context("Testing how `DBCache` handles data writes and reads when"
@ "the actual database's data is already setup.");
Test_DBCache_BasicWriteRead_AfterLoading();
Test_DBCache_AllOperations_AfterLoading();
}
/* Creates following object:
{
"A": "simpleValue",
"B": {
"A": [true, {
"A": "simpleValue",
"B": 11.12,
"": [true, null, "huh"]
}, "huh"],
"B": -13.95
},
"C": -5,
"D": []
} */
protected static function HashTable MakeTestJSONObject()
{
local ArrayList outerArray, innerArray;
local HashTable result, outerObject, innerObject;
innerArray = __().collections.EmptyArrayList();
innerArray.AddBool(true).AddItem(none).AddString("huh");
innerObject = __().collections.EmptyHashTable();
innerObject.SetString(P("A"), "simpleValue");
innerObject.SetFloat(P("B"), 11.12).SetItem(P(""), innerArray);
outerArray = __().collections.EmptyArrayList();
outerArray.AddBool(true).AddItem(innerObject).AddString("huh");
outerObject = __().collections.EmptyHashTable();
outerObject.SetItem(P("A"), outerArray).SetFloat(P("B"), -13.95);
result = __().collections.EmptyHashTable();
result.SetString(P("A"), "simpleValue");
result.SetItem(P("B"), outerObject);
result.SetInt(P("C"), -5);
result.SetItem(P("D"), __().collections.EmptyArrayList());
return result;
}
protected static function CheckLocation(string pointer, string expectedResult)
{
local AcediaObject value;
value = default.cache
.Read(__().json.Pointer(__().text.FromString(pointer)));
if (BaseText(value) != none)
{
TEST_ExpectTrue(
__().json.Print(value).ToString()
== ("\"" $ expectedResult $ "\""));
}
else {
TEST_ExpectTrue(__().json.Print(value).ToString() == expectedResult);
}
}
// This is a rather extensive and long test.
// Despite its length, I've chosen to not break it up into smaller parts,
// since the same value `default.cache` is changed and built up all the way
// through this method.
// Trying to break it up into smaller and simpler tests would only mean
// putting less stress of `DBCache` than we currently do.
protected static function Test_DBCache_BasicWriteRead_DuringLoading()
{
local AcediaObject complexData;
Issue("Simple write/read sequence not working correctly.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
complexData = MakeTestJSONObject();
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away")),
complexData);
default.cache.Write(
__().json.Pointer(P("/just/another/place/not/near")),
complexData);
CheckLocation("/just/another/place/not/near/A", "simpleValue");
CheckLocation("/just/some/place/far/away/B/A/2", "huh");
CheckLocation("/just/another/place/not/near/B/A/1//2", "huh");
CheckLocation("/just/some/place/far/away/B/A/1/", "[true,null,\"huh\"]");
Issue("Data is read incorrectly after being overridden.");
default.cache.Write(
__().json.Pointer(P("/just/another/place/not/near/B/A/1//1")),
__().box.bool(false));
CheckLocation(
"/just/another/place/not/near/B/A/1/",
"[true,false,\"huh\"]");
default.cache.Write(
__().json.Pointer(P("/just/another/place/not/near/B/A")),
__().box.int(121));
CheckLocation("/just/another/place/not/near/B/A", "121");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/B/A/1/B")),
complexData);
CheckLocation("/just/some/place/far/away/B/A/1/B/B/A/0", "true");
default.cache.Write(
__().json.Pointer(
P("/just/some/place/far/away/C/inside_the_number!/hey")),
__().box.float(1.1));
CheckLocation("/just/some/place/far/away/C/inside_the_number!/hey", "null");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/7/hey")),
__().box.int(-345));
CheckLocation("/just/some/place/far/away/D", "[]");
CheckLocation("/just/some/place/far/away/D/7/hey", "-345");
default.cache.Write(
__().json.Pointer(
P("/just/some/place/far/away/D/inside_the_array!/hey")),
__().box.float(1.1));
CheckLocation("/just/some/place/far/away/D/inside_the_array!/hey", "null");
CheckLocation("/just/some/place/far/away/D", "[]");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/-")),
__().box.bool(true));
CheckLocation("/just/some/place/far/away/D", "[true]");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/7")),
__().box.bool(true));
CheckLocation(
"/just/some/place/far/away/D",
"[true,null,null,null,null,null,null,true]");
Issue("Writing at the end of a JSON array several times doesn't correctly"
@ "keep all values.");
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/-")),
__().box.int(13524));
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/-")), none);
default.cache.Write(
__().json.Pointer(P("/just/some/place/far/away/D/-")),
__().box.int(121));
CheckLocation(
"/just/some/place/far/away/D",
"[true,null,null,null,null,null,null,true,13524,null,121]");
}
protected static function Test_DBCache_AllOperations_DuringLoading()
{
local AcediaObject complexData;
local ArrayList newArrayData;
newArrayData = __().collections.EmptyArrayList();
newArrayData = newArrayData.AddInt(45);
newArrayData = newArrayData.AddItem(none);
newArrayData = newArrayData.AddString("lol");
Issue("Increment/remove/read sequence not working correctly.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
complexData = MakeTestJSONObject();
default.cache.Increment(__().json.Pointer(P("")), complexData);
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("oi"));
default.cache.Remove(__().json.Pointer(P("/B/A/1/B")));
default.cache.Remove(__().json.Pointer(P("/B/A/1/")));
default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5));
default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true));
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("! Yeah!"));
// Override all increments!
default.cache.Write(__().json.Pointer(P("/D")), newArrayData);
CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!");
CheckLocation("/B/A",
"[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]");
CheckLocation("/C", "29.5");
CheckLocation("/D", "[45,null,\"lol\"]");
}
protected static function Test_DBCache_ApplyingPendingEdits()
{
SubTest_DBCache_ApplyingPendingEdits_Simple();
SubTest_DBCache_ApplyingPendingEdits_Complex();
}
protected static function SubTest_DBCache_ApplyingPendingEdits_Simple()
{
local int i;
local array<DBCache.PendingEdit> result;
Issue("Pending writes successfully apply in simple JSON object case.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.Write(
__().json.Pointer(P("")),
__().collections.EmptyHashTable());
default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476));
default.cache.Write(__().json.Pointer(P("/hope")), none);
default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3));
result = default.cache.SetRealData(__().text.FromString("woah"));
for (i = 0; i < result.length; i += 1) {
TEST_ExpectTrue(result[i].successful);
}
CheckLocation("/hey", "476");
CheckLocation("/hope", "null");
CheckLocation("/-", "324.3");
Issue("Pending don't fail in a simple case of writing into JSON string.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476));
default.cache.Write(__().json.Pointer(P("/hope")), none);
default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3));
result = default.cache.SetRealData(__().text.FromString("woah"));
for (i = 0; i < result.length; i += 1) {
TEST_ExpectFalse(result[i].successful);
}
CheckLocation("/hey", "null");
CheckLocation("/hope", "null");
CheckLocation("/-", "null");
}
protected static function SubTest_DBCache_ApplyingPendingEdits_Complex()
{
local array<DBCache.PendingEdit> result;
Issue("Pending writes incorrectly apply in complex case.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777));
default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true));
default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1));
default.cache.Write(
__().json.Pointer(P("/new")),
__().collections.EmptyHashTable());
default.cache.Write(
__().json.Pointer(P("/new/sub")),
__().text.FromString("!SubString!"));
default.cache.Write(
__().json.Pointer(P("/D/impossiburu")),
__().text.FromString("!SubString!"));
result = default.cache.SetRealData(MakeTestJSONObject());
CheckLocation("/B/A/1/B", "777");
CheckLocation("/B/A/3", "true");
CheckLocation("/D/5", "1.1");
CheckLocation("/D/2", "null");
CheckLocation("/new", "{\"sub\":\"!SubString!\"}");
TEST_ExpectTrue(result[0].successful);
TEST_ExpectTrue(result[1].successful);
TEST_ExpectTrue(result[2].successful);
TEST_ExpectTrue(result[3].successful);
TEST_ExpectTrue(result[4].successful);
TEST_ExpectFalse(result[5].successful);
}
protected static function Test_DBCache_ApplyingPendingEdit_AllOperations()
{
local ArrayList newArrayData;
local array<DBCache.PendingEdit> result;
newArrayData = __().collections.EmptyArrayList();
newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol");
Issue("Pending increments and removals incorrectly apply in complex case.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("oi"));
default.cache.Remove(__().json.Pointer(P("/B/A/1/B")));
default.cache.Remove(__().json.Pointer(P("/B/A/1/")));
default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5));
default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true));
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData);
default.cache.Remove(__().json.Pointer(P("/B/A/Y")));
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("! Yeah!"));
default.cache.Write(__().json.Pointer(P("/D")), newArrayData);
result = default.cache.SetRealData(MakeTestJSONObject());
TEST_ExpectTrue(result.length == 9);
TEST_ExpectTrue(result[0].successful);
TEST_ExpectTrue(result[1].successful);
TEST_ExpectTrue(result[2].successful);
TEST_ExpectTrue(result[3].successful);
TEST_ExpectTrue(result[4].successful);
TEST_ExpectFalse(result[5].successful);
TEST_ExpectFalse(result[6].successful);
TEST_ExpectTrue(result[7].successful);
TEST_ExpectTrue(result[8].successful);
}
protected static function Test_DBCache_BasicWriteRead_AfterLoading()
{
local AcediaObject complexData;
Issue("Simple write/read sequence not working.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
complexData = MakeTestJSONObject();
TEST_ExpectTrue(default.cache.SetRealData(complexData).length == 0);
default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777));
default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true));
default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1));
default.cache.Write(
__().json.Pointer(P("/new")),
__().collections.EmptyHashTable());
default.cache.Write(
__().json.Pointer(P("/new/sub")),
__().text.FromString("!SubString!"));
default.cache.Write(
__().json.Pointer(P("/D/impossiburu")),
__().text.FromString("!SubString!"));
CheckLocation("/B/A/1/B", "777");
CheckLocation("/B/A/3", "true");
CheckLocation("/D/5", "1.1");
CheckLocation("/new", "{\"sub\":\"!SubString!\"}");
Issue("Simple write/read are affecting data when they shouldn't.");
CheckLocation("/D/2", "null");
default.cache.Write(__().json.Pointer(P("")), __().box.float(1.1));
default.cache.Write(__().json.Pointer(P("/hm?")), __().box.float(2.2));
CheckLocation("", "1.1");
}
protected static function Test_DBCache_AllOperations_AfterLoading()
{
local ArrayList newArrayData;
newArrayData = __().collections.EmptyArrayList();
newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol");
Issue("Increment/remove/read sequence not working correctly.");
default.cache = DBCache(__().memory.Allocate(class'DBCache'));
default.cache.SetRealData(MakeTestJSONObject());
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("oi")));
TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/B"))));
TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/"))));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A")),
newArrayData));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/C")),
__().box.float(34.5)));
TEST_ExpectFalse(default.cache.Increment(__().json.Pointer(P("/C")),
__().box.bool(true)));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")),
newArrayData));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")),
newArrayData));
TEST_ExpectFalse(default.cache.Remove(__().json.Pointer(P("/B/A/Y"))));
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")),
__().text.FromString("! Yeah!")));
default.cache.Write(__().json.Pointer(P("/D")), newArrayData);
CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!");
CheckLocation("/B/A",
"[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]");
CheckLocation("/C", "29.5");
CheckLocation("/D", "[45,null,\"lol\"]");
}
defaultproperties
{
caseGroup = "Database"
caseName = "DBConnection related tests"
}

64
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.
* *
@ -55,7 +55,6 @@ public final function Database Load(BaseText databaseLink)
{ {
local Parser parser; local Parser parser;
local Database result; local Database result;
local Text immutableDatabaseName;
local MutableText databaseName; local MutableText databaseName;
if (databaseLink == none) { if (databaseLink == none) {
@ -71,11 +70,9 @@ public final function Database Load(BaseText databaseLink)
parser.FreeSelf(); parser.FreeSelf();
return none; return none;
} }
immutableDatabaseName = databaseName.Copy(); result = LoadLocal(databaseName);
result = LoadLocal(immutableDatabaseName);
parser.FreeSelf(); parser.FreeSelf();
databaseName.FreeSelf(); databaseName.FreeSelf();
immutableDatabaseName.FreeSelf();
return result; return result;
} }
@ -114,6 +111,56 @@ public final function JSONPointer GetPointer(BaseText databaseLink)
return result; return result;
} }
/**
* Opens a new `DBConnection` to the data referred to by the database link.
*
* Opened `DBConnection` doesn't automatically start a connection, so you
* need to call its `Connect()` method.
*
* @param databaseLink Database link to the data we want to connect to.
* @return Initialized `DBConnection` in case given link is valid and `none`
* otherwise.
*/
public final function DBConnection OpenConnection(BaseText databaseLink)
{
local DBConnection result;
local Parser parser;
local Database databaseToConnect;
local JSONPointer locationToConnect;
local MutableText databaseName, textPointer;
if (databaseLink == none) {
return none;
}
parser = _.text.Parse(databaseLink);
// Only local DBs are supported for now!
// So just consume this prefix, if it's present.
parser.Match(P("[local]")).Confirm();
textPointer = parser
.R()
.MUntil(databaseName, _.text.GetCharacter(":"))
.MatchS(":")
.GetRemainderM();
if (parser.Ok())
{
databaseToConnect = LoadLocal(databaseName);
locationToConnect = _.json.Pointer(textPointer);
result = DBConnection(_.memory.Allocate(class'DBConnection'));
result.Initialize(databaseToConnect, locationToConnect);
_.memory.Free(databaseToConnect);
_.memory.Free(locationToConnect);
}
parser.FreeSelf();
_.memory.Free(databaseName);
_.memory.Free(textPointer);
if (result != none && !result.IsInitialized())
{
result.FreeSelf();
result = none;
}
return result;
}
/** /**
* Creates new local database with name `databaseName`. * Creates new local database with name `databaseName`.
* *
@ -169,6 +216,7 @@ public final function LocalDatabaseInstance LoadLocal(BaseText databaseName)
local Text rootRecordName; local Text rootRecordName;
local LocalDatabase newConfig; local LocalDatabase newConfig;
local LocalDatabaseInstance newLocalDBInstance; local LocalDatabaseInstance newLocalDBInstance;
local Text dbKey;
if (databaseName == none) { if (databaseName == none) {
return none; return none;
@ -189,7 +237,9 @@ public final function LocalDatabaseInstance LoadLocal(BaseText databaseName)
return none; return none;
} }
newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass)); newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
loadedLocalDatabases.SetItem(databaseName.Copy(), newLocalDBInstance); dbKey = databaseName.Copy();
loadedLocalDatabases.SetItem(dbKey, newLocalDBInstance);
dbKey.FreeSelf();
if (newConfig.HasDefinedRoot()) if (newConfig.HasDefinedRoot())
{ {
rootRecordName = newConfig.GetRootName(); rootRecordName = newConfig.GetRootName();
@ -279,7 +329,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.

24
sources/Data/Database/DBTask.uc

@ -6,7 +6,7 @@
* completed and will self-destruct afterwards. Concrete delegates are * completed and will self-destruct afterwards. Concrete delegates are
* declared in child classes of this `DBTask`, since they can have different * declared in child classes of this `DBTask`, since they can have different
* signatures, depending on the query. * signatures, depending on the query.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -54,6 +54,7 @@ var private int previousTaskLifeVersion;
var private Database.DBQueryResult taskResult; var private Database.DBQueryResult taskResult;
var private bool isReadyToComplete; var private bool isReadyToComplete;
var private int requestID;
var private LoggerAPI.Definition errLoopInTaskChain; var private LoggerAPI.Definition errLoopInTaskChain;
@ -65,6 +66,18 @@ protected function Finalizer()
previousTask = none; previousTask = none;
previousTaskLifeVersion = -1; previousTaskLifeVersion = -1;
isReadyToComplete = false; isReadyToComplete = false;
requestID = 0;
}
/**
* Returns ID of the request set inside `SetResult()` for the caller `DBTask`.
*
* @return ID of the request set inside `SetResult()` for the caller `DBTask`.
* If `SetResult()` wasn't yet called returns `0`.
*/
protected function int GetRequestID()
{
return requestID;
} }
/** /**
@ -105,12 +118,17 @@ public final function Database.DBQueryResult GetResult()
* This value can be assigned several times and the last assigned value will * This value can be assigned several times and the last assigned value will
* be used. * be used.
* *
* @param result Result of the query, relevant to the caller task. * @param result Result of the query, relevant to the caller task.
* @param requestID ID of the request this task is responding to, specified
* at the time request was made.
*/ */
public final function SetResult(Database.DBQueryResult result) public final function SetResult(
Database.DBQueryResult result,
optional int completedRequestID)
{ {
taskResult = result; taskResult = result;
isReadyToComplete = true; isReadyToComplete = true;
requestID = completedRequestID;
} }
/** /**

129
sources/Data/Database/Database.uc

@ -7,7 +7,7 @@
* All of the methods are asynchronous - they do not return requested * All of the methods are asynchronous - they do not return requested
* values immediately and instead require user to provide a handler function * values immediately and instead require user to provide a handler function
* that will be called once operation is completed. * that will be called once operation is completed.
* Copyright 2021-2022 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -73,15 +73,27 @@ enum DBQueryResult
* to load data as immutable Acedia's types and `true` will make it load * to load data as immutable Acedia's types and `true` will make it load
* data as mutable types. This setting does not affect `Collection`s into * data as mutable types. This setting does not affect `Collection`s into
* which JSON arrays and objects are converted - they are always mutable. * which JSON arrays and objects are converted - they are always mutable.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `ReadData()` call. * @return Task object that corresponds to this `ReadData()` call.
* * Guaranteed to be not `none`; * * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete: * * Use it to connect a handler for when reading task is complete:
* `ReadData(...).connect = handler`, * `ReadData(...).connect = handler`,
* where `handler` must have the following signature: * where `handler` must have the following signature:
* `connect(DBQueryResult result, AcediaObject data)`; * ```
* connect(
* DBQueryResult result,
* take AcediaObject data,
* Database source,
* int requestID)`;
* ```
* * Ownership of `data` object returned in the `connect()` is considered * * Ownership of `data` object returned in the `connect()` is considered
* to be transferred to whoever handled result of this query. * to be transferred to whoever handled result of this query.
* It must be deallocated once no longer needed. * It must be deallocated once no longer needed.
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`, * * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer` and `DBR_InvalidDatabase`; * `DBR_InvalidPointer` and `DBR_InvalidDatabase`;
* * `data` is guaranteed to be `none` if `result != DBR_Success`; * * `data` is guaranteed to be `none` if `result != DBR_Success`;
@ -90,7 +102,8 @@ enum DBQueryResult
*/ */
public function DBReadTask ReadData( public function DBReadTask ReadData(
JSONPointer pointer, JSONPointer pointer,
optional bool makeMutable) optional bool makeMutable,
optional int requestID)
{ {
return none; return none;
} }
@ -110,12 +123,18 @@ public function DBReadTask ReadData(
* @param data Data that needs to be written at the specified location * @param data Data that needs to be written at the specified location
* inside the database. For method to succeed this object needs to have * inside the database. For method to succeed this object needs to have
* JSON-compatible type (see `_.json.IsCompatible()` for more details). * JSON-compatible type (see `_.json.IsCompatible()` for more details).
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `WriteData()` call. * @return Task object that corresponds to this `WriteData()` call.
* * Guaranteed to be not `none`; * * Guaranteed to be not `none`;
* * Use it to connect a handler for when writing task is complete: * * Use it to connect a handler for when writing task is complete:
* `WriteData(...).connect = handler`, * `WriteData(...).connect = handler`,
* where `handler` must have the following signature: * where `handler` must have the following signature:
* `connect(DBQueryResult result)`; * `connect(DBQueryResult result, Database source, int requestID)`;
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`, * * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer`, `DBR_InvalidDatabase` and `DBR_InvalidData`; * `DBR_InvalidPointer`, `DBR_InvalidDatabase` and `DBR_InvalidData`;
* * Data is actually written inside the database iff * * Data is actually written inside the database iff
@ -128,7 +147,10 @@ public function DBReadTask ReadData(
* Example: writing data at "/sub-object/valueA" will always fail if * Example: writing data at "/sub-object/valueA" will always fail if
* "sub-object" does not exist. * "sub-object" does not exist.
*/ */
public function DBWriteTask WriteData(JSONPointer pointer, AcediaObject data) public function DBWriteTask WriteData(
JSONPointer pointer,
AcediaObject data,
optional int requestID)
{ {
return none; return none;
} }
@ -141,12 +163,18 @@ public function DBWriteTask WriteData(JSONPointer pointer, AcediaObject data)
* *
* @param pointer JSON pointer to the location of the data to remove from * @param pointer JSON pointer to the location of the data to remove from
* database. `none` is always treated as an invalid JSON pointer. * database. `none` is always treated as an invalid JSON pointer.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `RemoveData()` call. * @return Task object that corresponds to this `RemoveData()` call.
* * Guaranteed to be not `none`; * * Guaranteed to be not `none`;
* * Use it to connect a handler for when writing task is complete: * * Use it to connect a handler for when writing task is complete:
* `RemoveData(...).connect = handler`, * `RemoveData(...).connect = handler`,
* where `handler` must have the following signature: * where `handler` must have the following signature:
* `connect(DBQueryResult result)`. * `connect(DBQueryResult result, Database source, int requestID)`.
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`, * * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer` and `DBR_InvalidDatabase`; * `DBR_InvalidPointer` and `DBR_InvalidDatabase`;
* * Data is actually removed from the database iff * * Data is actually removed from the database iff
@ -154,7 +182,9 @@ public function DBWriteTask WriteData(JSONPointer pointer, AcediaObject data)
* * `DBR_InvalidPointer` can be produced if either `pointer == none` or * * `DBR_InvalidPointer` can be produced if either `pointer == none` or
* it does not point at any existing value inside the caller database. * it does not point at any existing value inside the caller database.
*/ */
public function DBRemoveTask RemoveData(JSONPointer pointer) public function DBRemoveTask RemoveData(
JSONPointer pointer,
optional int requestID)
{ {
return none; return none;
} }
@ -166,12 +196,24 @@ public function DBRemoveTask RemoveData(JSONPointer pointer)
* @param pointer JSON pointer to the location of the data for which type * @param pointer JSON pointer to the location of the data for which type
* needs to be checked. * needs to be checked.
* `none` is always treated as an invalid JSON pointer. * `none` is always treated as an invalid JSON pointer.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `CheckDataType()` call. * @return Task object that corresponds to this `CheckDataType()` call.
* * Guaranteed to be not `none`; * * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete: * * Use it to connect a handler for when reading task is complete:
* `CheckDataType(...).connect = handler`, * `CheckDataType(...).connect = handler`,
* where `handler` must have the following signature: * where `handler` must have the following signature:
* `connect(DBQueryResult result, Database.DataType type)`; * ```
* connect(
* DBQueryResult result,
* Database.DataType type,
* Database source,
* int requestID)
* ```
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`, * * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer` and `DBR_InvalidDatabase`; * `DBR_InvalidPointer` and `DBR_InvalidDatabase`;
* * This task can only fail if either caller database is broken * * This task can only fail if either caller database is broken
@ -181,7 +223,9 @@ public function DBRemoveTask RemoveData(JSONPointer pointer)
* * Data is actually removed from the database iff * * Data is actually removed from the database iff
* `result == DBR_Success`. * `result == DBR_Success`.
*/ */
public function DBCheckTask CheckDataType(JSONPointer pointer) public function DBCheckTask CheckDataType(
JSONPointer pointer,
optional int requestID)
{ {
return none; return none;
} }
@ -197,12 +241,24 @@ public function DBCheckTask CheckDataType(JSONPointer pointer)
* @param pointer JSON pointer to the location of the JSON object or array * @param pointer JSON pointer to the location of the JSON object or array
* for which size needs to be obtained. * for which size needs to be obtained.
* `none` is always treated as an invalid JSON pointer. * `none` is always treated as an invalid JSON pointer.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `GetDataSize()` call. * @return Task object that corresponds to this `GetDataSize()` call.
* * Guaranteed to be not `none`; * * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete: * * Use it to connect a handler for when reading task is complete:
* `GetDataSize(...).connect = handler`, * `GetDataSize(...).connect = handler`,
* where `handler` must have the following signature: * where `handler` must have the following signature:
* `connect(DBQueryResult result, int size)`. * ```
* connect(
* DBQueryResult result,
* int size,
* Database source,
* int requestID)
* ```
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`, * * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer` and `DBR_InvalidDatabase`; * `DBR_InvalidPointer` and `DBR_InvalidDatabase`;
* * Returned `size` value is actually a size of referred * * Returned `size` value is actually a size of referred
@ -211,7 +267,9 @@ public function DBCheckTask CheckDataType(JSONPointer pointer)
* it does not point at a JSON object or array inside the * it does not point at a JSON object or array inside the
* caller database. * caller database.
*/ */
public function DBSizeTask GetDataSize(JSONPointer pointer) public function DBSizeTask GetDataSize(
JSONPointer pointer,
optional int requestID)
{ {
return none; return none;
} }
@ -225,15 +283,27 @@ public function DBSizeTask GetDataSize(JSONPointer pointer)
* @param pointer JSON pointer to the location of the JSON object for which * @param pointer JSON pointer to the location of the JSON object for which
* keys need to be obtained. * keys need to be obtained.
* `none` is always treated as an invalid JSON pointer. * `none` is always treated as an invalid JSON pointer.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `GetDataKeys()` call. * @return Task object that corresponds to this `GetDataKeys()` call.
* * Guaranteed to be not `none`; * * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete: * * Use it to connect a handler for when reading task is complete:
* `GetDataKeys(...).connect = handler`, * `GetDataKeys(...).connect = handler`,
* where `handler` must have the following signature: * where `handler` must have the following signature:
* `connect(DBQueryResult result, ArrayList keys)`. * ```
* connect(
* DBQueryResult result,
* take ArrayList keys,
* Database source,
* int requestID)
* ```
* * Ownership of `keys` array returned in the `connect()` is considered * * Ownership of `keys` array returned in the `connect()` is considered
* to be transferred to whoever handled result of this query. * to be transferred to whoever handled result of this query.
* It must be deallocated once no longer needed. * It must be deallocated once no longer needed.
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`, * * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer`, `DBR_InvalidData` and `DBR_InvalidDatabase`; * `DBR_InvalidPointer`, `DBR_InvalidData` and `DBR_InvalidDatabase`;
* * Returned `keys` will be non-`none` and contain keys of the referred * * Returned `keys` will be non-`none` and contain keys of the referred
@ -243,7 +313,9 @@ public function DBSizeTask GetDataSize(JSONPointer pointer)
* point at a JSON object inside caller database * point at a JSON object inside caller database
* (value can either not exist at all or have some other type). * (value can either not exist at all or have some other type).
*/ */
public function DBKeysTask GetDataKeys(JSONPointer pointer) public function DBKeysTask GetDataKeys(
JSONPointer pointer,
optional int requestID)
{ {
return none; return none;
} }
@ -255,19 +327,21 @@ public function DBKeysTask GetDataKeys(JSONPointer pointer)
* "Incrementing" is an operation that is safe from the point of view of * "Incrementing" is an operation that is safe from the point of view of
* simultaneous access. What "incrementing" actually does depends on * simultaneous access. What "incrementing" actually does depends on
* the passed JSON value (`increment` parameter): * the passed JSON value (`increment` parameter):
* (0. Unless `pointer` points at the JSON null value - then "increment" *
* acts as a `WriteData()` method regardless of `increment`'s value); * (0. ...unless `pointer` points at the JSON null or missing value (within
* existing container - then "increment" acts as a `WriteData()` method
* regardless of `increment`'s value;)
* 1. JSON null: it never modifies existing value and reports an error if * 1. JSON null: it never modifies existing value and reports an error if
* existing value was not itself JSON null; * existing value was not itself JSON null;
* 2. JSON bool: if combines with stored JSON bool value - * 2. JSON bool: if combined with stored JSON bool value -
* performs logical "or" operation. Otherwise fails; * performs logical "or" operation. Otherwise fails;
* 3. JSON number: if combines with stored JSON numeric value - * 3. JSON number: if combined with stored JSON numeric value -
* adds values together. Otherwise fails. * adds values together. Otherwise fails.
* 4. JSON string: if combines with stored JSON string value - * 4. JSON string: if combined with stored JSON string value -
* concatenates itself at the end. Otherwise fails. * concatenates itself at the end. Otherwise fails.
* 5. JSON array: if combines with stored JSON array value - * 5. JSON array: if combined with stored JSON array value -
* concatenates itself at the end. Otherwise fails. * concatenates itself at the end. Otherwise fails.
* 6. JSON object: if combines with stored JSON object value - * 6. JSON object: if combined with stored JSON object value -
* `increment` adds it's own values with new keys into the stored * `increment` adds it's own values with new keys into the stored
* JSON object. Does not override old values. * JSON object. Does not override old values.
* Fails when combined with any other type. * Fails when combined with any other type.
@ -280,12 +354,18 @@ public function DBKeysTask GetDataKeys(JSONPointer pointer)
* with `increment` parameter. * with `increment` parameter.
* @param increment JSON-compatible value to be used as an increment for * @param increment JSON-compatible value to be used as an increment for
* the data at the specified location inside the database. * the data at the specified location inside the database.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `IncrementData()` call. * @return Task object that corresponds to this `IncrementData()` call.
* * Guaranteed to be not `none`; * * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete: * * Use it to connect a handler for when reading task is complete:
* `IncrementData(...).connect = handler`, * `IncrementData(...).connect = handler`,
* where `handler` must have the following signature: * where `handler` must have the following signature:
* `connect(DBQueryResult result)`. * `connect(DBQueryResult result, Database source, int requestID)`.
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`, * * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer`, `DBR_InvalidData` and `DBR_InvalidDatabase`; * `DBR_InvalidPointer`, `DBR_InvalidData` and `DBR_InvalidDatabase`;
* * Data is actually incremented iff `result == DBR_Success`; * * Data is actually incremented iff `result == DBR_Success`;
@ -299,7 +379,8 @@ public function DBKeysTask GetDataKeys(JSONPointer pointer)
*/ */
public function DBIncrementTask IncrementData( public function DBIncrementTask IncrementData(
JSONPointer pointer, JSONPointer pointer,
AcediaObject increment) AcediaObject increment,
optional int requestID)
{ {
return none; return none;
} }

19
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.
* *
@ -184,6 +184,7 @@ private final function DBRecordPointer MakeRecordPointer(
return pointer; return pointer;
} }
// Converts `JSONPointer` into our internal representation.
private final function DBRecordPointer ConvertPointer(JSONPointer jsonPointer) private final function DBRecordPointer ConvertPointer(JSONPointer jsonPointer)
{ {
if (jsonPointer == none) { if (jsonPointer == none) {
@ -192,6 +193,8 @@ private final function DBRecordPointer ConvertPointer(JSONPointer jsonPointer)
return ConvertPointerPath(jsonPointer, 0, jsonPointer.GetLength()); return ConvertPointerPath(jsonPointer, 0, jsonPointer.GetLength());
} }
// Produced out internal pointer representation `DBRecordPointer` to
// the container that stores object, referred to by a given `JSONPointer`.
private final function DBRecordPointer ConvertContainerPointer( private final function DBRecordPointer ConvertContainerPointer(
JSONPointer jsonPointer) JSONPointer jsonPointer)
{ {
@ -264,9 +267,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 +307,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 +333,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 +692,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 +726,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 +875,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;

107
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;
} }
@ -103,7 +103,7 @@ private final function CompleteAllTasks(
optional float dilationCoefficient) optional float dilationCoefficient)
{ {
if (lastTask != none && lastTask.GetLifeVersion() == lastTaskLifeVersion) { if (lastTask != none && lastTask.GetLifeVersion() == lastTaskLifeVersion) {
lastTask.TryCompleting(); lastTask.TryCompleting(self);
} }
lastTask = none; lastTask = none;
lastTaskLifeVersion = -1; lastTaskLifeVersion = -1;
@ -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);
} }
} }
@ -149,54 +149,61 @@ private final function DBTask MakeNewTask(class<DBTask> newTaskClass)
return newTask; return newTask;
} }
private function bool ValidatePointer(JSONPointer pointer, DBTask relevantTask) private function bool ValidatePointer(
JSONPointer pointer,
DBTask relevantTask,
int requestID)
{ {
if (pointer != none) { if (pointer != none) {
return true; return true;
} }
relevantTask.SetResult(DBR_InvalidPointer); relevantTask.SetResult(DBR_InvalidPointer, requestID);
return false; return false;
} }
private function bool ValidateRootRecord(DBTask relevantTask) private function bool ValidateRootRecord(DBTask relevantTask, int requestID)
{ {
if (rootRecord != none) { if (rootRecord != none) {
return true; return true;
} }
relevantTask.SetResult(DBR_InvalidDatabase); relevantTask.SetResult(DBR_InvalidDatabase, requestID);
return false; return false;
} }
public function DBReadTask ReadData( public function DBReadTask ReadData(
JSONPointer pointer, JSONPointer pointer,
optional bool makeMutable) optional bool makeMutable,
optional int requestID)
{ {
local AcediaObject queryResult; local AcediaObject queryResult;
local DBReadTask readTask; local DBReadTask readTask;
readTask = DBReadTask(MakeNewTask(class'DBReadTask')); readTask = DBReadTask(MakeNewTask(class'DBReadTask'));
if (!ValidatePointer(pointer, readTask)) return readTask; if (!ValidatePointer(pointer, readTask, requestID)) return readTask;
if (!ValidateRootRecord(readTask)) return readTask; if (!ValidateRootRecord(readTask, requestID)) return readTask;
if (rootRecord.LoadObject(pointer, queryResult, makeMutable)) if (rootRecord.LoadObject(pointer, queryResult, makeMutable))
{ {
readTask.SetReadData(queryResult); readTask.SetReadData(queryResult);
readTask.SetResult(DBR_Success); readTask.SetResult(DBR_Success, requestID);
} }
else else
{ {
readTask.SetResult(DBR_InvalidPointer); readTask.SetResult(DBR_InvalidPointer, requestID);
_.memory.Free(queryResult); // just in case _.memory.Free(queryResult); // just in case
} }
return readTask; return readTask;
} }
public function DBWriteTask WriteData(JSONPointer pointer, AcediaObject data) public function DBWriteTask WriteData(
JSONPointer pointer,
AcediaObject data,
optional int requestID)
{ {
local bool isDataStorable; local bool isDataStorable;
local DBWriteTask writeTask; local DBWriteTask writeTask;
writeTask = DBWriteTask(MakeNewTask(class'DBWriteTask')); writeTask = DBWriteTask(MakeNewTask(class'DBWriteTask'));
if (!ValidatePointer(pointer, writeTask)) return writeTask; if (!ValidatePointer(pointer, writeTask, requestID)) return writeTask;
if (!ValidateRootRecord(writeTask)) return writeTask; if (!ValidateRootRecord(writeTask, requestID)) return writeTask;
// We can only write JSON array as the root value // We can only write JSON array as the root value
if (data != none && pointer.GetLength() <= 0) { if (data != none && pointer.GetLength() <= 0) {
@ -207,99 +214,111 @@ public function DBWriteTask WriteData(JSONPointer pointer, AcediaObject data)
} }
if (!isDataStorable) if (!isDataStorable)
{ {
writeTask.SetResult(DBR_InvalidData); writeTask.SetResult(DBR_InvalidData, requestID);
return writeTask; return writeTask;
} }
if (rootRecord.SaveObject(pointer, data)) if (rootRecord.SaveObject(pointer, data))
{ {
writeTask.SetResult(DBR_Success); writeTask.SetResult(DBR_Success, requestID);
ScheduleDiskUpdate(); ScheduleDiskUpdate();
} }
else { else {
writeTask.SetResult(DBR_InvalidPointer); writeTask.SetResult(DBR_InvalidPointer, requestID);
} }
return writeTask; return writeTask;
} }
public function DBRemoveTask RemoveData(JSONPointer pointer) public function DBRemoveTask RemoveData(
JSONPointer pointer,
optional int requestID)
{ {
local DBRemoveTask removeTask; local DBRemoveTask removeTask;
removeTask = DBRemoveTask(MakeNewTask(class'DBRemoveTask')); removeTask = DBRemoveTask(MakeNewTask(class'DBRemoveTask'));
if (!ValidatePointer(pointer, removeTask)) return removeTask; if (!ValidatePointer(pointer, removeTask, requestID)) return removeTask;
if (!ValidateRootRecord(removeTask)) return removeTask; if (!ValidateRootRecord(removeTask, requestID)) return removeTask;
if (pointer.GetLength() == 0) if (pointer.GetLength() == 0)
{ {
rootRecord.EmptySelf(); rootRecord.EmptySelf();
removeTask.SetResult(DBR_Success); removeTask.SetResult(DBR_Success, requestID);
return removeTask; return removeTask;
} }
if (rootRecord.RemoveObject(pointer)) if (rootRecord.RemoveObject(pointer))
{ {
removeTask.SetResult(DBR_Success); removeTask.SetResult(DBR_Success, requestID);
ScheduleDiskUpdate(); ScheduleDiskUpdate();
} }
else { else {
removeTask.SetResult(DBR_InvalidPointer); removeTask.SetResult(DBR_InvalidPointer, requestID);
} }
return removeTask; return removeTask;
} }
public function DBCheckTask CheckDataType(JSONPointer pointer) public function DBCheckTask CheckDataType(
JSONPointer pointer,
optional int requestID)
{ {
local DBCheckTask checkTask; local DBCheckTask checkTask;
checkTask = DBCheckTask(MakeNewTask(class'DBCheckTask')); checkTask = DBCheckTask(MakeNewTask(class'DBCheckTask'));
if (!ValidatePointer(pointer, checkTask)) return checkTask; if (!ValidatePointer(pointer, checkTask, requestID)) return checkTask;
if (!ValidateRootRecord(checkTask)) return checkTask; if (!ValidateRootRecord(checkTask, requestID)) return checkTask;
checkTask.SetDataType(rootRecord.GetObjectType(pointer)); checkTask.SetDataType(rootRecord.GetObjectType(pointer));
checkTask.SetResult(DBR_Success); checkTask.SetResult(DBR_Success, requestID);
return checkTask; return checkTask;
} }
public function DBSizeTask GetDataSize(JSONPointer pointer) public function DBSizeTask GetDataSize(
JSONPointer pointer,
optional int requestID)
{ {
local DBSizeTask sizeTask; local DBSizeTask sizeTask;
sizeTask = DBSizeTask(MakeNewTask(class'DBSizeTask')); sizeTask = DBSizeTask(MakeNewTask(class'DBSizeTask'));
if (!ValidatePointer(pointer, sizeTask)) return sizeTask; if (!ValidatePointer(pointer, sizeTask, requestID)) return sizeTask;
if (!ValidateRootRecord(sizeTask)) return sizeTask; if (!ValidateRootRecord(sizeTask, requestID)) return sizeTask;
sizeTask.SetDataSize(rootRecord.GetObjectSize(pointer)); sizeTask.SetDataSize(rootRecord.GetObjectSize(pointer));
sizeTask.SetResult(DBR_Success); sizeTask.SetResult(DBR_Success, requestID);
return sizeTask; return sizeTask;
} }
public function DBKeysTask GetDataKeys(JSONPointer pointer) public function DBKeysTask GetDataKeys(
JSONPointer pointer,
optional int requestID)
{ {
local ArrayList keys; local ArrayList keys;
local DBKeysTask keysTask; local DBKeysTask keysTask;
keysTask = DBKeysTask(MakeNewTask(class'DBKeysTask')); keysTask = DBKeysTask(MakeNewTask(class'DBKeysTask'));
if (!ValidatePointer(pointer, keysTask)) return keysTask; if (!ValidatePointer(pointer, keysTask, requestID)) return keysTask;
if (!ValidateRootRecord(keysTask)) return keysTask; if (!ValidateRootRecord(keysTask, requestID)) return keysTask;
keys = rootRecord.GetObjectKeys(pointer); keys = rootRecord.GetObjectKeys(pointer);
keysTask.SetDataKeys(keys); keysTask.SetDataKeys(keys);
if (keys == none) { if (keys == none) {
keysTask.SetResult(DBR_InvalidData); keysTask.SetResult(DBR_InvalidData, requestID);
} }
else { else {
keysTask.SetResult(DBR_Success); keysTask.SetResult(DBR_Success, requestID);
} }
return keysTask; return keysTask;
} }
public function DBIncrementTask IncrementData( public function DBIncrementTask IncrementData(
JSONPointer pointer, JSONPointer pointer,
AcediaObject increment) AcediaObject increment,
optional int requestID)
{ {
local DBQueryResult queryResult; local DBQueryResult queryResult;
local DBIncrementTask incrementTask; local DBIncrementTask incrementTask;
incrementTask = DBIncrementTask(MakeNewTask(class'DBIncrementTask')); incrementTask = DBIncrementTask(MakeNewTask(class'DBIncrementTask'));
if (!ValidatePointer(pointer, incrementTask)) return incrementTask; if (!ValidatePointer(pointer, incrementTask, requestID)) {
if (!ValidateRootRecord(incrementTask)) return incrementTask; return incrementTask;
}
if (!ValidateRootRecord(incrementTask, requestID)) {
return incrementTask;
}
queryResult = rootRecord.IncrementObject(pointer, increment); queryResult = rootRecord.IncrementObject(pointer, increment);
incrementTask.SetResult(queryResult); incrementTask.SetResult(queryResult, requestID);
if (queryResult == DBR_Success) { if (queryResult == DBR_Success) {
ScheduleDiskUpdate(); ScheduleDiskUpdate();
} }

7
sources/Data/Database/Tasks/DBCheckTask.uc

@ -1,6 +1,6 @@
/** /**
* Variant of `DBTask` for `CheckDataType()` query. * Variant of `DBTask` for `CheckDataType()` query.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -24,7 +24,8 @@ var private Database.DataType queryTypeResponse;
delegate connect( delegate connect(
Database.DBQueryResult result, Database.DBQueryResult result,
Database.DataType type, Database.DataType type,
Database source) {} Database source,
int requestID) {}
protected function Finalizer() protected function Finalizer()
{ {
@ -40,7 +41,7 @@ public function SetDataType(Database.DataType type)
protected function CompleteSelf(Database source) protected function CompleteSelf(Database source)
{ {
connect(GetResult(), queryTypeResponse, source); connect(GetResult(), queryTypeResponse, source, GetRequestID());
} }
defaultproperties defaultproperties

9
sources/Data/Database/Tasks/DBIncrementTask.uc

@ -1,6 +1,6 @@
/** /**
* Variant of `DBTask` for `IncrementData()` query. * Variant of `DBTask` for `IncrementData()` query.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -19,7 +19,10 @@
*/ */
class DBIncrementTask extends DBTask; class DBIncrementTask extends DBTask;
delegate connect(Database.DBQueryResult result, Database source) {} delegate connect(
Database.DBQueryResult result,
Database source,
int requestID) {}
protected function Finalizer() protected function Finalizer()
{ {
@ -29,7 +32,7 @@ protected function Finalizer()
protected function CompleteSelf(Database source) protected function CompleteSelf(Database source)
{ {
connect(GetResult(), source); connect(GetResult(), source, GetRequestID());
} }
defaultproperties defaultproperties

9
sources/Data/Database/Tasks/DBKeysTask.uc

@ -1,6 +1,6 @@
/** /**
* Variant of `DBTask` for `GetDataKeys()` query. * Variant of `DBTask` for `GetDataKeys()` query.
* Copyright 2021-2022 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -23,8 +23,9 @@ var private ArrayList queryKeysResponse;
delegate connect( delegate connect(
Database.DBQueryResult result, Database.DBQueryResult result,
ArrayList keys, /*take*/ ArrayList keys,
Database source) {} Database source,
int requestID) {}
protected function Finalizer() protected function Finalizer()
{ {
@ -40,7 +41,7 @@ public function SetDataKeys(/* take */ ArrayList keys)
protected function CompleteSelf(Database source) protected function CompleteSelf(Database source)
{ {
connect(GetResult(), queryKeysResponse, source); connect(GetResult(), queryKeysResponse, source, GetRequestID());
} }
defaultproperties defaultproperties

9
sources/Data/Database/Tasks/DBReadTask.uc

@ -1,6 +1,6 @@
/** /**
* Variant of `DBTask` for `ReadData()` query. * Variant of `DBTask` for `ReadData()` query.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -23,8 +23,9 @@ var private AcediaObject queryDataResponse;
delegate connect( delegate connect(
Database.DBQueryResult result, Database.DBQueryResult result,
AcediaObject data, /*take*/ AcediaObject data,
Database source) {} Database source,
int requestID) {}
protected function Finalizer() protected function Finalizer()
{ {
@ -40,7 +41,7 @@ public function SetReadData(AcediaObject data)
protected function CompleteSelf(Database source) protected function CompleteSelf(Database source)
{ {
connect(GetResult(), queryDataResponse, source); connect(GetResult(), queryDataResponse, source, GetRequestID());
} }
defaultproperties defaultproperties

9
sources/Data/Database/Tasks/DBRemoveTask.uc

@ -1,6 +1,6 @@
/** /**
* Variant of `DBTask` for `RemoveData()` query. * Variant of `DBTask` for `RemoveData()` query.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -19,7 +19,10 @@
*/ */
class DBRemoveTask extends DBTask; class DBRemoveTask extends DBTask;
delegate connect(Database.DBQueryResult result, Database source) {} delegate connect(
Database.DBQueryResult result,
Database source,
int requestID) {}
protected function Finalizer() protected function Finalizer()
{ {
@ -29,7 +32,7 @@ protected function Finalizer()
protected function CompleteSelf(Database source) protected function CompleteSelf(Database source)
{ {
connect(GetResult(), source); connect(GetResult(), source, GetRequestID());
} }
defaultproperties defaultproperties

10
sources/Data/Database/Tasks/DBSizeTask.uc

@ -1,6 +1,6 @@
/** /**
* Variant of `DBTask` for `GetDataSize()` query. * Variant of `DBTask` for `GetDataSize()` query.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -21,7 +21,11 @@ class DBSizeTask extends DBTask;
var private int querySizeResponse; var private int querySizeResponse;
delegate connect(Database.DBQueryResult result, int size, Database source) {} delegate connect(
Database.DBQueryResult result,
int size,
Database source,
int requestID) {}
protected function Finalizer() protected function Finalizer()
{ {
@ -37,7 +41,7 @@ public function SetDataSize(int size)
protected function CompleteSelf(Database source) protected function CompleteSelf(Database source)
{ {
connect(GetResult(), querySizeResponse, source); connect(GetResult(), querySizeResponse, source, GetRequestID());
} }
defaultproperties defaultproperties

9
sources/Data/Database/Tasks/DBWriteTask.uc

@ -1,6 +1,6 @@
/** /**
* Variant of `DBTask` for `WriteData()` query. * Variant of `DBTask` for `WriteData()` query.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -19,7 +19,10 @@
*/ */
class DBWriteTask extends DBTask; class DBWriteTask extends DBTask;
delegate connect(Database.DBQueryResult result, Database source) {} delegate connect(
Database.DBQueryResult result,
Database source,
int requestID) {}
protected function Finalizer() protected function Finalizer()
{ {
@ -29,7 +32,7 @@ protected function Finalizer()
protected function CompleteSelf(Database source) protected function CompleteSelf(Database source)
{ {
connect(GetResult(), source); connect(GetResult(), source, GetRequestID());
} }
defaultproperties defaultproperties

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

252
sources/Data/Database/Tests/TEST_LocalDatabase.uc

@ -27,63 +27,78 @@ var protected Database.DBQueryResult resultType;
var protected Database.DataType resultDataType; var protected Database.DataType resultDataType;
var protected HashTable resultData; var protected HashTable resultData;
var protected AcediaObject resultObject; var protected AcediaObject resultObject;
var protected int resultRequestID;
protected function DBReadingHandler( protected function DBReadingHandler(
Database.DBQueryResult result, Database.DBQueryResult result,
AcediaObject data, AcediaObject data,
Database source) Database source,
int requestID)
{ {
default.resultType = result; default.resultType = result;
default.resultObject = data; default.resultObject = data;
default.resultData = HashTable(data); default.resultData = HashTable(data);
default.resultRequestID = requestID;
} }
protected function DBKeysHandler( protected function DBKeysHandler(
Database.DBQueryResult result, Database.DBQueryResult result,
ArrayList keys, ArrayList keys,
Database source) Database source,
int requestID)
{ {
default.resultType = result; default.resultType = result;
default.resultKeys = keys; default.resultKeys = keys;
default.resultRequestID = requestID;
} }
protected function DBCheckHandler( protected function DBCheckHandler(
Database.DBQueryResult result, Database.DBQueryResult result,
Database.DataType type, Database.DataType type,
Database source) Database source,
int requestID)
{ {
default.resultType = result; default.resultType = result;
default.resultDataType = type; default.resultDataType = type;
default.resultRequestID = requestID;
} }
protected function DBSizeHandler( protected function DBSizeHandler(
Database.DBQueryResult result, Database.DBQueryResult result,
int size, int size,
Database source) Database source,
int requestID)
{ {
default.resultType = result; default.resultType = result;
default.resultSize = size; default.resultSize = size;
default.resultRequestID = requestID;
} }
protected function DBWritingHandler( protected function DBWritingHandler(
Database.DBQueryResult result, Database.DBQueryResult result,
Database source) Database source,
int requestID)
{ {
default.resultType = result; default.resultType = result;
default.resultRequestID = requestID;
} }
protected function DBIncrementHandler( protected function DBIncrementHandler(
Database.DBQueryResult result, Database.DBQueryResult result,
Database source) Database source,
int requestID)
{ {
default.resultType = result; default.resultType = result;
default.resultRequestID = requestID;
} }
protected function DBRemoveHandler( protected function DBRemoveHandler(
Database.DBQueryResult result, Database.DBQueryResult result,
Database source) Database source,
int requestID)
{ {
default.resultType = result; default.resultType = result;
default.resultRequestID = requestID;
} }
protected static function ReadFromDB(LocalDatabaseInstance db, string pointer) protected static function ReadFromDB(LocalDatabaseInstance db, string pointer)
@ -117,7 +132,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()
@ -220,19 +235,20 @@ protected static function TESTS()
Test_TaskChaining(); Test_TaskChaining();
Test_Removal(); Test_Removal();
Test_Increment(); Test_Increment();
Test_RequestID();
} }
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 +487,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 +511,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 +546,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 +792,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 +803,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 +877,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 +920,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)
@ -1232,8 +1248,9 @@ protected static function SubTest_IncrementRewriteArray(
protected static function SubTest_IncrementMissing(LocalDatabaseInstance db) protected static function SubTest_IncrementMissing(LocalDatabaseInstance db)
{ {
local DBIncrementTask task; local DBIncrementTask task;
Issue("New values are created in database after incrementing with path" local DBCheckTask checkTask;
Issue("New values are not created in database after incrementing with path"
@ "pointing to non-existing value."); @ "pointing to non-existing value.");
task = db.IncrementData(__().json.Pointer(P("/L")), __().box.int(345)); task = db.IncrementData(__().json.Pointer(P("/L")), __().box.int(345));
task.connect = DBIncrementHandler; task.connect = DBIncrementHandler;
@ -1245,13 +1262,168 @@ protected static function SubTest_IncrementMissing(LocalDatabaseInstance db)
task.connect = DBIncrementHandler; task.connect = DBIncrementHandler;
task.TryCompleting(); task.TryCompleting();
TEST_ExpectTrue(default.resultType == DBR_Success); TEST_ExpectTrue(default.resultType == DBR_Success);
db.CheckDataType(__().json.Pointer(P("/L"))).connect = DBCheckHandler; checkTask = db.CheckDataType(__().json.Pointer(P("/L")));
checkTask.connect = DBCheckHandler;
checkTask.TryCompleting();
TEST_ExpectTrue(default.resultDataType == JSON_Number);
TEST_ExpectTrue(default.resultType == DBR_Success);
ReadFromDB(db, "/B/A/1/"); ReadFromDB(db, "/B/A/1/");
TEST_ExpectTrue(default.resultDataType == JSON_Number); TEST_ExpectTrue(default.resultDataType == JSON_Number);
TEST_ExpectTrue(ArrayList(default.resultObject).GetLength() == 12); TEST_ExpectTrue(ArrayList(default.resultObject).GetLength() == 12);
TEST_ExpectTrue(ArrayList(default.resultObject).GetInt(11) == 85); TEST_ExpectTrue(ArrayList(default.resultObject).GetInt(11) == 85);
} }
protected static function Test_RequestID()
{
local LocalDatabaseInstance db;
local ArrayList templateArray;
local HashTable templateObject;
templateObject = GetJSONSubTemplateObject();
templateArray = GetJSONSubTemplateArray();
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));
db.WriteData(__().json.Pointer(P("/D")), __().box.bool(false));
db.WriteData(__().json.Pointer(P("/B/A")), templateArray);
db.WriteData(__().json.Pointer(P("/B/A/1")), templateObject);
db.WriteData(__().json.Pointer(P("/B/A/1/")), templateArray);
/* `db` now contains:
{
"A": "simpleValue",
"B": {
"A": [true, {
"A": "simpleValue",
"B": 11.12,
"": [true, null, "huh"]
}, "huh"],
"B": 11.12
},
"C": -5,
"D": false
}
*/
// Constantly recreating `db` takes time, so we make test dependent
// on each other.
// Generally speaking this is not great, but we cannot run them in
// parallel anyway.
Context("Testing whether database operations report correct request ID.");
SubTest_RequestIDForCheck(db);
SubTest_RequestIDForIncrement(db);
SubTest_RequestIDForKeys(db);
SubTest_RequestIDForRead(db);
SubTest_RequestIDForRemove(db);
SubTest_RequestIDForSize(db);
SubTest_RequestIDForWrite(db);
__core().db.DeleteLocal(P("TEST_DB"));
}
protected static function SubTest_RequestIDForCheck(LocalDatabaseInstance db)
{
local DBCheckTask task;
Issue("Type checking operation isn't returning correct request ID.");
task = db.CheckDataType(__().json.Pointer(P("/L")));
task.connect = DBCheckHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 0);
task = db.CheckDataType(__().json.Pointer(P("/L")), 29);
task.connect = DBCheckHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 29);
}
protected static function SubTest_RequestIDForIncrement(
LocalDatabaseInstance db)
{
local DBIncrementTask task;
Issue("Increment operation isn't returning correct request ID.");
task = db.IncrementData(__().json.Pointer(P("/L")), __().box.int(29));
task.connect = DBIncrementHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 0);
task = db.IncrementData(__().json.Pointer(P("/L")), __().box.int(29), -7);
task.connect = DBIncrementHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == -7);
}
protected static function SubTest_RequestIDForKeys(LocalDatabaseInstance db)
{
local DBKeysTask task;
Issue("Keys list operation isn't returning correct request ID.");
task = db.GetDataKeys(__().json.Pointer(P("")));
task.connect = DBKeysHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 0);
task = db.GetDataKeys(__().json.Pointer(P("")), 11);
task.connect = DBKeysHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 11);
}
protected static function SubTest_RequestIDForRead(LocalDatabaseInstance db)
{
local DBReadTask task;
Issue("Reading operation isn't returning correct request ID.");
task = db.ReadData(__().json.Pointer(P("/L")),);
task.connect = DBReadingHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 0);
task = db.ReadData(__().json.Pointer(P("/L")),, 666);
task.connect = DBReadingHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 666);
}
protected static function SubTest_RequestIDForRemove(LocalDatabaseInstance db)
{
local DBRemoveTask task;
Issue("Removing operation isn't returning correct request ID.");
task = db.RemoveData(__().json.Pointer(P("/L")));
task.connect = DBRemoveHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 0);
task = db.RemoveData(__().json.Pointer(P("/L")), 80);
task.connect = DBRemoveHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 80);
}
protected static function SubTest_RequestIDForSize(LocalDatabaseInstance db)
{
local DBSizeTask task;
Issue("Size getting operation isn't returning correct request ID.");
task = db.GetDataSize(__().json.Pointer(P("/L")));
task.connect = DBSizeHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 0);
task = db.GetDataSize(__().json.Pointer(P("/L")), 7);
task.connect = DBSizeHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 7);
}
protected static function SubTest_RequestIDForWrite(LocalDatabaseInstance db)
{
local DBWriteTask task;
Issue("Writing operation isn't returning correct request ID.");
task = db.WriteData(__().json.Pointer(P("/L")), none);
task.connect = DBWritingHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 0);
task = db.WriteData(__().json.Pointer(P("/L")), none, 42);
task.connect = DBWritingHandler;
task.TryCompleting();
TEST_ExpectTrue(default.resultRequestID == 42);
}
defaultproperties defaultproperties
{ {
caseGroup = "Database" caseGroup = "Database"

10
sources/Manifest.uc

@ -24,7 +24,8 @@ defaultproperties
{ {
features(0) = class'Aliases_Feature' features(0) = class'Aliases_Feature'
features(1) = class'Commands_Feature' features(1) = class'Commands_Feature'
features(2) = class'Avarice_Feature' features(2) = class'Users_Feature'
features(3) = class'Avarice_Feature'
testCases(0) = class'TEST_Base' testCases(0) = class'TEST_Base'
testCases(1) = class'TEST_ActorService' testCases(1) = class'TEST_ActorService'
testCases(2) = class'TEST_Boxes' testCases(2) = class'TEST_Boxes'
@ -53,7 +54,8 @@ defaultproperties
testCases(25) = class'TEST_BigInt' testCases(25) = class'TEST_BigInt'
testCases(26) = class'TEST_DatabaseCommon' testCases(26) = class'TEST_DatabaseCommon'
testCases(27) = class'TEST_LocalDatabase' testCases(27) = class'TEST_LocalDatabase'
testCases(28) = class'TEST_AcediaConfig' testCases(28) = class'TEST_DBConnection'
testCases(29) = class'TEST_UTF8EncoderDecoder' testCases(29) = class'TEST_AcediaConfig'
testCases(30) = class'TEST_AvariceStreamReader' testCases(30) = class'TEST_UTF8EncoderDecoder'
testCases(31) = class'TEST_AvariceStreamReader'
} }

31
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.
* *
@ -57,6 +57,7 @@ protected function Finalizer()
{ {
_.memory.Free(controller); _.memory.Free(controller);
_.memory.Free(consoleInstance); _.memory.Free(consoleInstance);
_.memory.Free(identity);
controller = none; controller = none;
consoleInstance = none; consoleInstance = none;
// No need to deallocate `User` objects, since they are all have unique // No need to deallocate `User` objects, since they are all have unique
@ -96,7 +97,6 @@ public final /* unreal */ function bool Initialize(
idHash = _.text.FromString(initController.GetPlayerIDHash()); idHash = _.text.FromString(initController.GetPlayerIDHash());
identity = _.users.FetchByIDHash(idHash); identity = _.users.FetchByIDHash(idHash);
idHash.FreeSelf(); idHash.FreeSelf();
idHash = none;
} }
signalsReferences = playerSignals; signalsReferences = playerSignals;
controller = _server.unreal.ActorRef(initController); controller = _server.unreal.ActorRef(initController);
@ -121,6 +121,9 @@ public function EInterface Copy()
// not initialized // not initialized
return playerCopy; return playerCopy;
} }
if (identity != none) {
identity.NewRef();
}
playerCopy.identity = identity; playerCopy.identity = identity;
playerCopy.Initialize( PlayerController(controller.Get()), playerCopy.Initialize( PlayerController(controller.Get()),
signalsReferences); signalsReferences);
@ -231,15 +234,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)

48
sources/Text/BaseText.uc

@ -404,6 +404,54 @@ public final function MutableText UpperMutableCopy(
return textCopy; return textCopy;
} }
/**
* Checks whether all letters in the caller text is in the lower case.
*
* @return `true` if there are no characters that qualify as upper case and
* `false` otherwise.
*/
public final function bool IsLowerCase()
{
local int i;
local Character nextCharacter;
if (IsEmpty()) {
return true;
}
for (i = 0; i < GetLength(); i += 1)
{
nextCharacter = GetCharacter(i);
if (_.text.IsUpper(nextCharacter)) {
return false;
}
}
return true;
}
/**
* Checks whether all letters in the caller text is in the lower case.
*
* @return `true` if there are no characters that qualify as lower case and
* `false` otherwise.
*/
public final function bool IsUpperCase()
{
local int i;
local Character nextCharacter;
if (IsEmpty()) {
return true;
}
for (i = 0; i < GetLength(); i += 1)
{
nextCharacter = GetCharacter(i);
if (_.text.IsLower(nextCharacter)) {
return false;
}
}
return true;
}
/** /**
* Checks if caller `BaseText` contains a valid name object or not. * Checks if caller `BaseText` contains a valid name object or not.
* *

311
sources/Text/JSON/JSONAPI.uc

@ -5,7 +5,7 @@
* both valid and invalid JSON. However only correctly parsing valid JSON * both valid and invalid JSON. However only correctly parsing valid JSON
* is guaranteed. This means that you should not rely on these methods to parse * is guaranteed. This means that you should not rely on these methods to parse
* any JSON extensions or validate JSON for you. * any JSON extensions or validate JSON for you.
* Copyright 2021-2022 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -1381,6 +1381,315 @@ private final function int GetEscapedVersion(int codePoint)
return codePoint; return codePoint;
} }
/**
* Increments given value by another value, producing result as a new
* JSON-compatible value.
*
* What "incrementing" actually does depends on the passed JSON values:
* `valueToIncrement` and `increment` parameters. Unless either of them is `none`
* (then "increment" simply acts as a `_.json.Copy()` method for
* the non-`none` one), they must represent the same JSON type and:
*
* 1. JSON bool: performs logical "or" operation on given values;
* 2. JSON number: adds values together;
* 3. JSON string: appends `increment` at the end of `valueToIncrement`;
* 4. JSON array: appends copy of elements of `increment` at the end of
* array of copies of elements of `valueToIncrement`
* (calls `ArrayList::append()` method);
* 5. JSON object: creates new collection, based on `valueToIncrement`
* with added key-value pairs from `increment` (all elements are
* copied). Does not override old values
* (calls `HashTable::append()` method).
*
* In case they represent different JSON types (that aren't "null") -
* incrementing should produce `none`.
*
* @param valueToIncrement Value to increment. Can be any JSON-compatible
* value.
* @param increment Value to increment it by. Can be any
* JSON-compatible value.
* @return Incremented data (guaranteed to contain copies and not actual
* objects from either `valueToIncrement` or `increment`). `none` if
* argument types were incompatible. Whether type of the result will be
* immutable (boxes and `Text`) or mutable (refs and `MutableText`)
* depends on immutability of `valueToIncrement`. When adding two numbers,
* whether result will be boxed (this includes both boxes and refs) `int`
* or `float` depends on both parameters - if either of them if `float`,
* then result will be `float`.
*/
public final function AcediaObject Increment(
AcediaObject valueToIncrement,
AcediaObject increment)
{
local AcediaObject result;
if (valueToIncrement == none) {
result = _.json.Copy(increment);
}
else if (increment == none) {
result = _.json.Copy(valueToIncrement);
}
else if ( valueToIncrement.class == class'IntBox'
|| valueToIncrement.class == class'IntRef'
|| valueToIncrement.class == class'FloatBox'
|| valueToIncrement.class == class'FloatRef')
{
result = Increment_Number(valueToIncrement, increment);
}
else if ( valueToIncrement.class == class'BoolBox'
|| valueToIncrement.class == class'BoolRef')
{
result = Increment_Bool(valueToIncrement, increment);
}
else if ( valueToIncrement.class == class'Text'
|| valueToIncrement.class == class'MutableText')
{
result = Increment_Text(valueToIncrement, increment);
}
else {
result = Increment_Collections(valueToIncrement, increment);
}
return result;
}
// Assumes `valueToIncrement` and `increment` aren't `none`
private final function AcediaObject Increment_Collections(
AcediaObject valueToIncrement,
AcediaObject increment)
{
local AcediaObject result;
local ArrayList arrayListCopy;
local HashTable hashTableCopy;
if ( valueToIncrement.class == class'ArrayList'
&& increment.class == class'ArrayList')
{
arrayListCopy = ArrayList(Copy(ArrayList(increment)));
result = CopyArrayList(ArrayList(valueToIncrement))
.Append(arrayListCopy);
_.memory.Free(arrayListCopy);
}
else if ( valueToIncrement.class == class'HashTable'
&& increment.class == class'HashTable')
{
hashTableCopy = HashTable(Copy(HashTable(increment)));
result = CopyHashTable(HashTable(valueToIncrement))
.Append(hashTableCopy);
_.memory.Free(hashTableCopy);
}
return result;
}
// Assumes `valueToIncrement` and `increment` aren't `none`
private final function AcediaObject Increment_Text(
AcediaObject valueToIncrement,
AcediaObject increment)
{
local BaseText textIncrement;
local MutableText builder;
textIncrement = BaseText(increment);
if (BaseText(increment) == none) {
return none;
}
builder = BaseText(valueToIncrement).MutableCopy();
builder.Append(textIncrement);
if (Text(valueToIncrement) != none) {
return builder.IntoText();
}
return builder;
}
// Assumes `valueToIncrement` and `increment` aren't `none`
private final function AcediaObject Increment_Bool(
AcediaObject valueToIncrement,
AcediaObject increment)
{
local bool value1, value2;
if (valueToIncrement.class == class'BoolBox') {
value1 = BoolBox(valueToIncrement).Get();
}
if (valueToIncrement.class == class'BoolRef') {
value1 = BoolRef(valueToIncrement).Get();
}
if (increment.class == class'BoolBox') {
value2 = BoolBox(increment).Get();
}
else if (increment.class == class'BoolRef') {
value2 = BoolRef(increment).Get();
}
else {
return none;
}
if (ValueBox(valueToIncrement) != none) {
return _.box.bool(value1 || value2);
}
return _.ref.bool(value1 || value2);
}
// Assumes `valueToIncrement` and `increment` aren't `none`
// Assumes `valueToIncrement` is one of four classes: `IntBox`, `IntRef`,
// `FloatBox` or `FloatRef`.
private final function AcediaObject Increment_Number(
AcediaObject valueToIncrement,
AcediaObject increment)
{
local bool hasFloats;
local int intSummand1, intSummand2, intSum;
local float floatSummand1, floatSummand2, floatSum;
hasFloats = valueToIncrement.class == class'FloatBox'
|| valueToIncrement.class == class'FloatRef'
|| increment.class == class'FloatBox'
|| increment.class == class'FloatRef';
// `valueToIncrement` is guaranteed to have an appropriate type,
// but `increment` might not, so only do check on second call
ExtractBoxedNumericValue(valueToIncrement, intSummand1, floatSummand1);
if (!ExtractBoxedNumericValue(increment, intSummand2, floatSummand2)) {
return none;
}
if (hasFloats)
{
floatSum = floatSummand1 + floatSummand2
+ float(intSummand1 + intSummand2);
if (ValueBox(valueToIncrement) != none) {
return _.box.float(floatSum);
}
return _.ref.float(floatSum);
}
intSum = intSummand1 + intSummand2 + int(floatSummand1 + floatSummand2);
if (ValueBox(valueToIncrement) != none) {
return _.box.int(intSum);
}
return _.ref.int(intSum);
}
// Extracts numeric value and records it into one of two out arguments:
//
// * `asInteger` iff `value` is either `IntBox` or `IntRef`;
// * `asFloat` iff `value` is either `FloatBox` or `FloatRef`;
//
// Does not change the value in remaining parameter.
// Returns `true` in case of success (method managed to read the value) and
// `false` otherwise (`value` had non-numeric or unknown type).
private final function bool ExtractBoxedNumericValue(
AcediaObject value,
out int asInteger,
out float asFloat)
{
local bool success;
if (value.class == class'IntBox')
{
asInteger = IntBox(value).Get();
success = true;
}
else if (value.class == class'IntRef')
{
asInteger = IntRef(value).Get();
success = true;
}
else if (value.class == class'FloatBox')
{
asFloat = FloatBox(value).Get();
success = true;
}
else if (value.class == class'FloatRef')
{
asFloat = FloatRef(value).Get();
success = true;
}
return success;
}
/**
* Performs a deep copy of the `inputData`. This means it copies not only
* `inputData` itself, but (in case it is a container) all of the values
* within it, instead of simply storing the same references (as opposed to
* shallow copy). Stored values also recursively deep copied.
*
* @param inputData Data to copy. Can be any JSON-compatible value.
* @return Copy of the `inputData`. Any non-JSON values are copied as `none`.
*/
public final function AcediaObject Copy(AcediaObject inputData)
{
if (inputData == none) {
return none;
}
if (inputData.class == class'IntBox') {
return _.box.int(IntBox(inputData).Get());
}
if (inputData.class == class'IntRef') {
return _.ref.int(IntRef(inputData).Get());
}
if (inputData.class == class'BoolBox') {
return _.box.bool(BoolBox(inputData).Get());
}
if (inputData.class == class'BoolRef') {
return _.ref.bool(BoolRef(inputData).Get());
}
if (inputData.class == class'FloatBox') {
return _.box.float(FloatBox(inputData).Get());
}
if (inputData.class == class'FloatRef') {
return _.ref.float(FloatRef(inputData).Get());
}
if (inputData.class == class'Text') {
return Text(inputData).Copy();
}
if (inputData.class == class'MutableText') {
return MutableText(inputData).MutableCopy();
}
if (inputData.class == class'ArrayList') {
return CopyArrayList(ArrayList(inputData));
}
if (inputData.class == class'HashTable') {
return CopyHashTable(HashTable(inputData));
}
return none;
}
private final function ArrayList CopyArrayList(ArrayList inputList)
{
local int i, inputListLength;
local ArrayList result;
local AcediaObject nextObject, nextCopy;
result = _.collections.EmptyArrayList();
inputListLength = inputList.GetLength();
for (i = 0; i < inputListLength; i += 1)
{
nextObject = inputList.GetItem(i);
nextCopy = Copy(nextObject);
result.AddItem(nextCopy);
_.memory.Free(nextCopy);
_.memory.Free(nextObject);
}
return result;
}
private final function HashTable CopyHashTable(HashTable inputTable)
{
local int i;
local HashTable result;
local array<Text> textKeys;
local AcediaObject nextObject, nextCopy;
result = _.collections.EmptyHashTable();
textKeys = inputTable.GetTextKeys();
for (i = 0; i < textKeys.length; i += 1)
{
nextObject = inputTable.GetItem(textKeys[i]);
nextCopy = Copy(nextObject);
result.SetItem(textKeys[i], nextCopy);
_.memory.Free(nextCopy);
_.memory.Free(nextObject);
}
_.memory.FreeMany(textKeys);
return result;
}
defaultproperties defaultproperties
{ {
MAX_FLOAT_PRECISION = 4 MAX_FLOAT_PRECISION = 4

143
sources/Text/JSON/JSONPointer.uc

@ -5,7 +5,7 @@
* Path "/a/b/c" will be stored as a sequence of components "a", "b" and "c", * Path "/a/b/c" will be stored as a sequence of components "a", "b" and "c",
* path "/" will be stored as a singular empty component "" * path "/" will be stored as a singular empty component ""
* and empty path "" would mean that there is not components at all. * and empty path "" would mean that there is not components at all.
* Copyright 2021 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -293,6 +293,30 @@ public final function int GetNumericComponent(int index)
return components[index].asNumber; return components[index].asNumber;
} }
/**
* Checks whether component at given index can be used to index array.
*
* This method accepts numeric components plus component equal to "-", that can
* be used to point at the element after the last on in the `JSONArray`.
*
* @param index Index of the component to check.
* @param `true` if component with given index exists and it either positive
* number or "-".
*/
public final function bool IsComponentArrayApplicable(int index)
{
local bool isAddElementAlias;
local Text component;
if (GetNumericComponent(index) >= 0) {
return true;
}
component = GetComponent(index);
isAddElementAlias = P("-").IsEqual(component);
_.memory.Free(component);
return isAddElementAlias;
}
/** /**
* Converts caller `JSONPointer` into it's `Text` representation. * Converts caller `JSONPointer` into it's `Text` representation.
* *
@ -375,18 +399,42 @@ public final function int GetFoldsAmount()
/** /**
* Makes an exact copy of the caller `JSONPointer`. * Makes an exact copy of the caller `JSONPointer`.
* *
* @return Copy of the caller `JSONPointer`. * Copies components in the range `[startIndex; startIndex + maxLength - 1]`
* If provided parameters `startIndex` and `maxLength` define a range that
* goes beyond `[0; self.GetLength() - 1]`, then intersection with a valid
* range will be used.
*
* @param startIndex Position of the first component to copy.
* By default `0`, corresponding to the very first component.
* @param maxLength Max length of the extracted JSON pointer (in amount of
* components). By default `0` - that and all negative values mean that
* method should extract all components to the right of `startIndex`.
* @return Copy of the specified range of the caller `JSONPointer`.
*/ */
public final function JSONPointer Copy() public final function JSONPointer Copy(
optional int startIndex,
optional int maxLength)
{ {
local int i; local int i, endIndex;
local JSONPointer newPointer; local JSONPointer newPointer;
local array<Component> newComponents; local array<Component> newComponents;
newComponents = components;
for (i = 0; i < newComponents.length; i += 1) if (maxLength <= 0) {
maxLength = components.length - startIndex;
}
endIndex = startIndex + maxLength;
if (endIndex <= 0) {
return JSONPointer(_.memory.Allocate(class'JSONPointer'));
}
startIndex = Max(startIndex, 0);
endIndex = Min(endIndex, components.length);
for (i = startIndex; i < endIndex; i += 1)
{ {
if (newComponents[i].asText != none) { newComponents[newComponents.length] = components[i];
newComponents[i].asText = newComponents[i].asText.MutableCopy(); if (components[i].asText != none)
{
newComponents[newComponents.length - 1].asText =
components[i].asText.MutableCopy();
} }
} }
newPointer = JSONPointer(_.memory.Allocate(class'JSONPointer')); newPointer = JSONPointer(_.memory.Allocate(class'JSONPointer'));
@ -394,6 +442,85 @@ public final function JSONPointer Copy()
return newPointer; return newPointer;
} }
/**
* Appends path, contained in JSON pointer `other` to the caller JSON pointer.
* Appending "/A/B/7/C" to "/object/hey/1/there/" produces
* "/object/hey/1/there//A/B/7/C".
*
* @param other Pointer to append. If `none` - caller `JSONPointer` will
* not change.
* @return Reference to the caller `JSONPointer` to allow for method chaining.
*/
public final function JSONPointer Append(JSONPointer other)
{
local int i;
local array<Component> otherComponents;
if (other == none) {
return self;
}
otherComponents = other.components;
for (i = 0; i < otherComponents.length; i += 1)
{
if (otherComponents[i].asText != none) {
otherComponents[i].asText = otherComponents[i].asText.MutableCopy();
}
components[components.length] = otherComponents[i];
}
return self;
}
/**
* Checks if given pointer corresponds with the beginning of the caller one.
*
* Pointer starts with another one if it includes all of its fields from
* the beginning and in order
* E.g. "/A/B/C" starts with "/A/B", but not with "/A/B/C/D", "/D/A/B/C" or
* "/A/B/CD".
*
* @param other Candidate into being caller pointer's prefix.
* @return `true` if `other` is prefix and `false` otherwise. `none` is
* considered to be an empty pointer and, therefore, prefix to any other
* pointer.
*/
public final function bool StartsWith(JSONPointer other)
{
local int i;
local array<Component> otherComponents;
// `none` is same as empty
if (other == none) return true;
otherComponents = other.components;
// Not enough length
if (components.length < otherComponents.length) return false;
for (i = 0; i < otherComponents.length; i += 1)
{
// Compare numeric components if at least one is such
if ( components[i].testedForBeingNumeric
|| otherComponents[i].testedForBeingNumeric)
{
if (GetNumericComponent(i) != other.GetNumericComponent(i)) {
return false;
}
// End this iteration for numeric component, but continue for
// text ones
if (GetNumericComponent(i) >= 0) {
continue;
}
}
// We can reach here if:
// 1. Neither components have `testedForBeingNumeric` set to
// `true`, neither `asText` fields are `none` by the invariant;
// 2. At least one had `testedForBeingNumeric`, but they tested
// negative for being numeric.
if (!components[i].asText.Compare(otherComponents[i].asText)) {
return false;
}
}
return true;
}
defaultproperties defaultproperties
{ {
TSLASH = 0 TSLASH = 0

514
sources/Text/Tests/TEST_JSON.uc

@ -1,6 +1,6 @@
/** /**
* Set of tests for functionality of JSON printing/parsing. * Set of tests for functionality of JSON printing/parsing.
* Copyright 2021-2022 Anton Tarasenko * Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -22,11 +22,18 @@ class TEST_JSON extends TestCase
var string simpleJSONObject, complexJSONObject; var string simpleJSONObject, complexJSONObject;
protected static function JSONPointer MakePtr(string str)
{
return __().json.Pointer(__().text.FromString(str));
}
protected static function TESTS() protected static function TESTS()
{ {
Test_Pointer(); Test_Pointer();
Test_Print(); Test_Print();
Test_Parse(); Test_Parse();
Test_Copy();
Test_Incrementing();
} }
protected static function Test_Pointer() protected static function Test_Pointer()
@ -37,6 +44,10 @@ protected static function Test_Pointer()
SubTest_PointerPushPop(); SubTest_PointerPushPop();
SubTest_PointerNumeric(); SubTest_PointerNumeric();
SubTest_PopWithoutRemoving(); SubTest_PopWithoutRemoving();
SubTest_Append();
SubText_Copy();
SubTest_StartsWith();
SubTest_IsComponentArrayApplicable();
} }
protected static function SubTest_PointerCreate() protected static function SubTest_PointerCreate()
@ -187,6 +198,114 @@ protected static function SubTest_PopWithoutRemoving()
TEST_ExpectTrue(pointer.Pop(true).ToString() == "simple"); TEST_ExpectTrue(pointer.Pop(true).ToString() == "simple");
} }
protected static function SubTest_Append()
{
local JSONPointer pointer, append;
Issue("Appending another JSON pointer is not working correctly.");
pointer = __().json.Pointer(P("/object/hey/1/there/"));
append = __().json.Pointer(P("/A/B/7/C"));
pointer.Append(append);
TEST_ExpectTrue(
pointer.ToText().ToString()
== "/object/hey/1/there//A/B/7/C");
pointer = __().json.Pointer(P(""));
append = __().json.Pointer(P("/A/B/7/C"));
pointer.Append(append);
TEST_ExpectTrue(pointer.ToText().ToString() == "/A/B/7/C");
pointer = __().json.Pointer(P("/object/hey/1/there/"));
append = __().json.Pointer(P(""));
pointer.Append(append);
TEST_ExpectTrue(pointer.ToText().ToString() == "/object/hey/1/there/");
pointer = __().json.Pointer(P("/object/hey/1/there/"));
pointer.Append(none);
TEST_ExpectTrue(pointer.ToText().ToString() == "/object/hey/1/there/");
}
protected static function SubText_Copy()
{
Issue("JSON pointer's `Copy()` method does not correctly copy the whole"
@ "pointer.");
TEST_ExpectTrue(MakePtr("/A/B/3/D").Copy().ToText().ToString()
== "/A/B/3/D");
TEST_ExpectTrue(MakePtr("/").Copy().ToText().ToString() == "/");
TEST_ExpectTrue(MakePtr("").Copy().ToText().ToString() == "");
TEST_ExpectTrue(MakePtr("/A/B/3/D").Copy(0, 4).ToText().ToString()
== "/A/B/3/D");
TEST_ExpectTrue(MakePtr("/A/B/3/D").Copy(-2).ToText().ToString()
== "/A/B/3/D");
TEST_ExpectTrue(MakePtr("/A/B/3/D").Copy(0, 7).ToText().ToString()
== "/A/B/3/D");
TEST_ExpectTrue(MakePtr("/A/B/3/D").Copy(-3, 7).ToText().ToString()
== "/A/B/3/D");
TEST_ExpectTrue(MakePtr("/A/B/3/D").Copy(-5, 20).ToText().ToString()
== "/A/B/3/D");
TEST_ExpectTrue(MakePtr("").Copy(-1).ToText().ToString() == "");
Issue("JSON pointer's `Copy()` method does not correctly copy empty range"
@ "of the pointer.");
TEST_ExpectTrue(MakePtr("/A/B/3/D").Copy(-5, 4).ToText().ToString() == "");
TEST_ExpectTrue(MakePtr("/A/B/3/D").Copy(4, 11).ToText().ToString() == "");
Issue("JSON pointer's `Copy()` method does not correctly copy partial"
@ "intersection range of the pointer.");
TEST_ExpectTrue(MakePtr("/A//3/D").Copy(-5, 8).ToText().ToString()
== "/A//3"); // left
TEST_ExpectTrue(MakePtr("/A//3/D").Copy(1, 11).ToText().ToString()
== "//3/D"); // right
}
protected static function SubTest_StartsWith()
{
local JSONPointer pointer;
Issue("Any pointers start with `none` JSON pointer.");
TEST_ExpectTrue(__().json.Pointer(P("/A/B/C")).StartsWith(none));
TEST_ExpectTrue(__().json.Pointer(P("/")).StartsWith(none));
TEST_ExpectTrue(__().json.Pointer(P("")).StartsWith(none));
Issue("`StartsWith()` correctly detects JSON pointers that are actually"
@ "their prefixes.");
TEST_ExpectTrue(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P("/A/7/C"))));
// Same, but constructed manually to handle components added as numeric
pointer = __().json.Pointer().Push(P("A")).PushNumeric(7).Push(P("C"));
TEST_ExpectTrue(pointer.StartsWith(__().json.Pointer(P("/A/7/C"))));
TEST_ExpectTrue(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P("/A/7"))));
TEST_ExpectTrue(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P(""))));
TEST_ExpectTrue(__().json.Pointer(P(""))
.StartsWith(__().json.Pointer(P(""))));
Issue("`StartsWith()` correctly detects JSON pointers that aren't actually"
@ "their prefixes.");
TEST_ExpectFalse(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P("/A/3/C"))));
// Constructed manually to handle components added as numeric
pointer = __().json.Pointer().Push(P("A")).PushNumeric(8).Push(P("C"));
TEST_ExpectFalse(pointer.StartsWith(__().json.Pointer(P("/A/3/C"))));
TEST_ExpectFalse(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P("/A/7/"))));
TEST_ExpectFalse(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P("/"))));
}
protected static function SubTest_IsComponentArrayApplicable()
{
Issue("`IsComponentArrayApplicable()` method wrongly detects numeric"
@ "components.");
TEST_ExpectFalse(
__().json.Pointer(P("/A/B/C")).IsComponentArrayApplicable(0));
TEST_ExpectTrue(
__().json.Pointer(P("/A/2/C")).IsComponentArrayApplicable(1));
TEST_ExpectTrue(
__().json.Pointer(P("/A/B/-")).IsComponentArrayApplicable(2));
TEST_ExpectFalse(
__().json.Pointer(P("/A/7/C")).IsComponentArrayApplicable(-2));
TEST_ExpectFalse(
__().json.Pointer(P("/A/7/C")).IsComponentArrayApplicable(10));
}
protected static function Test_Print() protected static function Test_Print()
{ {
Context("Testing printing simple JSON values."); Context("Testing printing simple JSON values.");
@ -651,6 +770,399 @@ protected static function SubTest_ParseComplex()
TEST_ExpectTrue(FloatBox(inner.GetItem(P("maybe"))).Get() == 0.003); TEST_ExpectTrue(FloatBox(inner.GetItem(P("maybe"))).Get() == 0.003);
} }
protected static function Test_Copy()
{
Context("Testing method for copying JSON values.");
SubTest_CopySimple();
SubTest_CopyComplex();
}
protected static function SubTest_CopySimple()
{
Issue("JSON's `Copy()` method incorrectly copies copy boxed built-in"
@ "types.");
TEST_ExpectNone(__().json.Copy(none));
TEST_ExpectTrue(
BoolBox(__().json.Copy(__().box.bool(true))).Get()
== true);
TEST_ExpectTrue(
BoolRef(__().json.Copy(__().ref.bool(false))).Get()
== false);
TEST_ExpectTrue(IntBox(__().json.Copy(__().box.int(-7))).Get() == -7);
TEST_ExpectTrue(
IntRef(__().json.Copy(__().ref.int(234234))).Get()
== 234234);
TEST_ExpectTrue(
FloatBox(__().json.Copy(__().box.float(3.76))).Get()
== 3.76);
TEST_ExpectTrue(
FloatRef(__().json.Copy(__().ref.float(-213.1))).Get()
== -213.1);
TEST_ExpectTrue(Text(__().json.Copy(P("Hey!"))).ToString() == "Hey!");
TEST_ExpectTrue(
MutableText(__().json.Copy(__().text.FromStringM("Hey!")))
.ToString()
== "Hey!");
}
protected static function HashTable ConstructComplexJSONObject()
{
local HashTable result, innerObject, deepObject, oneMoreObject;
local ArrayList innerArray;
deepObject = __().collections.EmptyHashTable();
deepObject.SetItem(P("something \"here\""), P("yes"));
deepObject.SetFloat(P("maybe"), 0.003);
innerArray = __().collections.EmptyArrayList();
innerArray.AddString("Engine.Actor");
innerArray.AddBool(false);
innerArray.AddItem(none);
innerArray.AddItem(deepObject);
innerArray.AddFloat(56.6);
oneMoreObject = __().collections.EmptyHashTable();
oneMoreObject.SetInt(P("nope"), 324532);
oneMoreObject.SetBool(P("whatever"), false);
oneMoreObject.SetString(P("o rly?"), "ya rly");
innerObject = __().collections.EmptyHashTable();
innerObject.SetBool(P("my_bool"), true);
innerObject.SetItem(P("array"), innerArray);
innerObject.SetItem(P("one more"), oneMoreObject);
innerObject.SetInt(P("my_int"), -9823452);
result = __().collections.EmptyHashTable();
result.SetItem(P("innerObject"), innerObject);
result.SetFloat(P("some_var"), -7.32);
result.SetString(P("another_var"), "aye!");
return result;
}
protected static function SubTest_CopyComplex()
{
local HashTable complexCopy;
complexCopy = HashTable(__().json.Copy(ConstructComplexJSONObject()));
TEST_ExpectTrue(complexCopy.GetBoolBy(P("/innerObject/my_bool")) == true);
TEST_ExpectTrue(
complexCopy.GetStringBy(P("/innerObject/array/0")) == "Engine.Actor");
TEST_ExpectTrue(
complexCopy.GetBoolBy(P("/innerObject/array/1")) == false);
TEST_ExpectTrue(
complexCopy.GetItemBy(P("/innerObject/array/2")) == none);
TEST_ExpectTrue(
complexCopy.GetStringBy(P("/innerObject/array/3/something \"here\""))
== "yes");
TEST_ExpectTrue(
complexCopy.GetFloatBy(P("/innerObject/array/3/maybe")) == 0.003);
TEST_ExpectTrue(
complexCopy.GetFloatBy(P("/innerObject/array/4")) == 56.6);
TEST_ExpectTrue(
complexCopy.GetIntBy(P("/innerObject/one more/nope")) == 324532);
TEST_ExpectTrue(
complexCopy.GetBoolBy(P("/innerObject/one more/whatever")) == false);
TEST_ExpectTrue(
complexCopy.GetStringBy(P("/innerObject/one more/o rly?")) == "ya rly");
TEST_ExpectTrue(
complexCopy.GetIntBy(P("/innerObject/my_int")) == -9823452);
TEST_ExpectTrue(
complexCopy.GetFloatBy(P("/some_var")) == -7.32);
TEST_ExpectTrue(
complexCopy.GetStringBy(P("/another_var")) == "aye!");
}
protected static function Test_Incrementing()
{
Context("Testing incrementing JSON values with `_.json.Increment()`.");
SubTest_Incrementing_Null();
SubTest_Incrementing_Bool();
SubTest_Incrementing_Number();
SubTest_Incrementing_String();
SubTest_Incrementing_Array();
SubTest_Incrementing_Object();
SubTest_Incrementing_Incompatible();
}
protected static function SubTest_Incrementing_Null()
{
local FloatRef ref;
Issue("Null values aren't incremented correctly.");
TEST_ExpectNone(__().json.Increment(none, none));
TEST_ExpectTrue(BoolBox(__().json.Increment(
none, __().box.bool(true))).Get() == true);
TEST_ExpectTrue(FloatRef(__().json.Increment(
__().ref.float(11.5), none)).Get() == 11.5);
Issue("Incrementing null values simply copies reference.");
ref = __().ref.float(1032423.91);
TEST_ExpectFalse(__().json.Increment(ref, none) == none);
TEST_ExpectFalse(__().json.Increment(none, ref) == none);
}
protected static function SubTest_Incrementing_Bool()
{
Issue("Boolean values aren't incremented correctly.");
TEST_ExpectTrue(BoolBox(__().json.Increment(
__().box.bool(false), __().box.bool(false))).Get() == false);
TEST_ExpectTrue(BoolBox(__().json.Increment(
__().box.bool(true), __().box.bool(false))).Get() == true);
TEST_ExpectTrue(BoolBox(__().json.Increment(
__().box.bool(false), __().box.bool(true))).Get() == true);
TEST_ExpectTrue(BoolBox(__().json.Increment(
__().box.bool(true), __().box.bool(true))).Get() == true);
Issue("Incrementing boolean values produces incorrect type.");
TEST_ExpectTrue(BoolBox(__().json.Increment(
__().box.bool(false), __().box.bool(true))).Get() == true);
TEST_ExpectTrue(BoolBox(__().json.Increment(
__().box.bool(false), __().ref.bool(true))).Get() == true);
TEST_ExpectTrue(BoolRef(__().json.Increment(
__().ref.bool(false), __().box.bool(true))).Get() == true);
TEST_ExpectTrue(BoolRef(__().json.Increment(
__().ref.bool(false), __().ref.bool(true))).Get() == true);
}
protected static function SubTest_Incrementing_Number()
{
SubSubTest_Incrementing_Number_Pure();
SubSubTest_Incrementing_Number_Mixed();
}
protected static function SubSubTest_Incrementing_Number_Pure()
{
Issue("Numeric values aren't incremented correctly (for boxed `int`s).");
TEST_ExpectTrue(IntBox(__().json.Increment(
__().box.int(3), __().box.int(-3))).Get() == 0);
TEST_ExpectTrue(IntBox(__().json.Increment(
__().box.int(-4), __().ref.int(11))).Get() == 7);
TEST_ExpectTrue(IntRef(__().json.Increment(
__().ref.int(124), __().box.int(624))).Get() == 748);
TEST_ExpectTrue(IntRef(__().json.Increment(
__().ref.int(345), __().ref.int(-23423))).Get() == -23078);
Issue("Numeric values aren't incremented correctly (for boxed `float`s).");
TEST_ExpectTrue(FloatBox(__().json.Increment(
__().box.float(11.2), __().box.float(-0.2))).Get() == 11);
TEST_ExpectTrue(FloatBox(__().json.Increment(
__().box.float(1012.78), __().ref.float(0.12))).Get() == 1012.9);
TEST_ExpectTrue(FloatRef(__().json.Increment(
__().ref.float(12), __().box.float(13))).Get() == 25);
TEST_ExpectTrue(FloatRef(__().json.Increment(
__().ref.float(-0.32), __().ref.float(0.32))).Get() == 0);
}
protected static function SubSubTest_Incrementing_Number_Mixed()
{
Issue("Numeric values aren't incremented correctly (for mixed `int`s and"
@ "`float`s).");
TEST_ExpectTrue(FloatBox(__().json.Increment(
__().box.float(11.2), __().box.int(0))).Get() == 11.2);
TEST_ExpectTrue(FloatBox(__().json.Increment(
__().box.float(1012.78), __().ref.int(2))).Get() == 1014.78);
TEST_ExpectTrue(FloatRef(__().json.Increment(
__().ref.float(12), __().box.int(13))).Get() == 25);
TEST_ExpectTrue(FloatRef(__().json.Increment(
__().ref.float(-0.32), __().ref.int(14))).Get() == 13.68);
TEST_ExpectTrue(FloatBox(__().json.Increment(
__().box.int(11), __().box.float(-0.2))).Get() == 10.8);
TEST_ExpectTrue(FloatBox(__().json.Increment(
__().box.int(1012), __().ref.float(7.12))).Get() == 1019.12);
TEST_ExpectTrue(FloatRef(__().json.Increment(
__().ref.int(12), __().box.float(13.1))).Get() == 25.1);
TEST_ExpectTrue(FloatRef(__().json.Increment(
__().ref.int(-10), __().ref.float(0.32))).Get() == -9.68);
}
protected static function SubTest_Incrementing_String()
{
Issue("String values aren't incremented correctly.");
TEST_ExpectTrue(Text(__().json.Increment(
__().text.FromString("Whatever"), __().text.FromString("revetahW")))
.ToString() == "WhateverrevetahW");
TEST_ExpectTrue(MutableText(__().json.Increment(
__().text.FromStringM("Whatever"), __().text.FromString("revetahW")))
.ToString() == "WhateverrevetahW");
TEST_ExpectTrue(Text(__().json.Increment(
__().text.FromString("Whatever"), __().text.FromStringM("revetahW")))
.ToString() == "WhateverrevetahW");
TEST_ExpectTrue(MutableText(__().json.Increment(
__().text.FromStringM("Whatever"), __().text.FromStringM("revetahW")))
.ToString() == "WhateverrevetahW");
}
protected static function SubTest_Incrementing_Array()
{
local ArrayList array1, array2, result;
Issue("Array values aren't incremented correctly.");
array1 = __().collections.EmptyArrayList();
array2 = __().collections.EmptyArrayList();
array1.AddItem(__().box.int(5));
array2.AddItem(__().box.int(3));
array2.AddItem(__().box.int(-7));
result = ArrayList(__().json.Increment(array1, array2));
TEST_ExpectTrue(__().json.Print(result).ToString() == "[5,3,-7]");
TEST_ExpectTrue(result.GetItem(0) != array1.GetItem(0));
TEST_ExpectTrue(result.GetItem(1) != array2.GetItem(0));
TEST_ExpectTrue(result.GetItem(2) != array2.GetItem(1));
Issue("Incrementing array values incorrectly handles reference counts.");
// +1 after copy, +2 after getters (before and here)
TEST_ExpectTrue(result.GetItem(0)._getRefCount() == 3);
TEST_ExpectTrue(result.GetItem(1)._getRefCount() == 3);
TEST_ExpectTrue(result.GetItem(2)._getRefCount() == 3);
TEST_ExpectTrue(array1._getRefCount() == 1);
TEST_ExpectTrue(array2._getRefCount() == 1);
TEST_ExpectTrue(result._getRefCount() == 1);
}
protected static function SubTest_Incrementing_Object()
{
local HashTable table1, table2, result;
Issue("Object values aren't incremented correctly.");
table1 = __().collections.EmptyHashTable();
table2 = __().collections.EmptyHashTable();
table1.Setitem(P("A"), __().box.int(5));
table2.Setitem(P("B"), __().box.int(3));
table2.Setitem(P("C"), __().box.int(-7));
result = HashTable(__().json.Increment(table1, table2));
TEST_ExpectTrue(result.GetLength() == 3);
TEST_ExpectTrue(result.GetInt(P("A")) == 5);
TEST_ExpectTrue(result.GetInt(P("B")) == 3);
TEST_ExpectTrue(result.GetInt(P("C")) == -7);
TEST_ExpectTrue(result.GetItem(P("A")) != table1.GetItem(P("A")));
TEST_ExpectTrue(result.GetItem(P("B")) != table2.GetItem(P("B")));
TEST_ExpectTrue(result.GetItem(P("C")) != table2.GetItem(P("C")));
// +1 after copy, +2 after getters (before and here)
Issue("Incrementing object values incorrectly handles reference counts.");
TEST_ExpectTrue(result.GetItem(P("A"))._getRefCount() == 3);
TEST_ExpectTrue(result.GetItem(P("B"))._getRefCount() == 3);
TEST_ExpectTrue(result.GetItem(P("C"))._getRefCount() == 3);
TEST_ExpectTrue(table1._getRefCount() == 1);
TEST_ExpectTrue(table2._getRefCount() == 1);
TEST_ExpectTrue(result._getRefCount() == 1);
}
protected static function SubTest_Incrementing_Incompatible()
{
Issue("Incrementing with incompatible values doesn't produce `none`.");
SubSubTest_Incrementing_Incompatible_bool();
SubSubTest_Incrementing_Incompatible_int();
SubSubTest_Incrementing_Incompatible_float();
SubSubTest_Incrementing_Incompatible_text();
SubSubTest_Incrementing_Incompatible_arraylist();
SubSubTest_Incrementing_Incompatible_hashtable();
}
protected static function SubSubTest_Incrementing_Incompatible_bool()
{
TEST_ExpectNone(__().json.Increment(
__().box.bool(true),
__().ref.int(32)));
TEST_ExpectNone(__().json.Increment(
__().box.bool(true),
__().ref.float(32.5)));
TEST_ExpectNone(__().json.Increment(
__().box.bool(true),
__().text.FromString("Hello there!")));
TEST_ExpectNone(__().json.Increment(
__().box.bool(true),
__().collections.EmptyArrayList()));
TEST_ExpectNone(__().json.Increment(
__().box.bool(true),
__().collections.EmptyHashTable()));
}
protected static function SubSubTest_Incrementing_Incompatible_int()
{
TEST_ExpectNone(__().json.Increment(
__().box.int(3),
__().ref.bool(false)));
TEST_ExpectNone(__().json.Increment(
__().box.int(234),
__().text.FromString("Hello there!")));
TEST_ExpectNone(__().json.Increment(
__().box.int(2),
__().collections.EmptyArrayList()));
TEST_ExpectNone(__().json.Increment(
__().box.int(782),
__().collections.EmptyHashTable()));
}
protected static function SubSubTest_Incrementing_Incompatible_float()
{
TEST_ExpectNone(__().json.Increment(
__().box.float(3),
__().ref.bool(false)));
TEST_ExpectNone(__().json.Increment(
__().box.float(234),
__().text.FromString("Hello there!")));
TEST_ExpectNone(__().json.Increment(
__().box.float(2),
__().collections.EmptyArrayList()));
TEST_ExpectNone(__().json.Increment(
__().box.float(782),
__().collections.EmptyHashTable()));
}
protected static function SubSubTest_Incrementing_Incompatible_text()
{
TEST_ExpectNone(__().json.Increment(
__().text.FromString("yo"),
__().ref.bool(true)));
TEST_ExpectNone(__().json.Increment(
__().text.FromString("yo"),
__().ref.int(32)));
TEST_ExpectNone(__().json.Increment(
__().text.FromString("yo"),
__().ref.float(32.5)));
TEST_ExpectNone(__().json.Increment(
__().text.FromString("yo"),
__().collections.EmptyArrayList()));
TEST_ExpectNone(__().json.Increment(
__().text.FromString("yo"),
__().collections.EmptyHashTable()));
}
protected static function SubSubTest_Incrementing_Incompatible_arraylist()
{
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyArrayList(),
__().ref.bool(true)));
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyArrayList(),
__().ref.int(32)));
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyArrayList(),
__().ref.float(32.5)));
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyArrayList(),
__().text.FromString("Not a collection!")));
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyArrayList(),
__().collections.EmptyHashTable()));
}
protected static function SubSubTest_Incrementing_Incompatible_hashtable()
{
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyHashTable(),
__().ref.bool(true)));
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyHashTable(),
__().ref.int(32)));
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyHashTable(),
__().ref.float(32.5)));
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyHashTable(),
__().text.FromString("Not a collection!")));
TEST_ExpectNone(__().json.Increment(
__().collections.EmptyHashTable(),
__().collections.EmptyArrayList()));
}
defaultproperties defaultproperties
{ {
caseName = "JSON" caseName = "JSON"

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.

655
sources/Users/ACommandUserGroups.uc

@ -0,0 +1,655 @@
/**
* Command for displaying help information about registered Acedia's commands.
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandUserGroups extends Command
dependson(Users_Feature);
protected function BuildData(CommandDataBuilder builder)
{
builder.Name(P("usergroups"))
.Group(P("admin"))
.Summary(P("User groups management."))
.Describe(P("Allows to add/remove user groups and users to these:"
@ "groups. Changes made by it will always affect current session,"
@ "but might fail to be saved in case user groups are stored in"
@ "a database that is either corrupted or in read-only mode."));
builder.SubCommand(P("list"))
.Describe(P("Lists specified groups along with users that belong to"
@ "them. If no groups were specified at all - lists all available"
@ "groups."))
.OptionalParams()
.ParamTextList(P("groups"));
builder.SubCommand(P("add"))
.Describe(P("Adds a new group"))
.ParamText(P("group_name"));
builder.SubCommand(P("remove"))
.Describe(P("Removes a group"))
.ParamText(P("group_name"));
builder.SubCommand(P("addplayer"))
.Describe(F("Adds new user to the group, specified by the player"
@ "selector. Can add several players at once."
@ "Allows to also optionally specify annotation"
@ "(human-readable name) that can be thought of as"
@ "a {$TextEmphasis comment}. If annotation isn't specified"
@ "current nickname will be used as one."))
.ParamText(P("group_name"))
.ParamPlayers(P("player_selector"))
.OptionalParams()
.ParamText(P("annotation"));
builder.SubCommand(P("removeplayer"))
.Describe(P("Removes user from the group, specified by player selector."
@ "Can remove several players at once."))
.ParamText(P("group_name"))
.ParamPlayers(P("player_selector"));
builder.SubCommand(P("adduser"))
.Describe(F("Adds new user to the group. Allows to also optionally"
@ "specify annotation (human-readable name) that can be thought of"
@ "as a {$TextEmphasis comment}."))
.ParamText(P("group_name"))
.ParamText(P("user_id"))
.OptionalParams()
.ParamText(P("annotation"));
builder.SubCommand(P("removeuser"))
.Describe(P("Removes user from the group. User can be specified by both"
@ "user's id or annotation, with id taking priority."))
.ParamText(P("group_name"))
.ParamText(P("user_name"));
builder.Option(P("force"))
.Describe(P("Allows to force usage of invalid user IDs."));
}
protected function Executed(CallData arguments, EPlayer instigator)
{
local bool forceOption;
local Text groupName, userID, userName, annotation;
local ArrayList players, groups;
groupName = arguments.parameters.GetText(P("group_name"));
// For parameters named `user_id`, can only be ID
userID = arguments.parameters.GetText(P("user_id"));
// For parameters named `user_id`, can be either ID or annotation
userName = arguments.parameters.GetText(P("user_name"));
annotation = arguments.parameters.GetText(P("annotation"));
// An array of players that can be specified for some commands
players = arguments.parameters.GetArrayList(P("player_selector"));
groups = arguments.parameters.GetArrayList(P("groups"));
forceOption = arguments.options.HasKey(P("force"));
if (arguments.subCommandName.IsEmpty()) {
DisplayUserGroups();
}
else if (arguments.subCommandName.Compare(P("list"), SCASE_SENSITIVE)) {
DisplayUserGroupsWithUsers(groups);
}
else if (arguments.subCommandName.Compare(P("add"), SCASE_SENSITIVE)) {
AddGroup(groupName);
}
else if (arguments.subCommandName.Compare(P("remove"), SCASE_SENSITIVE)) {
RemoveGroup(groupName);
}
else if (arguments.subCommandName.Compare(P("adduser"), SCASE_SENSITIVE)) {
AddOrAnnotateUser(groupName, userID, annotation, forceOption);
}
else if (arguments.subCommandName.Compare(P("removeuser"), SCASE_SENSITIVE))
{
RemoveUser(groupName, userName);
}
else if (arguments.subCommandName.Compare(P("addplayer"), SCASE_SENSITIVE)) {
AddOrAnnotatePlayers(groupName, players, annotation);
}
else if (arguments.subCommandName
.Compare(P("removeplayer"), SCASE_SENSITIVE))
{
RemovePlayers(groupName, players);
}
_.memory.Free(groupName);
_.memory.Free(userID);
_.memory.Free(userName);
_.memory.Free(annotation);
_.memory.Free(players);
_.memory.Free(groups);
}
private function bool ValidateGroupExistence(BaseText groupName)
{
if (_.users.IsGroupExisting(groupName)) {
return true;
}
callerConsole
.Write(P("Group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.UseColorOnce(_.color.TextFailure)
.Write(P(" doesn't exists"))
.WriteLine(P("!"));
return false;
}
private function bool ValidateUserID(BaseText textUserID)
{
local int i;
if (textUserID == none) {
return false;
}
if (textUserID.IsEmpty())
{
callerConsole.WriteLine(F("Valid User ID"
@ "{$TextFailure shouldn't be empty},"
@ "use {$TextEmphasis --force} flag if you want to enforce"
@ "using it."));
return false;
}
for (i = 0; i < textUserID.GetLength(); i += 1)
{
if (!_.text.IsDigit(textUserID.GetCharacter(i)))
{
callerConsole.WriteLine(F("Valid User ID"
@ "{$TextFailure should consist only of digits},"
@ "use {$TextEmphasis --force} flag if you want"
@ "to enforce using it."));
return false;
}
}
return true;
}
private function bool TryAddingUserID(
BaseText groupName,
UserID userID,
BaseText userSpecifiedID)
{
if (_.users.IsUserIDInGroup(userID, groupName))
{
callerConsole
.Write(P("User id specified as "))
.UseColorOnce(_.color.Gray)
.Write(userSpecifiedID)
.UseColorOnce(_.color.TextFailure)
.Write(P(" is already in the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P("!"));
}
else if (_.users.AddUserIDToGroup(userID, groupName))
{
callerConsole
.Write(F("{$TextPositive Added} user id specified as "))
.UseColorOnce(_.color.Gray)
.Write(userSpecifiedID)
.Write(P(" to the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P("!"));
}
else
{
callerConsole
.UseColorOnce(_.color.TextFailure)
.Write(P("Failed (for unknown reason)"))
.Write(P(" to add user id "))
.UseColorOnce(_.color.Gray).Write(userSpecifiedID)
.Write(P(" to the group "))
.UseColorOnce(_.color.TextEmphasis).Write(groupName)
.WriteLine(P("!"));
return false;
}
return true;
}
private function DisplayAnnotation(
BaseText userSpecifiedName,
BaseText groupName,
BaseText annotation)
{
callerConsole
.Write(P("Annotation for user id specified as "))
.UseColorOnce(_.color.Gray)
.Write(userSpecifiedName)
.UseColorOnce(_.color.TextPositive)
.Write(P(" in the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.Write(P(" is set to "))
.UseColorOnce(_.color.TextNeutral)
.WriteLine(annotation);
}
private function AddOrAnnotateUser(
BaseText groupName,
BaseText textUserID,
BaseText annotation,
bool forceOption)
{
local UserID id;
if (groupName == none) return;
if (textUserID == none) return;
if (!ValidateGroupExistence(groupName)) return;
if (!forceOption && !ValidateUserID(textUserID)) return;
id = UserID(_.memory.Allocate(class'UserID'));
id.Initialize(textUserID);
if (!TryAddingUserID(groupName, id, textUserID) || annotation == none)
{
_.memory.Free(id);
return;
}
_.users.SetAnnotationForUserID(groupName, id, annotation);
_.memory.Free(id);
DisplayAnnotation(textUserID, groupName, annotation);
}
private function AddOrAnnotatePlayers(
BaseText groupName,
ArrayList players,
BaseText annotation)
{
local int i;
local BaseText playerName, nextAnnotation;
local EPlayer nextPlayer;
local UserID nextID;
if (groupName == none) return;
if (players == none) return;
if (!ValidateGroupExistence(groupName)) return;
for (i = 0; i < players.GetLength(); i += 1)
{
nextPlayer = EPlayer(players.GetItem(i));
if (nextPlayer == none) {
continue;
}
playerName = nextPlayer.GetName();
nextID = nextPlayer.GetUserID();
if (TryAddingUserID(groupName, nextID, playerName))
{
if (annotation == none) {
nextAnnotation = playerName;
}
else {
nextAnnotation = annotation;
}
_.users.SetAnnotationForUserID(groupName, nextID, nextAnnotation);
DisplayAnnotation(playerName, groupName, nextAnnotation);
_.memory.Free(nextID);
nextAnnotation = none;
}
_.memory.Free(nextPlayer);
_.memory.Free(playerName);
_.memory.Free(nextID);
nextPlayer = none;
playerName = none;
nextID = none;
}
}
private function TryRemovingUserID(
BaseText groupName,
UserID idToRemove,
BaseText userSpecifiedName)
{
local Text idAsText;
idAsText = idToRemove.GetUniqueID();
if (_.users.RemoveUserIDFromGroup(idToRemove, groupName))
{
callerConsole
.Write(F("{$TextNegative Removed} user "))
.UseColorOnce(_.color.Gray)
.Write(userSpecifiedName)
.Write(P(" (with id "))
.UseColorOnce(_.color.Gray)
.Write(idAsText)
.Write(P(") from the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P("!"));
}
else
{
callerConsole
.UseColorOnce(_.color.TextFailure)
.Write(P("Failed (for unknown reason)"))
.Write(P("to remove user with id "))
.UseColorOnce(_.color.Gray)
.Write(idAsText)
.Write(P(" from the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P("."));
}
_.memory.Free(idAsText);
}
private function bool RemoveUsersByAnnotation(
BaseText groupName,
BaseText userName)
{
local int i;
local bool removedUser;
local array<Users_Feature.AnnotatedUserID> annotatedUsers;
annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName);
for (i = 0; i < annotatedUsers.length; i += 1)
{
if (userName.Compare(annotatedUsers[i].annotation, SCASE_INSENSITIVE))
{
TryRemovingUserID(groupName, annotatedUsers[i].id, userName);
removedUser = true;
}
}
for (i = 0; i < annotatedUsers.length; i += 1)
{
_.memory.Free(annotatedUsers[i].id);
_.memory.Free(annotatedUsers[i].annotation);
}
return removedUser;
}
private function RemoveUser(BaseText groupName, BaseText userName)
{
local bool matchedUserName;
local UserID idFromName;
if (groupName == none) return;
if (userName == none) return;
if (!ValidateGroupExistence(groupName)) return;
idFromName = UserID(_.memory.Allocate(class'UserID'));
idFromName.Initialize(userName);
if ( idFromName.IsInitialized()
&& _.users.IsUserIDInGroup(idFromName, groupName))
{
TryRemovingUserID(groupName, idFromName, userName);
matchedUserName = true;
}
else {
matchedUserName = RemoveUsersByAnnotation(groupName, userName);
}
_.memory.Free(idFromName);
if (!matchedUserName)
{
callerConsole
.Write(P("User "))
.UseColorOnce(_.color.Gray)
.Write(userName)
.UseColorOnce(_.color.TextFailure)
.Write(P(" doesn't belong to the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P("!"));
}
}
private function RemovePlayers(BaseText groupName, ArrayList players)
{
local int i;
local Text playerName;
local EPlayer nextPlayer;
local UserID nextID;
if (groupName == none) return;
if (players == none) return;
if (!ValidateGroupExistence(groupName)) return;
for (i = 0; i < players.GetLength(); i += 1)
{
nextPlayer = EPlayer(players.GetItem(i));
if (nextPlayer == none) {
continue;
}
playerName = nextPlayer.GetName();
nextID = nextPlayer.GetUserID();
if (!_.users.IsUserIDInGroup(nextID, groupName))
{
callerConsole
.Write(P("Player "))
.UseColorOnce(_.color.Gray)
.Write(playerName)
.Write(F(" {$TextFailure doesn't belong} to the group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P("!"));
}
else {
TryRemovingUserID(groupName, nextID, playerName);
}
_.memory.Free(nextPlayer);
_.memory.Free(playerName);
_.memory.Free(nextID);
nextPlayer = none;
playerName = none;
nextID = none;
}
}
private function AddGroup(BaseText groupName)
{
if (_.users.IsGroupExisting(groupName))
{
callerConsole
.Write(P("Group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.UseColorOnce(_.color.TextNegative)
.Write(P(" already exists"))
.WriteLine(P("!"));
return;
}
if (_.users.AddGroup(groupName))
{
callerConsole
.Write(P("Group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.UseColorOnce(_.color.TextPositive)
.Write(P(" was added"))
.WriteLine(P("!"));
}
else
{
callerConsole
.UseColorOnce(_.color.TextFailure)
.Write(P("Cannot add"))
.Write(P(" group with a name "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P(" for unknown reason."));
}
}
private function RemoveGroup(BaseText groupName)
{
if (!_.users.IsGroupExisting(groupName))
{
callerConsole
.Write(P("Group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.UseColorOnce(_.color.TextNegative)
.Write(P(" doesn't exists"))
.WriteLine(P("!"));
return;
}
if (_.users.RemoveGroup(groupName))
{
callerConsole
.Write(P("Group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.UseColorOnce(_.color.TextPositive)
.Write(P(" was removed"))
.WriteLine(P("!"));
}
else
{
callerConsole
.UseColorOnce(_.color.TextFailure)
.Write(P("Cannot remove"))
.Write(P(" group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(groupName)
.WriteLine(P(" for unknown reason."));
}
}
private function DisplayUserGroups()
{
local int i;
local array<Text> availableGroups;
if (!ValidateUsersFeature()) {
return;
}
availableGroups = _.users.GetAvailableGroups();
if (availableGroups.length <= 0)
{
callerConsole.WriteLine(F("{$TextNegative No user groups}"
@ "currently available."));
return;
}
callerConsole
.UseColorOnce(_.color.TextEmphasis)
.Write(P("Available user groups"))
.Write(P(": "));
for (i = 0; i < availableGroups.length; i += 1)
{
if (i > 0) {
callerConsole.Write(P(", "));
}
callerConsole.Write(availableGroups[i]);
}
callerConsole.Flush();
_.memory.FreeMany(availableGroups);
}
private function bool ValidateUsersFeature()
{
if (class'Users_Feature'.static.IsEnabled()) {
return true;
}
callerConsole
.UseColorOnce(_.color.TextFailure)
.WriteLine(P("`Users_Feature` is currently disabled."));
return false;
}
private function bool IsGroupSpecified(
ArrayList specifiedGroups,
BaseText groupToCheck)
{
local int i;
local int length;
local Text nextGroup;
if (groupToCheck == none) return false;
if (specifiedGroups == none) return true;
length = groupToCheck.GetLength();
if (length <= 0) return true;
for (i = 0; i < length; i += 1)
{
nextGroup = specifiedGroups.GetText(i);
if (groupToCheck.Compare(nextGroup, SCASE_INSENSITIVE))
{
nextGroup.FreeSelf();
return true;
}
_.memory.Free(nextGroup);
}
return false;
}
private function DisplayUserGroupsWithUsers(ArrayList specifiedGroups)
{
local int i;
local bool displayedGroup;
local array<Text> availableGroups;
if (!ValidateUsersFeature()) {
return;
}
availableGroups = _.users.GetAvailableGroups();
if (availableGroups.length <= 0)
{
callerConsole.WriteLine(F("{$TextNegative No user groups}"
@ "currently available."));
return;
}
for (i = 0; i < availableGroups.length; i += 1)
{
if (IsGroupSpecified(specifiedGroups, availableGroups[i]))
{
displayedGroup = true;
callerConsole
.Write(P("User group "))
.UseColorOnce(_.color.TextEmphasis)
.Write(availableGroups[i])
.WriteLine(P(":"));
DisplayUsersFor(availableGroups[i]);
}
}
callerConsole.Flush();
_.memory.FreeMany(availableGroups);
if (!displayedGroup && specifiedGroups != none) {
callerConsole.WriteLine(F("{$TextFailure No valid groups} specified!"));
}
}
private function DisplayUsersFor(Text groupName)
{
local int i;
local Text nextID;
local array<Users_Feature.AnnotatedUserID> annotatedUsers;
annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName);
if (annotatedUsers.length <= 0)
{
callerConsole.WriteBlock(P("No users"));
return;
}
for (i = 0; i < annotatedUsers.length; i += 1)
{
if (annotatedUsers[i].id == none) {
continue;
}
nextID = annotatedUsers[i].id.GetUniqueID();
if (annotatedUsers[i].annotation != none)
{
callerConsole
.Write(nextID)
.UseColorOnce(_.color.TextNeutral)
.Write(P(" aka "))
.WriteBlock(annotatedUsers[i].annotation);
}
else {
callerConsole.WriteBlock(nextID);
}
_.memory.Free(nextID);
}
for (i = 0; i < annotatedUsers.length; i += 1)
{
_.memory.Free(annotatedUsers[i].id);
_.memory.Free(annotatedUsers[i].annotation);
}
}
defaultproperties
{
}

40
sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Signal.uc

@ -0,0 +1,40 @@
/**
* Signal class for `PersistentDataManager`'s `OnPersistentDataReady()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class PersistentDataManager_OnPersistentDataReady_Signal extends Signal
dependson(DBConnection);
public final function Emit(UserID id, bool online)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
PersistentDataManager_OnPersistentDataReady_Slot(nextSlot)
.connect(id, online);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'PersistentDataManager_OnPersistentDataReady_Slot'
}

41
sources/Users/PersistentData/Events/PersistentDataManager_OnPersistentDataReady_Slot.uc

@ -0,0 +1,41 @@
/**
* Slot class for `PersistentDataManager`'s `OnPersistentDataReady()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class PersistentDataManager_OnPersistentDataReady_Slot extends Slot
dependson(DBConnection);
delegate connect(UserID id, bool online)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

407
sources/Users/PersistentData/PersistentDataManager.uc

@ -0,0 +1,407 @@
/**
* This tool is for simplifying writing and reading persistent user data.
* All it requires is a setup of database + json pointer to data and it will
* take care of data caching and database connection.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class PersistentDataManager extends AcediaObject;
/**
* # `PersistentDataManager`
*
* This tool is for simplifying writing and reading persistent user data.
* All it requires is a setup of database + json pointer to data and it will
* take care of data caching and database connection.
*
* ## Usage
*
* Create an instance and use `Setup()` to connect to the database with
* persistent data. You can use `Setup()` again on the same object to setup
* a different database as a source. All data will be automatically reloaded.
* After that you can use `GetPersistentData()`/`SetPersistentData()` to
* read/write persistent data for the particular user.
* Since loading data from the database takes time, you don't have an
* immediate access to it.
* But you can use `_.users.OnPersistentDataAvailable()` signal to track
* whenever new user data from database becomes available. However, you can
* start writing persistent data (and reading what you've wrote) at any time it
* - these changes will be reapplied whenever data is actually loaded from
* database.
*
* ## Implementation
*
* Implementation consists of simply creating `DBConnection` for every user
* and storing them in the `HashTable` that maps user IDs into those
* `DBConnection`s.
* We also maintain a reverse map to figure out what `DBConnection` belongs
* to what user when connection signals an update. We borrow the signal that
* `UsersAPI` provides to inform everyone interested about which users
* have updated.
*/
var private bool initialized;
var private Database database;
var private JSONPointer rootPointer;
var private HashTable userToConnection, connectionToUser;
var private PersistentDataManager_OnPersistentDataReady_Signal onPersistentDataReadySignal;
protected function Constructor()
{
_.players.OnNewPlayer(self).connect = ConnectPersistentData;
onPersistentDataReadySignal = _.users._getOnReadySignal();
}
protected function Finalizer()
{
Reset();
_.players.OnNewPlayer(self).Disconnect();
}
private final function Reset()
{
_.memory.Free(database);
_.memory.Free(rootPointer);
_.memory.Free(userToConnection);
_.memory.Free(connectionToUser);
_.memory.Free(onPersistentDataReadySignal);
database = none;
rootPointer = none;
userToConnection = none;
connectionToUser = none;
onPersistentDataReadySignal = none;
initialized = false;
}
/**
* Sets up database and location inside it as a source of users' persistent
* data.
*
* Must be successfully called at least once for the caller
* `PersistentDataManager` to be usable.
*
* @param db Database inside which persistent data is stored.
* @param location Location inside specified database to the root of
* persistent data.
* @return `true` if setup was successful (requires both arguments to be not
* `none`) and `false` otherwise.
*/
public final function bool Setup(Database db, JSONPointer location)
{
if (db == none) return false;
if (location == none) return false;
Reset();
database = db;
database.NewRef();
rootPointer = location.Copy();
userToConnection = _.collections.EmptyHashTable();
connectionToUser = _.collections.EmptyHashTable();
// Using `userToConnection` as an empty hash table, not related to its
// actual meaning
database.IncrementData(location, userToConnection);
initialized = true;
return true;
}
/**
* Reads specified named persistent data for the specified group.
*
* @param id ID of the user to read persistent data from.
* @param groupName Group to which this persistent data belongs to.
* Groups are used as namespaces to avoid duplicate persistent variables
* between mods. If your mod needs several subgroups, its recommended to
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and
* "MyAwesomeMod.enemies".
* @param dataName Name of persistent data variable to read inside
* `groupName` persistent data group. Not `none` value must be provided.
* @param data Data to set as persistent value. Must be
* JSON-compatible. If `none` is passed, returns the all data for
* the given group.
* @return Data read from the persistent variable. `none` in case of any kind
* of failure.
*/
public final function AcediaObject GetPersistentData(
UserID id,
BaseText groupName,
optional BaseText dataName)
{
local AcediaObject result;
local Text textID;
local JSONPointer location;
local DBConnection relevantConnection;
if (!initialized) return none;
if (id == none) return none;
if (groupName == none) return none;
textID = id.GetUniqueID();
relevantConnection = DBConnection(userToConnection.GetItem(textID));
textID.FreeSelf();
if (relevantConnection != none)
{
location = _.json.Pointer();
location.Push(groupName);
if (dataName != none) {
location.Push(dataName);
}
result = relevantConnection.ReadDataByJSON(location);
relevantConnection.FreeSelf();
location.FreeSelf();
}
return result;
}
/**
* Writes specified named persistent data for the specified group.
*
* @param id ID of the user to change persistent data of.
* @param groupName Group to which this persistent data belongs to.
* Groups are used as namespaces to avoid duplicate persistent variables
* between mods. If your mod needs several subgroups, its recommended to
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and
* "MyAwesomeMod.enemies".
* @param dataName Name of persistent data variable to change inside
* `groupName` persistent data group.
* @param data Data to set as persistent value. Must be
* JSON-compatible.
* @return `true` if change succeeded in local cached version of database with
* persistent values and `false` otherwise. Such local changes can
* potentially be not applied to the actual database. But successful local
* changes should persist for the game session.
*/
public final function bool WritePersistentData(
UserID id,
BaseText groupName,
BaseText dataName,
AcediaObject data)
{
local bool result;
local Text textID;
local JSONPointer location;
local DBConnection relevantConnection;
local HashTable emptyObject;
if (!initialized) return false;
if (id == none) return false;
if (groupName == none) return false;
if (dataName == none) return false;
textID = id.GetUniqueID();
relevantConnection = DBConnection(userToConnection.GetItem(textID));
textID.FreeSelf();
if (relevantConnection != none)
{
emptyObject = _.collections.EmptyHashTable();
location = _.json.Pointer();
location.Push(groupName);
relevantConnection.IncrementDataByJSON(location, emptyObject);
location.Push(dataName);
result = relevantConnection.WriteDataByJSON(location, data);
relevantConnection.FreeSelf();
location.FreeSelf();
emptyObject.FreeSelf();
}
return result;
}
/**
* Increments specified named persistent data for the specified group.
*
* @param id ID of the user to change persistent data of.
* @param groupName Group to which this persistent data belongs to.
* Groups are used as namespaces to avoid duplicate persistent variables
* between mods. If your mod needs several subgroups, its recommended to
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and
* "MyAwesomeMod.enemies".
* @param dataName Name of persistent data variable to change inside
* `groupName` persistent data group.
* @param data Data by which to increment existing persistent value.
* Must be JSON-compatible.
* @return `true` if change succeeded in local cached version of database with
* persistent values and `false` otherwise. Such local changes can
* potentially be not applied to the actual database. But successful local
* changes should persist for the game session.
*/
public final function bool IncrementPersistentData(
UserID id,
BaseText groupName,
BaseText dataName,
AcediaObject data)
{
local bool result;
local Text textID;
local JSONPointer location;
local DBConnection relevantConnection;
if (!initialized) return false;
if (id == none) return false;
if (groupName == none) return false;
if (dataName == none) return false;
textID = id.GetUniqueID();
relevantConnection = DBConnection(userToConnection.GetItem(textID));
textID.FreeSelf();
if (relevantConnection != none)
{
location = _.json.Pointer();
location.Push(groupName).Push(dataName);
result = relevantConnection.IncrementDataByJSON(location, data);
relevantConnection.FreeSelf();
location.FreeSelf();
}
return result;
}
/**
* Removes specified named persistent data for the specified group.
*
* @param id ID of the user to remove persistent data of.
* @param groupName Group to which this persistent data belongs to.
* Groups are used as namespaces to avoid duplicate persistent variables
* between mods. If your mod needs several subgroups, its recommended to
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and
* "MyAwesomeMod.enemies".
* @param dataName Name of persistent data variable to remove inside
* `groupName` persistent data group.
* @return `true` if removal succeeded in local cached version of database with
* persistent values and `false` otherwise. Such local changes can
* potentially be not applied to the actual database. But successful local
* changes should persist for the game session.
*/
public final function bool RemovePersistentData(
UserID id,
BaseText groupName,
BaseText dataName)
{
local bool result;
local Text textID;
local JSONPointer location;
local DBConnection relevantConnection;
if (!initialized) return false;
if (id == none) return false;
if (groupName == none) return false;
if (dataName == none) return false;
textID = id.GetUniqueID();
relevantConnection = DBConnection(userToConnection.GetItem(textID));
textID.FreeSelf();
if (relevantConnection != none)
{
location = _.json.Pointer();
location.Push(groupName).Push(dataName);
result = relevantConnection.RemoveDataByJSON(location);
relevantConnection.FreeSelf();
location.FreeSelf();
}
return result;
}
/**
* Connects and starts synchronizing persistent data for the given player.
*
* @param player Player to synchronize persistent data for.
*/
public final function ConnectPersistentData(EPlayer player)
{
local UserID playerID;
if (initialized && player != none)
{
playerID = player.GetUserID();
ConnectPersistentDataByID(playerID);
_.memory.Free(playerID);
}
}
/**
* Connects and starts synchronizing persistent data for the player given by
* their ID.
*
* @param id User ID for which to synchronize persistent data from
* the database.
*/
public final function ConnectPersistentDataByID(UserID id)
{
local Text textID;
local DBConnection newConnection;
if (!initialized) return;
if (id == none) return;
textID = id.GetUniqueID();
if (userToConnection.HasKey(textID))
{
_.memory.Free(textID);
return;
}
rootPointer.Push(textID);
newConnection = DBConnection(_.memory.Allocate(class'DBConnection'));
newConnection.Initialize(database, rootPointer);
_.memory.Free(rootPointer.Pop());
newConnection.Connect();
userToConnection.SetItem(textID, newConnection);
connectionToUser.SetItem(newConnection, textID);
newConnection.OnStateChanged(self).connect = UserUpdated;
textID.FreeSelf();
newConnection.FreeSelf();
}
private final function UserUpdated(
DBConnection instance,
DBConnection.DBConnectionState oldState,
DBConnection.DBConnectionState newState)
{
local UserID id;
if (!initialized) return;
if (newState == DBCS_Connecting) return;
if (onPersistentDataReadySignal == none) return;
if (!onPersistentDataReadySignal.IsAllocated()) return;
id = UserID(connectionToUser.GetItem(instance));
if (id != none)
{
onPersistentDataReadySignal.Emit(id, newState == DBCS_Connected);
id.FreeSelf();
}
}
/**
* Attempts to start persistent data synchronization for all players currently
* on the server.
*/
public final function LoadCurrentPlayers()
{
local int i;
local array<EPlayer> currentPlayers;
if (initialized)
{
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
ConnectPersistentData(currentPlayers[i]);
}
_.memory.FreeMany(currentPlayers);
}
}
defaultproperties
{
}

4
sources/Users/Tests/TEST_User.uc

@ -55,9 +55,9 @@ protected static function Test_UserID()
testID3 = UserID(__().memory.Allocate(class'UserID')); testID3 = UserID(__().memory.Allocate(class'UserID'));
testID2.Initialize(P("76561198025127722")); testID2.Initialize(P("76561198025127722"));
testID3.Initialize(P("76561198044316328")); testID3.Initialize(P("76561198044316328"));
TEST_ExpectTrue(testID.IsEqualTo(testID2)); TEST_ExpectTrue(testID.IsEqual(testID2));
TEST_ExpectTrue(testID.IsEqualToSteamID(testID2.GetSteamID())); TEST_ExpectTrue(testID.IsEqualToSteamID(testID2.GetSteamID()));
TEST_ExpectFalse(testID3.IsEqualTo(testID)); TEST_ExpectFalse(testID3.IsEqual(testID));
Issue("Steam data returned by `UserID` is incorrect."); Issue("Steam data returned by `UserID` is incorrect.");
SteamID = testID3.GetSteamID(); SteamID = testID3.GetSteamID();

211
sources/Users/User.uc

@ -2,7 +2,7 @@
* Object that is supposed to store a persistent data about the * Object that is supposed to store a persistent data about the
* certain player. That is data that will be remembered even after player * certain player. That is data that will be remembered even after player
* reconnects or server changes map/restarts. * reconnects or server changes map/restarts.
* Copyright 2020 - 2021 Anton Tarasenko * Copyright 2020-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -27,26 +27,57 @@ var private UserID id;
// an easy reference in console commands // an easy reference in console commands
var private int key; var private int key;
// Database where user's persistent data is stored var private HashTable sessionData;
var private Database persistentDatabase;
// Pointer to this user's "settings" data in particular
var private JSONPointer persistentSettingsPointer;
var private LoggerAPI.Definition errNoUserDataDatabase; var private int persistentDataLifeVersion;
var private PersistentDataManager persistentData;
protected function Finalizer()
{
if (id != none) {
id.FreeSelf();
}
id = none;
}
private final function UpdatePersistentDataManager()
{
local Users_Feature feature;
if ( persistentData != none
&& persistentData.GetLifeVersion() != persistentDataLifeVersion)
{
persistentData = none;
}
if (persistentData == none)
{
feature =
Users_Feature(class'Users_Feature'.static.GetEnabledInstance());
if (feature != none) {
persistentData = feature.BorrowPersistentDataManager();
}
if (persistentData != none) {
persistentDataLifeVersion = persistentData.GetLifeVersion();
}
_.memory.Free(feature);
}
}
// TODO: redo this comment
/** /**
* Initializes caller `User` with id and it's session key. Should be called * Initializes caller `User` with id and it's session key.
* right after `EPlayer` was created.
* *
* Initialization should (and can) only be done once. * Initialization should (and can) only be done once.
* Before a `Initialize()` call, any other method calls on such `User` * Before a `Initialize()` call, any other method calls on such `User`
* must be considerate to have undefined behavior. * must be considerate to have undefined behavior.
* DO NOT CALL THIS METHOD MANUALLY.
*/ */
public final function Initialize(UserID initID, int initKey) public final function Initialize(UserID initID, int initKey)
{ {
id = initID; id = initID;
key = initKey; key = initKey;
if (initID != none) {
initID.NewRef();
}
} }
/** /**
@ -56,6 +87,9 @@ public final function Initialize(UserID initID, int initKey)
*/ */
public final function UserID GetID() public final function UserID GetID()
{ {
if (id != none) {
id.NewRef();
}
return id; return id;
} }
@ -70,152 +104,59 @@ public final function int GetKey()
} }
/** /**
* Reads user's persistent data saved inside group `groupName`, saving it into * Returns persistent data for the caller user. Data is specified by the its
* a collection using mutable data types. * name along with the name of the data group it is stored in.
* Only should be used if `_.users.PersistentStorageExists()` returns `true`.
* *
* @param groupName Name of the group these settings belong to. * @param groupName Name of the group to get data from. Cannot be `none`.
* This exists to help reduce name collisions between different mods. * @param dataName Name of the data to return. If `none` value is provided,
* Acedia stores all its settings under "Acedia" group. We suggest that you * all the data in specified group will be returned.
* pick at least one name to use for your own mods. * @return Requested data, `none` in case of failure (i.e. data is missing).
* It should be unique enough to not get picked by others - "weapons" is
* a bad name, while "CoolModMastah79" is actually a good pick.
* @return Task object for reading specified persistent data from the database.
* For more info see `Database.ReadData()` method.
* Guaranteed to not be `none` iff
* `_.users.PersistentStorageExists() == true`.
*/ */
public final function DBReadTask ReadGroupOfPersistentData(BaseText groupName) public final function AcediaObject GetPersistentData(
{
local DBReadTask task;
if (groupName == none) return none;
if (!SetupDatabaseVariables()) return none;
persistentSettingsPointer.Push(groupName);
task = persistentDatabase.ReadData(persistentSettingsPointer, true);
_.memory.Free(persistentSettingsPointer.Pop());
return task;
}
/**
* Reads user's persistent data saved under name `dataName`, saving it into
* a collection using mutable data types.
* Only should be used if `_.users.PersistentStorageExists()` returns `true`.
*
* @param groupName Name of the group these settings belong to.
* This exists to help reduce name collisions between different mods.
* Acedia stores all its settings under "Acedia" group. We suggest that you
* pick at least one name to use for your own mods.
* It should be unique enough to not get picked by others - "weapons" is
* a bad name, while "CoolModMastah79" is actually a good pick.
* @param dataName Any name, from under which settings you are interested
* (inside `groupName` group) should be read.
* @return Task object for reading specified persistent data from the database.
* For more info see `Database.ReadData()` method.
* Guaranteed to not be `none` iff
* `_.users.PersistentStorageExists() == true`.
*/
public final function DBReadTask ReadPersistentData(
BaseText groupName, BaseText groupName,
BaseText dataName) BaseText dataName)
{ {
local DBReadTask task; local AcediaObject result;
if (groupName == none) return none; local UserID myID;
if (dataName == none) return none;
if (!SetupDatabaseVariables()) return none;
persistentSettingsPointer.Push(groupName).Push(dataName); UpdatePersistentDataManager();
task = persistentDatabase.ReadData(persistentSettingsPointer, true); if (persistentData == none) {
_.memory.Free(persistentSettingsPointer.Pop()); return none;
_.memory.Free(persistentSettingsPointer.Pop()); }
return task; myID = GetID();
result = persistentData.GetPersistentData(myID, groupname, dataName);
_.memory.Free(myID);
return result;
} }
/** /**
* Writes user's persistent data under name `dataName`. * Changes persistent data for the caller user. Data to change is specified by
* Only should be used if `_.users.PersistentStorageExists()` returns `true`. * the its name along with the name of the data group it is stored in.
* *
* @param groupName Name of the group these settings belong to. * @param groupName Name of the group to get data from. Cannot be `none`.
* This exists to help reduce name collisions between different mods. * @param dataName Name of the data to return. Cannot be `none`.
* Acedia stores all its settings under "Acedia" group. We suggest that you * @param data New data to record.
* pick at least one name to use for your own mods. * @return `true` in case operation was successful and `false` otherwise.
* It should be unique enough to not get picked by others - "weapons" is
* a bad name, while "CoolModMastah79" is actually a good pick.
* @param dataName Any name, under which settings you are interested
* (inside `groupName` group) should be written.
* @param data JSON-compatible (see `_.json.IsCompatible()`) data that
* should be written into database.
* @return Task object for writing specified persistent data into the database.
* For more info see `Database.WriteData()` method.
* Guarantee to not be `none` iff
* `_.users.PersistentStorageExists() == true`.
*/ */
public final function DBWriteTask WritePersistentData( public final function bool SetPersistentData(
BaseText groupName, BaseText groupName,
BaseText dataName, BaseText dataName,
AcediaObject data) AcediaObject data)
{ {
local DBWriteTask task; local bool result;
local HashTable emptyObject; local UserID myID;
if (groupName == none) return none;
if (dataName == none) return none;
if (!SetupDatabaseVariables()) return none;
emptyObject = _.collections.EmptyHashTable();
persistentSettingsPointer.Push(groupName);
persistentDatabase.IncrementData(persistentSettingsPointer, emptyObject);
persistentSettingsPointer.Push(dataName);
task = persistentDatabase.WriteData(persistentSettingsPointer, data);
_.memory.Free(persistentSettingsPointer.Pop());
_.memory.Free(persistentSettingsPointer.Pop());
_.memory.Free(emptyObject);
return task;
}
// Setup database `persistentDatabase` and pointer to this user's data UpdatePersistentDataManager();
// `persistentSettingsPointer`. if (persistentData == none) {
// Return `true` if these variables were setup (during this call or before)
// and `false` otherwise.
private function bool SetupDatabaseVariables()
{
local Text userDataLink;
local Text userTextID;
local HashTable emptyObject, skeletonObject;
if ( persistentDatabase != none && persistentSettingsPointer != none
&& persistentDatabase.IsAllocated())
{
return true;
}
if (id == none || !id.IsInitialized()) {
return false;
}
_.memory.Free(persistentSettingsPointer);
userDataLink = _.users.GetUserDataLink();
persistentDatabase = _.db.Load(userDataLink);
if (persistentDatabase == none)
{
_.logger.Auto(errNoUserDataDatabase).Arg(userDataLink);
return false; return false;
} }
persistentSettingsPointer = _.db.GetPointer(userDataLink); myID = GetID();
userTextID = id.GetSteamID64String(); result = persistentData
skeletonObject = _.collections.EmptyHashTable(); .WritePersistentData(myID, groupname, dataName, data);
skeletonObject.SetItem(P("statistics"), _.collections.EmptyHashTable()); _.memory.Free(myID);
skeletonObject.SetItem(P("settings"), _.collections.EmptyHashTable()); return result;
emptyObject = _.collections.EmptyHashTable();
persistentDatabase.IncrementData(persistentSettingsPointer, emptyObject);
persistentSettingsPointer.Push(userTextID);
persistentDatabase.IncrementData(persistentSettingsPointer, skeletonObject);
persistentSettingsPointer.Push(P("settings"));
_.memory.Free(userTextID);
_.memory.Free(userDataLink);
_.memory.Free(skeletonObject);
_.memory.Free(emptyObject);
return true;
} }
defaultproperties defaultproperties
{ {
errNoUserDataDatabase = (l=LOG_Error,m="Failed to load persistent user database instance given by link \"%1\".")
} }

1397
sources/Users/UserAPI.uc

File diff suppressed because it is too large Load Diff

16
sources/Users/UserDatabase.uc

@ -1,7 +1,7 @@
/** /**
* Simple user database for Acedia. * Simple user database for Acedia.
* Only stores data for a session, map or server restarts will clear it. * Only stores data for a session, map or server restarts will clear it.
* Copyright 2020 Anton Tarasenko * Copyright 2020-2023 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -23,7 +23,7 @@ class UserDatabase extends AcediaObject
// This is used as a global variable only (`default.activeDatabase`) to store // This is used as a global variable only (`default.activeDatabase`) to store
// a reference to main database for persistent data, used by Acedia. // a reference to main database for persistent data, used by Acedia.
var public UserDatabase activeDatabase; var public UserDatabase activeDatabase;
// `User` records that were stored this session // `User` records that were stored this session
var private array<User> sessionUsers; var private array<User> sessionUsers;
// `UserID`s generated during this session. // `UserID`s generated during this session.
@ -52,6 +52,7 @@ public final static function UserDatabase GetInstance()
default.activeDatabase = default.activeDatabase =
UserDatabase(__().memory.Allocate(class'UserDatabase')); UserDatabase(__().memory.Allocate(class'UserDatabase'));
} }
default.activeDatabase.NewRef();
return default.activeDatabase; return default.activeDatabase;
} }
@ -80,6 +81,7 @@ public final function UserID FetchUserID(BaseText idHash)
if (storedUserIDs[i].IsEqualToSteamID(steamID)) if (storedUserIDs[i].IsEqualToSteamID(steamID))
{ {
_.memory.Free(steamID.steamID64); _.memory.Free(steamID.steamID64);
storedUserIDs[i].NewRef();
return storedUserIDs[i]; return storedUserIDs[i];
} }
} }
@ -88,6 +90,7 @@ public final function UserID FetchUserID(BaseText idHash)
if (newUserID.IsInitialized()) if (newUserID.IsInitialized())
{ {
storedUserIDs[storedUserIDs.length] = newUserID; storedUserIDs[storedUserIDs.length] = newUserID;
newUserID.NewRef();
return newUserID; return newUserID;
} }
_.memory.Free(steamID.steamID64); _.memory.Free(steamID.steamID64);
@ -109,13 +112,16 @@ public final function User FetchUser(UserID userID)
local User newUser; local User newUser;
for (i = 0; i < sessionUsers.length; i += 1) for (i = 0; i < sessionUsers.length; i += 1)
{ {
if (sessionUsers[i].GetID().IsEqualTo(userID)) { if (sessionUsers[i].GetID().IsEqual(userID))
{
sessionUsers[i].NewRef();
return sessionUsers[i]; return sessionUsers[i];
} }
} }
newUser = User(__().memory.Allocate(class'User')); newUser = User(__().memory.Allocate(class'User'));
newUser.Initialize(userID, sessionUsers.length + 1); newUser.Initialize(userID, sessionUsers.length + 1);
sessionUsers[sessionUsers.length] = newUser; sessionUsers[sessionUsers.length] = newUser;
newUser.NewRef();
return newUser; return newUser;
} }
@ -132,7 +138,9 @@ public final function User FetchUserByKey(int userKey)
local int i; local int i;
for (i = 0; i < sessionUsers.length; i += 1) for (i = 0; i < sessionUsers.length; i += 1)
{ {
if (sessionUsers[i].GetKey() == userKey) { if (sessionUsers[i].GetKey() == userKey)
{
sessionUsers[i].NewRef();
return sessionUsers[i]; return sessionUsers[i];
} }
} }

70
sources/Users/UserGroup.uc

@ -0,0 +1,70 @@
/**
* Acedia's class for defining user group in config files.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class UserGroup extends AcediaConfig
perobjectconfig
config(AcediaUsers);
var public config array<string> user;
protected function HashTable ToData()
{
local int i;
local HashTable data;
local ArrayList wrappedUserArray;
data = __().collections.EmptyHashTable();
wrappedUserArray = __().collections.EmptyArrayList();
for (i = 0; i < user.length; i += 1) {
wrappedUserArray.AddString(user[i]);
}
data.SetItem(P("user"), wrappedUserArray);
wrappedUserArray.FreeSelf();
return data;
}
protected function FromData(HashTable source)
{
local int i;
local ArrayList wrappedUserArray;
DefaultIt();
if (source == none) {
return;
}
wrappedUserArray = source.GetArrayList(P("user"));
if (wrappedUserArray == none) {
return;
}
for (i = 0; i < wrappedUserArray.GetLength(); i += 1) {
user[user.length] = wrappedUserArray.GetString(i);
}
wrappedUserArray.FreeSelf();
}
protected function DefaultIt()
{
user.length = 0;
}
defaultproperties
{
configName = "AcediaUsers"
supportsDataConversion = true
}

37
sources/Users/UserID.uc

@ -1,6 +1,6 @@
/** /**
* Acedia's class for storing user's ID. * Acedia's class for storing user's ID.
* Copyright 2020 - 2021 Anton Tarasenko * Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -45,6 +45,17 @@ var protected SteamID initializedData;
// after `initialized` is set to `true`. // after `initialized` is set to `true`.
var protected bool initialized; var protected bool initialized;
protected function Finalizer()
{
initialized = false;
_.memory.Free(initializedData.steamID64);
initializedData.steamID64 = none;
initializedData.accountType = 0;
initializedData.universe = 0;
initializedData.instance = 0;
initializedData.steamID32 = 0;
}
// Given a number in form of array (`digits`) of it's digits // Given a number in form of array (`digits`) of it's digits
// (425327 <-> [4, 2, 5, 3, 2, 7]) // (425327 <-> [4, 2, 5, 3, 2, 7])
// return given number mod 2 and // return given number mod 2 and
@ -82,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) {
@ -233,21 +248,23 @@ public final function SteamID GetSteamID()
return initializedData; return initializedData;
} }
/** public function bool IsEqual(Object other)
* Checks if two `UserID`s are the same.
*
* @param otherID `UserID` to compare caller object to.
* @return `true` if caller `UserID` is identical to `otherID` and
* `false` otherwise. If at least one of the `UserID`s being compared is
* uninitialized, the result will be `false`.
*/
public final function bool IsEqualTo(UserID otherID)
{ {
local UserID otherID;
if (!IsInitialized()) return false; if (!IsInitialized()) return false;
otherID = UserID(other);
if (otherID == none) return false;
if (!otherID.IsInitialized()) return false; if (!otherID.IsInitialized()) return false;
return (initializedData.steamID32 == otherID.initializedData.steamID32); return (initializedData.steamID32 == otherID.initializedData.steamID32);
} }
protected function int CalculateHashCode()
{
return initializedData.steamID32;
}
/** /**
* Checks if caller `UserID`s is the same as what's described by * Checks if caller `UserID`s is the same as what's described by
* given `SteamID`. * given `SteamID`.

99
sources/Users/Users.uc

@ -0,0 +1,99 @@
/**
* Config object for `Users_Feature`.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Users extends FeatureConfig
perobjectconfig
config(AcediaUsers);
var public config bool usePersistentData;
var public config string persistentDataDatabaseLink;
var public config bool useDatabaseForGroupsData;
var public config string groupsDatabaseLink;
var public config array<string> localUserGroup;
protected function HashTable ToData()
{
local int i;
local HashTable data;
local ArrayList userGroupList;
data = __().collections.EmptyHashTable();
data.SetBool(P("usePersistentData"), usePersistentData);
data.SetString(P("persistentDataDatabaseLink"), persistentDataDatabaseLink);
data.SetBool(P("useDatabaseForGroupsData"), useDatabaseForGroupsData);
data.SetString(P("groupsDatabaseLink"), groupsDatabaseLink);
userGroupList = _.collections.EmptyArrayList();
for (i = 0; i < localUserGroup.length; i += 1) {
userGroupList.AddString(localUserGroup[i]);
}
data.SetItem(P("userGroups"), userGroupList);
userGroupList.FreeSelf();
return data;
}
protected function FromData(HashTable source)
{
local int i;
local ArrayList userGroupList;
if (source == none) {
return;
}
usePersistentData = source.GetBool(P("usePersistentData"));
persistentDataDatabaseLink = source.GetString(
P("persistentDataDatabaseLink"),
"[local]database:/persistent_data");
useDatabaseForGroupsData = source.GetBool(P("useDatabaseForGroupsData"));
groupsDatabaseLink = source.GetString(
P("groupsDatabaseLink"),
"[local]database:/groups_data");
userGroupList = source.GetArrayList(P("userGroups"));
localUserGroup.length = 0;
if (userGroupList == none) {
return;
}
for (i = 0; i < userGroupList.GetLength(); i += 1) {
localUserGroup[localUserGroup.length] = userGroupList.GetString(i);
}
userGroupList.FreeSelf();
}
protected function DefaultIt()
{
usePersistentData = false;
persistentDataDatabaseLink = "[local]database:/persistent_data";
useDatabaseForGroupsData = false;
groupsDatabaseLink = "[local]database:/groups_data";
localUserGroup.length = 0;
localUserGroup[0] = "admin";
localUserGroup[1] = "moderator";
localUserGroup[2] = "trusted";
}
defaultproperties
{
configName = "AcediaUsers"
usePersistentData = false
persistentDataDatabaseLink = "[local]database:/persistent_data"
useDatabaseForGroupsData = false
groupsDatabaseLink = "[local]database:/groups_data"
localUserGroup(0) = "admin"
localUserGroup(1) = "moderator"
localUserGroup(2) = "trusted"
}

2158
sources/Users/Users_Feature.uc

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