game-maker / suite (push) Successful in 6m48s
The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.
The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.
Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.
564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.
Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
159 lines
5.9 KiB
JavaScript
159 lines
5.9 KiB
JavaScript
import { edge, node, port, variable, written } from '../lib/builders.mjs';
|
||
import { MAKE_OBJECT } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* Shared state: a Variables asset holds one value per key for the whole run, and a state machine
|
||
* declared `global` one current state. The step checks the run (two objects counting into one
|
||
* number), that the node offers the asset's declarations, and the switch on the machine.
|
||
*
|
||
* The step brings its own boxes, behavior, Variables asset and machine; everything goes at the end.
|
||
*/
|
||
|
||
const SOURCE = { variablesAsset: 'sc-vars' };
|
||
|
||
/** `On start → Variables{set score} ← Add(score, 1)`, counting on the shared asset rather than its own. */
|
||
const counterGraph = {
|
||
nodes: [
|
||
node('sc-start', 'OnStart', 0, 0),
|
||
node('sc-read', 'Variables', 0, 200, SOURCE, [port('score', 'number')]),
|
||
node('sc-add', 'Add', 260, 200, { b: 1 }),
|
||
node('sc-write', 'Variables', 560, 0, SOURCE, [written('score', 'number')]),
|
||
],
|
||
edges: [
|
||
edge('sc-e1', 'sc-start', 'exec', 'sc-write', 'exec'),
|
||
edge('sc-e2', 'sc-read', 'score', 'sc-add', 'a'),
|
||
edge('sc-e3', 'sc-add', 'result', 'sc-write', 'score'),
|
||
],
|
||
};
|
||
|
||
export async function scope(t) {
|
||
const { check, sleep, json, shot, click, locate, asset, mutate, stop, has } = t;
|
||
|
||
const selectAsset = async (id) => {
|
||
await t.select({ kind: 'asset', assetId: id });
|
||
};
|
||
const selectNode = async (nodeId) => {
|
||
await t.select({ kind: 'node', assetId: 'sc-logic', nodeId });
|
||
};
|
||
/** The inspector is drawn over the graph, so the last match is the row rather than a port. */
|
||
const clickLast = async (text) => {
|
||
const p = await locate(text, true);
|
||
await click(p.x, p.y);
|
||
};
|
||
|
||
// 32a. A Variables asset, a behavior counting on it, and two boxes wearing that behavior.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
const mover = p.assets.find((a) => a.id === 'mover');
|
||
const a = structuredClone(mover);
|
||
Object.assign(a, {
|
||
id: 'sc-logic',
|
||
name: 'Counter',
|
||
props: { enabled: true, runInEditor: false },
|
||
variables: [],
|
||
graph: ${JSON.stringify(counterGraph)},
|
||
});
|
||
p.assets.unshift(a, {
|
||
id: 'sc-vars',
|
||
type: 'variables',
|
||
name: 'Score',
|
||
props: { score: 0 },
|
||
variables: ${JSON.stringify([variable('score', 'number', 0)])},
|
||
});
|
||
${MAKE_OBJECT}
|
||
scene.objects.push(
|
||
make('sc-box-a', 'crate', '', 1500, 'Counter box A'),
|
||
make('sc-brain-a', 'sc-logic', 'sc-box-a', 0, 'Counter A'),
|
||
make('sc-box-b', 'crate', '', 1700, 'Counter box B'),
|
||
make('sc-brain-b', 'sc-logic', 'sc-box-b', 0, 'Counter B'),
|
||
);
|
||
`);
|
||
|
||
// 32b. The run: two instances, one number. A per-instance variable would stand at 1.
|
||
await t.play();
|
||
await sleep(600);
|
||
const world = await json('__gm.world()');
|
||
const carriers = world.entities.filter((e) => e.props && 'score' in e.props).map((e) => e.name);
|
||
const shared = world.shared.variables['sc-vars'];
|
||
check(
|
||
'two behavior instances count into one shared variable',
|
||
shared && shared.score === 2,
|
||
`score = ${JSON.stringify(shared && shared.score)} after both started – 1 would mean each kept its own`,
|
||
);
|
||
check(
|
||
'and the value belongs to the run, not to either placed object',
|
||
carriers.length === 0,
|
||
`objects carrying "score": ${JSON.stringify(carriers)}`,
|
||
);
|
||
await stop();
|
||
|
||
// 32c. The asset is where the declaration lives, and it says what it is for.
|
||
await selectAsset('sc-vars');
|
||
const declared = await has('score');
|
||
const explains = await has('Every graph pointing here shares them.');
|
||
check(
|
||
'the Variables asset lists its declarations and says what they are',
|
||
declared && explains,
|
||
`row: ${declared}, note: ${explains}`,
|
||
);
|
||
await shot('01-variables-asset');
|
||
|
||
// 32d. The node picks its ports from the asset it names – the whole point of naming one.
|
||
await selectNode('sc-read');
|
||
const offersShared = await has('score');
|
||
const complains = await has('"score" is not a variable of this behavior.');
|
||
check(
|
||
'a node naming the asset offers its variables instead of the behavior’s',
|
||
offersShared && !complains,
|
||
`row: ${offersShared}, warning shown: ${complains}`,
|
||
);
|
||
await shot('02-node-source');
|
||
|
||
// Emptying the setting points the node back at the behavior, which declares nothing – so the
|
||
// very same port is now a stranger. That is the switch doing something visible.
|
||
await mutate(`
|
||
const g = __gm.state.project.assets.find((a) => a.id === 'sc-logic').graph;
|
||
g.nodes.find((n) => n.id === 'sc-read').params.variablesAsset = '';
|
||
`);
|
||
const complainsNow = await has('"score" is not a variable of this behavior.');
|
||
check(
|
||
'and clearing it points the node back at the behavior, which has no such variable',
|
||
complainsNow,
|
||
`warning after clearing: ${complainsNow}`,
|
||
);
|
||
|
||
// 32e. A machine carries its scope on the asset too, with its own switch.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
p.assets.unshift({
|
||
id: 'sc-machine',
|
||
type: 'statemachine',
|
||
name: 'Shared doors',
|
||
props: {},
|
||
graph: { nodes: [], edges: [] },
|
||
});
|
||
`);
|
||
await selectAsset('sc-machine');
|
||
const machineScope = await has('Local');
|
||
const pick = await locate('Local', true);
|
||
await click(pick.x, pick.y);
|
||
await clickLast('Global');
|
||
const machine = await asset('sc-machine');
|
||
check(
|
||
'a state machine is switched between one current state per object and one for the run',
|
||
machineScope && machine.scope === 'global',
|
||
`dropdown found: ${machineScope}, scope now ${JSON.stringify(machine.scope)}`,
|
||
);
|
||
await shot('03-machine-scope');
|
||
|
||
// 32f. Everything this step added goes again.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => !o.id.startsWith('sc-'));
|
||
p.assets = p.assets.filter((a) => !a.id.startsWith('sc-'));
|
||
__gm.state.select(null);
|
||
`);
|
||
}
|