diff --git a/src/engine/execute.js b/src/engine/execute.js index f0ed1f3dd..98f85b9b4 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -65,6 +65,7 @@ const handleReport = function (resolvedValue, sequencer, thread, blockCached, la if (!thread.stackClick) { const oldEdgeValue = sequencer.runtime.updateEdgeActivatedValue( currentBlockId, + thread.target.id, resolvedValue ); const edgeWasActivated = !oldEdgeValue && resolvedValue; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 071d8a4a7..978af6ef6 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1233,12 +1233,14 @@ class Runtime extends EventEmitter { /** * Update an edge-activated hat block value. * @param {!string} blockId ID of hat to store value for. + * @param {!string} threadTargetId Target ID for the thread that the block belongs to * @param {*} newValue Value to store for edge-activated hat. * @return {*} The old value for the edge-activated hat. */ - updateEdgeActivatedValue (blockId, newValue) { - const oldValue = this._edgeActivatedHatValues[blockId]; - this._edgeActivatedHatValues[blockId] = newValue; + updateEdgeActivatedValue (blockId, threadTargetId, newValue) { + const blockAndTargetId = `${blockId}_${threadTargetId}`; + const oldValue = this._edgeActivatedHatValues[blockAndTargetId]; + this._edgeActivatedHatValues[blockAndTargetId] = newValue; return oldValue; } diff --git a/test/fixtures/edge-triggered-hat.sb3 b/test/fixtures/edge-triggered-hat.sb3 new file mode 100644 index 000000000..762ec1af4 Binary files /dev/null and b/test/fixtures/edge-triggered-hat.sb3 differ diff --git a/test/integration/hat-threads-run-every-frame.js b/test/integration/hat-threads-run-every-frame.js index 5d3c3527f..80a4711cf 100644 --- a/test/integration/hat-threads-run-every-frame.js +++ b/test/integration/hat-threads-run-every-frame.js @@ -5,6 +5,7 @@ const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer const VirtualMachine = require('../../src/index'); const Thread = require('../../src/engine/thread'); const Runtime = require('../../src/engine/runtime'); +const execute = require('../../src/engine/execute.js'); const projectUri = path.resolve(__dirname, '../fixtures/loudness-hat-block.sb2'); const project = readFileToBuffer(projectUri); @@ -103,6 +104,100 @@ test('edge activated hat thread not added twice', t => { }); }); + +/** + * Duplicating a sprite should also track duplicated edge activated hat in + * runtime's _edgeActivatedHatValues map. + */ +test('edge activated hat should trigger for both sprites when sprite is duplicated', t => { + + // Project that is similar to loudness-hat-block.sb2, but has code on the sprite so that + // the sprite can be duplicated + const projectWithSpriteUri = path.resolve(__dirname, '../fixtures/edge-triggered-hat.sb3'); + const projectWithSprite = readFileToBuffer(projectWithSpriteUri); + + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = 0; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(projectWithSprite).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + t.equal(vm.runtime.threads.length, 1); + checkIsHatThread(t, vm, vm.runtime.threads[0]); + t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING); + // Run execute on the thread to populate the runtime's + // _edgeActivatedHatValues object + execute(vm.runtime.sequencer, vm.runtime.threads[0]); + t.equal(Object.keys(vm.runtime._edgeActivatedHatValues).length, 1); + + vm.duplicateSprite(vm.runtime.targets[1].id).then(() => { + vm.runtime._step(); + // Check that the runtime's _edgeActivatedHatValues object has two separate keys + // after execute is run on each thread + vm.runtime.threads.forEach(thread => execute(vm.runtime.sequencer, thread)); + t.equal(Object.keys(vm.runtime._edgeActivatedHatValues).length, 2); + t.end(); + }); + + }); + }); +}); + +/** + * Cloning a sprite should also track cloned edge activated hat separately + * runtime's _edgeActivatedHatValues map. + */ +test('edge activated hat should trigger for both sprites when sprite is cloned', t => { + + // Project that is similar to loudness-hat-block.sb2, but has code on the sprite so that + // the sprite can be duplicated + const projectWithSpriteUri = path.resolve(__dirname, '../fixtures/edge-triggered-hat.sb3'); + const projectWithSprite = readFileToBuffer(projectWithSpriteUri); + + const vm = new VirtualMachine(); + vm.attachStorage(makeTestStorage()); + + // Start VM, load project, and run + t.doesNotThrow(() => { + // Note: don't run vm.start(), we handle calling _step() manually in this test + vm.runtime.currentStepTime = 0; + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + + vm.loadProject(projectWithSprite).then(() => { + t.equal(vm.runtime.threads.length, 0); + + vm.runtime._step(); + t.equal(vm.runtime.threads.length, 1); + checkIsHatThread(t, vm, vm.runtime.threads[0]); + t.assert(vm.runtime.threads[0].status === Thread.STATUS_RUNNING); + // Run execute on the thread to populate the runtime's + // _edgeActivatedHatValues object + execute(vm.runtime.sequencer, vm.runtime.threads[0]); + t.equal(Object.keys(vm.runtime._edgeActivatedHatValues).length, 1); + + vm.runtime.targets.push(vm.runtime.targets[1].makeClone()); + + vm.runtime._step(); + // Check that the runtime's _edgeActivatedHatValues object has two separate keys + // after execute is run on each thread + vm.runtime.threads.forEach(thread => execute(vm.runtime.sequencer, thread)); + t.equal(Object.keys(vm.runtime._edgeActivatedHatValues).length, 2); + t.end(); + }); + }); +}); + /** * When adding a stack click thread first, make sure that the edge activated hat thread and * the stack click thread are both pushed and run (despite having the same top block)