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.
111 lines
5.1 KiB
JavaScript
111 lines
5.1 KiB
JavaScript
/** Text assets rendered in the scene, and the enabled switch in the editor and in the running game. */
|
||
export async function textAndEnabled(t) {
|
||
const {
|
||
check,
|
||
evaluate,
|
||
json,
|
||
has,
|
||
shot,
|
||
click,
|
||
clickText,
|
||
locate,
|
||
mutate,
|
||
object,
|
||
play,
|
||
press,
|
||
stop,
|
||
typeText,
|
||
} = t;
|
||
|
||
// 10a. Text assets: created through the + menu, placed, and rendered as real text in the scene.
|
||
await clickText('+');
|
||
await clickText('Text');
|
||
const textAssetId = await evaluate(`(__gm.state.project.assets.find(a => a.type === 'text') || {}).id`);
|
||
check('+ menu creates a text asset', Boolean(textAssetId));
|
||
await mutate(`
|
||
const id = ${JSON.stringify(textAssetId)};
|
||
__gm.state.project.assets.find(a => a.id === id).props.text = 'Hello world';
|
||
// On the camera so the game view really renders it (outside the frame it would be culled).
|
||
const cam = __gm.state.activeScene.objects.find(o => o.id === 'o3').position;
|
||
__gm.state.activeScene.objects.push({ id: 'otext', assetId: id, name: '', position: { x: cam.x, y: cam.y, z: 5 }, editorOffset: { x: 0, y: 0 }, overrides: {}, parentId: '', instanceEdits: {} });
|
||
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
|
||
check('a placed text object draws its content in the scene', await has('Hello world'));
|
||
await shot('29-text-object');
|
||
|
||
// 10b. The enabled switch: every object has it, switching it off is a normal override, and a
|
||
// switched-off object disappears from the scene view too (it stays in the hierarchy).
|
||
const enabledRow = await json("__gm.inspectorField('Enabled')");
|
||
check('the inspector shows the Enabled switch for a scene object', Boolean(enabledRow));
|
||
const visibleBefore = (await json('__gm.viewportInfo()')).visibleItems;
|
||
const gizmoOf = async (id) => (await json('__gm.gizmos()')).find((g) => g.id === id) ?? null;
|
||
const gizmoOn = await gizmoOf('otext');
|
||
check('a selected object is framed at full strength', gizmoOn?.alpha === 1, JSON.stringify(gizmoOn));
|
||
await click(enabledRow.x, enabledRow.y);
|
||
const textObject = await object('otext');
|
||
check(
|
||
'clicking it writes an override instead of changing the asset',
|
||
textObject.overrides.enabled === false,
|
||
JSON.stringify(textObject.overrides),
|
||
);
|
||
check(
|
||
'a switched-off object is not drawn in the scene view',
|
||
!(await json('__gm.drawOrder()')).includes('otext'),
|
||
);
|
||
const visibleAfter = (await json('__gm.viewportInfo()')).visibleItems;
|
||
check(
|
||
'the scene view renders one item less',
|
||
visibleAfter === visibleBefore - 1,
|
||
`${visibleBefore} → ${visibleAfter}`,
|
||
);
|
||
check('it stays selectable in the hierarchy, so its switch stays reachable', await has('Enabled'));
|
||
await shot('30-disabled');
|
||
|
||
// 17a-2. It draws nothing, so its outline is the only thing left that says where it sits – which
|
||
// is the whole point of being able to pick it in the hierarchy. Selecting another object
|
||
// and coming back proves the outline follows the panel, not a leftover from before.
|
||
const gizmoOff = await gizmoOf('otext');
|
||
check(
|
||
'a switched-off object is still framed, dimmed',
|
||
gizmoOff?.primary === true && gizmoOff.alpha > 0 && gizmoOff.alpha < 1,
|
||
JSON.stringify(gizmoOff),
|
||
);
|
||
const otherRow = await evaluate(`__gm.hierarchyRow('o3')`);
|
||
await click(otherRow.x, otherRow.y);
|
||
check('selecting something else drops the frame', (await gizmoOf('otext')) === null);
|
||
const offRow = await evaluate(`__gm.hierarchyRow('otext')`);
|
||
await click(offRow.x, offRow.y);
|
||
const gizmoBack = await gizmoOf('otext');
|
||
check(
|
||
'picking the switched-off object in the hierarchy frames it again',
|
||
gizmoBack?.alpha > 0 && gizmoBack.alpha < 1,
|
||
JSON.stringify(gizmoBack),
|
||
);
|
||
check('and it is still not drawn', !(await json('__gm.drawOrder()')).includes('otext'));
|
||
await shot('30b-disabled-gizmo');
|
||
|
||
// 10c. In the running game a switched-off object does not exist: it is not rendered, and
|
||
// switching it back on brings it back.
|
||
await play();
|
||
const offItems = (await json('__gm.gameInfo()')).visibleItems;
|
||
await mutate(`__gm.state.activeScene.objects.find(o => o.id === 'otext').overrides.enabled = true`);
|
||
const onItems = (await json('__gm.gameInfo()')).visibleItems;
|
||
check(
|
||
'the game renders one item more once the object is switched on',
|
||
onItems === offItems + 1,
|
||
`${offItems} → ${onItems}`,
|
||
);
|
||
await stop();
|
||
check('stop leaves the play-mode switch behind', (await object('otext')).overrides.enabled === false);
|
||
|
||
// 10d. A field that shows no decimals must not store any either. Font size is such a field, so
|
||
// typing a fraction has to round: showing "33" while the project holds 32.6 would be a lie.
|
||
const fontLabel = await locate('Font size');
|
||
check('Font size reads as a whole number', Boolean(await has('32')) && !(await has('32.000')));
|
||
await click(fontLabel.x + 160, fontLabel.y);
|
||
await typeText('32.6');
|
||
await press('Enter');
|
||
const fontSize = (await object('otext')).overrides.fontSize;
|
||
check('typing a fraction into it rounds the stored value', fontSize === 33, String(fontSize));
|
||
check('and the field shows exactly what was stored', Boolean(await has('33')));
|
||
}
|