Query objects in bulk and filter, count or test a list (#259)
game-maker / gate (push) Successful in 3m36s
game-maker / suite (push) Failing after 10m6s

The object queries only ever returned one object: Object by name,
Child by name and Find all stop at the first hit. Getting every coin,
every child or every item that passes a test needed a For each with
its own bookkeeping. These nodes return the whole set as a list of
Object, so For each and Find can use item straight away.

- Objects by name returns every scene object with that name in scene
  order.
- Children by name returns every match below object, nearer levels
  first, the same order as the singular node.
- Children returns the direct children, with recursive all of them
  level by level.
- Objects of asset returns every instance of an object asset or
  container by the same rule as Is asset, spawned ones included.
- All four take a snapshot. Destroyed objects are never in it,
  disabled ones are, as with the singular nodes.
- Find has a Mode setting: First, All, Any and Count. First is the
  default and hides the row, so existing graphs look the same.
- Match follows the mode of its paired Find: done with items and
  indices, done with any, or done with count. An unpaired Match shows
  First, since the pairing only comes from the wiring.
- First and Any stop at the first hit, All and Count walk the list.
- Changing the mode drops edges at outputs the Match no longer has,
  in one undo step. SetNodeParamCommand now cleans the whole graph,
  but only edges that were valid before the change.
- The palette entry "filter" adds a Find set to All with its Match.

Found on the way:

- After adding the filter entry, "find" + Enter picked Filter. The
  entry has node type Find, counted as an exact hit, and the tie went
  to the alphabet. The search now ranks by the entry id. e2e 6z-v
  caught it.

Tests:

- ./check.sh: 188 files, 2746 tests, CHECK EXIT 0.
- find-modes.test.ts with 11 tests, 11 catalog cases for the four
  nodes, the asset input guard now expects 3.
- e2e 6z-vii with 3 checks: filter from the palette, Count picked in
  the inspector drops the items edge, a run logs 2, 2, 2, 3, 4, "0, 2".
- GM_STEPS=1-6: all passed, 278 checks.
- Counter-checks fail as expected: every mode stopping early (2),
  Match ignoring the mode (6), cleanup on the own node only (1), no
  recursion (2), no inOrder (2), old ranking (1). In the browser a
  Match ignoring the mode fails both editor checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-28 15:43:03 +02:00
1 parent f84defb9b6
commit 4e6aeb5f72
22 files changed
+671 -35

No files matched your search

+158
View File
@@ -1104,6 +1104,7 @@ export async function graph(t) {
await forEachNode();
await findMatchNodes();
await splitOutputs();
await pluralLookups();
await mutate(`__gm.state.select({ kind: 'asset', assetId: 'mover' });`);
@@ -1497,4 +1498,161 @@ export async function graph(t) {
scene.objects = scene.objects.filter((o) => o.id !== 'sp-holder');
`);
}
// 6z-vii. Plural lookups and Find modes (#259): "filter" in the palette adds a Find set to All,
// whose Match hands on lists. Count picked in the inspector reshapes the Match and drops
// the edge on a list output it no longer has. A run over a small tree answers every new
// node: Objects by name, Children by name, Children (direct and recursive), Objects of
// asset, and a Filter over the children.
async function pluralLookups() {
await mutate(`
const p = __gm.state.project;
p.assets.push({
id: 'plury',
type: 'behavior',
name: 'Plury',
props: { enabled: true, runInEditor: false },
variables: [],
graph: { nodes: [], edges: [] },
});
__gm.state.select({ kind: 'asset', assetId: 'plury' });
`);
await showTab('graph');
const spot = await json('__gm.graph.emptySpot()');
await click(spot.x, spot.y, 'right');
await typeText('filter');
await press('Enter');
const [find, match] = (await asset('plury')).graph.nodes;
const outputsOf = async (id) =>
Object.keys((await json(`__gm.graph.portTypes(${JSON.stringify(id)})`))?.outputs ?? {});
const settings = find ? await json(`__gm.graph.nodeSettings(${JSON.stringify(find.id)})`) : null;
if (!find || !match) {
check(
'typing "filter" into the palette adds a Find and its Match',
false,
JSON.stringify({ find, match }),
);
return;
}
const f = JSON.stringify(find.id);
const m = JSON.stringify(match.id);
// Only a Match whose test is built from the Find follows its mode: the test is wired first.
await mutate(`
const p = __gm.state.project;
const behavior = p.assets.find((a) => a.id === 'plury');
const at = behavior.graph.nodes.find((n) => n.id === ${m});
const edge = (id, from, fromPort, to, toPort) =>
({ id, from: { node: from, port: fromPort }, to: { node: to, port: toPort } });
behavior.graph.nodes.push(
{ id: 'pl-test', type: 'Not', x: at.x - 240, y: at.y + 120, params: {} },
{ id: 'pl-count', type: 'Count', x: at.x + 260, y: at.y + 60, params: {} },
);
behavior.graph.edges.push(
edge('pl-in', ${f}, 'item', 'pl-test', 'value'),
edge('pl-out', 'pl-test', 'result', ${m}, 'match'),
edge('pl-items', ${m}, 'items', 'pl-count', 'list'),
);
`);
const listOutputs = await outputsOf(match.id);
check(
'typing "filter" into the palette adds a Find set to All; its Match hands on items and indices',
find.type === 'Find' &&
find.params.mode === 'all' &&
match.type === 'Match' &&
JSON.stringify(settings) === JSON.stringify(['Mode: All']) &&
JSON.stringify(listOutputs) === JSON.stringify(['done', 'items', 'indices']),
JSON.stringify({ find, match, settings, listOutputs }),
);
await select({ kind: 'node', assetId: 'plury', nodeId: find.id });
const modeField = await json(`__gm.inspectorField('Mode')`);
if (modeField) {
await click(modeField.x, modeField.y);
await clickText('Count');
}
const reshaped = await asset('plury');
check(
'picking Count in the inspector gives the Match done and count, and drops the edge on items',
reshaped.graph.nodes.find((n) => n.id === find.id)?.params.mode === 'count' &&
JSON.stringify(await outputsOf(match.id)) === JSON.stringify(['done', 'count']) &&
!reshaped.graph.edges.some((e) => e.id === 'pl-items'),
JSON.stringify({ modeField, edges: reshaped.graph.edges }),
);
await clickText('Fit');
await shot('26-find-modes');
await mutate(`
const p = __gm.state.project;
const behavior = p.assets.find((a) => a.id === 'plury');
const sprite = structuredClone(p.assets.find((a) => a.type === 'sprite'));
Object.assign(sprite, { id: 'pl-sprite', name: 'Plural sprite' });
p.assets.push(sprite);
const node = (id, type, params = {}) => ({ id, type, x: 0, y: 0, params });
const edge = (from, fromPort, to, toPort) =>
({ id: from + '>' + to + '.' + toPort, from: { node: from, port: fromPort }, to: { node: to, port: toPort } });
behavior.graph = { nodes: [
node('pl-start', 'OnStart'),
node('pl-root', 'ObjectByName', { name: 'Crowd 7' }),
node('pl-named', 'ObjectsByName', { name: 'Bee' }),
node('pl-below', 'ChildrenByName', { name: 'Bee' }),
node('pl-direct', 'Children', { recursive: false }),
node('pl-all', 'Children', { recursive: true }),
node('pl-kind', 'ObjectsOfAsset', { asset: 'pl-sprite' }),
node('pl-find', 'Find', { mode: 'all' }),
node('pl-name', 'NameOf'),
node('pl-bee', 'Equals', { b: 'Bee' }),
node('pl-match', 'Match'),
...[1, 2, 3, 4, 5].map((i) => node('pl-n' + i, 'Count')),
...[1, 2, 3, 4, 5, 6].map((i) => node('pl-log' + i, 'Log')),
], edges: [
edge('pl-start', 'exec', 'pl-log1', 'exec'),
edge('pl-root', 'object', 'pl-below', 'object'),
edge('pl-root', 'object', 'pl-direct', 'object'),
edge('pl-root', 'object', 'pl-all', 'object'),
edge('pl-named', 'objects', 'pl-n1', 'list'),
edge('pl-below', 'children', 'pl-n2', 'list'),
edge('pl-direct', 'children', 'pl-n3', 'list'),
edge('pl-all', 'children', 'pl-n4', 'list'),
edge('pl-kind', 'objects', 'pl-n5', 'list'),
...[1, 2, 3, 4, 5].map((i) => edge('pl-n' + i, 'count', 'pl-log' + i, 'message')),
...[1, 2, 3, 4].map((i) => edge('pl-log' + i, 'exec', 'pl-log' + (i + 1), 'exec')),
edge('pl-log5', 'exec', 'pl-find', 'exec'),
edge('pl-all', 'children', 'pl-find', 'list'),
edge('pl-find', 'item', 'pl-name', 'object'),
edge('pl-name', 'name', 'pl-bee', 'a'),
edge('pl-bee', 'result', 'pl-match', 'match'),
edge('pl-match', 'done', 'pl-log6', 'exec'),
edge('pl-match', 'indices', 'pl-log6', 'message'),
] };
const scene = __gm.state.activeScene;
const place = (id, assetId, parentId, name) => {
const o = structuredClone(scene.objects[0]);
Object.assign(o, { id, assetId, parentId, name, position: { x: 0, y: 0, z: 0 }, overrides: {}, instanceEdits: {} });
delete o.groupId;
scene.objects.push(o);
};
place('pl-holder', 'plury', '', 'Plural holder');
place('pl-crowd', 'pl-sprite', '', 'Crowd 7');
place('pl-a', 'pl-sprite', 'pl-crowd', 'Bee');
place('pl-b', 'pl-sprite', 'pl-a', 'Bee');
place('pl-c', 'pl-sprite', 'pl-crowd', 'Ant');
`);
const logFrom = t.consoleLog.length;
await t.play();
await sleep(600);
await stop();
const logged = t.consoleLog
.slice(logFrom)
.filter((line) => line.startsWith('[behavior Plural holder]'))
.map((line) => line.split(']').pop().trim());
// By name 2 · below Crowd 2 · direct children 2 · all below 3 · of the asset 4 · Bees at 0 and 2.
check(
'run over Crowd 7 › (Bee › Bee), Ant: every plural lookup and the Filter answer as planned',
JSON.stringify(logged) === JSON.stringify(['2', '2', '2', '3', '4', '0, 2']),
JSON.stringify(logged),
);
await mutate(`
const p = __gm.state.project;
p.assets = p.assets.filter((a) => a.id !== 'plury' && a.id !== 'pl-sprite');
const scene = __gm.state.activeScene;
scene.objects = scene.objects.filter((o) => !o.id.startsWith('pl-'));
`);
}
}