Check the declared type when a graph writes a typed variable (#271)
A list variable with an item type only kept its shape in the editor. A graph could write any list into it at runtime, and the List node had no type at all. Now the declaration decides what a write may store. - Writing a list into a typed list variable leaves out every item that does not have exactly the fields of the type. The rest is written and the console names each item that was left out, like Add item does. - The same rule holds for typed list fields inside a struct variable and for lists inside typed items, at any depth. - A typed struct variable refuses a struct with a field the type does not have, and a value that is no struct. A struct with missing fields still merges in as before, so a partial Pack keeps working. - The check runs in the Variables node (own variables and Variables assets) and in the Properties node when it sets a behavior variable of another object. - Ports learn their declaration through GraphLookup: what a variable declares about its type, and which behavior owns a graph. The rule itself lives in the new src/core/graph/typedValues.ts, fitsType moved there from SetItem. - The List node gets a Type setting. With a type its items are shaped and marked, follow the struct asset, count when the type is deleted and offer Split into fields in For each. The mark never reaches the running game. - A type on a List node holding something other than structs is refused with a console line. - Setting the List node back to None removes the marks, unlike a list variable. A node list has no per-item type row, so marked items could never be freed there. Undo brings everything back. - tests/core/kinds.test.ts looked for the value as the first parameter of a literal. Type now comes first on the List node, so the test finds the value by its key. Tests: - Gate: 198 files, 2869 tests (2856 + 13), CHECK EXIT 0 - e2e 55: 19 checks (+55j write at runtime, +2 for 55k List node) - e2e 54: 16 checks, smokes 24 / 19 / 21 - Five mutations, each fails between 1 and 4 tests - e2e total 1606 is computed (1603 + 3), no full run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2282a81fb
commit
0f6e628616
19 files changed
+682
-52
No files matched your search
@@ -6,6 +6,8 @@
|
||||
* `Add item` with a type takes an item of that list and a Pack of it, and turns a number away. A
|
||||
* typed item's asset field is drawn as the asset picker its type declares, not as the text it holds.
|
||||
* A shared typed list keeps what a run added to it when the document changes during the run.
|
||||
* Writing a typed list variable leaves out what is no item of the type (#271), and a List node takes
|
||||
* a type in its settings like a list variable does.
|
||||
*
|
||||
* Everything it adds carries a `tl55` id or is an asset it made, and goes again at the end.
|
||||
*/
|
||||
@@ -306,6 +308,100 @@ export async function typedLists(t) {
|
||||
JSON.stringify(logged),
|
||||
);
|
||||
|
||||
// 55j. Writing a typed list variable (#271): a mixed List node hands a Loot, a 9 and a struct of
|
||||
// other fields to `kept`, typed Loot 55 – only the Loot stays, the other two are reported.
|
||||
const writerGraph = {
|
||||
nodes: [
|
||||
node('tl55-w-start', 'OnStart', -400, 0),
|
||||
node('tl55-w-list', 'List', -400, 160, { value: [{ Name: 'Gem', Amount: 4 }, 9, { Name: 'X' }] }),
|
||||
node('tl55-w-set', 'Variables', -200, 0, {}, [{ key: 'kept', kind: 'list', write: true }]),
|
||||
node('tl55-w-get', 'Variables', -200, 160, {}, [{ key: 'kept', kind: 'list' }]),
|
||||
node('tl55-w-count', 'Count', 0, 160),
|
||||
node('tl55-w-log', 'Log', 200, 0),
|
||||
node('tl55-w-typed', 'List', -400, 320, { value: [{ Name: 'Gem' }] }),
|
||||
],
|
||||
edges: [
|
||||
edge('tl55-w-e1', 'tl55-w-start', 'exec', 'tl55-w-set', 'exec'),
|
||||
edge('tl55-w-e2', 'tl55-w-list', 'value', 'tl55-w-set', 'kept'),
|
||||
edge('tl55-w-e3', 'tl55-w-set', 'exec', 'tl55-w-log', 'exec'),
|
||||
edge('tl55-w-e4', 'tl55-w-get', 'kept', 'tl55-w-count', 'list'),
|
||||
edge('tl55-w-e5', 'tl55-w-count', 'count', 'tl55-w-log', 'message'),
|
||||
],
|
||||
};
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
const scene = __gm.state.activeScene;
|
||||
p.assets.push({
|
||||
id: 'tl55-writer',
|
||||
type: 'behavior',
|
||||
name: 'Writer 55',
|
||||
props: { enabled: true, runInEditor: false },
|
||||
variables: [{ key: 'kept', label: 'kept', kind: 'list', itemType: '${LOOT}', default: [] }],
|
||||
graph: ${JSON.stringify(writerGraph)},
|
||||
});
|
||||
${MAKE_OBJECT}
|
||||
scene.objects.push(make('tl55-writer-obj', 'tl55-writer', '', 480, 'Writer 55'));
|
||||
`);
|
||||
const writeFrom = t.consoleLog.length;
|
||||
await t.play();
|
||||
await sleep(600);
|
||||
await stop();
|
||||
const written = t.consoleLog
|
||||
.slice(writeFrom)
|
||||
.filter((line) => line.startsWith('[behavior Writer 55]'))
|
||||
.map((line) => line.split(']').pop().trim());
|
||||
check(
|
||||
'a typed list variable keeps the Loot a graph writes into it and reports the 9 and the stranger',
|
||||
JSON.stringify(written) ===
|
||||
JSON.stringify([
|
||||
'Set kept: 9 is no Loot 55 – left out',
|
||||
'Set kept: Name: X is no Loot 55 – left out',
|
||||
'1',
|
||||
]),
|
||||
JSON.stringify(written),
|
||||
);
|
||||
|
||||
// 55k. A List node picks Loot 55 in its Type setting: its items become Loots, marked, with fixed
|
||||
// fields, and + Item adds one at the defaults. The mixed List with a 9 in it refuses.
|
||||
const listNode = async (id) =>
|
||||
(await asset('tl55-writer')).graph.nodes.find((n) => n.id === id)?.params ?? null;
|
||||
await evaluate('__gm.console.clear()');
|
||||
await select({ kind: 'node', assetId: 'tl55-writer', nodeId: 'tl55-w-list' });
|
||||
await sleep(150);
|
||||
await press_(await json("__gm.inspectorField('Type')"), 'Type of the mixed List');
|
||||
await pick('Loot 55 (struct)');
|
||||
await sleep(300);
|
||||
const nodeLines = ((await json('__gm.console.lines()')) ?? []).map((l) => l.text);
|
||||
check(
|
||||
'a List node holding a 9 refuses the type, in the console, and stays as it was',
|
||||
nodeLines.includes('The list holds 1 item that is no struct – only a list of structs can take a type') &&
|
||||
!(await listNode('tl55-w-list'))?.structType,
|
||||
JSON.stringify({ nodeLines, params: await listNode('tl55-w-list') }),
|
||||
);
|
||||
await select({ kind: 'node', assetId: 'tl55-writer', nodeId: 'tl55-w-typed' });
|
||||
await sleep(150);
|
||||
await press_(await json("__gm.inspectorField('Type')"), 'Type of the struct List');
|
||||
await pick('Loot 55 (struct)');
|
||||
await sleep(200);
|
||||
const nodeHead = await json(`__gm.values.headAt('inspector', 'List · Value')`);
|
||||
await press_(nodeHead?.add, '+ Item of the List node');
|
||||
const typedNode = await listNode('tl55-w-typed');
|
||||
const nodeLocked =
|
||||
((await json("__gm.values.lists('inspector')")) ?? []).find((l) => l.title === 'List · Value')?.locked ??
|
||||
null;
|
||||
check(
|
||||
'the List node takes the type: Gem becomes a Loot, + Item adds one, both with fixed fields',
|
||||
typedNode?.structType === LOOT &&
|
||||
JSON.stringify(typedNode.value) ===
|
||||
JSON.stringify([
|
||||
{ Name: 'Gem', Amount: 1, [MARK]: LOOT },
|
||||
{ Name: 'Coin', Amount: 1, [MARK]: LOOT },
|
||||
]) &&
|
||||
JSON.stringify(nodeLocked) === '[true,true]',
|
||||
JSON.stringify({ typedNode, nodeLocked }),
|
||||
);
|
||||
await shot('05-list-node-type');
|
||||
|
||||
// 55i. A shared typed list the run has added to keeps the item when the document changes while
|
||||
// the run is on: its declaration is a fresh copy each time, which is no edit of it.
|
||||
const fillGraph = {
|
||||
@@ -371,11 +467,12 @@ export async function typedLists(t) {
|
||||
await mutate(`
|
||||
const p = __gm.state.project;
|
||||
const scene = __gm.state.activeScene;
|
||||
scene.objects = scene.objects.filter((o) => o.id !== 'tl55-obj' && o.id !== 'tl55-fill-obj');
|
||||
const placed = ['tl55-obj', 'tl55-fill-obj', 'tl55-writer-obj'];
|
||||
scene.objects = scene.objects.filter((o) => !placed.includes(o.id));
|
||||
const made = ${JSON.stringify(made?.id ?? '')};
|
||||
p.assets = p.assets.filter(
|
||||
(a) =>
|
||||
!['tl55-chest', 'tl55-save', 'tl55-filler', '${LOOT}', '${DROP}', made].includes(a.id),
|
||||
!['tl55-chest', 'tl55-save', 'tl55-filler', 'tl55-writer', '${LOOT}', '${DROP}', made].includes(a.id),
|
||||
);
|
||||
`);
|
||||
}
|
||||
Reference in new issue
Block a user