rott/rottlib/tests/fixtures/DBRecord.uc
dkanus 588790b9b4 Refactor everything
Huge dump of refactored code. Still in the middle of the changes that
are to be squashed later in a one huge monster commit, because there is
no value in anything atomic here.
2026-04-05 20:32:11 +07:00

1199 lines
41 KiB
Ucode

/**
* This should be considered an internal class and a detail of
* implementation.
* This is a data object that is used to store JSON data inside
* Unreal Engine's save packages (see `GameInfo` class, starting from
* `CreateDataObject()` method).
* Auxiliary data object that can store either a JSON array or an object in
* the local Acedia database. It is supposed to be saved and loaded
* to / from packages.
* Copyright 2021-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 DBRecord extends Object
perobjectconfig
config(AcediaDB);
/**
* # How can JSON information be stored in per-config-objects?
* Standard way to store information locally would be by simply recording
* it inside a config file. This is enough for almost anything.
* Even something like ServerPerks' player database is implemented with just
* a per-config-objects: since it just stores a particular data per player -
* it can do with simply creating one storage object per player.
* We, however, want to store an arbitrary JSON object inside our database
* that can contain any other kind of JSON data and not just player's
* numeric stats. With some additional work this again can also be done with
* per-config-objects. For example, if we want to store a JSON object inside
* another JSON object - we can create both of them separately, give them some
* different arbitrary names and then make the first one refer to the second
* one by it's given name.
* This way we can create a config object for each JSON array/object and
* then store it's data as an array of plain types (same as ServerPerks' one):
* null, boolean, number and string can be stored as is and other
* JSON arrays/objects can be stored by their references.
*
* # Why are we using data objects instead of per-object-configs?
* Despite everything described above, Acedia's local databases DO NOT use
* per-object-configs to store their data, opting for data objects and
* Unreal Engine's save packages instead.
* Data objects can be created, loaded and saved inside Unreal Engine's
* binary packages with methods available from `GameInfo` class (look them up
* starting from `CreateDataObject()` or browsing through
* [wiki](https://wiki.beyondunreal.com/Legacy:DataObject)).
* They can essentially act the same as per-object-configs, but have
* an advantage of allowing us to cheaply (execution time-wise) create/delete
* as many objects as we need and then update their package on the disk instead
* of calling `SaveConfig()` or `ClearConfig()` on them one-by-one. This both
* simplifies and speed up a bunch of necessary operations.
* They also seem to behave more predictably.
*
* # Some terminology
* Acedia's objects (representing JSON values) that are getting loaded
* into the `DBRecord`s are called "objects". We then refer to their
* representation within `DBRecord`s as "items". For example, this class has
* two methods for conversion between the two: `ConvertObjectToItem()` and
* `ConvertItemToObject()`.
* Most other methods are:
* 1. either methods that actually perform Acedia's database queries;
* 2. or methods that provide safe and easy access to the `DBRecord`'s
* items array (like making sure to remove unneeded data objects).
* All of the methods that perform database query rely on the
* `ConvertPointer()` method that take `JSONPointer` and convert it into
* internal pointer representation that immediately points at `DBRecord` that
* represents referred data (or contains it).
*/
// Name of the database package this object belongs to
var private string package;
// Does this record store a JSON array (`true`) or object (`false`)?
var private bool isJSONArray;
// `ToCollection()` and `EraseSelf()` methods make recursive calls on their
// "sub-objects" (referred via name). If database was somehow damaged - a loop
// of references can occur, leading to infinite recursive calls (which results
// in a crash). These variable help to avoid that by preventing re-entry into
// these methods for the same object.
var private bool lockToCollection;
var private bool lockEraseSelf;
/**
* We pack as much information into the type of the record:
* whether it's 'null', 'boolean', 'number', 'string' or reference to another
* `DBRecord`.
* If it's 'boolean', then record value in the type
* (`DBAT_False` / `DBAT_True`), if `number` record whether it's `int` or
* `float`.
* While JSON does not distinguish between `int` and `float`, we still
* have to pick one of these type when transferring JSON numeric value into
* UnrealScript, plus it's easier for us to store it in one of these types.
*/
enum DBDataType
{
DBAT_Null,
DBAT_False,
DBAT_True,
DBAT_Int,
DBAT_Float,
DBAT_String,
// We actually store the name of another `DBRecord` that represents either
// sub-array or sub-object.
DBAT_Reference,
// Some integer values we might want to store won't fit into `int`, so we
// store them as `BigIntData`
DBAT_BigInt,
};
/**
* Store JSON array / object as a bunch of values.
* Which variable is used to store value depends on the type `t`.
*/
struct StorageItem
{
// Determines whether variable's value is stored in `i`, `f` or `s`.
var DBDataType t;
// For JSON objects only (`isJSONArray == false`), stores the key of
// corresponding value.
var string k;
var int i;
var float f;
// For both `DBRecord` references and JSON strings
var string s;
// For storing `BigInt`'s `BigIntData` - last `byte` stores `negative`
// value
var array<byte> b;
};
var private config array<StorageItem> storage;
var private const int LATIN_LETTERS_AMOUNT;
var private const int LOWER_A_CODEPOINT, UPPER_A_CODEPOINT;
var private const string JSONPOINTER_NEW_ARRAY_ELEMENT;
/**
* Since `DBRecord` represents JSON array or object, we can use
* JSON pointers to refer to any sub-value inside it.
* However, JSON pointers are not convenient or efficient enough for that,
* so internally we use this struct that provides quick and easy access to
* any sub-value.
*/
struct DBRecordPointer
{
// `DBRecord` inside which referred value is directly stored.
// `record == none` automatically makes `DBRecordPointer` invalid.
var DBRecord record;
// Index in `record`'s `storage` variable that corresponds to
// referred (simple) value.
// Negative `index` values mean `record` itself is pointed at.
// To point at JSON array / object represented by a `DBRecord`, always set
// `record` to that record and `index` to negative value (e.g. `-1`).
var int index;
};
private final function bool IsValidPointer(DBRecordPointer pointer)
{
return pointer.record != none;
}
private final function bool IsPointerToRecord(DBRecordPointer pointer)
{
return (pointer.record != none && pointer.index < 0);
}
// Auxiliary method serving as a simple constructor.
private final function DBRecordPointer MakeRecordPointer(
DBRecord record,
optional int index)
{
local DBRecordPointer pointer;
pointer.record = record;
pointer.index = index;
return pointer;
}
// Converts `JSONPointer` into our internal representation.
private final function DBRecordPointer ConvertPointer(BaseJSONPointer jsonPointer)
{
if (jsonPointer == none) {
return MakeRecordPointer(none);
}
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(BaseJSONPointer jsonPointer)
{
local DBRecordPointer pointer;
if (jsonPointer == none) {
return MakeRecordPointer(none);
}
pointer = ConvertPointerPath(jsonPointer, 0, jsonPointer.GetLength() - 1);
if (!IsPointerToRecord(pointer)) {
pointer.record = none; // invalidate pointer
}
return pointer;
}
// Converts `JSONPointer` into internal `DBRecordPointer`.
// Only uses sub-pointer: components from `startIndex` to `endIndex`.
private final function DBRecordPointer ConvertPointerPath(
BaseJSONPointer pointer,
int startIndex,
int endIndex)
{
local int index;
local StorageItem nextElement;
local DBRecord nextRecord;
local string nextComponent;
if (pointer == none) {
return MakeRecordPointer(none);
}
// We are done!
if (startIndex >= endIndex) {
return MakeRecordPointer(self, -1);
}
// Use first available to us component to find next sub-object
if (isJSONArray)
{
index = pointer.GetNumericComponent(startIndex);
if (index < 0 || index >= storage.length) {
return MakeRecordPointer(none); // fail: out-of-bounds index
}
}
else
{
nextComponent = __().text.IntoString(pointer.GetComponent(startIndex));
index = FindItem(nextComponent);
}
if (index < 0) {
return MakeRecordPointer(none); // fail: missing key for component
}
nextElement = storage[index];
if (nextElement.t != DBAT_Reference)
{
if (startIndex + 1 >= endIndex) {
return MakeRecordPointer(self, index);
}
// fail: found value cannot contain sub-values,
// but pointer is not exhausted
return MakeRecordPointer(none);
}
nextRecord = LoadRecordFor(nextElement.s, package);
if (nextRecord == none) {
return MakeRecordPointer(none); // fail: bad database
}
// Success for the component, do recursive call
startIndex += 1;
return nextRecord.ConvertPointerPath(pointer, startIndex, endIndex);
}
public static final function Global __()
{
return class'Global'.static.GetInstance();
}
public static final function CoreGlobal __level()
{
return class'CoreGlobal'.static.GetGenericInstance();
}
/**
* Method for creating a new `DBRecord` in a package named `dbPackageName`,
* picking an appropriate and unique name for it.
*
* @param dbPackageName Name of the package new `DBRecord` must belong to.
* @return New `DBRecord`, created in specified package.
* `none` iff `dbPackageName == none`.
*/
public final static function DBRecord NewRecord(BaseText dbPackageName)
{
if (dbPackageName == none) {
return none;
}
return NewRecordFor(dbPackageName.ToString());
}
// Auxiliary method that does what `NewRecord()` does, but for `string`
// parameter. This makes it cheaper to call for internal use.
private final static function DBRecord NewRecordFor(string dbPackageName)
{
local string nextName;
local DBRecord recordCandidate;
// Try to generate new random name.
// This cycle can in theory be infinite. However in practice it will
// only run for one iteration (unless user messed with settings and
// set length of randomized names too low), since by default there is
// 26^20 == 19,928,148,895,209,409,152,340,197,376 different
// random names and the chance of duplicate in infinitesimal.
while (true)
{
nextName = GetRandomName();
recordCandidate = LoadRecordFor(nextName, dbPackageName);
if (recordCandidate != none) {
continue;
}
recordCandidate = __level().unreal_api().GetGameType()
.CreateDataObject(class'DBRecord', nextName, dbPackageName);
recordCandidate.package = dbPackageName;
return recordCandidate;
}
// We cannot actually reach here
return none;
}
public final static function DBRecord LoadRecord(
BaseText recordName,
BaseText dbPackageName)
{
if (dbPackageName == none) return none;
if (recordName == none) return none;
return LoadRecordFor( recordName.ToString(),
dbPackageName.ToString());
}
// Auxiliary method that does what `LoadRecord()` does, but for `string`
// parameter. This makes it cheaper to call for internal use.
private final static function DBRecord LoadRecordFor(
string name,
string package)
{
return __level().unreal_api().GetGameType()
.LoadDataObject(class'DBRecord', name, package);
}
private final static function string GetRandomName()
{
local int i;
local int length;
local string result;
length = Max(1, class'LocalDBSettings'.default.randomNameLength);
for (i = 0; i < length; i += 1) {
result = result $ GetRandomLetter();
}
return result;
}
private final static function string GetRandomLetter()
{
return Chr(Rand(default.LATIN_LETTERS_AMOUNT) + default.LOWER_A_CODEPOINT);
}
/**
* Loads Acedia's representation of JSON value stored at `pointer` inside
* the JSON object/array represented by the caller `DBRecord`.
*
* @param jsonPointer JSON pointer to the value to load
* (either simple, array or object one).
* @param result Loaded value will be recorded inside this variable.
* Set to `none` on failure.
* @param makeMutable `false` if you want simple value to be recorded as
* immutable "boxes" (and `Text` for JSON strings) and `true` if you want
* them to be recorded as mutable "references"
* (`MutableText` for JSON strings).
* @return `true` if method successfully loaded JSON value and
* `false` otherwise. Failure can happen if passed `pointer` is invalid
* (either does not point at any existing value or is equal to `none`).
*/
public final function bool LoadObject(
BaseJSONPointer jsonPointer,
out AcediaObject result,
bool makeMutable)
{
local int itemIndex;
local DBRecord container;
local DBRecordPointer pointer;
if (jsonPointer == none) return false;
pointer = ConvertPointer(jsonPointer);
if (!IsValidPointer(pointer)) return false;
if (IsPointerToRecord(pointer)) {
result = pointer.record.ToCollection(makeMutable);
}
else
{
itemIndex = pointer.index;
container = pointer.record;
result = ConvertItemToObject(container.GetItem(itemIndex), makeMutable);
}
return true;
}
/**
* Saves Acedia's representation of JSON value at a `pointer` inside
* the JSON object/array represented by the caller `DBRecord`.
*
* @param jsonPointer JSON pointer to location at which to save the value.
* Only the last segment of the path will be created (if missing), the rest
* must already exist and will not be automatically created.
* If another value is already recorded at `pointer` - it will be erased.
* @param newItem New value to save at `pointer` inside
* the caller `DBRecord`.
* @return `true` if method successfully saved new JSON value and
* `false` otherwise. Failure can happen if passed `pointer` is invalid
* (either missing some necessary segments or is equal to `none`).
*/
public final function bool SaveObject(
BaseJSONPointer jsonPointer,
AcediaObject newItem)
{
local int index;
local string itemKey;
local DBRecord directContainer;
local Collection newItemAsCollection;
local DBRecordPointer pointer;
if (jsonPointer == none) {
return false;
}
if (jsonPointer.IsEmpty())
{
// Special case - rewriting caller `DBRecord` itself
newItemAsCollection = Collection(newItem);
if (newItemAsCollection == none) {
return false;
}
EmptySelf();
isJSONArray = (newItemAsCollection.class == class'ArrayList');
FromCollection(newItemAsCollection);
return true;
}
pointer = ConvertContainerPointer(jsonPointer);
if (!IsValidPointer(pointer)) {
return false;
}
directContainer = pointer.record;
itemKey = __().text.IntoString(jsonPointer.Peek());
if (directContainer.isJSONArray)
{
index = jsonPointer.PeekNumeric();
if (index < 0 && itemKey == JSONPOINTER_NEW_ARRAY_ELEMENT) {
index = directContainer.GetStorageLength();
}
if (index < 0) {
return false;
}
}
else {
index = directContainer.FindItem(itemKey);
}
directContainer.SetItem(index, ConvertObjectToItem(newItem), itemKey);
return true;
}
/**
* Removes Acedia's values stored in the database at `pointer` inside
* the JSON object/array represented by the caller `DBRecord`.
*
* @param jsonPointer JSON pointer to the value to remove
* (either simple, array or object one).
* @return `true` if method successfully removed JSON value and
* `false` otherwise. Failure can happen if passed `pointer` is invalid
* (either does not point at any existing value or equal to `none`).
*/
public final function bool RemoveObject(BaseJSONPointer jsonPointer)
{
local int itemIndex;
local string itemKey;
local DBRecord directContainer;
local DBRecordPointer containerPointer;
if (jsonPointer == none) return false;
containerPointer = ConvertContainerPointer(jsonPointer);
if (!IsValidPointer(containerPointer)) return false;
directContainer = containerPointer.record;
if (directContainer.isJSONArray) {
itemIndex = jsonPointer.PeekNumeric();
}
else
{
itemKey = __().text.IntoString(jsonPointer.Peek());
itemIndex = directContainer.FindItem(itemKey);
}
if (itemIndex >= 0)
{
directContainer.RemoveItem(itemIndex);
return true;
}
return false;
}
/**
* Checks type of the JSON value stored at `pointer` inside
* the JSON object/array represented by the caller `DBRecord`.
*
* @param jsonPointer JSON pointer to the value for which type
* should be checked.
* @return `Database.DataType` that corresponds to the type of referred value.
* `JSON_Undefined` if value is missing or passed pointer is invalid.
*/
public final function LocalDatabaseInstance.DataType GetObjectType(
BaseJSONPointer jsonPointer)
{
local DBRecord directContainer;
local DBRecordPointer pointer;
if (jsonPointer == none) return JSON_Undefined;
pointer = ConvertPointer(jsonPointer);
if (!IsValidPointer(pointer)) return JSON_Undefined;
if (IsPointerToRecord(pointer))
{
if (pointer.record.isJSONArray) {
return JSON_Array;
}
else {
return JSON_Object;
}
}
directContainer = pointer.record;
switch (directContainer.GetItem(pointer.index).t)
{
case DBAT_Null:
return JSON_Null;
case DBAT_False:
case DBAT_True:
return JSON_Boolean;
case DBAT_Int:
case DBAT_Float:
return JSON_Number;
case DBAT_String:
return JSON_String;
}
// We should not reach here
return JSON_Undefined;
}
/**
* Returns "size" of the JSON value stored at `pointer` inside
* the JSON object/array represented by the caller `DBRecord`.
*
* For JSON arrays and objects it's the amount of stored elements.
* For other values it's considered undefined and method returns negative
* value instead.
*
* @param jsonPointer JSON pointer to the value for which method should
* return size.
* @return If `pointer` refers to the JSON array or object - amount of it's
* elements is returned. Otherwise returns `-1`.
*/
public final function int GetObjectSize(BaseJSONPointer jsonPointer)
{
local DBRecordPointer pointer;
if (jsonPointer == none) {
return -1;
}
pointer = ConvertPointer(jsonPointer);
if (IsPointerToRecord(pointer)) {
return pointer.record.GetStorageLength();
}
return -1;
}
/**
* Returns keys of the JSON object stored at `pointer` inside
* the JSON object/array represented by the caller `DBRecord`.
*
* @param jsonPointer JSON pointer to the value for which method should
* return size.
* @return If `pointer` refers to the JSON object - all available keys.
* `none` otherwise (including case of JSON arrays).
*/
public final function ArrayList GetObjectKeys(BaseJSONPointer jsonPointer)
{
local int i;
local ArrayList resultKeys;
local array<StorageItem> items;
local DBRecord referredObject;
local DBRecordPointer pointer;
if (jsonPointer == none) return none;
pointer = ConvertPointer(jsonPointer);
if (!IsValidPointer(pointer)) return none;
if (!IsPointerToRecord(pointer)) return none;
referredObject = pointer.record;
if (referredObject.isJSONArray) return none;
resultKeys = __().collections.EmptyArrayList();
items = referredObject.storage;
for (i = 0; i < items.length; i += 1) {
resultKeys.AddString(items[i].k);
}
return resultKeys;
}
/**
* Increments JSON value at a `pointer` inside the JSON object/array
* represented by the caller `DBRecord` by a given Acedia's value.
*
* For "increment" operation description refer to `Database.IncrementData()`.
*
* @param jsonPointer JSON pointer to location at which to save the value.
* Only the last segment of the path might be created (if missing),
* the rest must already exist and will not be automatically created.
* If another value is already recorded at `pointer` - it will be erased.
* @param object Value by which to increment another value, stored at
* `pointer` inside the caller `DBRecord`.
* @return Returns query result that is appropriate for "increment" operation,
* according to `Database.IncrementData()` specification.
*/
public final function Database.DBQueryResult IncrementObject(
BaseJSONPointer jsonPointer,
AcediaObject object)
{
local int index;
local string itemKey;
local DBRecord directContainer;
local HashTable objectAsHashTable;
local DBRecordPointer pointer;
if (jsonPointer == none) {
return DBR_InvalidPointer;
}
if (jsonPointer.IsEmpty())
{
// Special case - incrementing caller `DBRecord` itself
objectAsHashTable = HashTable(object);
if (objectAsHashTable == none) {
return DBR_InvalidData;
}
FromCollection(objectAsHashTable);
return DBR_Success;
}
// All the work will be done by the separate `IncrementItem()` method;
// But it is applied to the `DBRecord` that contains referred item,
// so we have to find it.
pointer = ConvertContainerPointer(jsonPointer);
if (!IsValidPointer(pointer)) {
return DBR_InvalidPointer;
}
directContainer = pointer.record;
itemKey = __().text.IntoString(jsonPointer.Peek());
if (directContainer.isJSONArray)
{
index = jsonPointer.PeekNumeric();
if (index < 0 && itemKey == JSONPOINTER_NEW_ARRAY_ELEMENT) {
index = directContainer.GetStorageLength();
}
if (index < 0) {
return DBR_InvalidPointer;
}
}
else {
index = directContainer.FindItem(itemKey);
}
if (directContainer.IncrementItem(index, object, itemKey)) {
return DBR_Success;
}
return DBR_InvalidData;
}
private final function StorageItem GetItem(int index)
{
local StorageItem emptyResult;
if (index < 0) return emptyResult;
if (index >= storage.length) return emptyResult;
return storage[index];
}
// Negative `index` means that value will need to be appended to the end
// of the `storage`.
// Optionally lets you specify item's key (via `itemName`) for
// JSON objects.
private final function SetItem(
int index,
StorageItem newItem,
optional string itemName)
{
local DBRecord oldRecord;
local StorageItem oldItem;
if (index < 0) {
index = storage.length;
}
if (index < storage.length)
{
// Clean up old value
oldItem = storage[index];
if (oldItem.t == DBAT_Reference)
{
oldRecord = LoadRecordFor(oldItem.s, package);
if (oldRecord != none) {
oldRecord.EmptySelf();
}
__level().unreal_api().GetGameType()
.DeleteDataObject(class'DBRecord', oldItem.s, package);
}
}
storage[index] = newItem;
storage[index].k = itemName;
}
// Auxiliary getter that helps us avoid referring to `storage` array
// directly from `DBRecord` reference, which would cause unnecessary copying of
// it's data.
private final function int GetStorageLength()
{
return storage.length;
}
// Auxiliary method for removing items from `storage` array that helps us
// avoid referring to it directly from `DBRecord` reference, which would cause
// unnecessary copying of it's data.
private final function RemoveItem(int index)
{
local DBRecord oldRecord;
local StorageItem oldItem;
if (index >= storage.length) return;
if (index < 0) return;
// Clean up old value
oldItem = storage[index];
if (oldItem.t == DBAT_Reference)
{
oldRecord = LoadRecordFor(oldItem.s, package);
if (oldRecord != none) {
oldRecord.EmptySelf();
}
__level().unreal_api().GetGameType()
.DeleteDataObject(class'DBRecord', oldItem.s, package);
}
storage.Remove(index, 1);
}
private final function int FindItem(string itemName)
{
local int index;
if (isJSONArray) {
return -1;
}
for (index = 0; index < storage.length; index += 1)
{
if (storage[index].k == itemName) {
return index;
}
}
return -1;
}
// Negative `index` means that `object` value needs to be appended to the
// end of the `storage`, instead of incrementing an existing value.
// Returns `true` if changes were successfully made and `false` otherwise.
private final function bool IncrementItem(
int index,
AcediaObject object,
optional string itemName)
{
local StorageItem itemToIncrement;
if (index < 0)
{
index = storage.length;
// `itemToIncrement` is blank at this point and has type `DBAT_Null`,
// which will simply be rewritten by `IncrementItemByObject()`
// call later
storage[index] = itemToIncrement;
}
else if (index < storage.length) {
itemToIncrement = storage[index];
}
if (IncrementItemByObject(itemToIncrement, object))
{
// Increment object cannot overwrite existing `DBRecord` with
// other value, so it's safe to skip cleaning check
storage[index] = itemToIncrement;
storage[index].k = itemName;
return true;
}
return false;
}
/**
* Extracts JSON object or array data from caller `DBRecord` as either
* `HashTable` (for JSON objects) or `ArrayList` (for JSON arrays).
*
* Type conversion rules in immutable case:
* 1. 'null' -> `none`;
* 2. 'boolean' -> `BoolBox`;
* 3. 'number' -> either `IntBox` or `FloatBox`, depending on
* what seems to fit better;
* 4. 'string' -> `Text`;
* 5. 'array' -> `ArrayList`;
* 6. 'object' -> `HashTable`.
*
* Type conversion rules in mutable case:
* 1. 'null' -> `none`;
* 2. 'boolean' -> `BoolRef`;
* 3. 'number' -> either `IntRef` or `FloatRef`, depending on
* what seems to fit better;
* 4. 'string' -> `MutableText`;
* 5. 'array' -> `ArrayList`;
* 6. 'object' -> `HashTable`.
*
* @param makeMutable `false` if you want this method to produce
* immutable types and `true` otherwise.
* @return `HashTable` if caller `DBRecord` represents a JSON object
* and `ArrayList` if it represents JSON array.
* Returned collection must have all of it's keys deallocated before being
* discarded.
* `none` iff caller `DBRecord` was not initialized as either.
*/
public final function Collection ToCollection(bool makeMutable)
{
local Collection result;
if (lockToCollection) {
return none;
}
lockToCollection = true;
if (isJSONArray) {
result = ToArrayList(makeMutable);
}
else {
result = ToHashTable(makeMutable);
}
lockToCollection = false;
return result;
}
// Does not do any validation check, assumes caller `DBRecord`
// represents an array.
private final function Collection ToArrayList(bool makeMutable)
{
local int i;
local ArrayList result;
local AcediaObject nextObject;
result = __().collections.EmptyArrayList();
for (i = 0; i < storage.length; i += 1)
{
nextObject = ConvertItemToObject(storage[i], makeMutable);
result.AddItem(nextObject);
__().memory.Free(nextObject);
}
return result;
}
// Does not do any validation check, assumes caller `DBRecord`
// represents an object.
private final function Collection ToHashTable(bool makeMutable)
{
local int i;
local HashTable result;
local Text nextKey;
local AcediaObject nextObject;
result = __().collections.EmptyHashTable();
for (i = 0; i < storage.length; i += 1)
{
nextKey = __().text.FromString(storage[i].k);
nextObject = ConvertItemToObject(storage[i], makeMutable);
result.SetItem(nextKey, nextObject);
__().memory.Free(nextKey);
__().memory.Free(nextObject);
}
return result;
}
/**
* Completely erases all data inside a caller `DBRecord`, recursively deleting
* all referred `DBRecord`.
*/
public final function EmptySelf()
{
local int i;
local GameInfo game;
local DBRecord subRecord;
if (lockEraseSelf) {
return;
}
lockEraseSelf = true;
game = __level().unreal_api().GetGameType();
for (i = 0; i < storage.length; i += 1)
{
if (storage[i].t != DBAT_Reference) continue;
subRecord = LoadRecordFor(storage[i].s, package);
if (subRecord == none) continue;
subRecord.EmptySelf();
game.DeleteDataObject(class'DBRecord', string(subRecord.name), package);
}
storage.length = 0;
lockEraseSelf = false;
}
/**
* Takes all available values from `source` and records them into caller
* `DBRecord`. Does not erase untouched old values, but will overwrite them
* in case of the conflict.
*
* Can only convert items in passed collection that return `true` for
* `__().json.IsCompatible()` check. Any other values will be treated as `none`.
*
* Only works as long as caller `DBRecord` has the same container type as
* `source`. `isJSONArray` iff `source.class == class'ArrayList` and
* `!isJSONArray` iff `source.class == class'HashTable`.
*
* Values that cannot be converted into JSON will be replaced with `none`.
*
* @param source `Collection` to write into the caller `DBRecord`.
*/
public final function FromCollection(Collection source)
{
local ArrayList asArrayList;
local HashTable asHashTable;
asArrayList = ArrayList(source);
asHashTable = HashTable(source);
if (asArrayList != none && isJSONArray) {
FromArrayList(asArrayList);
}
if (asHashTable != none && !isJSONArray) {
FromHashTable(asHashTable);
}
}
// Does not do any validation check.
private final function FromArrayList(ArrayList source)
{
local int i, length;
local AcediaObject nextObject;
length = source.GetLength();
for (i = 0; i < length; i += 1)
{
nextObject = source.GetItem(i);
storage[storage.length] = ConvertObjectToItem(nextObject);
__().memory.Free(nextObject);
}
}
// Does not do any validation check.
private final function FromHashTable(HashTable source)
{
local int i, originalStorageLength;
local CollectionIterator iter;
local string nextKey;
local bool isNewKey;
local AcediaObject nextObject;
originalStorageLength = storage.length;
for (iter = source.Iterate(); !iter.HasFinished(); iter.Next())
{
if (iter.GetKey() == none) {
continue;
}
nextKey = __().text.IntoString(BaseText(iter.GetKey()));
isNewKey = true;
for (i = 0; i < originalStorageLength; i += 1)
{
if (storage[i].k == nextKey)
{
isNewKey = false;
break;
}
}
if (isNewKey)
{
nextObject = iter.Get();
SetItem(storage.length, ConvertObjectToItem(nextObject), nextKey);
__().memory.Free(nextObject);
}
}
iter.FreeSelf();
}
// Converts `AcediaObject` into it's internal representation.
private final function StorageItem ConvertObjectToItem(AcediaObject data)
{
local StorageItem result;
local DBRecord newDBRecord;
if (Text(data) != none)
{
result.t = DBAT_String;
result.s = Text(data).ToString();
}
else if(Collection(data) != none)
{
result.t = DBAT_Reference;
newDBRecord = NewRecordFor(package);
newDBRecord.isJSONArray = (data.class == class'ArrayList');
newDBRecord.FromCollection(Collection(data));
result.s = string(newDBRecord.name);
}
else if (FloatBox(data) != none || FloatRef(data) != none)
{
result.t = DBAT_Float;
if (FloatBox(data) != none) {
result.f = FloatBox(data).Get();
}
else {
result.f = FloatRef(data).Get();
}
}
else if (IntBox(data) != none || IntRef(data) != none)
{
result.t = DBAT_Int;
if (IntBox(data) != none) {
result.i = IntBox(data).Get();
}
else {
result.i = IntRef(data).Get();
}
}
else if (BoolBox(data) != none || BoolRef(data) != none)
{
result.t = DBAT_False;
if (BoolBox(data) != none && BoolBox(data).Get()) {
result.t = DBAT_True;
}
if (BoolRef(data) != none && BoolRef(data).Get()) {
result.t = DBAT_True;
}
}
return result;
}
// Converts internal data representation into `AcediaObject`.
private final function AcediaObject ConvertItemToObject(
StorageItem item,
bool makeMutable)
{
local DBRecord subRecord;
switch (item.t) {
case DBAT_False:
case DBAT_True:
if (makeMutable) {
return __().ref.bool(item.t == DBAT_True);
}
else {
return __().box.bool(item.t == DBAT_True);
}
case DBAT_Int:
if (makeMutable) {
return __().ref.int(item.i);
}
else {
return __().box.int(item.i);
}
case DBAT_Float:
if (makeMutable) {
return __().ref.float(item.f);
}
else {
return __().box.float(item.f);
}
case DBAT_String:
if (makeMutable) {
return __().text.FromStringM(item.s);
}
else {
return __().text.FromString(item.s);
}
case DBAT_Reference:
subRecord = LoadRecordFor(item.s, package);
if (subRecord != none) {
return subRecord.ToCollection(makeMutable);
}
default:
}
return none;
}
// "Increments" internal data representation by value inside given
// `AcediaObject`.
// See `IncrementObject()` method for details.
private final function bool IncrementItemByObject(
out StorageItem item,
AcediaObject object)
{
local DBRecord itemRecord;
if (object == none) {
return (item.t == DBAT_Null);
}
if (item.t == DBAT_Null)
{
item = ConvertObjectToItem(object);
return true;
}
else if (item.t == DBAT_String && Text(object) != none)
{
item.s $= Text(object).ToString();
return true;
}
else if(item.t == DBAT_Reference && Collection(object) != none)
{
itemRecord = LoadRecordFor(item.s, package);
if (itemRecord == none)
{
itemRecord = NewRecordFor(package); // DB was broken somehow
item.s = string(itemRecord.name);
itemRecord.isJSONArray = (object.class == class'ArrayList');
}
if ( (itemRecord.isJSONArray && object.class != class'ArrayList')
|| ( !itemRecord.isJSONArray
&& object.class != class'HashTable'))
{
return false;
}
itemRecord.FromCollection(Collection(object));
return true;
}
else if ( (item.t == DBAT_False || item.t == DBAT_True)
&& (BoolBox(object) != none || BoolRef(object) != none))
{
if (BoolBox(object) != none && BoolBox(object).Get()) {
item.t = DBAT_True;
}
if (BoolRef(object) != none && BoolRef(object).Get()) {
item.t = DBAT_True;
}
return true;
}
return IncrementNumericItemByObject(item, object);
}
private final function bool IncrementNumericItemByObject(
out StorageItem item,
AcediaObject object)
{
local int storedValueAsInteger, incrementAsInteger;
local float storedValueAsFloat, incrementAsFloat;
if (item.t != DBAT_Float && item.t != DBAT_Int) {
return false;
}
if (!ReadNumericObjectInto(object, incrementAsInteger, incrementAsFloat)) {
return false;
}
if (item.t == DBAT_Float)
{
storedValueAsInteger = int(item.f);
storedValueAsFloat = item.f;
}
else
{
storedValueAsInteger = item.i;
storedValueAsFloat = float(item.i);
}
// Later we want to implement arbitrary precision arithmetic for storage,
// but for now let's just assume that if either value is a float -
// then user wants a float precision.
if ( item.t == DBAT_Float || FloatBox(object) != none
|| FloatRef(object) != none)
{
item.t = DBAT_Float;
item.f = storedValueAsFloat + incrementAsFloat;
item.i = 0;
}
else
{
item.t = DBAT_Int;
item.i = storedValueAsInteger + incrementAsInteger;
item.f = 0;
}
return true;
}
private final function bool ReadNumericObjectInto(
AcediaObject object,
out int valueAsInt,
out float valueAsFloat)
{
if (IntBox(object) != none || IntRef(object) != none)
{
if (IntBox(object) != none) {
valueAsInt = IntBox(object).Get();
}
else {
valueAsInt = IntRef(object).Get();
}
valueAsFloat = float(valueAsInt);
return true;
}
if (FloatBox(object) != none || FloatRef(object) != none)
{
if (FloatBox(object) != none) {
valueAsFloat = FloatBox(object).Get();
}
else {
valueAsFloat = FloatRef(object).Get();
}
valueAsInt = int(valueAsFloat);
return true;
}
return false;
}
// Add storing bytes
defaultproperties
{
LATIN_LETTERS_AMOUNT = 26
LOWER_A_CODEPOINT = 97
UPPER_A_CODEPOINT = 65
// JSON Pointers allow using "-" as an indicator that element must be
// added at the end of the array
JSONPOINTER_NEW_ARRAY_ELEMENT = "-"
}