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.

527 lines
20 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.
3 years ago
Below we attempt to give a detailed description of everything you need to know
to efficiently use Acedia's collections.
## Usage examples
### Dynamic arrays
3 years ago
Dynamic 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);
3 years ago
2. They have richer interface;
3. They automatically handle 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`;
3 years ago
// There's also an optional 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:
3 years ago
1. Even though we've deallocated `item`, its reference still points at
the `Text` object.
3 years ago
This happens because object being *deallocated* is simply Acedia's flag for it
and, from the Unreal Engine's point of view, `item` still exists and
is being used;
2. `storage.GetItem(0)` no longer points at that `Text` object.
3 years ago
Unlike a simple `array<AcediaObject>`, `DynamicObject` tracks status of its
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
3 years ago
objects stored in Acedia's `Collection`s.
3. Since our collection has forgotten 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())`.
3 years ago
This creates a problem - `storage`, as we've just explained, 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
3 years ago
depends only on how you plan to use your collections.
> **NOTE:**
3 years ago
> The same collection can technically contain both managed and unmanaged items,
> but it is best you avoid mixing these types of 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;
3 years ago
// Store an independent, but managed copy,
// that will be gone along with the `storage`
storage.AddItem(newNickName.Copy(), true);
}
...
```
3 years ago
> **IMPORTANT:**
> While items added to collections aren't managed by *default*,
> it is a convention that if you return a collection from your function and
> make another piece of code responsible for it - that piece of code also
> becomes responsible for that collection's items
> (meaning that it must deallocate them when they are no longer needed).
> If you expect a different behavior - you must specify so in the function's
> description.
### 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`.
3 years ago
However it is inefficient to each time create `Text` anew just to get an item:
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,
3 years ago
caching it for later use.
But if we create `Text` object every time we want to access value in
`AssociativeArray` we will only get more overhead without any benefits.
3 years ago
2. It leads to creation of useless objects, that we didn't even 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.
3 years ago
`AssociativeArray` will not 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.
3 years ago
Another way is to completely clear your collection along with any keys inside
with `Empty(true)` method.
This method recursively clears your collection (also making `Empty()` calls on
any collections stored inside yours) and passing it `true` as a parameter makes
it deallocate any key objects used in these collections.
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
3 years ago
the caller `AssociativeArray` will 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.
3 years ago
Ultimately, any `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
```
3 years ago
Therefore, if you used one `Text` as a key, then you will be able to obtain its
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.
3 years ago
## Better 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.