Collections recipes

A collection of how-tos for working with Acedia's collections: ArrayList and HashTable classes.

Storing built-in values

Storing in the array

ArrayList serves the role of the regular array and storing built-in value data inside it is simple:

local ArrayList data;

data = _.collections.EmptyArrayList();
//  Add as the last element
data.AddFloat(-2.5);
//  Set at a particular index (length will be auto-adjusted)
data.Set(2, "just a string");

Log("data[0] =" @ data.GetFloat(0));    //  data[0] = -2.5
Log("data[1] =" @ data.IsNone(1));      //  data[1] = true
Log("data[2] =" @ data.GetString(2));   //  data[2] = just a string

Getting string as a Text

By default Acedia's collections use Text to store strings, so we can also get their values as Text:

local Text textInstance;
local ArrayList data;

data = _.collections.EmptyArrayList();
data.SetString(0, "Hello, world!");
textInstance = data.GetText(0);
Log("data[0] =" @ textInstance.ToString());
//  Same as any object returned by a function, `textInstance` must be released
textInstance.FreeSelf();

Storing values by keys

For storing values using keys instead of numeric indices, the Acedia's way is to use HashTable:

local HashTable data;

data = _.collections.EmptyHashTable();
data.SetBool(P("Deal damage?"), true);
data.SetInt(P("Damage amount"), 9001);

Log("Deal damage?" @ data.GetFloat(P("Deal damage?"))); //  Deal damage? true
Log("Damage amount:" @ data.IsNone(1));                 //  Damage amount: 9001

Using iterators

Simple iteration

For both ArrayList and HashTable:

function ListKeyValue(Collection data)
{
    local AcediaObject          key, value;
    local CollectionIterator    iter;

    for (iter = data.Iterate(); !iter.HasFinished(); iter.Next())
    {
        key     = iter.GetKey();
        value   = iter.Get();
        //  Do what you want here
        _.memory.Free(key);
        _.memory.Free(value);
    }
    iter.FreeSelf();
}

Skipping none values

function ListKeyValue(Collection data)
{
    local AcediaObject          key, value;
    local CollectionIterator    iter;

    iter = data.Iterate().LeaveOnlyNotNone();
    while (!iter.HasFinished())
    {
        key     = iter.GetKey();
        value   = iter.Get();
        //  Do what you want here
        _.memory.Free(key);
        _.memory.Free(value);
        iter.Next();
    }
    iter.FreeSelf();
}

Iteration over Text keys only

Text keys are only relevant for HashTables:

function ListKeyValue(HashTable data)
{
    local AcediaObject          key, value;
    local Text                  textKey;
    local CollectionIterator    iter;

    for (iter = data.Iterate(); !iter.HasFinished(); iter.Next())
    {
        key     = iter.GetKey();
        value   = iter.Get();
        textKey = Text(key);
        if (textKey != none)
        {
            Log(textKey.ToString() $ ":"
                @ _.text.IntoString(_.json.Print(value)));
        }
        _.memory.Free(key);
        _.memory.Free(value);
        //  `textKey` is the same reference as `key`!
    }
    iter.FreeSelf();
}

Using JSON data

JSON data can be stored inside Acedia's collections using only built-in values types and ArrayList/HashTable collections:

  • JSON's null can be stored as none;
  • JSON's true/false can be stored as bool'
  • JSON's number can be stored as either int or float (actually Acedia also contains BigInt type for storing arbitrarily large integer values, but it isn't yet implemented into JSON parsing);
  • JSON's string can be stored as string/Text/MutableText;
  • JSON's array can be stored as an ArrayList;
  • JSON's object can be stored as HashTable with Text keys.

NOTE: JSON does not have a separate undefined type and Acedia uses none instead of the missing values.

Converting JSON into Acedia collections

Parsing JSON input

To get an Acedia collection from a JSON object like this one:

{
    "innerObject": {
        "my_bool": true,
        "array": [
            "Engine.Actor",
            false,
            null,
            {
            "something \"here\"": "yes",
            "maybe": 0.003
            },
            56.6
        ],
        "one more": {
            "nope": 324532,
            "whatever": false,
            "o rly?": "ya rly"
        },
        "my_int": -9823452
    },
    "some_var": -7.32,
    "another_var": "aye!"
}

recorded in the string named jsonData you can do the following:

local Parser parser;
local HashTable jsonObject;

parser = _.text.ParseString(jsonData);
jsonObject = HashTable(_.json.ParseWith(parser));
if (!parser.Ok()) {
    // Handle errors
}
else if (!parser.Skip().HasFinished()) {
    // There is more input left after parsing JSON value - is this a problem?
}
// '/innerObject/array/3/maybe' is  0.003
Log("'/innerObject/array/3/maybe' is"
    @ jsonObject.GetFloatBy(P("/innerObject/array/3/maybe")));

Constructing by hand

Example of constructing the same object by hand:

local HashTable jsonObject;
local HashTable innerObject, oneMore, anonymousObject;
local ArrayList jsonArray;

anonymousObject = _.collections.EmptyHashTable();
anonymousObject.SetString(P("something \"here\""), "yes");
anonymousObject.SetFloat(P("maybe"), 0.003);

jsonArray = _.collections.EmptyArrayList()
    .AddString("Engine.Actor")
    .jsonArray.AddBool(false)
    .jsonArray.AddItem(none)
    .jsonArray.AddItem(anonymousObject)
    .jsonArray.AddFloat(56.6);

oneMore = _.collections.EmptyHashTable()
    .SetString(P("o rly?"), "ya rly")
    .SetFloat(P("nope"), 324532)
    .SetBool(P("whatever"), false);

innerObject = _.collections.EmptyHashTable()
    .SetItem(P("array"), jsonArray)
    .SetItem(P("one more"), oneMore)
    .SetBool(P("my_bool"), true)
    .SetInt(P("my_int"), -9823452);

//  Put it all together!
jsonObject = _.collections.EmptyHashTable()
    .SetItem(P("innerObject"), innerObject)
    .SetFloat(P("some_var"), -7.32)
    .SetString(P("another_var"), "aye!");
//  If you only want to keep `jsonObject`, release other references -
//  they won't disappear
anonymousObject.FreeSelf();
jsonArray.FreeSelf();
oneMore.FreeSelf();
innerObject.FreeSelf();

Converting Acedia collections into JSON

Use _.json.Print() to get a compact JSON representation:

//  {"innerObject":{"my_bool":true,"array":["Engine.Actor",false,null,{"something \"here\"":"yes","maybe":0.003},56.6],"one more":{"nope":324532,"whatever":false,"o rly?":"ya rly"},"my_int":-9823452},"some_var":-7.32,"another_var":"aye!"}
Log(_.text.IntoString(_.json.Print(jsonObject)));

or _.json.PrettyPrint() for a nice-looking (multiline, indented and colored) result.

Acedia events system

A collection of how-tos for working with Acedia's event system: Signals and Slots.

Connecting to signal

From inside AcediaObject (or its child class)

Supposing you want to connect to a signal function _server.unreal.gameRules.OnNetDamage():

_server.unreal.gameRules.OnNetDamage(self).connect = handler;

where handler() can be any function with appropriate signature:

function int Handler(
    int                 originalDamage,
    int                 damage,
    Pawn                injured,
    Pawn                instigator,
    Vector              hitLocation,
    out Vector          momentum,
    class<DamageType>   damageType)
{
    //  Do whatever
    return damage;
}

From inside non-AcediaObject

Pass any other AcediaObject object as an argument to your signal function. For example:

local ServiceAnchor receiver;

receiver = ServiceAnchor(_.memory.Allocate(class'ServiceAnchor'));
_server.unreal.gameRules.OnNetDamage(receiver).connect = handler;

NOTE: Signal does not keep the reference to passed argument and once receiver gets deallocated - signal will stop notifying handlers connected through it.

Disconnecting from signal

From inside AcediaObject (or its child class)

Supposing you want to disconnect from a signal function _server.unreal.gameRules.OnNetDamage():

_server.unreal.gameRules.OnNetDamage(self).Disconnect();

From inside non-AcediaObject

To do that you must have passed another AcediaObject as an argument to OnNetDamage() (or any other signal). Supposing that object was called receiver:

_server.unreal.gameRules.OnNetDamage(receiver).Disconnect();

Custom signals

Simple notification events

If you need to add an event with handlers that don't take any parameters and don't return anything, then easiest way it to use SimpleSignal / SingleSlot classes:

class MyEventClass extends AcediaObject;

var private SimpleSignal onMyEventSignal;

protected function Constructor()
{
    onMyEventSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
}

protected function Finalizer()
{
    _.memory.Free(onMyEventSignal);
    onMyEventSignal = none;
}

public function SimpleSlot OnMyEvent(AcediaObject receiver)
{
    return SimpleSlot(onMyEventSignal.NewSlot(receiver));
}

//  Suppose you want to emit the `signal` when this function is called...
public function SimpleSlot FireOffMyEvent(AcediaObject receiver)
{
    //  ...simply call this and all the slots will have their handlers called
    onMyEventSignal.Emit();
}

Then you can use OnMyEvent() as a signal function:

//  To add handlers
myEventClassInstance.OnMyEvent(self).connect = handler;
//  To remove handlers
myEventClassInstance.OnMyEvent(self).Disconnect();

Events with parameters

Some of the events, like OnNetDamage(), can take parameters. To create signals like that, follow the template and define new classes like so:

class MySignal extends Signal;

public final function Emit(<PARAMETERS>)
{
    local Slot nextSlot;
    StartIterating();
    nextSlot = GetNextSlot();
    while (nextSlot != none)
    {
        MySlot(nextSlot).connect(<PARAMETERS>);
        nextSlot = GetNextSlot();
    }
    CleanEmptySlots();
}

defaultproperties
{
    relatedSlotClass = class'MySlot'
}
class MySlot extends Slot;

delegate connect(<PARAMETERS>)
{
    DummyCall(); // This allows Acedia to cleanup slots without set handlers
}

protected function Constructor()
{
    connect = none;
}

protected function Finalizer()
{
    super.Finalizer();
    connect = none;
}

defaultproperties
{
}

Here you can use any set of parameters instead of <PARAMETERS>.

Events with return values

Sometimes you want your handlers to respond in some way to the event. You can either allow them to modify input parameters (e.g. by declaring them as out) or allow them to have return value. OnNetDamage(), for example, is allowed to modify incoming damage by returning a new value.

To add signals / slots that handle return value use following templates:

class MySignal extends Signal;

public final function <RETURN_TYPE> Emit(<PARAMETERS>)
{
    local <RETURN_TYPE> newValue;
    local Slot          nextSlot;
    StartIterating();
    nextSlot = GetNextSlot();
    while (nextSlot != none)
    {
        newValue = <SLOT_CLASS>(nextSlot).connect(<PARAMETERS>);
        //  This check is necessary before using returned value
        if (!nextSlot.IsEmpty())
        {
            //  Now handle `newValue` however you see fit
        }
        nextSlot = GetNextSlot();
    }
    CleanEmptySlots();
    //  Return whatever you see fit after handling all the slots
    return <END_RETURN_VALUE>;
}

defaultproperties
{
    relatedSlotClass = class'MySlot'
}
class MySlot extends Slot;

delegate <RETURN_TYPE> connect(<PARAMETERS>)
{
    DummyCall();
    //  Return anything you want:
    //  this value will be filtered inside corresponding `Signal`
    //  if no handler is set to the associated slot
    return <???>;
}

protected function Constructor()
{
    connect = none;
}

protected function Finalizer()
{
    super.Finalizer();
    connect = none;
}

Feature recipes

A collection of how-tos for working with Feature class.

This class is Acedia's replacement for Mutators: a certain subset of functionality that can be enabled or disabled, according to server owner's wishes. Unlike Mutators:

  • There is no limit for the amount of Features that can be active at the same time;
  • They also provide built-in ability to have several different configs that can be swapped during the runtime;
  • They can be enabled / disabled during the runtime. Achieving these points currently comes at the cost of developer having to perform additional work;
  • They are server-side and for now are not supposed to be created for the clients.

Creating a Feature

Prepare classes

When creating a Mutator you must create a child Mutator class, but when creating a Feature you must create two classes: one for the Feature itself and one for its config. Name your config class something human-readable and your Feature class should have the same name, but with '_Feature' suffix. For example, if you want your configs to be stored inside 'MyFeatureConfig.ini', bare minimum class skeletons are:

//  MyFeature.uc
class MyFeature extends FeatureConfig
    perobjectconfig
    config(MyFeatureConfig);

defaultproperties
{
    configName = "MyFeatureConfig"
}
//  MyFeature_Feature.uc
class MyFeature_Feature extends Feature;

defaultproperties
{
    configClass = class'MyFeature'
}

Setup config variables in config file

Config variables are meant to be added into your FeatureConfig class. Assuming you want to add bool and int settings, simply add two public config variables:

//  MyFeature.uc
class MyFeature extends FeatureConfig
    perobjectconfig
    config(MyFeatureConfig);

var public config int   levelOfAwesome;
var public config bool  enableTrollEngine;

defaultproperties
{
    configName = "MyFeatureConfig"
}

To setup their default values, instead of defaultproperties define DefaultIt() method:

//  MyFeature.uc
protected function DefaultIt()
{
    levelOfAwesome      = 11;
    enableTrollEngine   = true;
}

Conversion to/from HashTable

To make full use out of your Feature, Acedia also requires that you provide methods to convert to and from its collection. For our example simply add two more methods:

//  MyFeature.uc
protected function HashTable ToData()
{
    local HashTable data;

    data = __().collections.EmptyHashTable();
    data.SetInt(P("levelOfAwesome"), levelOfAwesome);
    data.SetBool(P("enableTrollEngine"), enableTrollEngine);
    return data;
}

protected function FromData(HashTable source)
{
    if (source == none) {
        return;
    }
    //  Second parameters should are fallback - use values from `DefaultIt()`
    levelOfAwesome      = source.GetInt(P("levelOfAwesome"), 11);
    enableTrollEngine   = source.GetBool(P("enableTrollEngine"), true);
}

Setup your feature to use your config

Recommended way to do it is to duplicate your config variables in your Feature class:

//  MyFeature_Feature.uc
class MyFeature_Feature extends Feature;

//  Not actually config variables
var public /*config*/ int   levelOfAwesome;
var public /*config*/ bool  enableTrollEngine;

defaultproperties
{
    configClass = class'MyFeature'
}

Then add code that will handle setting config data for your Feature:

//  MyFeature_Feature.uc
protected function SwapConfig(FeatureConfig config)
{
    local MyFeature newConfig;
    newConfig = MyFeature(config);
    if (newConfig == none) {
        return;
    }
    levelOfAwesome      = newConfig.levelOfAwesome;
    enableTrollEngine   = enableTrollEngine.disableTick;
    //      Here you can also add any logic that needs to be performed when
    //  values of the config variables were swapped mid-game, if you care to
    //  support it.
    //      Just beware that this method can be called when you `Feature` is
    //  both enabled and not (can be checked with `IsEnabled()`).
}

Run initialization / shutdown logic

Instead of normal Acedia's Constructor() or Finalizer(), for Featuires one should use:

protected function OnEnabled()
{
}

protected function OnDisabled()
{
}

These methods are called when feature gets enabled or disabled, which can, in theory, happen several times per game. They aren't called when Feature's config is changed mid-game, so sometimes it is convenient to move part of the initialization logic into SwapConfig().

Packaging a Feature

Suppose you've created your own Feature class and want to compile it to be usable with Acedia. In this how-to we will assume that your class is named MyFeature_Feature (same as here).

Create regular UnrealScript mod

First step is still to prepare compilation of *.u file, like one would do for any regular UnrealScript mod and put your *.uc script files inside.

Create manifest

To know what is available in each package, Acedia reads its manifest. To add a manifest create a file named exactly 'Manifest.uc' in your package:

//  Manifest.uc
class Manifest extends _manifest
    abstract;

defaultproperties
{
}

Register feature in the manifest

To add your Feature into manifest, simply fill it inside features array:

//  Manifest.uc
 class Manifest extends _manifest
    abstract;

defaultproperties
{
    //  You can add any amount of features from a single package here
    features(0) = class'MyFeature_Feature'
}

AcediaObject recipes

A collection of how-tos for working with AcediaObject class and related topics.

Base object class to be used in Acedia instead of an Object. AcediaObject provides access to Acedia's APIs through an accessor to a Global object, built-in mechanism for storing unneeded references in an object pool and constructor/finalizer.

Allocating object

If you've created a new child class (say, MyCoolObject) of AcediaObject and want to allocate it, simply do:

local MyCoolObject instance;
instance = MyCoolObject(_.memory.Allocate(class'MyCoolObject'));

It is guaranteed to succeed for non-abstract classes.

Detect object reallocation

Sometimes you want to keep track of an object without declaring that you own its reference with NewRef(), thus allowing it to get deallocated even while you are keeping a reference to it. How to do it in a safe way and detect that it was deallocated/reallocated?

What to do when you start storing a reference

When you first get an instance of AcediaObject you'd like to store in such a way, check if it's allocated and remember its life version:

var int             storedReferenceLifeVersion;
var AcediaObject    storedReference;

function Store(AcediaObject newReference)
{
    if (newReference == none)           return;
    if (!newReference.IsAllocated())    return;

    storedReference             = newReference;
    storedReferenceLifeVersion  = newReference.GetLifeVersion();
    //  Not calling `newReference.NewRef()`, so `newReference` can get
    //  deallocated at any time!
}

What to do when you want to check if stored reference was deallocated

Before using such reference again you simply need to check if life version has changed. If it did, then this instance was reallocated and repurposed and you shouldn't use it anymore. Othewrwise it is still the same object.

function AcediaObject Get()
{
    if (storedReference == none) {
        return none;
    }
    if (storedReference.GetLifeVersion() != storedReferenceLifeVersion)
    {
        //  This object was reallocated! Time to forget about it.
        storedReference = none;
    }
    return storedReference;
}

Forcing object deallocation

self.FreeSelf() and _.memory.Free() only reduce reference counter of the object. But how to forcefully deallocate it?

Just do not do that you fucking idiot.

If you need to somehow dispose of what that object represents, simply add a flag that marks that object as disposed.