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.
 

11 KiB

Collections

All Acedia's collections store AcediaObjects. 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 (indexed, variable sized array) and associative arrays (collection of key-value pairs with quick access to values via keys). Using them is fairly straightforward, but, since they're dealing with objects, some explanation about their memory management is needed. Next section aims to explain all that with some examples.

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. It's passed by reference, rather than by value (DynamicArray isn't copied each time it's passed as an argument to a function);
  2. It has richer interface;
  3. It can handle Acedia's object deallocation.

As an example to illustrate basic usage of DynamicArray let's create a trivial class that remembers players by their nickname:

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:

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);
TEST_ExpectNone(storage.GetItem(0));
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 Text object. This is because deallocation is an Acedia's convention 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 are deallocated. This cleanup is something we cannot do with simple FreeSelf() or even _.memory.Deallocate() for regular object, but can for objects stored in collections.
  3. Since collection forgot about item after it was deallocated, storage.GetItem(0) == item will be false even if an instance of item will be later reused from it's object pool.

What happens if we deallocate our DynamicArray collection?

By default nothing.

To avoid items disappearing from our collections, we can put in their copies instead. For Text it can be accomplished with a simple Copy() method: storage.AddItem(item.Copy()). But this leads us to another problem - storage won't actually deallocate this item if we simply remove it. We will have to do so manually to prevent memory leaks:

...
_.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 do it for us. To add item as managed we need to simply specify second argument for AddItem(, true) method:

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());

It depends on your needs whether you'd want your collection to auto-deallocate your items or not. Note also that 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:

...
public function RegisterNick(Text newNickName)
{
    if (newNickName == none)            return;
    if (storage.Find(newNickName) >= 0) return;
    storage.AddItem(newNickName.Copy(), true);
}
...

Associative arrays

NOTE: It is assumed you've read previous section about DynamicArrays and it's managed objects first.

Associative arrays allow to store and access AcediaObject values via AcediaObject keys 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 like so:

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).ToPlainString() == "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's inefficient to each time create Text anew:

  1. It defeats the purpose of using Text over string, since one of Text's main benefits is that once created, it allows cheaper access to individual characters and 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's recommended that, whenever possible, your class would define Text constant that it'd 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 it's health, you can do the following:

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 has more information about how else you can efficiently create Text constants. For example, in the above use case of upgrading zed's health we can instead do this:

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 support the concept of managed objects in the same way as DynamicArrays: 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 AssociativeArrays is whether they deallocate their keys. And the answer is: they do not. AssociativeArray will never deallocate it's keys, even if managed value is recorded with them. This way one can use the same pre-allocated key in several different AssociativeArrays. If you do need to deallocate them, you will have to do it manually.

In case of the opposite situation, where one deallocates an AcediaObject used as a key: AssociativeArray will automatically remove appropriate entry in it's entirety. However this is only a clean-up attempt: you should never deallocate objects that are still used as keys in AssociativeArray. One of the negative consequences is that it'll screw up it's GetLength() results, making it possibly overestimate the amount of stored items (there is no guarantee on when an entry with deallocated key will be detected and disposed of).

Associative array keys

AssociativeArray allows to store AcediaObject values by AcediaObject keys. Object of any class (derivative of AcediaObject) can be used for either, but behavior of the key depends on how their IsEqual() and GetHashCode() methods are implemented.

For example Text's hash and equality is determined by it's content:

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 contents
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 and so it cannot afford to base it's equality and hash on it's contents:

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 will only be obtainable by providing the exact instance of MutableText used as a key, no matter it's contents.

It's for the similar reason that immutable boxes are more fitting than mutable references as keys.