UnrealScript library and basis for all Acedia Framework mods
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

511 lines
19 KiB

# Collections
All Acedia's collections store `AcediaObject`s. By taking advantage of boxing
we can use them to store arbitrary types:
both value types (native variables and structs)
and reference types (`AcediaObject` and it's children).
Currently Acedia provides dynamic arrays (regular integer-indexed array)
and associative arrays (collection of key-value pairs with quick access to
values via `AcediaObject` keys).
Using them is fairly straightforward, but, since they're dealing with objects,
some explanation about their memory management is needed.
Below we attempt to give a detailed description of everything you need to
efficiently use Acedia's collections.
## Usage examples
### Dynamic arrays
Dynamics arrays can be created via either of
`_.collections.EmptyDynamicArray()` / `_.collections.NewDynamicArray()` methods.
`_.collections.NewDynamicArray()` takes an `array<Acedia>` argument and
populates returned `DynamicArray` with it's items, while
`_.collections.EmptyDynamicArray()` simply creates an empty `DynamicArray`.
They are similar to regular dynamic `array<AcediaObject>`s with
several differences:
1. They're passed by reference, rather than by value (no additional copies are
made when passing `DynamicArray` as an argument to a function or assigning it
to another variable);
2. It has richer interface;
3. It automatically handles necessary object deallocations.
As an example to illustrate basic usage of `DynamicArray` let's create
a trivial class that remembers players' nicknames:
```unrealscript
class PlayerDB extends AcediaObject;
var private DynamicArray storage;
// Constructor and destructor allow for memory management
protected function Constructor()
{
storage = _.collections.EmptyDynamicArray();
}
protected function Finalizer()
{
storage.FreeSelf();
storage = none;
}
public function RegisterNick(Text newNickName)
{
if (newNickName == none) return;
// `Find` returns `-1` if object is not found
if (storage.Find(newNickName) >= 0) return;
storage.AddItem(newNickName);
}
public function IsRegisteredID(Text toCheck)
{
return (storage.Find(toCheck) >= 0);
}
public function ForgetNick(Text toForget)
{
// This method removes all instances of `toForget` in `storage`;
// Optionally there's a flag to only remove the first one.
storage.RemoveItem(toForget);
}
```
#### What happens if we deallocate stored objects?
They will turn into `none`:
```unrealscript
local Text item;
local DynamicArray storage;
storage = _.collections.EmptyDynamicArray();
item = _.text.FromString("example");
storage.AddItem(item);
// Everything is as expected here
TEST_ExpectNotNone(item);
TEST_ExpectNotNone(storage.GetItem(0));
TEST_ExpectTrue(storage.GetItem(0) == item);
// Now let's deallocate `item`
item.FreeSelf();
// Suddenly things are different:
TEST_ExpectNotNone(item); // `item` deallocated, but not dereferenced
TEST_ExpectNone(storage.GetItem(0)); // but it is gone from the collection
TEST_ExpectFalse(storage.GetItem(0) == item);
```
Let's explain what's changed after deallocation:
1. Even though we've deallocated `item`, it's reference still points at
the `Text` object.
This is because deallocation is an Acedia's concept and actual UnrealScript
objects are not destroyed by it;
2. `storage.GetItem(0)` no longer points at that `Text` object.
Unlike a simple `array<AcediaObject>`, `DynamicObject` tracks status of it's
items and replaces their values with `none` when they're deallocated.
This kind of cleanup is something we cannot do with simple `FreeSelf()` or even
`_.memory.Deallocate()` for object stored in a regular array, but can for
objects stored in collections.
3. Since collection forgot about `item` after it was deallocated,
`storage.GetItem(0) == item` will be false.
#### What happens if we remove an item from our `DynamicArray` collection?
By default nothing - stored items will continue to exist outside the collection.
This is because by default `DynamicArray` (and `AssociativeArray`) is not
responsible for deallocation of its items. But it can be made to.
Suppose that to avoid items disappearing from our collections, we put in their
copies instead.
For `Text` it can be accomplished with a simple `Copy()` method:
`storage.AddItem(item.Copy())`.
This creates a problem - `storage`, as we've explained before, won't actually
deallocate this item if we simply remove it. We will have to do so manually to
prevent memory leaks:
```unrealscript
...
_.memory.Deallocate(storage.GetItem(i));
storage.RemoveIndex(i);
```
which isn't ideal.
To solve this problem we can add a copy of an `item` to our `DynamicArray` as
a *managed object*: collections will consider themselves responsible for
deallocation of objects marked as managed and will automatically clean them up.
To add an item as managed we need to simply specify second argument for
`AddItem(, true)` method:
```unrealscript
local Text item;
local DynamicArray storage;
storage = _.collections.EmptyDynamicArray();
item = _.text.FromString("example");
storage.AddItem(item, true);
// Here added item is still allocated
TEST_ExpectTrue(item.IsAllocated());
// But after it's removed from `storage`...
storage.RemoveIndex(0);
// ...it's automatically gets deallocated
TEST_ExpectFalse(item.IsAllocated());
```
Whether you would want your collection to auto-deallocate your items or not
depends only on your needs.
> **NOTE:**
> The same collection can contain both managed and unmanaged items.
Let's rewrite `RegisterNick()` method of `PlayerDB` to make it independent from
whether `Text` objects passed to it are deallocated:
```unrealscript
...
public function RegisterNick(Text newNickName)
{
if (newNickName == none) return;
if (storage.Find(newNickName) >= 0) return;
// Store an independent, managed copy
storage.AddItem(newNickName.Copy(), true);
}
...
```
### Associative arrays
> **IMPORTANT:**
> It is assumed you've read previous section about `DynamicArray`s and
> its managed objects first.
Associative arrays allow to efficiently store and access `AcediaObject` values
via `AcediaObject` keys by using hash map under the hood.
While objects of any `AcediaObject`'s subclass can be used as keys, the main
reason for implementing associative arrays was to allow for `Text` keys and
examples in this sections will focus on them specifically.
The basic interface is simple and can be demonstrated with this:
```unrealscript
local AcediaObject item;
local AssociativeArray storage;
storage = _.collection.NewAssociativeArray();
// Add some values
storage.SetItem(_.text.FromString("year"), _.ref.int(2021));
storage.SetItem( _.text.FromString("comment"),
_.text.FromString("What year it is?"));
// Then get them
item = storage.GetItem(_.text.FromString("year"));
TEST_ExpectTrue(IntRef(item).Get() == 2021);
item = storage.GetItem(_.text.FromString("comment"));
TEST_ExpectTrue(Text(item).ToString() == "What year it is?");
```
In above example we've created separate text instances (with the same contents)
to store and retrieve items in `AssociativeArray`.
However it is inefficient to each time create `Text` anew:
1. It defeats the purpose of using `Text` over `string`, since
(after initial creation cost) `Text` allows for a cheaper access to
individual characters and also allows us to compute `Text`'s hash only once,
caching it.
But if we create `Text` object every time we want to access value in
`AssociativeArray` we will only get more overhead without any benefits.
2. It leads to creation of useless objects, that we didn't deallocate in
the above example.
So it is recommended that, whenever possible, your class would define reusable
`Text` constant that it would want to use as keys beforehand.
If you want to implement a class that receives zed's data as
an `AssociativeArray` and wants to buff its health, you can do the following:
```unrealscript
class MyZedUpgrader extends AcediaObject;
var protected Text TMAX_HEALTH;
protected function StaticConstructor()
{
default.TMAX_HEALTH = _.text.FromString("maxhealth");
}
public final function UpgradeMyZed(AssociativeArray zedData)
{
local IntRef maxHealth;
maxHealth = IntRef(AssociativeArray.GetItem(TMAX_HEALTH));
maxHealth.Set(maxHealth.Get() * 2);
}
```
[Text](./text.md) has more information about convenient ways to
efficiently create `Text` constants.
For example, in the above use case of upgrading zed's health it is acceptable to
do this instead:
```unrealscript
class MyZedUpgrader extends AcediaObject;
public final function UpgradeMyZed(AssociativeArray zedData)
{
local IntRef maxHealth;
maxHealth = IntRef(AssociativeArray.GetItem(P("maxhealth")));
maxHealth.Set(maxHealth.Get() * 2);
}
```
#### Memory management and `AssociativeArray`
`AssociativeArray` supports the concept of managed objects in the same way as
`DynamicArray`s: by default objects are not managed, but can be added as such
when optional argument is used:
`AssociativeArray.GetItem(P("value"), someItem, true)`.
We'll just note here that it's possible to remove a managed item from
`AssociativeArray` without deallocating it with `TakeItem()`/`TakeEntry()`
methods.
A question specific for `AssociativeArray`s is whether they deallocate
their keys.
And the answer is: they do not.
`AssociativeArray` will never deallocate its keys, even if a managed value is
recorded with them.
This way one can use the same pre-allocated key in several different
`AssociativeArray`s.
If you do need to deallocate them, you will have to do it manually.
One good way to do so is to use `TakeEntry(AcediaObject key)` method that
returns a struct `Entry` with both key and recorded value inside:
```unrealscript
struct Entry
{
// Non-public fields are omitted
var public AcediaObject key;
var public AcediaObject value;
var public bool managed;
};
```
This method also always removes stored value from `AssociativeArray` without
deallocating it, even if it was managed, making you responsible for it.
In case of the opposite situation, where one deallocates an `AcediaObject` used
as a key, `AssociativeArray` will automatically remove appropriate entry
in its entirety.
However this is only a contingency measure:
**you should never deallocate objects that are still used as keys in `AssociativeArray`**.
One of the negative consequences is that it'll screw up `AssociativeArray`'s
`GetLength()` results, making it possibly overestimate the amount of
stored items (because there is no guarantee on *when* an entry with
deallocated key will be detected and cleaned up).
#### Capacity
Acedia's `AssociativeArray` works like a hash table and needs to allocate
sufficiently large dynamic array as a storage for its items.
If you keep adding new items that storage will eventually become too small for
hash table to work efficiently and we will have to reallocate and re-fill it.
If you want to add a huge enough amount of items into your `AssociativeArray`,
this process might be repeated several times.
This is not ideal, since it means doing a lot of iteration, each taking
noticeable time and increasing infinite loop counter
(game will crash if it gets high enough).
`AssociativeArray` allows you to set minimal capacity with
`SetMinimalCapacity()` method to force it to pre-allocate enough space for
the expected amount of items.
Setting minimal capacity to the maximum amount of items you expect to store in
the caller `AssociativeArray` can remove any need for reallocating the storage.
> **NOTE:**
> `AssociativeArray` always allocates storage array with length of at least
> `MINIMUM_SIZE = 50` and won't need any reallocations before you add at least
> `MINIMUM_SIZE * MAXIMUM_DENSITY = 50 * 0.75 ~= 38` items,
> no matter the current minimal capacity
> (that can be checked with `GetMinimalCapacity()` method).
#### [Advanced] Associative arrays' keys
`AssociativeArray` allows to store `AcediaObject` values by `AcediaObject` keys.
Object of any class (derived from `AcediaObject`) can be used for either, but
behavior of the `AssociativeArray` regarding its key depends on how key's
`IsEqual()` and `GetHashCode()` methods are implemented.
> **IMPORTANT:**
> [Refresh](../objects.md) your knowledge on how equality checks for
> Acedia's objects work, do not rely on intuition here.
For example `Text`'s hash and equality is determined by its content:
```unrealscript
local Text t1, t2;
t1 = _.text.FromString("Some random text");
t2 = _.text.FromString("Some random text");
// All of these assertions are correct:
TEST_ExpectTrue(t1.IsEqual(t2)); // same content
TEST_ExpectTrue(t1.GetHashCode() == t2.GetHashCode()); // same hashes
TEST_ExpectTrue(t1 != t2); // different objects
```
Therefore, if you used one `Text` as a key, then you will be able to obtain it's
value with another `Text` that contains the same `string`.
However `MutableText`'s contents can change dynamically, so it cannot afford to
base its equality and hash on its contents:
```unrealscript
local MutableText t1, t2;
t1 = _.text.FromStringM("Some random text");
t2 = _.text.FromStringM("Some random text");
// `IsEqual()` no longer compares contents;
// Use `Compare()` instead.
TEST_ExpectFalse(t1.IsEqual(t2));
TEST_ExpectFalse(t1.GetHashCode() == t2.GetHashCode()); // different hashes (most likely)
TEST_ExpectTrue(t1 != t2); // different objects
```
`MutableText` can still be used as a key, but value stored with it will only be
obtainable by providing the exact instance of `MutableText`, regardless of
its contents:
```unrealscript
local MutableText t1, t2;
local AssociativeArray storage;
storage = _.collection.NewAssociativeArray();
t1 = _.text.FromStringM("Some random text");
t2 = _.text.FromStringM("Some random text");
storage.SetItem(t1, _.text.FromString("Contents!"));
TEST_ExpectNone(storage.GetItem(t2));
TEST_ExpectNotNone(storage.GetItem(t1));
```
As far as base Acedia's classes go, only `Text` and boxed
(immutable ones, not refs) values are a good fit to be used as
contents-dependent keys.
## More accessors
While you can store simple values inside these arrays in a straightforward
manner of `storage.SetItem(_.text.FromString("year"), _.ref.int(2021))`,
it is not very convenient.
Especially getting items from such arrays can be problematic, since that `int`
can potentially be stored as both immutable `IntBox` or mutable `IntRef`.
To help with this problem Acedia's collections provide a bunch of convenience
accessors for UnrealScript's built-in types.
Let us start with getters
`GetBool()`, `GetByte()`, `GetInt()`, `GetFloat()`, `GetText()`
(since Acedia uses `Text` instead of `string`).
These take index for `DynamicArray` or `AcediaObject` keys for
`AssociativeArray` and return relevant type if they find either box or a ref
of such type in the caller array.
All of them, except `Text`, also allow you to provide default value as
a second argument - this value will be used if neither box or ref for
the desired type is found.
Then there's setter methods
`SetBool()`, `SetByte()`, `SetInt()`, `SetFloat()` that take
at least two parameters: index/key and value to store.
They automatically create either box or ref object to wrap around passed
primitive value and always store it as a *managed item*.
Third, optional, `bool` parameter `asRef` allows you to decide whether passed
value should be saved inside the array in an immutable box or in a mutable ref
(default `false` is to save that primitive type in a box).
> **NOTE:**
> There is no paired `SetText()` setter for `GetText()` getter,
> since `Text` itself is an object and can directly be saves with `SetItem()`.
Here is an example of how they work:
```unrealscript
local IntBox box;
local IntRef ref;
local DynamicArray storage;
storage = _.collection.NewDynamicArray();
storage.SetInt(0, 7);
// `int` value is not returned normally, but there is not auto-conversion
// into `float` and so `GetFloat()` returns provided default value instead
Log("Value as int:" @ storage.GetInt(0)); // Value as int: 7
Log("Value as float:" @ storage.GetFloat(0, 9)); // Value as int: 9
box = IntBox(storage.GetItem(0));
// `int` should be stored in an allocated box
TEST_ExpectNotNone(box);
TEST_ExpectTrue(box.IsAllocated());
// Re-recording `int` as ref causes previous box (managed by `storage`)
// to get destroyed
storage.SetInt(0, 11, true);
TEST_ExpectNotNone(box); // still not `none`
TEST_ExpectFalse(box.IsAllocated()); // but is not deallocated
Log("Value as int:" @ storage.GetInt(0)); // Value as int: 11
// `int` should be stored in an allocated ref now
ref = IntRef(storage.GetItem(0));
TEST_ExpectNotNone(ref);
TEST_ExpectTrue(ref.IsAllocated());
```
## Even more accessors
Collections `DynamicArray` and `AssociativeArray` are `AcediaObject`s themselves
and, therefore, can be stored in other arrays, producing hierarchical
structures, similar to those of JSON's arrays / objects.
```json
{
"main_guy": {
"status": "admin",
"maps": ["biotics", "bedlam", "waterworks"]
},
"other_guy": {
"status": "random",
"maps": ["biotics", "westlondon"]
}
}
```
To access some variable, nested deep inside such structure, one can either
manually get reference of each collection on the way, e.g. to access second map
of the "other_guy" we'd need to first get reference to "other_guy"'s collection
(`AssociativeArray`):
```json
{
"status": "random",
"maps": ["biotics", "westlondon"]
}
```
then to the array of his maps (`DynamicArray`):
```json
["biotics", "westlondon"]
```
and only then access second item.
This is too cumbersome!
Fortunately, Acedia's collections have an alternative solution:
```unrealscript
userCollection.GetTextBy(P("/other_guy/maps/1")); // westlondon!
```
`/other_guy/maps/1` line is describes a path to the element nested deep inside
hierarchy of collections and follows the rules of a
[JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901).
Both `DynamicArray` and `AssociativeArray` support following methods that work
with such pointers:
`GetItemBy()`, `GetBoolBy()`, `GetByteBy()`, `GetIntBy()`, `GetFloatBy()`,
`GetTextBy()`, `GetDynamicArrayBy()` and `GetAssociativeArrayBy()`.
Passing paths like `/other_guy/maps/1` requires collections to perform their
parsing every time such getter is called.
If you want to reuse the same path several times it might be better to convert
it into `JSONPointer` object (using `_.json.Pointer()` method) and then use
that object with following alternative methods:
`GetItemByJSON()`, `GetBoolByJSON()`, `GetByteByJSON()`, `GetIntByJSON()`,
`GetFloatByJSON()`, `GetTextByJSON()`, `GetDynamicArrayByJSON()`,
`GetAssociativeArrayByJSON()`.
This way parsing has to be done only once - when creating `JSONPointer` object.