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.
283 lines
11 KiB
JavaScript
283 lines
11 KiB
JavaScript
import { edge, node, propertiesOf, variable, written } from '../lib/builders.mjs';
|
||
import { MAKE_OBJECT } from './lib/scaffold.mjs';
|
||
|
||
/**
|
||
* Properties: properties of the object handed in, each read at any time and written where it is
|
||
* switched to Set. Its type decides what `+ Property` offers. The step brings its own box and
|
||
* behavior and removes them again.
|
||
*/
|
||
|
||
/** `On update → Properties{width 40, color red} on the owner`, whose width is then read into `seen`. */
|
||
const logicGraph = {
|
||
nodes: [
|
||
node('pp-update', 'OnUpdate', 0, 0),
|
||
node('pp-owner', 'Owner', 0, 240),
|
||
node('pp-set', 'Properties', 260, 0, { ...propertiesOf('sprite'), width: 40, color: 0xff0000 }, [
|
||
written('width', 'number'),
|
||
written('color', 'color'),
|
||
]),
|
||
node('pp-store', 'Variables', 560, 0, {}, [written('seen', 'number')]),
|
||
node('pp-any', 'Properties', 260, 420),
|
||
node('pp-beh', 'Properties', 560, 420, propertiesOf('behavior')),
|
||
],
|
||
edges: [
|
||
edge('pp-e1', 'pp-update', 'exec', 'pp-set', 'exec'),
|
||
edge('pp-e2', 'pp-owner', 'object', 'pp-set', 'object'),
|
||
edge('pp-e3', 'pp-set', 'exec', 'pp-store', 'exec'),
|
||
edge('pp-e4', 'pp-set', 'width', 'pp-store', 'seen'),
|
||
],
|
||
};
|
||
|
||
export async function propertyNodes(t) {
|
||
const { check, evaluate, json, shot, click, doubleClick, locate, find, undo, entity, asset } = t;
|
||
const { mutate, stop, typeText, press } = t;
|
||
const portsOf = async (nodeId) =>
|
||
((await asset('pp-logic')).graph.nodes.find((n) => n.id === nodeId).ports ?? []).map(
|
||
(p) => `${p.key}:${p.kind}${p.write ? ':set' : ''}`,
|
||
);
|
||
const title = (nodeId) => json(`__gm.graph.nodeLabel(${JSON.stringify(nodeId)})`);
|
||
const settings = async (nodeId) => (await json(`__gm.graph.nodeSettings(${JSON.stringify(nodeId)})`)) ?? [];
|
||
const select = async (nodeId) => {
|
||
await t.select({ kind: 'node', assetId: 'pp-logic', nodeId, nodeIds: [nodeId] });
|
||
};
|
||
/** A port section takes its choices when it is built, and re-selecting the same node is a no-op. */
|
||
const rebuildInspector = async () => {
|
||
await evaluate('__gm.state.select(null)');
|
||
await select('pp-set');
|
||
};
|
||
/** The inspector is drawn over the graph, so the last match is the row rather than the port. */
|
||
const clickLast = async (text) => {
|
||
const p = await locate(text, true);
|
||
await click(p.x, p.y);
|
||
};
|
||
const offered = async (shotName) => {
|
||
await clickLast('+ Property');
|
||
const texts = await json('__gm.overlayTexts()');
|
||
if (shotName) await shot(shotName);
|
||
await press('Escape');
|
||
return texts;
|
||
};
|
||
const addProperty = async (search) => {
|
||
await clickLast('+ Property');
|
||
await typeText(search);
|
||
await press('Enter');
|
||
};
|
||
const choose = async (setting, option) => {
|
||
const field = await json(`__gm.inspectorField(${JSON.stringify(setting)}, true)`);
|
||
await click(field.x, field.y);
|
||
await clickLast(option);
|
||
};
|
||
/** The name field, not the node's port or the unconnected-input row below it that say the same. */
|
||
const rename = async (from, to) => {
|
||
const canvas = await json('__gm.graph.canvasRect()');
|
||
const hits = await json(`__gm.locateAll(${JSON.stringify(from)})`);
|
||
const field = hits.find((p) => p.x > canvas.x + canvas.w);
|
||
await doubleClick(field.x, field.y);
|
||
await typeText(to);
|
||
await press('Enter');
|
||
};
|
||
|
||
// 24a. A box wearing a behavior that reshapes it through one node.
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
const scene = __gm.state.activeScene;
|
||
const a = structuredClone(p.assets.find((x) => x.id === 'mover'));
|
||
Object.assign(a, {
|
||
id: 'pp-logic',
|
||
name: 'Reshape',
|
||
props: { enabled: true, runInEditor: false, seen: 0 },
|
||
variables: ${JSON.stringify([variable('seen', 'number', 0)])},
|
||
graph: ${JSON.stringify(logicGraph)},
|
||
});
|
||
p.assets.unshift(a);
|
||
${MAKE_OBJECT}
|
||
scene.objects.push(make('pp-box', 'crate', '', 900, 'Reshaped box'), make('pp-brain', 'pp-logic', 'pp-box', 900, 'Reshape'));
|
||
`);
|
||
|
||
// 24b. The run: one node writes both properties, and its own output reads the width back after.
|
||
await t.play();
|
||
const box = (await entity('pp-box'))?.props;
|
||
const seen = (await entity('pp-brain'))?.props.seen;
|
||
await stop();
|
||
check(
|
||
'one node writes several properties and reads them back',
|
||
box?.width === 40 && box?.color === 0xff0000 && seen === 40,
|
||
JSON.stringify({ width: box?.width, color: box?.color, seen }),
|
||
);
|
||
|
||
// 24c. Titled Properties; the type stands on the node as a setting row, chosen above the list it decides.
|
||
await select('pp-set');
|
||
const typeRow = await json(`__gm.inspectorField('Type', true)`);
|
||
const header = await locate('PROPERTIES', true);
|
||
check(
|
||
'the node is titled Properties and shows its type, chosen above its properties',
|
||
(await title('pp-set')) === 'Properties' &&
|
||
(await settings('pp-set')).join() === 'Type: Sprite' &&
|
||
typeRow &&
|
||
typeRow.y < header.y,
|
||
JSON.stringify({
|
||
title: await title('pp-set'),
|
||
rows: await settings('pp-set'),
|
||
type: typeRow?.y,
|
||
header: header.y,
|
||
}),
|
||
);
|
||
check(
|
||
'the node shows a typed-in color as a color',
|
||
(await find('color #ff0000')) !== null,
|
||
JSON.stringify(await json(`__gm.graph.nodeInputs('pp-set')`)),
|
||
);
|
||
await shot('01-sprite-properties');
|
||
|
||
// 24d. A Sprite offers the fields of a sprite, and nothing of the other types.
|
||
const sprite = await offered();
|
||
check(
|
||
"a Sprite offers a sprite's properties",
|
||
['enabled', 'width', 'height', 'color', 'texture'].every((k) => sprite.includes(k)) &&
|
||
!sprite.includes('radius') &&
|
||
!sprite.includes('zoom'),
|
||
sprite.join(' | '),
|
||
);
|
||
|
||
// 24d-ii. Which sprite the node will meet is only known at runtime, so it offers the parameters of
|
||
// every shader – each entry saying whose it is, and one shared name naming both shaders.
|
||
await mutate(`
|
||
__gm.state.project.assets.push({
|
||
id: 'pp-neon',
|
||
type: 'shader',
|
||
name: 'Neon glow',
|
||
props: {},
|
||
graph: {
|
||
nodes: [
|
||
{ id: 'pp-n1', type: 'ShaderProperty', x: 0, y: 0, params: { name: 'brightness', default: 1, min: 0, max: 2 } },
|
||
{ id: 'pp-n2', type: 'ShaderProperty', x: 0, y: 120, params: { name: 'glow', default: 1, min: 0, max: 2 } },
|
||
],
|
||
edges: [],
|
||
},
|
||
});
|
||
`);
|
||
await rebuildInspector();
|
||
const shared = await offered('02-shader-parameters');
|
||
check(
|
||
'a shader parameter names the shader it comes from, a shared name both of them',
|
||
shared.includes('brightness (Adjust colors, Neon glow)') &&
|
||
shared.includes('glow (Neon glow)') &&
|
||
!shared.includes('brightness') &&
|
||
shared.includes('width'),
|
||
shared.filter((s) => s.includes('(') || s === 'brightness' || s === 'width').join(' | '),
|
||
);
|
||
await mutate(`
|
||
const list = __gm.state.project.assets;
|
||
const i = list.findIndex((a) => a.id === 'pp-neon');
|
||
if (i >= 0) list.splice(i, 1);
|
||
`);
|
||
await rebuildInspector();
|
||
const alone = await offered();
|
||
check(
|
||
'with that shader gone the parameter names only the one left',
|
||
alone.includes('brightness (Adjust colors)'),
|
||
alone.filter((s) => s.includes('(')).join(' | '),
|
||
);
|
||
|
||
// 24e. Picked, then switched to Set: its typed-in value starts at the property's own default –
|
||
// a height of 100, not a bare 0 that would flatten the box.
|
||
await addProperty('height');
|
||
const grown = await portsOf('pp-set');
|
||
check(
|
||
'picking a property adds it as an output',
|
||
grown.join(',') === 'width:number:set,color:color:set,height:number',
|
||
grown.join(','),
|
||
);
|
||
const toggle = await json(`__gm.inspectorField('Set', true)`);
|
||
await click(toggle.x, toggle.y);
|
||
const inputs = await json(`__gm.graph.nodeInputs('pp-set')`);
|
||
check(
|
||
"switched to Set, its typed-in value starts at the property's own default",
|
||
inputs.includes('height 100'),
|
||
JSON.stringify(inputs),
|
||
);
|
||
await shot('03-height-set');
|
||
await undo();
|
||
await undo();
|
||
check('undo takes both back', (await portsOf('pp-set')).length === 2, (await portsOf('pp-set')).join());
|
||
|
||
// 24f. Another type offers its own fields; a port it does not have is marked, not dropped.
|
||
await choose('Type', 'Camera');
|
||
const camera = await offered();
|
||
check(
|
||
'switching to Camera shows it on the node and offers the fields of a camera',
|
||
(await settings('pp-set')).join() === 'Type: Camera' &&
|
||
camera.includes('zoom') &&
|
||
!camera.includes('texture'),
|
||
JSON.stringify({ rows: await settings('pp-set'), offered: camera }),
|
||
);
|
||
check(
|
||
'a port the new type does not have stays, marked',
|
||
(await portsOf('pp-set')).length === 2 && (await find('"color" is not a property of a Camera.')) !== null,
|
||
(await portsOf('pp-set')).join(),
|
||
);
|
||
await shot('04-camera');
|
||
await undo();
|
||
check('undo switches back', (await settings('pp-set')).join() === 'Type: Sprite', await settings('pp-set'));
|
||
|
||
// 24g. Any names its ports by hand, and the kind is picked when adding one. A name that happens
|
||
// to be a property's stays of the kind picked.
|
||
await select('pp-any');
|
||
const kinds = await offered();
|
||
check(
|
||
'Any offers the kinds to pick from, not property names',
|
||
['Number', 'Text', 'Boolean', 'Color', 'Texture', 'List', 'Struct'].every((k) => kinds.includes(k)) &&
|
||
!kinds.includes('width'),
|
||
kinds.join(' | '),
|
||
);
|
||
await addProperty('text');
|
||
check('a port is added of the kind picked', (await portsOf('pp-any'))[0] === 'property1:text');
|
||
await rename('property1', 'width');
|
||
check(
|
||
'and renamed by typing, keeping its kind',
|
||
(await portsOf('pp-any'))[0] === 'width:text',
|
||
(await portsOf('pp-any')).join(),
|
||
);
|
||
await evaluate("__gm.state.notice = ''");
|
||
await rename('width', 'object');
|
||
const notice = await json('__gm.state.notice');
|
||
check(
|
||
'the object input keeps its key: a port cannot take it',
|
||
(await portsOf('pp-any'))[0] === 'width:text' && notice.includes('"object" is taken'),
|
||
JSON.stringify({ ports: await portsOf('pp-any'), notice }),
|
||
);
|
||
await shot('04-any');
|
||
|
||
// 24h. A Behavior offers the variables of the behavior picked.
|
||
await select('pp-beh');
|
||
const before = await offered();
|
||
check(
|
||
'a Behavior offers nothing until one is picked',
|
||
before.length === 0 && (await find('Pick a behavior first.')) !== null,
|
||
JSON.stringify(before),
|
||
);
|
||
await choose('Behavior', 'Reshape (behavior)');
|
||
const vars = await offered();
|
||
check(
|
||
'picking the behavior offers its variables and shows it on the node',
|
||
vars.includes('seen') &&
|
||
vars.includes('enabled') &&
|
||
(await settings('pp-beh')).join() === 'Type: Behavior,Behavior: Reshape',
|
||
JSON.stringify({ rows: await settings('pp-beh'), offered: vars }),
|
||
);
|
||
await addProperty('seen');
|
||
check('a variable is picked as a port', (await portsOf('pp-beh')).join() === 'seen:number');
|
||
await shot('05-behavior');
|
||
|
||
await mutate(`
|
||
const p = __gm.state.project;
|
||
p.assets = p.assets.filter((a) => a.id !== 'pp-logic');
|
||
const scene = __gm.state.activeScene;
|
||
scene.objects = scene.objects.filter((o) => o.id !== 'pp-box' && o.id !== 'pp-brain');
|
||
__gm.state.select(null);
|
||
__gm.state.preview();
|
||
`);
|
||
const left = await json('__gm.state.project.assets.filter((a) => a.id === "pp-logic").length');
|
||
check('the step leaves the fixture as it found it', left === 0, String(left));
|
||
}
|