Show the list a running game fills in the list tab (#251)
game-maker / gate (push) Successful in 3m23s
game-maker / suite (push) Successful in 10m16s

While playing, the inspector said "2 items" for a list a graph had
filled, but clicking it opened an empty list tab. The inspector reads a
Variables asset from the running game, the list tab only read the list
stored in the project. It did not follow the run's ticks either.

- resolveList takes the live values. For a Variables asset it uses the
  running game's value while one runs, the project's otherwise. This is
  the inspector's rule.
- ListEditor refreshes on runtime ticks about 10 times a second, and
  not while a field in the tab is being typed in, like the inspector.
- Editor hands the list editor the inspector's liveVariables.
- __gm.list.info() also returns rows, the rows the last build drew.
- Editing in the tab while playing goes into the run's copy of the
  project, as in the inspector. A very long, growing list rebuilds all
  rows up to 10 times a second.

Tests:
- ./check.sh: 180 files, 2634 tests, CHECK EXIT 0
- e2e GM_STEPS=1,32: ALL PASSED, 30 checks
- e2e 32b-ii, 2 checks: a graph adds an item every frame, the test
  pauses and clicks "N items" with a real mouse. The tab draws 26 rows
  for 26 items, grows to 50 after resuming and is empty after Stop.
- Counter-checks: without live values the tab draws 0 rows for 23
  items, and without the refresh it stays at 23
- README: 1481 editor checks, counted by hand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-27 21:07:14 +02:00
1 parent b90b62886b
commit 92fad6b9e3
5 files changed
+126 -9

No files matched your search

+81
View File
@@ -27,6 +27,21 @@ const counterGraph = {
],
};
/** `On update → Variables{set bag} ← Join lists(bag, 7)`: a shared list that grows every frame. */
const fillerGraph = {
nodes: [
node('sc-f-update', 'OnUpdate', 0, 0),
node('sc-f-read', 'Variables', 0, 200, { variablesAsset: 'sc-bag' }, [port('bag', 'list')]),
node('sc-f-join', 'Concat', 260, 200, { b: [7] }),
node('sc-f-write', 'Variables', 560, 0, { variablesAsset: 'sc-bag' }, [written('bag', 'list')]),
],
edges: [
edge('sc-f-e1', 'sc-f-update', 'exec', 'sc-f-write', 'exec'),
edge('sc-f-e2', 'sc-f-read', 'bag', 'sc-f-join', 'a'),
edge('sc-f-e3', 'sc-f-join', 'result', 'sc-f-write', 'bag'),
],
};
export async function scope(t) {
const { check, sleep, json, shot, click, locate, asset, mutate, stop, has } = t;
@@ -89,6 +104,72 @@ export async function scope(t) {
);
await stop();
// 32b-ii. A list the run fills: the inspector row counts the run's items, so the List tab it
// opens has to show those too and follow them – not the empty list the document declares.
await mutate(`
const p = __gm.state.project;
const scene = __gm.state.activeScene;
const mover = p.assets.find((a) => a.id === 'mover');
const f = structuredClone(mover);
Object.assign(f, {
id: 'sc-filler',
name: 'Filler',
props: { enabled: true, runInEditor: false },
variables: [],
graph: ${JSON.stringify(fillerGraph)},
});
p.assets.unshift(f, {
id: 'sc-bag',
type: 'variables',
name: 'Bag',
props: { bag: [] },
variables: ${JSON.stringify([variable('bag', 'list', [])])},
});
${MAKE_OBJECT}
scene.objects.push(make('sc-brain-f', 'sc-filler', 'sc-box-a', 0, 'Filler'));
`);
// Held, so the count the row shows stands still long enough to be found and clicked.
await t.play();
await sleep(300);
await t.clickToolbar('pause');
await sleep(150);
const held = (await json('__gm.world()')).shared.variables['sc-bag']?.bag ?? [];
await selectAsset('sc-bag');
await sleep(200);
const countRow = await t.find(`${held.length} items`);
if (countRow) await click(countRow.x, countRow.y);
await sleep(150);
const first = await json('__gm.list.info()');
await shot('01b-live-list');
await t.clickToolbar('pause');
await sleep(500);
const later = await json('__gm.list.info()');
await stop();
await sleep(200);
const stopped = await json('__gm.list.info()');
check(
'while playing, the List tab opened from a shared list shows the items the run put in',
held.length > 0 &&
Boolean(countRow) &&
(await json('__gm.tab()')) === 'list' &&
first.title === 'Bag · bag' &&
first.rows === held.length &&
first.items.every((item) => item === '7'),
JSON.stringify({ run: held.length, row: countRow, title: first.title, rows: first.rows }),
);
check(
'and follows the list as it grows, then shows the declared list again once the run stops',
later.rows > first.rows && stopped.rows === 0 && stopped.items.length === 0,
JSON.stringify({ first: first.rows, later: later.rows, stopped: stopped.rows }),
);
await mutate(`
const p = __gm.state.project;
__gm.state.activeScene.objects = __gm.state.activeScene.objects.filter((o) => o.id !== 'sc-brain-f');
p.assets = p.assets.filter((a) => a.id !== 'sc-filler' && a.id !== 'sc-bag');
__gm.state.openList(null);
__gm.state.select(null);
`);
// 32c. The asset is where the declaration lives, and it says what it is for.
await selectAsset('sc-vars');
const declared = await has('score');