Let structs and lists nest to any depth (#239)
game-maker / gate (push) Successful in 3m52s
game-maker / suite (push) Successful in 10m24s

Structs and lists were capped at two fixed levels (list of structs,
struct with a list field). Anything deeper was dropped silently on
load, in node settings and when written to a variable. The cap came
from an early design choice, not from a technical need, so it is
lifted completely instead of adding a third fixed level.

- One kind list NESTED_KINDS replaces ListItemKind and
  StructFieldKind. The flat special cases in listItems.ts are gone,
  every value goes through one recursive path.
- A struct inside a struct is declared, like a top-level struct.
  Values coming in over an edge are brought into that shape. Struct
  items in lists still take their fields from the value.
- New valuePath.ts addresses any nested spot by keys and indices.
  ListSlot.field is now ListSlot.path.
- Field commands (add, rename, remove) take the path to the struct.
  They change declaration, own value and all overrides in one undo
  step, as before on the top level.
- Inspector: "+ Field" offers Struct and List on every level. A
  nested struct is a header row with its own "+ Field", its fields
  are indented one step further.
- List tab: a list item shows "n items" and opens one level deeper.
  The title shows the path, e.g. "Listy · nums · [3]".
- Pack in Pack now writes a struct into a struct field instead of
  text. Objects are kept at any depth and stripped at every exit.
- A list edit in a struct field without an existing object override
  now starts from the shown value instead of an empty struct.
- Old projects load unchanged. Save, load and save again gives the
  same result.

Tests:
- ./check.sh: 172 files, 2498 tests, CHECK EXIT 0
- e2e 21 + 22: ALL PASSED, 90 checks (new 21d-iii, 22f-ii,
  21d and 22f updated)
- 20 new unit tests in nested-values.test.ts and
  nested-list-slot.test.ts, three tests that pinned the old cap
  turned around
- 6 counter-probes, each fails 1 to 8 tests, originals restored
  and checked with cmp
- Not covered: a nested struct inside a struct item in the list
  tab has no e2e click

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-26 22:33:20 +02:00
1 parent 483c34bb5c
commit 0c5ac32b59
29 files changed
+1046 -437

No files matched your search

+17 -3
View File
@@ -1,6 +1,7 @@
import { itemLike } from '../../../properties/listItems';
import type { PropertyValue } from '../../../model/Properties';
import { itemKindOf, itemLike, makeItem } from '../../../properties/listItems';
import type { NodeDef } from '../../NodeRegistry';
import { asList, asNumber, asValueItem, isEntity } from '../../values';
import { asList, asNumber, asValueItem, isEntity, type ValueList } from '../../values';
import { any, int, list } from '../shared';
export const SetItem: NodeDef = {
@@ -20,8 +21,21 @@ export const SetItem: NodeDef = {
const value = asValueItem(inputs.value);
const slot = items[i];
// An object has no kind to keep and none to take on, so it is written as it is (#157).
next[i] = isEntity(value) || isEntity(slot) ? value : itemLike(slot, value);
if (isEntity(value) || isEntity(slot)) next[i] = value;
else next[i] = nestedKind(slot) ? sameNested(slot, value) : itemLike(slot, value);
return { outputs: { result: next } };
},
params: [],
};
/** Whether a slot holds a list or a struct, which may carry objects `itemLike` would not keep. */
const nestedKind = (slot: ValueList[number] | undefined): boolean => {
const kind = itemKindOf(slot as PropertyValue);
return kind === 'list' || kind === 'struct';
};
/** A list or struct slot takes a value of its own kind as it is, anything else as its empty one. */
function sameNested(slot: ValueList[number] | undefined, value: ValueList[number]): ValueList[number] {
const kind = itemKindOf(slot as PropertyValue);
return itemKindOf(value as PropertyValue) === kind ? value : makeItem(kind, undefined);
}