/**
* This class implements JSON object storage capabilities.
* Whenever one wants to store JSON data, they need to define such object.
* It stores name-value pairs, where names are strings and values can be:
* ~ Boolean, string, null or number (float in this implementation) data;
* ~ Other JSON objects;
* ~ JSON Arrays (see `JArray` class).
*
* This implementation provides getters and setters for boolean, string,
* null or number types that allow to freely set and fetch their values
* by name.
* JSON objects and arrays can be fetched by getters, but you cannot
* add existing object or array to another object. Instead one has to create
* a new, empty object with a certain name and then fill it with data.
* This allows to avoid loop situations, where object is contained in itself.
* Functions to remove existing values are also provided and are applicable
* to all variable types.
* Setters can also be used to overwrite any value by a different value,
* even of a different type.
* Copyright 2020 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 .
*/
class JObject extends JSON;
// We will store all our properties as a simple array of name-value pairs.
struct JProperty
{
var string name;
var JStorageAtom value;
};
// Bucket of alias-value pairs, with the same alias hash.
struct PropertyBucket
{
var array properties;
};
var private array hashTable;
var private int storedElementCount;
// Reasonable lower and upper limits on hash table capacity,
// that will be enforced if user requires something outside those bounds
var private config const int MINIMUM_CAPACITY;
var private config const int MAXIMUM_CAPACITY;
var private config const float MINIMUM_DENSITY;
var private config const float MAXIMUM_DENSITY;
var private config const int MINIMUM_DIFFERENCE_FOR_REALLOCATION;
var private config const int ABSOLUTE_LOWER_CAPACITY_LIMIT;
// Helper method that is needed as a replacement for `%`, since it is
// an operation on `float`s in UnrealScript and does not have enough precision
// to work with hashes.
// Assumes positive input.
private function int Remainder(int number, int divisor)
{
local int quotient;
quotient = number / divisor;
return (number - quotient * divisor);
}
// Finds indices for:
// 1. Bucked that contains specified alias (`bucketIndex`);
// 2. Pair for specified alias in the bucket's collection
// (`propertyIndex`).
// `bucketIndex` is always found,
// `propertyIndex` is valid iff method returns `true`, otherwise it's equal to
// the index at which new property can get inserted.
private final function bool FindPropertyIndices(
string name,
out int bucketIndex,
out int propertyIndex)
{
local int i;
local array bucketProperties;
TouchHashTable();
bucketIndex = _.text.GetHash(name);
if (bucketIndex < 0) {
bucketIndex *= -1;
}
bucketIndex = Remainder(bucketIndex, hashTable.length);
// Check if bucket actually has given name.
bucketProperties = hashTable[bucketIndex].properties;
for (i = 0; i < bucketProperties.length; i += 1)
{
if (bucketProperties[i].name == name)
{
propertyIndex = i;
return true;
}
}
propertyIndex = bucketProperties.length;
return false;
}
// Creates hash table in case it does not exist yet
private final function TouchHashTable()
{
if (hashTable.length <= 0) {
UpdateHashTableCapacity();
}
}
// Attempts to find a property in a caller `JObject` by the name `name` and
// writes it into `result`. Returns `true` if it succeeds and `false` otherwise
// (in that case writes a blank property with a given name in `result`).
private final function bool FindProperty(string name, out JProperty result)
{
local JProperty newProperty;
local int bucketIndex, propertyIndex;
if (FindPropertyIndices(name, bucketIndex, propertyIndex))
{
result = hashTable[bucketIndex].properties[propertyIndex];
return true;
}
newProperty.name = name;
result = newProperty;
return false;
}
// Creates/replaces a property with a name `newProperty.name` in caller
// JSON object
private final function UpdateProperty(JProperty newProperty)
{
local bool overriddenProperty;
local int bucketIndex, propertyIndex;
overriddenProperty = !FindPropertyIndices( newProperty.name,
bucketIndex, propertyIndex);
hashTable[bucketIndex].properties[propertyIndex] = newProperty;
if (overriddenProperty) {
storedElementCount += 1;
UpdateHashTableCapacity();
}
}
// Removes a property with a name `newProperty.name` from caller JSON object
// Returns `true` if something was actually removed.
private final function bool RemoveProperty(string propertyName)
{
local JProperty propertyToRemove;
local int bucketIndex, propertyIndex;
// Ensure has table was initialized before any updates
if (hashTable.length <= 0) {
UpdateHashTableCapacity();
}
if (FindPropertyIndices(propertyName, bucketIndex, propertyIndex)) {
propertyToRemove = hashTable[bucketIndex].properties[propertyIndex];
if (propertyToRemove.value.complexValue != none) {
propertyToRemove.value.complexValue.Destroy();
}
hashTable[bucketIndex].properties.Remove(propertyIndex, 1);
storedElementCount = Max(0, storedElementCount - 1);
UpdateHashTableCapacity();
return true;
}
return false;
}
// Checks if we need to change our current capacity and does so if needed
private final function UpdateHashTableCapacity()
{
local int oldCapacity, newCapacity;
oldCapacity = hashTable.length;
// Calculate new capacity (and whether it is needed) based on amount of
// stored properties and current capacity
newCapacity = oldCapacity;
if (storedElementCount < newCapacity * MINIMUM_DENSITY) {
newCapacity /= 2;
}
if (storedElementCount > newCapacity * MAXIMUM_DENSITY) {
newCapacity *= 2;
}
// Enforce our limits
newCapacity = Clamp(newCapacity, MINIMUM_CAPACITY, MAXIMUM_CAPACITY);
newCapacity = Max(ABSOLUTE_LOWER_CAPACITY_LIMIT, newCapacity);
// Only resize if difference is huge enough or table does not exists yet
if ( newCapacity - oldCapacity > MINIMUM_DIFFERENCE_FOR_REALLOCATION
|| oldCapacity - newCapacity > MINIMUM_DIFFERENCE_FOR_REALLOCATION
|| oldCapacity <= 0) {
ResizeHashTable(newCapacity);
}
}
// Change size of the hash table, does not check any limits, does not check
// if `newCapacity` is a valid capacity (`newCapacity > 0`).
// Use `UpdateHashTableCapacity()` for that.
private final function ResizeHashTable(int newCapacity)
{
local int i, j;
local array bucketProperties;
local array oldHashTable;
oldHashTable = hashTable;
// Clean current hash table
hashTable.length = 0;
hashTable.length = newCapacity;
for (i = 0; i < oldHashTable.length; i += 1)
{
bucketProperties = oldHashTable[i].properties;
for (j = 0; j < bucketProperties.length; j += 1) {
UpdateProperty(bucketProperties[j]);
}
}
}
// Returns `JType` of a variable with a given name in our properties.
// This function can be used to check if certain variable exists
// in this object, since if such variable does not exist -
// function will return `JSON_Undefined`.
public final function JType GetTypeOf(string name)
{
local JProperty property;
FindProperty(name, property);
// If we did not find anything - `property` will be set up as
// a `JSON_Undefined` type value.
return property.value.type;
}
// Following functions are getters for various types of variables.
// Getter for null value simply checks if it's null
// and returns true/false as a result.
// Getters for simple types (number, string, boolean) can have optional
// default value specified, that will be returned if requested variable
// doesn't exist or has a different type.
// Getters for object and array types don't take default values and
// will simply return `none`.
public final function float GetNumber(string name, optional float defaultValue)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Number) {
return defaultValue;
}
return property.value.numberValue;
}
public final function int GetInteger(string name, optional int defaultValue)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Number) {
return defaultValue;
}
return property.value.numberValueAsInt;
}
public final function string GetString(
string name,
optional string defaultValue
)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_String) {
return defaultValue;
}
return property.value.stringValue;
}
public final function class