5
Structs
meleki edited this page 2026-10-03 21:45:43 +02:00

A struct asset is a named shape for structs. Instead of typing the same fields into every struct variable that holds, say, a piece of loot, you declare Loot once — Title, Amount, Rare with their defaults — and let the variables, struct fields and Pack/Expand nodes that hold loot follow it. Change Loot, and all of them change with it.

A struct that declares its own fields, as every struct did before, still works exactly as it did. A type is something you give a struct, not something it needs.

Making one

+ → Struct creates the asset; it stands in the assets panel under Structs. Its inspector has a FIELDS section that works like the VARIABLES section of a behavior or a Variables asset:

  • + Field adds a field of any kind — a number, a text, a yes/no, a colour, a texture, an asset, an object, a list or a struct.
  • The name field renames it, the remove button at the end of the row removes it.
  • The value next to it is the field's default: what a struct of this type starts with.
  • Dragging the mark in front of a field moves it to another place (#310). Every place of the type follows the new order: its variables and struct fields, the ports of Pack/Expand and the fields of a split output.
  • Open shows the fields in a tab of their own, as it does for variables.

A field may itself be a struct with a type. What it may not be is its own type, directly or through another type: Loot cannot hold a Loot, and if Chest holds a Loot, Loot cannot hold a Chest. Such a value would never end. The type picker does not offer those types. A list field is fine — it starts empty.

Giving a struct a type

Every struct variable, and every struct field inside one, has a Type row under its head. It reads Own fields while the struct declares its fields itself. Its menu offers:

  • Own fields — the struct keeps the fields it has now and declares them itself again.
  • every struct asset — the struct takes on that asset's fields. What its value already holds under the same name stays, if it fits the field's kind. Every other field starts at the default the asset declares. A field the asset does not have goes.
  • Make struct type — turns the fields the struct holds by itself into a new struct asset, named after the variable. The value it holds now becomes the asset's defaults, so nothing reads differently afterwards. This is the way to turn an existing struct into a type.

With a type, the fields are no longer edited at the variable: there is no + Field, and the field names cannot be changed or removed there. The values stay per variable, and a placed object may still override them, as with any variable.

Pack and Expand

Pack and Expand have a Type setting. With a type, their ports are the asset's fields — one input per field on Pack, one output per field on Expand — and they follow the asset from then on. The FIELDS section of such a node lists them without letting you change them. Without a type, the fields are declared on the node by hand as before (+ Field, From variable).

Split outputs

Reading one field of a struct does not need an Expand of its own: right-click the pin of an output that can carry a struct and split it. The output is replaced, where it stood, by one output per field of a struct asset — item becomes item.Name, item.Amount — each typed as its field.

  • Split into Loot fields is offered when the graph already knows the type: a struct variable with a type, the value of a typed Pack, a field of a typed Expand or of another split, and the item of For each, Find, Match and Item at over a typed list.
  • Split as … lists every other struct asset, for outputs whose type the graph does not know, such as an input of a behavior.
  • A right-click on one of the field pins offers Recombine, which puts the whole output back.

Splitting drops the edges on the whole output, recombining the edges on the field pins; one undo brings them back. The field pins follow the asset like a typed Expand: a new field gets a pin, a removed field loses its pin and edge, a renamed one keeps its edge. After the asset is deleted, the split keeps its last fields. Renaming the variable behind a split output takes the split along.

At run time the node still produces the whole value; a field pin reads that field out of it, the way Expand does — a field the value lacks reads as empty.

When the asset changes

Every change to a struct asset's fields reaches every place of that type at once, and one undo takes all of it back:

Change on the asset What happens at each place
Field added Every value gets it, at the field's default. Pack/Expand get a port.
Field removed It leaves every value. The port goes, and with it the edge on it and the value typed into it.
Field renamed Every value keeps its value under the new name, overrides included. The port is renamed and keeps its edge.
Default changed Nothing: a default is what a new struct of the type starts with.

This also works through several levels: if Chest holds a Loot and a variable holds a Chest, a field renamed on Loot is renamed inside that variable's value too.

Typed lists

A list says nothing about its items by default: each item carries its own kind, and a struct item its own fields. Such a list is mixed. A list can also be typed: every item is then one struct asset.

Every list variable, and every list field inside a struct, has an Items row under its head. It reads Mixed until you pick a type. Its menu offers:

  • Mixed — the list declares nothing again. Its items keep their type each (see below), so nothing is lost.
  • every struct asset — every item takes on that asset's fields, keeping what it holds under the same name and taking the default for the rest, exactly like a struct given a type. A list that holds anything but structs (a number, a text, a list) cannot take a type: the edit is refused and the console says how many items are in the way. Placed objects count too — an override holding a number refuses the type as well.
  • Make struct type — makes a new struct asset out of the list's items and types the list with it. The asset gets every field any item has, at the value of the first item that has it, so no item loses a field.

In a typed list:

  • + Item adds an item of the type at its defaults straight away. There is no kind to pick.
  • The items' fields are the type's: their names cannot be changed, and there is no + Field and no remove button on a field. The values are edited as before.
  • A list field of the type is typed through it: if Bag has a list inside of Loot, the inside of every Bag item is a typed list of Loot too.

A type for one item

In a mixed list, every struct item has a Type row of its own, as does a struct field inside such an item. Pick a struct asset there, and that one item takes on its fields and follows it, while the other items stay as they are. Own fields turns it back.

A typed List node

The List node, a constant list, has a Type setting too. Pick a struct asset there, and its items take on the type exactly as a typed list variable's do: what they hold under the same name stays, every other field starts at the default, + Item adds one of the type, and the field names are fixed. A list holding anything but structs refuses the type, with a line in the console. None makes it a mixed list again: the items keep their values but no longer follow the asset, since a node's list has no Type row per item. A For each over a typed List offers Split into Loot fields for its item, like one over a typed list variable.

When the asset changes

Typed items follow their asset like every other place of that type — the table above applies to them too: a field added, removed or renamed reaches every typed item, in the list's own value, in the overrides of placed objects and in lists nested inside other items, in one undo step.

Deleting a struct asset

The delete dialog counts the structs, the typed lists and the nodes that use it (Pack/Expand and List with the type, and every split output). After deleting, each of them keeps the fields it last had, now as its own fields — no value is lost, only the link. A Type row reads Own fields again, an Items row Mixed.

In the running game

A struct value is the same plain bundle of fields, with or without a type: the type does not travel with the value, so Equals, Index of, saving and loading treat it as before. Edges do not check the type either — any struct fits any struct port.

The places that check are Add item and Set item with their Type setting, and every variable with a type that a graph writes:

  • Add item and Set item with a type take a value only if it is a struct with exactly the type's field names — none missing, none more. Anything else is not added or set: the list comes out as it went in, and the console names the value and the type. An item taken out of a typed list, and a Pack with the same type, always fit. Without a type (or with one that was deleted) both take anything, as before.
  • A typed list keeps, of what is written into it, only the items that fit its type by the same rule. Every other item is left out and named in the console, one line each; the rest of the list is written. This holds for a list variable, for a typed list field inside a struct variable, and for the lists inside typed items, at any depth.
  • A struct variable with a type takes a struct whose fields are all fields of the type. It may leave some out: those keep their value, as in any struct variable, so a Pack of one field still sets just that field. A struct bringing a field the type does not have, or a value that is no struct at all, is not written, and the console says so.

Writing means the Variables node, on a behavior's own variables and on a Variables asset's alike, and the Properties node setting a behavior variable of the object it is handed.

Under the hood

A typed struct keeps a copy of the type's fields in its declaration (fields), next to structType, the asset's id. Everything that reads a struct's fields therefore works unchanged. syncStructTypes in src/core/model/structTypes.ts brings every copy, every value kept for it and every typed Pack/Expand back in line; it runs after each change to a struct asset and when a project is loaded, so a hand-edited file heals too. Its writes go through a Recorder (src/core/util/recorder.ts), which is what makes an edit of unknown reach one undo step. A struct asset declares its fields as variables, so the variable commands and the inspector's variable rows edit them unchanged.

A list declares nothing about its items, so an item carries its type itself: a typed list names the asset in itemType, and every struct item in it holds the asset's id under the key $struct. A struct item with a type of its own in a mixed list holds the same key. No field can have that name, since field names never start with $. The same sync brings every marked item into shape, wherever it sits. A run never sees the key: it is taken out where values enter the game (withoutItemTypes in src/core/model/Properties.ts), which is why Add item and Set item can compare field names exactly. A typed List node keeps the marks in its setting, and its output drops them the same way.

What a written variable is held to comes from its declaration, not from the value. When a Variables or Properties node is resolved, GraphLookup.variableTyping hands each port that writes a variable its item type, its struct type and the typed fields inside it (typingOf in structTypes.ts); writeDeclared in src/core/graph/typedValues.ts holds the written value to that. A run resolves each node once, so this costs nothing per frame beyond the check itself.

A typed item keeps a field's value as plain JSON, the way a declared struct does: an asset id or a texture id is a text, a color a number. What kind a field is comes from the type, so the item rows draw each field as the type declares it — an asset field with its asset menu, not as the text it holds.