Keep the game's music when Play ends a run in the editor
game-maker / gate (push) Successful in 3m39s
game-maker / suite (push) Failing after 19m26s

A behavior with "Run in editor" kept its world until the frame after
Play. By then the game's On start had already started its pattern or
song, and ending the editor run called stopAll() on the sound library.
The game came up silent. This is what turned the game blocks of e2e
64 and 65 red in suites 542 and 550.

- EditorRun.modeChanged() ends the run the moment the mode leaves
  edit. enterPlay() announces the mode before the game is built, so
  the editor run's sounds stop first and the game's start after.
- The Editor wires the mode event to it instead of invalidate().

Tests:
- e2e 64 starts its game with an idle "Run in editor" behavior in
  the scene. Without the fix it fails like CI did, "game": [] and
  2 FAILED. With it: ALL PASSED, 30 checks (stein2).
- Unit editor-run: 21 green, 1 new.
- Typecheck, ESLint, Prettier clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-06 21:52:24 +02:00
1 parent f1f4a57589
commit af9b202100
4 files changed
+50 -4

No files matched your search

+2 -2
View File
@@ -364,7 +364,7 @@ export class Editor implements PlayHost {
const playing = this.play.machineStates(assetId); const playing = this.play.machineStates(assetId);
return playing.length > 0 ? playing : this.editorRun.machineStates(assetId); return playing.length > 0 ? playing : this.editorRun.machineStates(assetId);
}; };
// Only one of the two ever runs a given asset: playing stops the editor run (see `EditorRun.tick`). // Only one of the two ever runs a given asset: playing stops the editor run (see `EditorRun.modeChanged`).
this.inspector.liveVariables = (assetId) => this.inspector.liveVariables = (assetId) =>
this.play.liveVariables(assetId) ?? this.editorRun.liveVariables(assetId); this.play.liveVariables(assetId) ?? this.editorRun.liveVariables(assetId);
this.variablesEditor.liveVariables = this.inspector.liveVariables; this.variablesEditor.liveVariables = this.inspector.liveVariables;
@@ -560,7 +560,7 @@ export class Editor implements PlayHost {
this.valueTargets.clear(); this.valueTargets.clear();
this.showTab('scene'); this.showTab('scene');
}); });
state.on('mode', () => this.editorRun.invalidate()); state.on('mode', () => this.editorRun.modeChanged());
state.on('pick', () => this.followPick()); state.on('pick', () => this.followPick());
} }
+9
View File
@@ -41,6 +41,15 @@ export class EditorRun {
if (!this.writing) this.stale = true; if (!this.writing) this.stale = true;
} }
/**
* Playing starts: the run ends at once, not on the next frame. The game's `On start` runs in the
* same click, and stopping the run's sounds a frame later would take its music along.
*/
modeChanged(): void {
if (this.state.mode !== 'edit') this.stop();
this.invalidate();
}
/** `step` is what the global clock moved this frame – already scaled and capped. */ /** `step` is what the global clock moved this frame – already scaled and capped. */
tick(step: number): void { tick(step: number): void {
if (this.state.mode !== 'edit') { if (this.state.mode !== 'edit') {
+18
View File
@@ -178,6 +178,24 @@ describe('the run-in-editor switch', () => {
expect(f.boxX()).toBe(0); expect(f.boxX()).toBe(0);
}); });
it('ends the moment playing starts, before the game’s On start, not a frame later', () => {
const f = scene();
f.state.on('mode', () => f.run.modeChanged());
f.state.execute(new SetOverrideCommand(f.sceneId, f.logic, 'runInEditor', true));
f.frame(0.1);
expect(f.run.debugInfo().running).toBe(true);
// The game is built right after `enterPlay` in the same click; a run ended only by the next
// frame stopped every sound, the music its On start had just begun with them.
f.state.enterPlay();
expect(f.run.debugInfo().running).toBe(false);
expect(f.keyboard.world).toBeNull();
f.state.exitPlay();
f.frame(0.1);
expect(f.run.debugInfo().running).toBe(true);
});
it('does not run a behavior whose scene is not the loaded one', () => { it('does not run a behavior whose scene is not the loaded one', () => {
const f = scene(); const f = scene();
f.state.execute(new SetOverrideCommand(f.sceneId, f.logic, 'runInEditor', true)); f.state.execute(new SetOverrideCommand(f.sceneId, f.logic, 'runInEditor', true));
+21 -2
View File
@@ -510,6 +510,21 @@ export async function pattern(t) {
edges: [edge('pt64-e3', 'pt64-start', 'exec', 'pt64-play', 'exec')], edges: [edge('pt64-e3', 'pt64-start', 'exec', 'pt64-play', 'exec')],
}, },
}); });
// A behavior running in the editor ends when the game starts, and its sounds with it – but only
// its own: the pattern the game's `On start` plays in the same click has to live on.
const idle = await scaffoldBehavior(t, {
assetId: 'pt64-idle',
name: 'Idle 64',
boxId: 'pt64-idlebox',
logicId: 'pt64-idler',
y: LANES.sound + 200,
props: { enabled: true, runInEditor: true },
graph: { nodes: [node('pt64-idle-start', 'OnStart', 0, 0)], edges: [] },
});
await waitFor(
() => json('__gm.editorRun().running'),
(running) => running,
);
const patterns = () => json('__gm.sound.playbacks()'); const patterns = () => json('__gm.sound.playbacks()');
await evaluate('__gm.sound.stopAll()'); await evaluate('__gm.sound.stopAll()');
await play(); await play();
@@ -556,6 +571,7 @@ export async function pattern(t) {
JSON.stringify({ once: once.map((g) => g.planned.length), gone, onceStats }), JSON.stringify({ once: once.map((g) => g.planned.length), gone, onceStats }),
); );
const left = await rig.remove(); const left = await rig.remove();
const idleLeft = await idle.remove();
// 64m. The step takes back everything it made. // 64m. The step takes back everything it made.
await evaluate('__gm.sound.stopAll()'); await evaluate('__gm.sound.stopAll()');
@@ -571,7 +587,10 @@ export async function pattern(t) {
`__gm.state.project.assets.filter((a) => a.id.startsWith('pt64') || a.type === 'pattern').length`, `__gm.state.project.assets.filter((a) => a.id.startsWith('pt64') || a.type === 'pattern').length`,
)) === 0 && )) === 0 &&
left.objects === 0 && left.objects === 0 &&
left.assets === 0, left.assets === 0 &&
JSON.stringify(left), idleLeft.objects === 0 &&
idleLeft.assets === 0 &&
!(await json('__gm.editorRun().running')),
JSON.stringify({ left, idleLeft }),
); );
} }