Explain typed writes at runtime and the typed List node (#271)

- Structs: new section A typed List node, In the running game split
  into lists and struct variables, and Under the hood for the check.
- Architecture: typedValues and what lookup tells a port.
- Development: e2e total 1606.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adminandClaude Opus 5.5 committed 2026-09-30 22:08:33 +02:00
1 parent 0b54e14387
commit 5952fa0bd0
3 files changed
+41 -9

No files matched your search

+2 -1
@@ -89,7 +89,8 @@ src/core/graph/ graph model, NodeRegistry/NodeContext/values (a travellin
portTypes (#207: ports of one node that carry the same value, and the type their inputs agree on),
savedValues (#166: the SaveArea a run reads persistent Variables assets from and writes them to, and restoring a save through today's declaration),
stateMachine (the State node, a machine's states, the ports they become), variablePorts, propertyPorts,
lookup (what a node may look up: behaviors, state machines), keyRepeat; nodes/{events,flow,data,action,variable}/<Name>.ts
typedValues (#271: what fits a struct type, and a written variable held to its declaration),
lookup (what a node may look up: behaviors, state machines, struct types, what a variable declares), keyRepeat; nodes/{events,flow,data,action,variable}/<Name>.ts
src/core/particles/ nodes (the catalogue), compile (a graph → one function per output),
emission (which particles are alive at a second — a pure rule, no state)
src/core/store/ serialize (untrusted JSON → Project), projectPlugins (which plugins a project switches on, read off the raw
+1 -1
@@ -18,7 +18,7 @@ php tests/php/accounts.test.php # the API's accoun
# has and the web server's container has not (SKIP there)
php tests/php/snapshots.test.php # the snapshot store (#231), the same way
php tests/php/releases.test.php # the release store (#266), the same way
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1603 editor checks, screenshots in /tmp/gm-e2e-*.png
node tools/seed-e2e.mjs && node tools/e2e.mjs # 1606 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result
+38 -7
@@ -116,6 +116,16 @@ In a **mixed** list, every struct item has a **Type** row of its own, as does a
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
@@ -125,7 +135,7 @@ the overrides of placed objects and in lists nested inside other items, in one u
## Deleting a struct asset
The delete dialog counts the structs, the typed lists and the nodes that use it (`Pack`/`Expand`
with the type, and every split output).
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**.
@@ -135,11 +145,25 @@ A struct value is the same plain bundle of fields, with or without a type: the t
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**, each with its **Type** setting. With a
type they 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.
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
@@ -158,7 +182,14 @@ struct item with a type of its own in a mixed list holds the same key. No field
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.
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