diff --git a/src/core/graph/GraphInterpreter.ts b/src/core/graph/GraphInterpreter.ts index efd4ce6a..dac9541c 100644 --- a/src/core/graph/GraphInterpreter.ts +++ b/src/core/graph/GraphInterpreter.ts @@ -240,17 +240,10 @@ export class GraphInterpreter { this.callStack.push(call.behaviorId); this.callSites.push(site); this.calledBehaviors.add(call.behaviorId); - // Fresh states every call, and nothing of the caller's variables or states. What it does to a - // local machine therefore dies with the call and is not reported; a `global` machine is the - // run's, so moving that one is a move like any other. - const inner: NodeContext = { - ...ctx, - variables, - states: {}, - stateChanged: (machineId, from, to) => { - if (machineId in ctx.shared.states) ctx.stateChanged(machineId, from, to); - }, - }; + // Nothing of the caller's variables, but its local machines: a local machine belongs to the + // running instance however deep the call, so what a call sets is still set on the next one, + // and the move is heard like one the caller made itself. + const inner: NodeContext = { ...ctx, variables }; try { for (const entry of entries) { this.trace?.onNode(entry.id); diff --git a/tests/core/keep-state.test.ts b/tests/core/keep-state.test.ts index ea01fdaf..8f3be77c 100644 --- a/tests/core/keep-state.test.ts +++ b/tests/core/keep-state.test.ts @@ -203,16 +203,17 @@ describe('Keep on a behavior variable (#229)', () => { expect([ctx.variables.r0, ctx.variables.r1]).toEqual([2, 2]); }); - it('starts a local state machine of the callee over every call, kept variables or not', () => { + it('leaves a local state machine with the caller, so a call finds what the last one set', () => { const fixture = makeProject(); const create = new CreateAssetCommand('statemachine', 'Doors'); fixture.stack.execute(create); const open = new AddNodeCommand(create.asset.id, STATE, 0, 0, { [STATE_NAME_PARAM]: 'Open' }); fixture.stack.execute(open); const machine = create.asset.id; - // No state yet → "first" and put it in Open; in Open → "open". The kept variable is only - // there to show that keeping one does not keep the machine along with it. + // No state yet → "first" and put it in Open; in Open → "open". The variable is not kept, which + // shows that the machine does not hang on the Keep switch. const callee = makeBehavior(fixture, 'Door', ['count']); + fixture.stack.execute(new SetVariableKeptCommand(callee.id, 'count', false)); const edit = graphEditor(fixture, callee); const start = edit.addNode(BEHAVIOR_START); const branch = edit.addNode('StateBranch', { [MACHINE_PORT]: machine }); @@ -237,9 +238,9 @@ describe('Keep on a behavior variable (#229)', () => { const ctx = fakeContext(); fire(interpreter, 'OnStart', ctx); fire(interpreter, 'OnStart', ctx); - expect(ctx.logs).toEqual(['first', 'first']); - // A local machine of the callee is none of the caller's business either. - expect(ctx.states).toEqual({}); + expect(ctx.logs).toEqual(['first', 'open']); + // The state sits in the caller's bag, where the running instance keeps its local machines. + expect(ctx.states).toEqual({ [machine]: open.node.id }); }); it('survives saving and loading, and a name that is not declared is dropped', () => { diff --git a/tests/core/state-machine.test.ts b/tests/core/state-machine.test.ts index 01bff98f..b4c6eee1 100644 --- a/tests/core/state-machine.test.ts +++ b/tests/core/state-machine.test.ts @@ -275,7 +275,7 @@ describe('a running state machine', () => { expect(started.world.log).toEqual(['open', 'open', 'closed', 'closed']); }); - it('starts a called behavior in no state, whatever the caller set', () => { + it('lets a called behavior see the state its caller set', () => { const f = makeProject(); const machine = makeMachine(f); const inner = makeBehavior(f, 'Inner'); @@ -299,7 +299,8 @@ describe('a running state machine', () => { const ctx = fakeContext(); fire(new GraphInterpreter(outer.graph, graphLookup(f.project)), 'OnStart', ctx); expect(ctx.states).toEqual({ [machine.id]: machine.ids[0] }); - expect(ctx.logs).toEqual([]); + // A local machine belongs to the instance, not to the graph that happens to run. + expect(ctx.logs).toEqual(['inner saw open']); }); it('is driven by name where the machine arrives as a value, and branches through else', () => { @@ -501,6 +502,45 @@ describe('On state entered and On state exited', () => { expect(open).not.toBe(''); }); + it('hears a move made inside a called behavior, each object in its own state', () => { + const f = makeSpriteScene(); + const machine = makeMachine(f); + const [open, closed] = machine.ids as [string, string]; + f.stack.execute(new SetMachineStartCommand(machine.id, open)); + // Called every frame: closes the door it finds open, says so once it finds it closed. + const step = makeBehavior(f, 'Step'); + const stepEdit = graphEditor(f, step); + const branch = stepEdit.addNode('StateBranch', { [MACHINE_PORT]: machine.id }); + const set = stepEdit.addNode('SetState', { [MACHINE_PORT]: machine.id }); + const said = stepEdit.addNode('Log', { message: 'Step: found closed' }); + stepEdit.wire([ + [stepEdit.addNode(BEHAVIOR_START), 'exec', branch, 'exec'], + [branch, open, set, closed], + [branch, closed, said, 'exec'], + ]); + const { behavior, edit } = watcher(f, machine.id, machine.ids); + const call = edit.addNode('RunBehavior', { [BEHAVIOR_PARAM]: step.id }); + edit.connect(edit.addNode('OnUpdate'), 'exec', call, 'exec'); + place(f, behavior.id, { x: 0, y: 0 }); + place(f, behavior.id, { x: 10, y: 0 }); + const started = startWorld(f, { behaviors: true }); + started.frame(); + // The call starts in the instance's start state and its move reaches that instance's events. + expect([...started.world.log].sort()).toEqual([ + 'Watcher: entered closed', + 'Watcher: entered closed', + 'Watcher: entered open', + 'Watcher: entered open', + 'Watcher: exited open', + 'Watcher: exited open', + ]); + expect(started.behaviors?.machineStates(machine.id).map((s) => s.state)).toEqual([closed, closed]); + started.world.log.length = 0; + started.frame(); + // The next call finds what the last one set, once per object. + expect(started.world.log).toEqual(['Step: found closed', 'Step: found closed']); + }); + it('reaches every instance when the machine is global, and enters its start once', () => { const f = makeSpriteScene(); const machine = makeMachine(f);