From 19fa428d461afe93c8064870190786cd9a27058b Mon Sep 17 00:00:00 2001 From: meleki Date: Sat, 26 Sep 2026 22:33:26 +0200 Subject: [PATCH] Let structs and lists nest to any depth (#239) --- Behaviors.md | 36 +++++++++++++++++++++--------------- Development.md | 2 +- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Behaviors.md b/Behaviors.md index df1e31f..7350ee4 100644 --- a/Behaviors.md +++ b/Behaviors.md @@ -39,7 +39,7 @@ playing use the same two words. type as a setting row (*Type: Sprite*, plus *Behavior: Game* under that type). Textures and scenes are no type here: they are not objects in a running game. A variable is a **number, a text, a yes/no, a colour, a texture, a state machine, an asset, an object, a list or a struct**; a list declares **nothing** about its items — - each one is a number, a text, a yes/no, a colour, a texture, a state machine, an asset, an object or a struct and says so itself, so one list may + each one is a number, a text, a yes/no, a colour, a texture, a state machine, an asset, an object, a list or a struct and says so itself, so one list may hold several kinds at once. A list is compared by its items, never by identity — otherwise every reload would look like an override. **Objects** can be held wherever a value can (#238): an **object variable**, an **object field** of a @@ -61,10 +61,12 @@ playing use the same two words. of type *Behavior* reads and writes another behavior's object variable. A list property is shown as a button saying how many items it holds; clicking it opens the **List** tab, which gives each item a row with the same widget the - inspector would use for that kind — a struct item is its fields as rows under each other. The inspector stays visible beside the tab, so what the list + inspector would use for that kind — a struct item is its fields as rows under each other, a list item is a count button again. The inspector stays visible beside the tab, so what the list belongs to is still on screen. Which list the tab shows is a `ListSlot` (asset property, object - override or node setting, plus the `field` of a struct that holds one) on the editor state, - because no tab may reach into the `Editor` to switch itself on. An **unconnected `list` input** + override or node setting, plus a `path` of struct keys and list indices when the list sits inside + the value, #239) on the editor state, because no tab may reach into the `Editor` to switch itself + on. The tab title spells that path out, for example `Enemies · [3] · drops`, and clicking the + count of a list item or of a list field opens the tab one level further down. An **unconnected `list` input** of a node is the same button and the same tab: its inline value lives in the very same params bag a setting does, and the slot stops resolving the moment an edge feeds the port. What an item holds is **its own** and is chosen there: the **kind mark** in front of a row (`#` for a number, `Aa` for a text, a @@ -74,7 +76,8 @@ playing use the same two words. value, every such change is an ordinary **value edit**: one command, one undo step, and no neighbouring item touched. A **struct item** carries its own shape the same way: its fields are renamed, retyped, added (`+ Field ▾`) and removed on its own rows, which grows that one item and - not the list. Three kinds need saying, because JSON cannot tell them from the value beside them: a + not the list. A field of a struct item may be a struct again, drawn below it one indent step + further in with a `+ Field ▾` of its own, or a list, which is a count button. Three kinds need saying, because JSON cannot tell them from the value beside them: a **colour** is a number, and a **texture** or a **state machine** is an id, which is a text. Such a *tagged* kind wears its name as the one key of a small object — `{"$color": 16711680}`, `{"$texture": "walk-up"}` (the picker a sprite's `texture` property has — a whole picture or one frame of a sheet), @@ -99,7 +102,10 @@ playing use the same two words. each other right in the inspector. Its head row renames it and adds fields (`+ Field`), each field row below carries its own name, its value and a remove — the port editor `Pack` already has, one level down and with a value column — and a field may itself be a **list**, which is - the one count button that opens the tab one level deeper. Adding, renaming or removing a field reaches the + the one count button that opens the tab one level deeper, or a **struct** (#239), which declares + its own fields: it is a head row with its own `+ Field`, and its fields sit below it one indent + step further in, at any depth. What arrives in such a field is brought into that declared shape, + like the struct variable itself. Adding, renaming or removing a field reaches the declaration, the behavior's own value **and every instance override** in one command, so no stored value keeps a shape its declaration no longer promises. A placed instance draws every variable, whatever its kind, with the very same rows (`buildVariable`), minus the renaming, the @@ -200,15 +206,15 @@ playing use the same two words. have yet as one undo step and leaves the ones it has alone. Writing a struct variable keeps the fields it declares: what arrives fills the ones it names, the rest stay put (`mergeStruct`), so one node can set part of a bundle without erasing it — "the same piece, one row lower" is a - `Pack` of that one field. Depth is bounded by the *value*, not by convention: a list item is a primitive, a - tagged colour, texture or state machine, or a struct of flat ones (`listItems.ts`), a struct field says what it holds - (`StructFieldKind`, primitives plus `list`), and the list a *field* holds takes flat items only. - Forbidding just that one combination is what keeps the shapes finite: - `list → struct → primitive` and `struct → list → primitive` are the two deepest a stored value - goes — exactly what `asStoredValue` lets through an untyped slot — so the deep compare and the - editor stay finite. The List tab **offers only the kinds its slot can hold** (`ListAccess.kinds`): - on a list of its own all six, on a list a struct field holds the five flat ones. Offering the - struct there would add an item the next write drops — which reads as the menu entry doing nothing. A live entity is + `Pack` of that one field. **Nothing bounds the depth** (#239): a list item and a struct field may + each be any kind, a list or a struct included (`NestedKind` in `kinds.ts`), and nothing deeper is + dropped on load, in a node setting or on the way through a graph. There is no depth limit either. + A struct inside a struct field is **declared** (`fields` on the field, like a struct variable), + while a list still declares nothing about its items, so a struct item in a list takes its shape + from its value at any depth. Named struct types that several variables share are a topic of their + own. `Pack`, `Expand` and `Item at` take nested data apart **one level at a time**: a struct in a + struct is two `Expand` nodes in a row, and there is no path syntax like `a.b[2].c`. The List tab + offers every kind, on a list of its own and on one a struct field holds alike. A live entity is an object on a data edge too, so it carries a `Symbol` brand (`ENTITY_HANDLE`) that no struct field name can impersonate; nothing may hand a property value to `String()` (`textOf` instead), or a struct would read as `[object Object]`. A struct port has no inline value (its parts come diff --git a/Development.md b/Development.md index f1b0a86..35d3d2a 100644 --- a/Development.md +++ b/Development.md @@ -15,7 +15,7 @@ npm run build # writes dist/ and regenerates index.html php tools/make-user.php [admin|user] # an account; every tool signs in (see Accounts) php tests/php/accounts.test.php # the API's account rules, no server needed (CI's gate job) php tests/php/snapshots.test.php # the snapshot store (#231), the same way -node tools/seed-e2e.mjs && node tools/e2e.mjs # 1407 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1416 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