Skip to content

Structs

ItsDeltin edited this page Sep 17, 2023 · 8 revisions

Structs are a data type used to group related variables.

struct Dictionary<K, V> {
    public K[] Keys;
    public V[] Values;

    public V Get(in K key) {
        return Values[Keys.IndexOf(key)];
    }
}

To initiate a struct, declare each field with its paired value inside curly brackets.

Dictionary<Button, String> bindings = {
    Keys: [Button.Interact, Button.Ultimate],
    Values: ["Press Interact to place an orb.", "Press Ultimate to detonate orbs."]
};

The types of the fields can optionally be explicitly declared.

Dictionary<Button, String> bindings = {
    Button[] Keys: [Button.Interact, Button.Ultimate],
    String[] Values: ["Press Interact to place an orb.", "Press Ultimate to detonate orbs."]
};

Struct update syntax

The .. syntax will fill the unspecified fields with the values from another struct value.

struct Item {
    public String Name;
    public String Description;
    public Number EffectId;
    public Number Charges;
}

Item equippedItem = {
    Name: 'Example',
    Description: 'This Item will give you the power of examplanius',
    EffectId: 0,
    Charges: 4
};

Item withUpdatedCharge = {
    Charges: equippedItem.Charges - 1,
    ..equippedItem
}

single structs

OSTW has two types of structs: 'parallel' and 'single' (also known as unparalleled). Parallel structs create multiple values for each variable in the struct which are all managed together. single structs will generate a workshop Array containing all of the struct's variables.

Structs are parallel by default. Add the single keyword to the struct definition to make it unparalleled.

Paralleled structs have little difference to unparalleled structs in the analysis portion of OSTW. The only difference is that unparalleled structs can be assigned to the 'Any' type. However, it affects how OSTW compiles your structs.

See the next section for more information on the differences in how they compile. As a rule of thumb, only use unparalleled structs when one or more of these conditions apply to your situation:

  • Running out of variable space: Unparalleled structs use less variables compared to parallel structs.
  • Read-only struct array: Modifying unparalleled struct arrays is expensive, try to avoid it.
  • Sorting/Filtering struct array: Unparalleled struct arrays are cheaper to sort/filter than parallel struct arrays.

How structs compile to the workshop

You can use this section to make an educated decision on which kind of struct is best for faster workshop code.

There are 2 types of structs: 'paralleled' and 'single'. Paralleled structs assigns a workshop variable for each value in the struct. Singles stores each value in the struct into an array.

// Remove the 'single' keyword to convert to a parallel value.
single struct Entity {
    public String Name;
    public Number Identifier;
}
Entity entity = { Name: "Ana", Identifier: 4 };
-- 🔧 Workshop code (Parallel ⏸️)
--    📗 This will generate workshop variables for each field: 'myStructVariable_X' and 'myStructVariable_Y'
Global.entity_Name = "Ana";
Global.entity_Identifier = 4;

-- 🔧 Workshop code (Unparalleled 📦)
--    📗 This will generate a single workshop variable, the values are contained inside an array.
Global.entity = ["Ana", 4];

The difference between parallel and unparalleled structs have the most impact when working with struct arrays. Depending on how the array is used, either paralleled or unparalleled will be the more viable choice.

Entity entities = [
	{ Name: "Ana", Identifier: 4 },
	{ Name: "Zenyatta", Identifier: 5 },
	{ Name: "Ashe", Identifier: 6 }
];
-- 🔧 Workshop code (Parallel ⏸️)
--    📗 Each field has it's own array. This is why they are named parallel structs, because each workshop variable is managed in parallel when you append, remove, or modify values.
Global.entities_Name = ["Ana", "Zenyatta", "Ashe"];
Global.entities_Identifier = [4, 5, 6];

-- 🔧 Workshop code (Unparalleled 📦)
Global.entities = [["Ana", 4], ["Zenyatta", 5], ["Ashe", 6]];

Parallel structs are more efficient at handling operations that deal with properties inside the struct. Unparalleled structs are more efficient with operating on the entire struct value.

⏸️ Parallel structs single/unparallel 📦
⭐ Efficient at modifying properties Efficient with Filter and sort
⭐ Efficient at IndexOf property search Efficient at using IndexOf with whole struct values ⭐
⭐ Efficient at Mapping specific properties and field updating Access to all array operations ⭐
⭐ Better in the extended collection and classes Assignable to Any

Setting fields in struct arrays

In the workshop, arrays and especially multidimensional arrays are expensive. Unparalleled arrays require an extra dimension to store the values in, while parallel arrays can be kept one dimensional.

entities[1].Identifier = 6;
-- 🔧 Workshop code (Parallel ⏸️)
--   ⭐ Simply sets a value in an array.
Global.entities_Identifier[1] = 6;

-- 🔧 Workshop code (Unparalleled 📦)
--    ❗ More expensive, the array builder is required for multidimensional modification.
Global._arrayConstructor = Global.entities[1];
Global._arrayConstructor[1] = 6;
Global.entities[1] = Global._arrayConstructor;

Mapping struct arrays

Selecting a property

A common requirement with struct arrays is to create a second array from one of the struct's properties. Parallel struct arrays can do this for free because of how they are formatted compared to unparalleled struct arrays.

struct Entity {
    public String Name;
    public Number Identifier;
}

# The list of active entities in the game.
Entity[] entities = [...];

# Create a second array containing all of the entities' identifiers.
Number[] identifiers = entities.Map(entity => entity.Identifier);
-- 🔧 Workshop code (Parallel ⏸️)
--    ⭐ completely free!
Global.identifiers = Global.entities_Identifier;

-- 🔧 Workshop code (Unparalleled 📦)
--    ❗ Expensive because a 'Mapped Array' is required.
Global.identifiers = Mapped Array(Global.entities, Current Array Element[1]);

Searching a struct array for a property

When we want to search for a struct in an array given a property, parallel arrays are much better for the task. Selecting a property explained how selecting a property in a struct array is free, so we can plug the array straight into an IndexOf.

# Find this identifier in the entities array.
Number targetId: 5;
# Get the index of the entity with an identifier of 5.
Number targetIndex = entities.Map(entity => entity.Identifier).IndexOf(targetId);
# The name of the player with the matching targetId.
String targetName = entities[targetIndex].Name;
-- 🔧 Workshop code (Parallel ⏸️)
--    ⭐ The Identifiers are free to access.
Global.targetIndex = Index Of Array Value(Global.entities_Identifier, 5);

-- 🔧 Workshop code (Unparalleled 📦)
--    ❗ The Identifiers must be extracted with a Mapped Array.
Global.targetIndex = Index Of Array Value(Mapped Array(Global.entities, Current Array Element[1]), 5);

Paralleled arrays are excellent at finding struct values from a specific property. However, unparalleled arrays are better at comparing the value of an entire struct.

# Find the index of an entity with the name Ana and an identifier of 5.
Number index = entities.IndexOf({ Name: 'Ana', Identifier: 5 });
-- 🔧 Workshop code (Parallel ⏸️)
--    ❗ Expensive to do comparisons on separated data.
Global.index = First Of(Append To Array(Filtered Array(Mapped Array(Global.entities_Name, Current Array Index), Global.entities_Name[Current Array Index] == Custom String("Ana") && Global.entities_Identifier[Current Array Index] == 5), -1));

-- 🔧 Workshop code (Unparalleled 📦)
--    ⭐ Unparalleled data is kept together, comparing entire structs is super fast!
Global.index = Index Of Array Value(Global.entities, Array(Custom String("Ana"), 5));

Updating a field in the entire array

If we use a Map to update fields in a parallel array, the unmodified fields will be ignored.

struct Entity {
    public Number UniqueId;
    public Any Effect;
    public Vector Position;
    public Vector Direction;
    public Number LastUpdated;
}
Entity[] entities = [];

# Update Position and LastUpdated, do not touch any other values.
entities = entities.Map(e => {
    Position: e.Position + e.Direction,
    LastUpdated: TotalTimeElapsed(),
    ..e
});
-- 🔧 Workshop code (Parallel ⏸️)
--    📗 Only the modified fields are touched, ostw won't add any unneeded modifications.
Global.entities_Position = Mapped Array(Global.entities_Position, Current Array Element + Global.entities_Direction[Current Array Index]);
Global.entities_LastUpdated = Mapped Array(Global.entities_Position, Total Time Elapsed);

-- 🔧 Workshop code (Unparalleled 📦)
--    📗 The struct will need to be reconstructed.
Global.entities = Mapped Array(Global.entities, Array(First Of(Current Array Element), Current Array Element[1], Current Array Element[2] + Current Array Element[3], Current Array Element[3], Total Time Elapsed));

Filtering and sorting struct arrays

Since unparalleled structs keeps the data together, it is much faster to use Filter and Sort operations on unparalleled structs.

Restrictions on parallel struct arrays

A few workshop array functions are not compatible with struct arrays. This includes Random, Randomize, and Remove (ModRemoveByIndex is still available).

// This is how you get a random value in a parallel array.
Number randomEntityIndex = RandomInteger(0, entities.Length - 1);
Entity randomEntity = entities[randomEntityIndex];

The single type argument constraint

Values with anonymous types have some restrictions because it is unknown if these values will be parallel. These restrictions includes assigning the value to Any or using the array operations described in the previous section. We add the single constraint to type parameters to make it illegal to apply parallel types to it, which allows us to bypass the restrictions.

void function<single T>(in T[] array)
{
    // Removing the 'single' constraint from the T type argument will cause errors on both of the following lines.
	Any any = array;
    T random = array.Random();
}

Useful design patterns

Object references with structs

An alternative to classes is using a global struct array. This will avoid the extra overhead that classes require for object management.

One issue is how to keep references to structs inside the array. Saving an index to the value can be volatile. Items can be removed or inserted into the array which will shift what the index should be pointing to. An alternative way is to give each value a unique number identifier, then using IndexOf as described in Searching a struct array for a property to retrieve the object. This is a good way to implement something similar to class references without the overhead of object management.

Example implementation:

struct WorldObject {
    public Vector Position;
    public Number Health;
    public Number Speed;
    public Vector Direction;
    public Color Color;
    public Number Id;
}

# The objects in the world.
globalvar WorldObject[] Objects = [];

# Gets the current index of a World Object. The caller should not save
# the value obtained here for future use, since it may point to something
# else when the `Objects` variable is modified.
# Example usage:
# ```
# Objects[ObjectIndexFromId(id)].Color = Color.Blue;
# ```
Number ObjectIndexFromId(Number id): Objects.Map(o => o.Id).IndexOf(id);

This strategy is used extensively in the Pathmap-Editor as a replacement for classes.

Automatic workshop entities

Creating and destroying entities like effects or texts on the fly is heavy on the server load. Extending on the global struct array from the previous section, if we want entities tied to our structs it is often a better idea to pre-spawn the entities. Instead of creating an effect when a new object is added then storing the entity in the struct, creating the entities beforehand will use less server load and is more responsive.

rule: 'Generate object entities' {
    for (Number i = 0; i < 50; i++) {
        Number ie: EvaluateOnce(i);

        CreateEffect(
            Type: Effect.Sphere
            // Only show the effect if the object index this was created for
            // exists.
            VisibleTo: ie < Objects.Length ? AllPlayers() : null,
            Color: Objects[ie].Color,
            Position: Objects[ie].Position,
            Radius: 0.5,
            Reevaluation: EffectRev.VisibleToPositionRadiusAndColor;
        );
    }
}

Combining parallel and single structs

We aren't restricted to using only parallel or single values in a data set. By combining them we can use the benefits of both struct types. From the WorldObject example earlier, using a FilteredArray to find objects of a specific color would be expensive with the current setup. Here is how that can be changed so we can keep identifier lookups while also allowing cheaper filtered arrays:

struct WorldObject {
    public Number Id;
    public ObjectInfo Inner; 
}
single struct ObjectInfo {
    public Vector Position;
    public Number Health;
    public Number Speed;
    public Vector Direction;
    public Color Color;
}

# The objects in the world.
globalvar WorldObject[] Objects = [];

# Gets the current index of a World Object. The caller should not save
# the value obtained here for future use, since it may point to something
# else when the `Objects` variable is modified.
# Example usage:
# ```
# Objects[ObjectIndexFromId(id)].Color = Color.Blue;
# ```
Number ObjectIndexFromId(Number id): Objects.Map(o => o.Id).IndexOf(id);

# Gets all of the red objects.
ObjectInfo[] GetRedObjects(): Objects.Map(o => o.Inner).FilteredArray(o => o.Color == Color.Red);
-- 🔧 GetRedObjects()'s Workshop code
Filtered Array(Global.Objects_Inner, Current Array Element[4] == Color(Red));

Making parallel values unparalleled

It can be helpful to make a struct be parallel or single depending on the situation. Parallel structs are made unparalleled when nested in a single struct. Rather than making duplicate structs with and without the single attribute, you can use a helper struct like the one below to wrap your parallel values.

# Wraps paralleled items so they can be used as a single value.
single struct Single<T> {
    public T Value;

    # Converts a parallel array into an unparalleled array.
    public static Single<T>[] New(in T[] parallelArray) {
        return parallelArray.Map(v => New(v));
    }

    # Converts a parallel value into an unparalleled value.
    public static Single<T> New(in T value) {
        return {
            Value: value
        };
    }
}

Note about recursive references

Unlike classes, structs cannot have recursive references because it would create an infinite loop.

struct A
{
    // illegal
    public A one;

    // illegal, allowed in programming languages such as c#
    // but not here since workshop/ostw arrays are value types rather than reference types.
    public A[] two;

    // illegal, 'B' has a value of the 'A' type.
    public B three;

    // legal, since 'C' is a reference type (class) it can have a value of type 'A' and not create an infinite loop.
    public C four;
}

// A struct with an A
struct B
{
    public A a;
}

// A class with an A
class C
{
    public A a;
}

Having a struct within that same struct using anonymous types is allowed. Because the nesting is explicitly created, this will not cause an infinite loop.

struct A<T>
{
    public T value;
}

A<A<Number>> doubleA = { value: { value: 5 } };