From bd405ecc4aabce1b157331bdc00d2b3525988d9b Mon Sep 17 00:00:00 2001 From: griffpatch Date: Sat, 28 Jan 2017 16:43:37 +0000 Subject: [PATCH 01/44] Optimisation - Only check browser compatability once This saves doing the checks everytime the time functions are referenced --- src/util/timer.js | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/util/timer.js b/src/util/timer.js index 17c66a567..a0b620112 100644 --- a/src/util/timer.js +++ b/src/util/timer.js @@ -27,13 +27,12 @@ Timer.prototype.startTime = 0; * Return the currently known absolute time, in ms precision. * @returns {number} ms elapsed since 1 January 1970 00:00:00 UTC. */ -Timer.prototype.time = function () { - if (Date.now) { +Timer.prototype.time = Date.now ? + function () { return Date.now(); - } else { + } : function () { return new Date().getTime(); - } -}; + }; /** * Returns a time accurate relative to other times produced by this function. @@ -42,14 +41,13 @@ Timer.prototype.time = function () { * Not guaranteed to produce the same absolute values per-system. * @returns {number} ms-scale accurate time relative to other relative times. */ -Timer.prototype.relativeTime = function () { - if (typeof self !== 'undefined' && - self.performance && 'now' in self.performance) { - return self.performance.now(); - } else { - return this.time(); - } -}; +Timer.prototype.relativeTime = + (typeof self !== 'undefined' && self.performance && 'now' in self.performance) ? + function () { + return self.performance.now(); + } : function () { + return this.time(); + }; /** * Start a timer for measuring elapsed time, From 30cdef57822b148b16f7ac522728c34271524baf Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 3 Feb 2017 15:13:39 +0000 Subject: [PATCH 02/44] chore(package): update webpack-dev-server to version 2.3.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17baf3138..0b7410140 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,6 @@ "tap": "10.0.2", "travis-after-all": "1.4.4", "webpack": "2.2.1", - "webpack-dev-server": "1.16.3" + "webpack-dev-server": "2.3.0" } } From c12cc25e5f556828b5078a8e318728b1d281cb45 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 7 Feb 2017 23:35:36 +0000 Subject: [PATCH 03/44] chore(package): update tap to version 10.1.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ab89c9f3..d7540477b 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "scratch-render": "latest", "script-loader": "0.7.0", "stats.js": "0.17.0", - "tap": "10.0.2", + "tap": "10.1.0", "travis-after-all": "1.4.4", "webpack": "2.2.1", "webpack-dev-server": "1.16.3" From 7d69ecc00595c936efee2561ea73bd1dfcf07395 Mon Sep 17 00:00:00 2001 From: griffpatch Date: Wed, 8 Feb 2017 08:11:10 +0000 Subject: [PATCH 04/44] Optimisation - Only check browser compatability once Disable use of more accurate timer temporarilly due to performance. However, with the rearrange and introduction of the 'nowObj' it turns out most of the delay is in resolving 'self.performance'... so keeping this cached in nowObj actually mitigates most of the delay. --- src/util/timer.js | 50 +++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/util/timer.js b/src/util/timer.js index a0b620112..9a60e5688 100644 --- a/src/util/timer.js +++ b/src/util/timer.js @@ -23,16 +23,36 @@ var Timer = function () {}; */ Timer.prototype.startTime = 0; +/** + * Disable use of self.performance for now as it results in lower performance + * However, instancing it like below (caching the self.performance to a local variable) negates most of the issues. + * @type {boolean} + */ +var USE_PERFORMANCE = false; + +/** + * Legacy object to allow for us to call now to get the old style date time (for backwards compatibility) + * @deprecated This is only called via the nowObj.now() if no other means is possible... + */ +var legacyDateCode = { + now: function () { + return new Date().getTime(); + } +}; + +/** + * Use this object to route all time functions through single access points. + */ +var nowObj = (USE_PERFORMANCE && typeof self !== 'undefined' && self.performance && 'now' in self.performance) ? + self.performance : Date.now ? Date : legacyDateCode; + /** * Return the currently known absolute time, in ms precision. * @returns {number} ms elapsed since 1 January 1970 00:00:00 UTC. */ -Timer.prototype.time = Date.now ? - function () { - return Date.now(); - } : function () { - return new Date().getTime(); - }; +Timer.prototype.time = function () { + return nowObj.now(); +}; /** * Returns a time accurate relative to other times produced by this function. @@ -41,28 +61,20 @@ Timer.prototype.time = Date.now ? * Not guaranteed to produce the same absolute values per-system. * @returns {number} ms-scale accurate time relative to other relative times. */ -Timer.prototype.relativeTime = - (typeof self !== 'undefined' && self.performance && 'now' in self.performance) ? - function () { - return self.performance.now(); - } : function () { - return this.time(); - }; +Timer.prototype.relativeTime = function () { + return nowObj.now(); +}; /** * Start a timer for measuring elapsed time, * at the most accurate precision possible. */ Timer.prototype.start = function () { - this.startTime = this.relativeTime(); + this.startTime = nowObj.now(); }; -/** - * Check time elapsed since `timer.start` was called. - * @returns {number} Time elapsed, in ms (possibly sub-ms precision). - */ Timer.prototype.timeElapsed = function () { - return this.relativeTime() - this.startTime; + return nowObj.now() - this.startTime; }; module.exports = Timer; From cf215bf0bcac1906393029b4e6f861ad012b4e2e Mon Sep 17 00:00:00 2001 From: griffpatch Date: Wed, 8 Feb 2017 08:38:50 +0000 Subject: [PATCH 05/44] Fix for the "Empty or white space strings equal 0" bug See: Empty or white space strings equal 0 #435 --- src/util/cast.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/util/cast.js b/src/util/cast.js index a8a1535b8..c2e1cfe0f 100644 --- a/src/util/cast.js +++ b/src/util/cast.js @@ -100,6 +100,12 @@ Cast.toRgbColorObject = function (value) { Cast.compare = function (v1, v2) { var n1 = Number(v1); var n2 = Number(v2); + if (n1 === 0 && (v1 == null || typeof v1 === 'string' && v1.trim().length === 0)) { + n1 = NaN; + } + if (n2 === 0 && (v2 == null || typeof v2 === 'string' && v2.trim().length === 0)) { + n2 = NaN; + } if (isNaN(n1) || isNaN(n2)) { // At least one argument can't be converted to a number. // Scratch compares strings as case insensitive. From 9cb595312e4a03502bdd3dbb191727a286070c33 Mon Sep 17 00:00:00 2001 From: griffpatch Date: Wed, 8 Feb 2017 09:44:10 +0000 Subject: [PATCH 06/44] Implement "Stop this script" function Existing implementation incorrectly terminates the entire thread. See: http://llk.github.io/scratch-vm/#144142535 --- src/blocks/scratch3_control.js | 2 +- src/engine/execute.js | 4 ++-- src/engine/thread.js | 21 +++++++++++++++++++++ test/unit/blocks_control.js | 8 ++++---- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index 28737b361..285b241ab 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -110,7 +110,7 @@ Scratch3ControlBlocks.prototype.stop = function (args, util) { option === 'other scripts in stage') { util.stopOtherTargetThreads(); } else if (option === 'this script') { - util.stopThread(); + util.stopThisScript(); } }; diff --git a/src/engine/execute.js b/src/engine/execute.js index 44951aba3..226a866a7 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -183,8 +183,8 @@ var execute = function (sequencer, thread) { stopOtherTargetThreads: function () { runtime.stopForTarget(target, thread); }, - stopThread: function () { - sequencer.retireThread(thread); + stopThisScript: function () { + thread.stopThisScript(); }, startProcedure: function (procedureCode) { sequencer.stepToProcedure(thread, procedureCode); diff --git a/src/engine/thread.js b/src/engine/thread.js index 9693b26f1..f093f886e 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -140,6 +140,27 @@ Thread.prototype.popStack = function () { return this.stack.pop(); }; +/** + * Pop back down the stack frame until we hit a procedure call or the stack frame is emptied + */ +Thread.prototype.stopThisScript = function () { + var blockID = this.peekStack(); + while (blockID !== null) { + var block = this.target.blocks.getBlock(blockID); + if (typeof block !== 'undefined' && block.opcode === 'procedures_callnoreturn') { + break; + } + this.popStack(); + blockID = this.peekStack(); + } + + if (this.stack.length === 0) { + // Clean up! + this.requestScriptGlowInFrame = false; + this.status = Thread.STATUS_DONE; + } +}; + /** * Get top stack item. * @return {?string} Block ID on top of stack. diff --git a/test/unit/blocks_control.js b/test/unit/blocks_control.js index 2dc6049df..7867524ce 100644 --- a/test/unit/blocks_control.js +++ b/test/unit/blocks_control.js @@ -104,7 +104,7 @@ test('stop', function (t) { var state = { stopAll: 0, stopOtherTargetThreads: 0, - stopThread: 0 + stopThisScript: 0 }; var util = { stopAll: function () { @@ -113,8 +113,8 @@ test('stop', function (t) { stopOtherTargetThreads: function () { state.stopOtherTargetThreads++; }, - stopThread: function () { - state.stopThread++; + stopThisScript: function () { + state.stopThisScript++; } }; @@ -125,6 +125,6 @@ test('stop', function (t) { c.stop({STOP_OPTION: 'this script'}, util); t.strictEqual(state.stopAll, 1); t.strictEqual(state.stopOtherTargetThreads, 2); - t.strictEqual(state.stopThread, 1); + t.strictEqual(state.stopThisScript, 1); t.end(); }); From 276ae827698dfc625b3bff1945a7656f72ec2d23 Mon Sep 17 00:00:00 2001 From: griffpatch Date: Wed, 8 Feb 2017 10:05:48 +0000 Subject: [PATCH 07/44] Fixes Case Insensitive Broadcast Bug Broadcasts are case sensitive #433 --- src/engine/runtime.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index f196a6a69..fc3b17a06 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -439,6 +439,11 @@ Runtime.prototype.startHats = function (requestedHatOpcode, } var instance = this; var newThreads = []; + + for (var opts in optMatchFields) { + optMatchFields[opts] = optMatchFields[opts].toUpperCase(); + } + // Consider all scripts, looking for hats with opcode `requestedHatOpcode`. this.allScriptsDo(function (topBlockId, target) { var potentialHatOpcode = target.blocks.getBlock(topBlockId).opcode; @@ -466,7 +471,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, if (optMatchFields) { for (var matchField in optMatchFields) { - if (hatFields[matchField].value !== + if (hatFields[matchField].value.toUpperCase() !== optMatchFields[matchField]) { // Field mismatch. return; From 2ba177aa0f7e69e236c877ae6006ff97d61bdc7a Mon Sep 17 00:00:00 2001 From: griffpatch Date: Thu, 9 Feb 2017 08:50:37 +0000 Subject: [PATCH 08/44] Bug - Return pen opacity to opaque When setting the pen color using pen blocks, the opacity should be reset to fully opaque if no alpha is supplied. --- src/blocks/scratch3_pen.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/blocks/scratch3_pen.js b/src/blocks/scratch3_pen.js index c8f98341e..9b9f64919 100644 --- a/src/blocks/scratch3_pen.js +++ b/src/blocks/scratch3_pen.js @@ -147,6 +147,7 @@ Scratch3PenBlocks.prototype._updatePenColor = function (penState) { penState.penAttributes.color4f[0] = rgb.r / 255.0; penState.penAttributes.color4f[1] = rgb.g / 255.0; penState.penAttributes.color4f[2] = rgb.b / 255.0; + penState.penAttributes.color4f[3] = 1; }; /** @@ -260,6 +261,8 @@ Scratch3PenBlocks.prototype.setPenColorToColor = function (args, util) { penState.penAttributes.color4f[2] = rgb.b / 255.0; if (rgb.hasOwnProperty('a')) { // Will there always be an 'a'? penState.penAttributes.color4f[3] = rgb.a / 255.0; + } else { + penState.penAttributes.color4f[3] = 1; } }; From 464722f7bddaf1f9e188d1ef61d45aa43a048b49 Mon Sep 17 00:00:00 2001 From: griffpatch Date: Thu, 9 Feb 2017 10:02:45 +0000 Subject: [PATCH 09/44] Feature - Fencing of sprites This change requires that the pull request for scratch-render (Feature fencing #82) be taken first --- src/sprites/rendered-target.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9bbc20b23..52d5d6471 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -170,6 +170,7 @@ RenderedTarget.prototype.setXY = function (x, y) { this.x = x; this.y = y; if (this.renderer) { + this.renderer.getFencedPositionOfDrawable(this.drawableID, this); this.renderer.updateDrawableProperties(this.drawableID, { position: [this.x, this.y] }); From 8354774fcf3f4b6d61b545aebe09f00ea63724a0 Mon Sep 17 00:00:00 2001 From: SillyInventor Date: Thu, 9 Feb 2017 14:09:01 -0500 Subject: [PATCH 10/44] Switch from return null to return empty string in keyCodeToScratchKey --- src/io/keyboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/keyboard.js b/src/io/keyboard.js index da0e41625..c7a210803 100644 --- a/src/io/keyboard.js +++ b/src/io/keyboard.js @@ -56,7 +56,7 @@ Keyboard.prototype._keyCodeToScratchKey = function (keyCode) { case 39: return 'right arrow'; case 40: return 'down arrow'; } - return null; + return ''; }; /** From cd750b5dda4ee7a845fd90349cd6075e3d0f0bd0 Mon Sep 17 00:00:00 2001 From: Eric Rosenbaum Date: Thu, 9 Feb 2017 14:52:15 -0500 Subject: [PATCH 11/44] pass volume to audioengine playnote --- src/blocks/scratch3_sound.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 20d137580..e9f9df4d7 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -135,8 +135,9 @@ Scratch3SoundBlocks.prototype.playNoteForBeats = function (args, util) { var beats = Cast.toNumber(args.BEATS); var soundState = this._getSoundState(util.target); var inst = soundState.currentInstrument; + var vol = soundState.volume; if (typeof this.runtime.audioEngine === 'undefined') return; - return this.runtime.audioEngine.playNoteForBeatsWithInst(note, beats, inst); + return this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, vol); }; Scratch3SoundBlocks.prototype.playDrumForBeats = function (args, util) { From 3324f51193079329b2d7a469d2e0cc850031d46f Mon Sep 17 00:00:00 2001 From: SillyInventor Date: Thu, 9 Feb 2017 18:18:53 -0500 Subject: [PATCH 12/44] Added some unit tests for some thread functions --- test/unit/engine_thread.js | 107 ++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/test/unit/engine_thread.js b/test/unit/engine_thread.js index 3bda4fc69..a06a316a7 100644 --- a/test/unit/engine_thread.js +++ b/test/unit/engine_thread.js @@ -3,6 +3,111 @@ var Thread = require('../../src/engine/thread'); test('spec', function (t) { t.type(Thread, 'function'); - // @todo + + var th = new Thread('arbitraryString'); + t.type(th, 'object'); + t.ok(th instanceof Thread); + t.type(th.pushStack, 'function'); + t.type(th.reuseStackForNextBlock, 'function'); + t.type(th.popStack, 'function'); + t.type(th.stopThisScript, 'function'); + t.type(th.peekStack, 'function'); + t.type(th.peekStackFrame, 'function'); + t.type(th.peekParentStackFrame, 'function'); + t.type(th.pushReportedValue, 'function'); + t.type(th.pushParam, 'function'); + t.type(th.peekStack, 'function'); + t.type(th.getParam, 'function'); + t.type(th.atStackTop, 'function'); + t.type(th.goToNextBlock, 'function'); + t.type(th.isRecursiveCall, 'function'); + + t.end(); +}); + +test('pushStack', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + + t.end(); +}); + +test('popStack', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + t.strictEquals(th.popStack(), 'arbitraryString'); + t.strictEquals(th.popStack(), undefined); + + t.end(); +}); + +test('atStackTop', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.pushStack('secondString'); + t.strictEquals(th.atStackTop(), false); + th.popStack(); + t.strictEquals(th.atStackTop(), true); + + t.end(); +}); + +test('reuseStackForNextBlock', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.reuseStackForNextBlock('secondString'); + t.strictEquals(th.popStack(), 'secondString'); + + t.end(); +}); + +test('peekStackFrame', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + t.strictEquals(th.peekStackFrame().warpMode, false); + th.popStack(); + t.strictEquals(th.peekStackFrame(), null); + + t.end(); +}); + +test('peekParentStackFrame', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.peekStackFrame().warpMode = true; + t.strictEquals(th.peekParentStackFrame(), null); + th.pushStack('secondString'); + t.strictEquals(th.peekParentStackFrame().warpMode, true); + + t.end(); +}); + +test('pushReportedValue', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.pushStack('secondString'); + th.pushReportedValue('value'); + t.strictEquals(th.peekParentStackFrame().reported.null, 'value'); + + t.end(); +}); + +test('peekStack', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + t.strictEquals(th.peekStack(), 'arbitraryString'); + th.popStack(); + t.strictEquals(th.peekStack(), null); + + t.end(); +}); + +test('PushGetParam', function (t) { + var th = new Thread('arbitraryString'); + th.pushStack('arbitraryString'); + th.pushParam('testParam', 'testValue'); + t.strictEquals(th.peekStackFrame().params.testParam, 'testValue'); + t.strictEquals(th.getParam('testParam'), 'testValue'); + t.end(); }); From 85eee4f18e85af2f69e9d079c665d073004a2a42 Mon Sep 17 00:00:00 2001 From: Magmaboat Date: Thu, 9 Feb 2017 21:12:47 -0500 Subject: [PATCH 13/44] Perform hasOwnProperty validation --- src/blocks/scratch3_sound.js | 1 + src/engine/adapter.js | 1 + src/engine/blocks.js | 4 ++++ src/engine/execute.js | 3 +++ src/engine/runtime.js | 3 +++ src/sprites/rendered-target.js | 6 ++++-- 6 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/blocks/scratch3_sound.js b/src/blocks/scratch3_sound.js index 20d137580..ecce15b35 100644 --- a/src/blocks/scratch3_sound.js +++ b/src/blocks/scratch3_sound.js @@ -192,6 +192,7 @@ Scratch3SoundBlocks.prototype.changeEffect = function (args, util) { Scratch3SoundBlocks.prototype.clearEffects = function (args, util) { var soundState = this._getSoundState(util.target); for (var effect in soundState.effects) { + if (!soundState.effects.hasOwnProperty(effect)) continue; soundState.effects[effect] = 0; } if (util.target.audioPlayer === null) return; diff --git a/src/engine/adapter.js b/src/engine/adapter.js index 56275e1cb..88c8a18c5 100644 --- a/src/engine/adapter.js +++ b/src/engine/adapter.js @@ -24,6 +24,7 @@ var domToBlocks = function (blocksDOM) { // Flatten blocks object into a list. var blocksList = []; for (var b in blocks) { + if (!blocks.hasOwnProperty(b)) continue; blocksList.push(blocks[b]); } return blocksList; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index ed03be324..d9172228c 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -149,6 +149,7 @@ Blocks.prototype.getTopLevelScript = function (id) { */ Blocks.prototype.getProcedureDefinition = function (name) { for (var id in this._blocks) { + if (!this._blocks.hasOwnProperty(id)) continue; var block = this._blocks[id]; if ((block.opcode === 'procedures_defnoreturn' || block.opcode === 'procedures_defreturn') && @@ -166,6 +167,7 @@ Blocks.prototype.getProcedureDefinition = function (name) { */ Blocks.prototype.getProcedureParamNames = function (name) { for (var id in this._blocks) { + if (!this._blocks.hasOwnProperty(id)) continue; var block = this._blocks[id]; if ((block.opcode === 'procedures_defnoreturn' || block.opcode === 'procedures_defreturn') && @@ -415,6 +417,7 @@ Blocks.prototype.blockToXML = function (blockId) { } // Add any inputs on this block. for (var input in block.inputs) { + if (!block.inputs.hasOwnProperty(input)) continue; var blockInput = block.inputs[input]; // Only encode a value tag if the value input is occupied. if (blockInput.block || blockInput.shadow) { @@ -431,6 +434,7 @@ Blocks.prototype.blockToXML = function (blockId) { } // Add any fields on this block. for (var field in block.fields) { + if (!block.fields.hasOwnProperty(field)) continue; var blockField = block.fields[field]; var value = blockField.value; if (typeof value === 'string') { diff --git a/src/engine/execute.js b/src/engine/execute.js index 226a866a7..598cd5783 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -110,6 +110,7 @@ var execute = function (sequencer, thread) { Object.keys(inputs).length === 0) { // One field and no inputs - treat as arg. for (var fieldKey in fields) { // One iteration. + if (!fields.hasOwnProperty(fieldKey)) continue; handleReport(fields[fieldKey].value); } } else { @@ -126,11 +127,13 @@ var execute = function (sequencer, thread) { // Add all fields on this block to the argValues. for (var fieldName in fields) { + if (!fields.hasOwnProperty(fieldName)) continue; argValues[fieldName] = fields[fieldName].value; } // Recursively evaluate input blocks. for (var inputName in inputs) { + if (!inputs.hasOwnProperty(inputName)) continue; var input = inputs[inputName]; var inputBlockId = input.block; // Is there no value for this input waiting in the stack frame? diff --git a/src/engine/runtime.js b/src/engine/runtime.js index fc3b17a06..5efb44fdd 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -441,6 +441,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, var newThreads = []; for (var opts in optMatchFields) { + if (!optMatchFields.hasOwnProperty(opts)) continue; optMatchFields[opts] = optMatchFields[opts].toUpperCase(); } @@ -463,6 +464,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, if (Object.keys(hatFields).length === 0) { var hatInputs = target.blocks.getInputs(topBlockId); for (var input in hatInputs) { + if (!hatInputs.hasOwnProperty(input)) continue; var id = hatInputs[input].block; var fields = target.blocks.getFields(id); hatFields = Object.assign(fields, hatFields); @@ -592,6 +594,7 @@ Runtime.prototype.stopAll = function () { Runtime.prototype._step = function () { // Find all edge-activated hats, and add them to threads to be evaluated. for (var hatType in this._hats) { + if (!this._hats.hasOwnProperty(hatType)) continue; var hat = this._hats[hatType]; if (hat.edgeActivated) { this.startHats(hatType); diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9bbc20b23..3f6ddf0da 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -315,6 +315,7 @@ RenderedTarget.prototype.setEffect = function (effectName, value) { */ RenderedTarget.prototype.clearEffects = function () { for (var effectName in this.effects) { + if (!this.effects.hasOwnProperty(effectName)) continue; this.effects[effectName] = 0; } if (this.renderer) { @@ -434,8 +435,9 @@ RenderedTarget.prototype.updateAllDrawableProperties = function () { costume.rotationCenterY / costume.bitmapResolution ] }; - for (var effectID in this.effects) { - props[effectID] = this.effects[effectID]; + for (var effectName in this.effects) { + if (!this.effects.hasOwnProperty(effectName)) continue; + props[effectName] = this.effects[effectName]; } this.renderer.updateDrawableProperties(this.drawableID, props); if (this.visible) { From ad5bc1afbda3e9e5fa0618a0d9de84a1a76d8b21 Mon Sep 17 00:00:00 2001 From: griffpatch Date: Fri, 10 Feb 2017 08:02:11 +0000 Subject: [PATCH 14/44] Pull white space checks into function Clean up code by pulling white space checks into a seperate helper function --- src/util/cast.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/util/cast.js b/src/util/cast.js index c2e1cfe0f..79e76b134 100644 --- a/src/util/cast.js +++ b/src/util/cast.js @@ -90,6 +90,15 @@ Cast.toRgbColorObject = function (value) { return color; }; +/** + * Determine if a Scratch argument is a white space string (or null / empty). + * @param {*} val value to check. + * @return {boolean} True if the argument is all white spaces or null / empty. + */ +Cast.isWhiteSpace = function (val) { + return val === null || typeof val === 'string' && val.trim().length === 0; +}; + /** * Compare two values, using Scratch cast, case-insensitive string compare, etc. * In Scratch 2.0, this is captured by `interp.compare.` @@ -100,10 +109,9 @@ Cast.toRgbColorObject = function (value) { Cast.compare = function (v1, v2) { var n1 = Number(v1); var n2 = Number(v2); - if (n1 === 0 && (v1 == null || typeof v1 === 'string' && v1.trim().length === 0)) { + if (n1 === 0 && Cast.isWhiteSpace(v1)) { n1 = NaN; - } - if (n2 === 0 && (v2 == null || typeof v2 === 'string' && v2.trim().length === 0)) { + } else if (n2 === 0 && Cast.isWhiteSpace(v2)) { n2 = NaN; } if (isNaN(n1) || isNaN(n2)) { From e8949dcdf5b8843f7bd1f2a4c189d7e8eb38f479 Mon Sep 17 00:00:00 2001 From: SillyInventor Date: Fri, 10 Feb 2017 13:06:39 -0500 Subject: [PATCH 15/44] Added remaining tests --- test/unit/engine_thread.js | 164 +++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/test/unit/engine_thread.js b/test/unit/engine_thread.js index a06a316a7..815de5ca3 100644 --- a/test/unit/engine_thread.js +++ b/test/unit/engine_thread.js @@ -1,5 +1,7 @@ var test = require('tap').test; var Thread = require('../../src/engine/thread'); +var RenderedTarget = require('../../src/sprites/rendered-target'); +var Sprite = require('../../src/sprites/sprite'); test('spec', function (t) { t.type(Thread, 'function'); @@ -111,3 +113,165 @@ test('PushGetParam', function (t) { t.end(); }); + +test('goToNextBlock', function (t) { + var th = new Thread('arbitraryString'); + var s = new Sprite(); + var rt = new RenderedTarget(s, null); + var block1 = {fields: Object, + id: 'arbitraryString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: 'secondString', + opcode: 'motion_movesteps', + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + var block2 = {fields: Object, + id: 'secondString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'procedures_callnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + + rt.blocks.createBlock(block1); + rt.blocks.createBlock(block2); + rt.blocks.createBlock(block2); + th.target = rt; + + t.strictEquals(th.peekStack(), null); + th.pushStack('secondString'); + t.strictEquals(th.peekStack(), 'secondString'); + th.goToNextBlock(); + t.strictEquals(th.peekStack(), null); + th.pushStack('secondString'); + th.pushStack('arbitraryString'); + t.strictEquals(th.peekStack(), 'arbitraryString'); + th.goToNextBlock(); + t.strictEquals(th.peekStack(), 'secondString'); + th.goToNextBlock(); + t.strictEquals(th.peekStack(), null); + + t.end(); +}); + +test('stopThisScript', function (t) { + var th = new Thread('arbitraryString'); + var s = new Sprite(); + var rt = new RenderedTarget(s, null); + var block1 = {fields: Object, + id: 'arbitraryString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'motion_movesteps', + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + var block2 = {fields: Object, + id: 'secondString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'procedures_callnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + + rt.blocks.createBlock(block1); + rt.blocks.createBlock(block2); + th.target = rt; + + th.stopThisScript(); + t.strictEquals(th.peekStack(), null); + th.pushStack('arbitraryString'); + t.strictEquals(th.peekStack(), 'arbitraryString'); + th.stopThisScript(); + t.strictEquals(th.peekStack(), null); + th.pushStack('arbitraryString'); + th.pushStack('secondString'); + th.stopThisScript(); + t.strictEquals(th.peekStack(), 'secondString'); + + t.end(); +}); + +test('isRecursiveCall', function (t) { + var th = new Thread('arbitraryString'); + var s = new Sprite(); + var rt = new RenderedTarget(s, null); + var block1 = {fields: Object, + id: 'arbitraryString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'motion_movesteps', + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + var block2 = {fields: Object, + id: 'secondString', + inputs: Object, + STEPS: Object, + block: 'fakeBlock', + name: 'STEPS', + next: null, + opcode: 'procedures_callnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + + rt.blocks.createBlock(block1); + rt.blocks.createBlock(block2); + th.target = rt; + + t.strictEquals(th.isRecursiveCall('fakeCode'), false); + th.pushStack('secondString'); + t.strictEquals(th.isRecursiveCall('fakeCode'), false); + th.pushStack('arbitraryString'); + t.strictEquals(th.isRecursiveCall('fakeCode'), true); + th.pushStack('arbitraryString'); + t.strictEquals(th.isRecursiveCall('fakeCode'), true); + th.popStack(); + t.strictEquals(th.isRecursiveCall('fakeCode'), true); + th.popStack(); + t.strictEquals(th.isRecursiveCall('fakeCode'), false); + th.popStack(); + t.strictEquals(th.isRecursiveCall('fakeCode'), false); + + t.end(); +}); From 243d68f88bef66d085870177b179341cad63d8ec Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Fri, 10 Feb 2017 14:21:37 -0800 Subject: [PATCH 16/44] Use Webpack to generate the whole playground The previous configuration mixed Webpack output with static content in order to create the playground. This change moves that static content from `/playground/` into `/src/playground/` and adds a Webpack rule to copy it into the playground as part of the build. On the surface this might seem unnecessary, but it provides at least two benefits: - It's no longer possible to accidentally load stale build output through `webpack-dev-server` in the event of a misconfiguration. This was very easy in the previous configuration, and might in fact be the only way that `webpack-dev-server` ever worked for this repository. - It's simpler to ensure that various rules apply to the hand-authored content and not build outputs. This includes lint rules, `.gitignore`, IDE symbol search paths, etc. --- .gitignore | 8 +--- {playground => src/playground}/index.html | 0 {playground => src/playground}/playground.css | 0 {playground => src/playground}/playground.js | 41 +++++++++---------- webpack.config.js | 4 +- 5 files changed, 24 insertions(+), 29 deletions(-) rename {playground => src/playground}/index.html (100%) rename {playground => src/playground}/playground.css (100%) rename {playground => src/playground}/playground.js (92%) diff --git a/.gitignore b/.gitignore index fdf767042..2f3825877 100644 --- a/.gitignore +++ b/.gitignore @@ -14,10 +14,4 @@ npm-* # Build /dist -/playground/assets -/playground/media -/playground/scratch-vm.js -/playground/scratch-vm.js.map -/playground/vendor.js -/playground/vendor.js.map -/playground/zenburn.css +/playground diff --git a/playground/index.html b/src/playground/index.html similarity index 100% rename from playground/index.html rename to src/playground/index.html diff --git a/playground/playground.css b/src/playground/playground.css similarity index 100% rename from playground/playground.css rename to src/playground/playground.css diff --git a/playground/playground.js b/src/playground/playground.js similarity index 92% rename from playground/playground.js rename to src/playground/playground.js index cb5a81a3e..9ee924122 100644 --- a/playground/playground.js +++ b/src/playground/playground.js @@ -1,4 +1,3 @@ - var loadProject = function () { var id = location.hash.substring(1); if (id.length < 1 || !isFinite(id)) { @@ -7,7 +6,7 @@ var loadProject = function () { var url = 'https://projects.scratch.mit.edu/internalapi/project/' + id + '/get/'; var r = new XMLHttpRequest(); - r.onreadystatechange = function() { + r.onreadystatechange = function () { if (this.readyState === 4) { if (r.status === 200) { window.vm.loadProject(this.responseText); @@ -18,7 +17,7 @@ var loadProject = function () { r.send(); }; -window.onload = function() { +window.onload = function () { // Lots of global variables to make debugging easier // Instantiate the VM. var vm = new window.VirtualMachine(); @@ -74,7 +73,7 @@ window.onload = function() { // Playground data tabs. // Block representation tab. var blockexplorer = document.getElementById('blockexplorer'); - var updateBlockExplorer = function(blocks) { + var updateBlockExplorer = function (blocks) { blockexplorer.innerHTML = JSON.stringify(blocks, null, 2); window.hljs.highlightBlock(blockexplorer); }; @@ -83,7 +82,7 @@ window.onload = function() { var threadexplorer = document.getElementById('threadexplorer'); var cachedThreadJSON = ''; var updateThreadExplorer = function (newJSON) { - if (newJSON != cachedThreadJSON) { + if (newJSON !== cachedThreadJSON) { cachedThreadJSON = newJSON; threadexplorer.innerHTML = cachedThreadJSON; window.hljs.highlightBlock(threadexplorer); @@ -101,7 +100,7 @@ window.onload = function() { // VM handlers. // Receipt of new playground data (thread, block representations). - vm.on('playgroundData', function(data) { + vm.on('playgroundData', function (data) { updateThreadExplorer(data.threads); updateBlockExplorer(data.blocks); }); @@ -125,7 +124,7 @@ window.onload = function() { var targetOption = document.createElement('option'); targetOption.setAttribute('value', data.targetList[i].id); // If target id matches editingTarget id, select it. - if (data.targetList[i].id == data.editingTarget) { + if (data.targetList[i].id === data.editingTarget) { targetOption.setAttribute('selected', 'selected'); } targetOption.appendChild( @@ -139,23 +138,23 @@ window.onload = function() { }; // Feedback for stacks and blocks running. - vm.on('SCRIPT_GLOW_ON', function(data) { + vm.on('SCRIPT_GLOW_ON', function (data) { workspace.glowStack(data.id, true); }); - vm.on('SCRIPT_GLOW_OFF', function(data) { + vm.on('SCRIPT_GLOW_OFF', function (data) { workspace.glowStack(data.id, false); }); - vm.on('BLOCK_GLOW_ON', function(data) { + vm.on('BLOCK_GLOW_ON', function (data) { workspace.glowBlock(data.id, true); }); - vm.on('BLOCK_GLOW_OFF', function(data) { + vm.on('BLOCK_GLOW_OFF', function (data) { workspace.glowBlock(data.id, false); }); - vm.on('VISUAL_REPORT', function(data) { + vm.on('VISUAL_REPORT', function (data) { workspace.reportValue(data.id, data.value); }); - vm.on('SPRITE_INFO_REPORT', function(data) { + vm.on('SPRITE_INFO_REPORT', function (data) { if (data.id !== selectedTarget.value) return; // Not the editingTarget document.getElementById('sinfo-x').value = data.x; document.getElementById('sinfo-y').value = data.y; @@ -213,7 +212,7 @@ window.onload = function() { // Feed keyboard events as VM I/O events. document.addEventListener('keydown', function (e) { // Don't capture keys intended for Blockly inputs. - if (e.target != document && e.target != document.body) { + if (e.target !== document && e.target !== document.body) { return; } window.vm.postIOData('keyboard', { @@ -222,7 +221,7 @@ window.onload = function() { }); e.preventDefault(); }); - document.addEventListener('keyup', function(e) { + document.addEventListener('keyup', function (e) { // Always capture up events, // even those that have switched to other targets. window.vm.postIOData('keyboard', { @@ -230,7 +229,7 @@ window.onload = function() { isDown: false }); // E.g., prevent scroll. - if (e.target != document && e.target != document.body) { + if (e.target !== document && e.target !== document.body) { e.preventDefault(); } }); @@ -239,25 +238,25 @@ window.onload = function() { vm.start(); // Inform VM of animation frames. - var animate = function() { + var animate = function () { stats.update(); requestAnimationFrame(animate); }; requestAnimationFrame(animate); // Handlers for green flag and stop all. - document.getElementById('greenflag').addEventListener('click', function() { + document.getElementById('greenflag').addEventListener('click', function () { vm.greenFlag(); }); - document.getElementById('stopall').addEventListener('click', function() { + document.getElementById('stopall').addEventListener('click', function () { vm.stopAll(); }); - document.getElementById('turbomode').addEventListener('change', function() { + document.getElementById('turbomode').addEventListener('change', function () { var turboOn = document.getElementById('turbomode').checked; vm.setTurboMode(turboOn); }); document.getElementById('compatmode').addEventListener('change', - function() { + function () { var compatibilityMode = document.getElementById('compatmode').checked; vm.setCompatibilityMode(compatibilityMode); }); diff --git a/webpack.config.js b/webpack.config.js index f58b7a1bf..29edbfcd1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,7 @@ var webpack = require('webpack'); var base = { devServer: { - contentBase: path.resolve(__dirname, 'playground'), + contentBase: false, host: '0.0.0.0', port: process.env.PORT || 8073 }, @@ -108,6 +108,8 @@ module.exports = [ to: 'media' }, { from: 'node_modules/highlightjs/styles/zenburn.css' + }, { + from: 'src/playground' }]) ]) }) From 4d63dcac3b4f2e12751f0a95aa36ea82d2ab0d10 Mon Sep 17 00:00:00 2001 From: griffpatch Date: Sat, 11 Feb 2017 14:30:18 +0000 Subject: [PATCH 17/44] Rename fencing method --- src/sprites/rendered-target.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 52d5d6471..cac450e1b 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -170,7 +170,7 @@ RenderedTarget.prototype.setXY = function (x, y) { this.x = x; this.y = y; if (this.renderer) { - this.renderer.getFencedPositionOfDrawable(this.drawableID, this); + this.renderer.fencePositionOfDrawable(this.drawableID, this); this.renderer.updateDrawableProperties(this.drawableID, { position: [this.x, this.y] }); From bd76395676c3af1ad276c306b81fabe470088d19 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 13 Feb 2017 14:44:51 -0800 Subject: [PATCH 18/44] Minor cleanup around cloning methods --- src/sprites/rendered-target.js | 6 +++--- src/sprites/sprite.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9bbc20b23..0a0a115f9 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -582,7 +582,7 @@ RenderedTarget.prototype.goBackLayers = function (nLayers) { /** * Move behind some other rendered target. - * @param {!Clone} other Other rendered target to move behind. + * @param {!RenderedTarget} other Other rendered target to move behind. */ RenderedTarget.prototype.goBehindOther = function (other) { if (this.renderer) { @@ -637,11 +637,11 @@ RenderedTarget.prototype.keepInFence = function (newX, newY, optFence) { /** * Make a clone, copying any run-time properties. * If we've hit the global clone limit, returns null. - * @return {!RenderedTarget} New clone. + * @return {RenderedTarget} New clone. */ RenderedTarget.prototype.makeClone = function () { if (!this.runtime.clonesAvailable() || this.isStage) { - return; // Hit max clone limit, or this is the stage. + return null; // Hit max clone limit, or this is the stage. } this.runtime.changeCloneCounter(1); var newClone = this.sprite.createClone(); diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js index bccc5d909..81626f3a7 100644 --- a/src/sprites/sprite.js +++ b/src/sprites/sprite.js @@ -46,7 +46,7 @@ var Sprite = function (blocks, runtime) { /** * Create a clone of this sprite. - * @returns {!Clone} Newly created clone. + * @returns {!RenderedTarget} Newly created clone. */ Sprite.prototype.createClone = function () { var newClone = new RenderedTarget(this, this.runtime); From 18107cde7b26ffe954e5a98ffc86e9ec655ec6c6 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Mon, 13 Feb 2017 14:35:50 -0800 Subject: [PATCH 19/44] Reverse target iteration in `allScriptsDo` The `allStacksAndOwnersDo` function in Scratch 2.0 runtime iterates targets in reverse and projects sometimes rely on that for correct initialization. If, for example, each sprite runs a "go back 999 layers" or "go to front" block as its first action, the order of execution will determine the ordering of the layers. This change makes Scratch 3.0 match the Scratch 2.0 execution order. --- src/engine/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index fc3b17a06..d9e59d36a 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -414,7 +414,7 @@ Runtime.prototype.allScriptsDo = function (f, optTarget) { if (optTarget) { targets = [optTarget]; } - for (var t = 0; t < targets.length; t++) { + for (var t = targets.length - 1; t >= 0; t--) { var target = targets[t]; var scripts = target.blocks.getScripts(); for (var j = 0; j < scripts.length; j++) { From 75e3f6002bc688cc4e82cc66b2fc1dc78c8b497a Mon Sep 17 00:00:00 2001 From: griffpatch Date: Tue, 14 Feb 2017 09:12:35 +0000 Subject: [PATCH 20/44] Fencing - Take 3 Dependant on scratch-render pull request. --- src/sprites/rendered-target.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index cac450e1b..3e1e06e12 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -167,16 +167,20 @@ RenderedTarget.prototype.setXY = function (x, y) { } var oldX = this.x; var oldY = this.y; - this.x = x; - this.y = y; if (this.renderer) { - this.renderer.fencePositionOfDrawable(this.drawableID, this); + var position = this.renderer.getFencedPositionOfDrawable(this.drawableID, [x, y]); + this.x = position.x; + this.y = position.y; + this.renderer.updateDrawableProperties(this.drawableID, { - position: [this.x, this.y] + position: position }); if (this.visible) { this.runtime.requestRedraw(); } + } else { + this.x = x; + this.y = y; } this.emit(RenderedTarget.EVENT_TARGET_MOVED, this, oldX, oldY); this.runtime.spriteInfoReport(this); From a69950add4d0e2f19bad940b0875b7ec2fcf08f2 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 14 Feb 2017 18:57:35 +0000 Subject: [PATCH 21/44] chore(package): update expose-loader to version 0.7.3 Closes #454 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c04928eec..37bd5086d 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "copy-webpack-plugin": "4.0.1", "eslint": "3.15.0", "eslint-config-scratch": "^3.1.0", - "expose-loader": "0.7.1", + "expose-loader": "0.7.3", "gh-pages": "0.12.0", "highlightjs": "9.8.0", "htmlparser2": "3.9.2", From fba054cc68760b6fd01a516dfc9ddbbc883639a6 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Tue, 14 Feb 2017 19:26:13 +0000 Subject: [PATCH 22/44] chore(package): update tap to version 10.1.1 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c04928eec..49d4c8043 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "scratch-render": "latest", "script-loader": "0.7.0", "stats.js": "0.17.0", - "tap": "10.1.0", + "tap": "10.1.1", "travis-after-all": "1.4.4", "webpack": "2.2.1", "webpack-dev-server": "2.3.0" From 3b3daf8aca77530823036a37fb45ca94755cf38d Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Tue, 14 Feb 2017 16:33:42 -0800 Subject: [PATCH 23/44] Add hat execution order test --- test/fixtures/hat-execution-order.sb2 | Bin 0 -> 4547 bytes test/integration/hat-execution-order.js | 39 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 test/fixtures/hat-execution-order.sb2 create mode 100644 test/integration/hat-execution-order.js diff --git a/test/fixtures/hat-execution-order.sb2 b/test/fixtures/hat-execution-order.sb2 new file mode 100644 index 0000000000000000000000000000000000000000..f9a7efdcc1583a0df68400c77cb46467fccf8841 GIT binary patch literal 4547 zcmeI0X;>528pnr(#1IIg$Rdkj-y2aj(NI9yLIlFDUJwFdUlJgK@W@((f(QjE)wW1M zx$FocDQXH_30{`UzAFkC4MqtjA*j{rorw2hxz6iCH{$C0C#ynM-bO?M>6r=0GrlBHPW zjD@Tgn;!`=#+|7=Z$?UND{3?_y{bcqy?3oA6>X`JIwqMFWxZ#|x2jfHI@96>PwA^N z7dxBF=tln>=9ucFvcWfbGDo70LK&m#Ra+&Dt^M)UTupP7|&9XW50N1H}!; z$|*28LThNqm$gKHOEj%4;$lLjLK3FcD(93dyOL_fN$ekWtXgIwWmc-=3Qf<^o3E3x z3C|W^JkF}e*Zbiazp;?AOfRD7_wc80u1U+~PU`MFaJjWL`ssDgf@zO>_Nl6Pqg}aV zn^C#wx#{Z=UvUGzl|4hENGCtm65o|k>Dm*@W+kSFZF^;qrxnqLrlkeB&wE6Xa!~=| z_(dwlMQ~}Epvv4%l~Da*#%MUcPLm_GRG1!)oc6`oJ`_y5g>oUg2nr)B;?H^anhK5- z!5z|nGtfD>Vt5I6tAc?~?!#LCXe7lb{%vu1K)CpYar2aCukmBtC7#69Dd_E6kdb}eDjC?gqmVsA#my2$>8#+{ z$Cn+l`|aMWaNN!wg5KjRbsrrs{^^ibI?;`%=k4=$PR+IZie;$1yRG3e!#wXQSwA8kL1kdFvz}z%*W$JZ(xGjW0gWX2EX?R}qWPWb?-=HX zD9}MCB=zTu<{X6^HLOlE4p(o|(MMDQK1KQjX@DFeA1If;{{M z_YIbRH5<8Fu}Zt&RlgN3c0u+V&ojJ8;+5k}w^IwAu++CwhkaPxKHFrUPFzpAqGS}4 zN_|XbmA@M?k0_R6oo}Bn%_R+Io*p=3)&NLCQ0Z_QGttB>dZtbOf`n_z9+}eWJxF*)-zFBgbz#jzh@#H@w{v(!6nEM5z<&y-zo`9@joE}`{@hpTbJQLH_;9h(xVh=v z{{s@sxThNpK_m-f&BrDXQV3Vg5#cy7}N05vZV-+&ZAB2tN> z;63926#C1SSk$tNyE0b&2aN@lI@vh5C)aPrpK-P&tfr{?QL}xw^*kT4q%~~sNK7WH zQ;@^~8@93@e`{}e)$fIxXF2E1TNh^+Bue+&_*3Fr6DvZeY(wN6nUSqYMfcMqvSP~e zdsN>n7$*BZNMxp(#nd>_lxkQmZFwcooUlTdM!~d?1b`*@EtBQKyqu95F}$PQA(v?9 zPF{FubyZM^eODnNsx*HmFt8TO?4mGV-?n<)kaqV-BuuMj?UX4zq zx1(vE?e*hyfu4MZk-$3B%3!yIv^TG8yroOfLGAzXVG<-sCC4<&a%A6rjV^c*OK~8p z1JNln==vlAk)o-xA7S0PuSDG)zI)MYQ12G|*0Et28@HucZPH{2`^>~q{g{|NM{z2l zAJrc`L1s5Yigp_5oiwb~$11NNNc@iZw#n6r`Ug_WcSLlhebCkF4(K}RD^NE+73vudO7O#(8 z!peLyxBTkm7z82iC{?s=TGC8xCnWK8>1xT!tYGpL`dz;0cL+ITF2Ylk_?CbdfgpiD z|4{)hpO21@e_O0qetKKAUX}+sgZG=PjU^j>$9l;{#0M9$v1FqUSTC^_```*TmTc6? ddWnamaA2Tn2zZ!qp9jha2Eaj|P$iTN@Gm;r?+O3_ literal 0 HcmV?d00001 diff --git a/test/integration/hat-execution-order.js b/test/integration/hat-execution-order.js new file mode 100644 index 000000000..0a302624b --- /dev/null +++ b/test/integration/hat-execution-order.js @@ -0,0 +1,39 @@ +var path = require('path'); +var test = require('tap').test; +var extract = require('../fixtures/extract'); +var VirtualMachine = require('../../src/index'); + +var projectUri = path.resolve(__dirname, '../fixtures/hat-execution-order.sb2'); +var project = extract(projectUri); + +test('complex', function (t) { + var vm = new VirtualMachine(); + + // Evaluate playground data and exit + vm.on('playgroundData', function (e) { + var threads = JSON.parse(e.threads); + t.ok(threads.length === 0); + + var results = vm.runtime.targets[0].lists.results.contents; + t.deepEqual(results, ['3', '2', '1', 'stage']); + + t.end(); + process.nextTick(process.exit); + }); + + // Start VM, load project, and run + t.doesNotThrow(function () { + vm.start(); + vm.clear(); + vm.setCompatibilityMode(false); + vm.setTurboMode(false); + vm.loadProject(project); + vm.greenFlag(); + }); + + // After two seconds, get playground data and stop + setTimeout(function () { + vm.getPlaygroundData(); + vm.stopAll(); + }, 2000); +}); From 39158c9d48831303a65b0d604b03cb4d27f2d039 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 15 Feb 2017 09:40:10 -0500 Subject: [PATCH 24/44] Revert "Feature fencing" --- src/sprites/rendered-target.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 3e1e06e12..9bbc20b23 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -167,20 +167,15 @@ RenderedTarget.prototype.setXY = function (x, y) { } var oldX = this.x; var oldY = this.y; + this.x = x; + this.y = y; if (this.renderer) { - var position = this.renderer.getFencedPositionOfDrawable(this.drawableID, [x, y]); - this.x = position.x; - this.y = position.y; - this.renderer.updateDrawableProperties(this.drawableID, { - position: position + position: [this.x, this.y] }); if (this.visible) { this.runtime.requestRedraw(); } - } else { - this.x = x; - this.y = y; } this.emit(RenderedTarget.EVENT_TARGET_MOVED, this, oldX, oldY); this.runtime.spriteInfoReport(this); From 7bc141e76e1f56714c2076a9205e9165fbe259a8 Mon Sep 17 00:00:00 2001 From: griffpatch Date: Wed, 15 Feb 2017 15:59:54 +0000 Subject: [PATCH 25/44] Feature Fencing This is a re-comit after the previous pull was reverted. --- src/sprites/rendered-target.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9bbc20b23..a60c35084 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -167,15 +167,20 @@ RenderedTarget.prototype.setXY = function (x, y) { } var oldX = this.x; var oldY = this.y; - this.x = x; - this.y = y; if (this.renderer) { + var position = this.renderer.getFencedPositionOfDrawable(this.drawableID, [x, y]); + this.x = position[0]; + this.y = position[1]; + this.renderer.updateDrawableProperties(this.drawableID, { - position: [this.x, this.y] + position: position }); if (this.visible) { this.runtime.requestRedraw(); } + } else { + this.x = x; + this.y = y; } this.emit(RenderedTarget.EVENT_TARGET_MOVED, this, oldX, oldY); this.runtime.spriteInfoReport(this); From af770981611c2cba9a72b8353430721b6c17cd54 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford Date: Wed, 15 Feb 2017 15:31:20 -0800 Subject: [PATCH 26/44] Update playground paths in README.md A recent change moved the location of the playground source files and the URL used to access the playground when running the dev server. This change updates README.md to reflect the new locations. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2888fc6c0..512bc1645 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ npm start ``` ## Playground -To run the Playground, make sure the dev server's running and go to [http://localhost:8073/](http://localhost:8073/) - you will be directed to the playground, which demonstrates various tools and internal state. +To run the Playground, make sure the dev server's running and go to [http://localhost:8073/playground/](http://localhost:8073/playground/) - you will be directed to the playground, which demonstrates various tools and internal state. ![VM Playground Screenshot](https://i.imgur.com/nOCNqEc.gif) @@ -50,7 +50,7 @@ npm run build ``` ## How to include in a Node.js App -For an extended setup example, check out the /playground directory, which includes a fully running VM instance. +For an extended setup example, check out the /src/playground directory, which includes a fully running VM instance. ```js var VirtualMachine = require('scratch-vm'); var vm = new VirtualMachine(); From 2fc2ee2c4bfc781bc4be3d98e925093f869e5abd Mon Sep 17 00:00:00 2001 From: SillyInventor Date: Thu, 16 Feb 2017 14:30:00 -0500 Subject: [PATCH 27/44] Adding sequencer unit tests for each function --- test/unit/engine_sequencer.js | 172 +++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 1 deletion(-) diff --git a/test/unit/engine_sequencer.js b/test/unit/engine_sequencer.js index 23ec4c13d..1e000ad36 100644 --- a/test/unit/engine_sequencer.js +++ b/test/unit/engine_sequencer.js @@ -1,8 +1,178 @@ var test = require('tap').test; var Sequencer = require('../../src/engine/sequencer'); +var Runtime = require('../../src/engine/runtime'); +var Thread = require('../../src/engine/thread'); +var RenderedTarget = require('../../src/sprites/rendered-target'); +var Sprite = require('../../src/sprites/sprite'); test('spec', function (t) { t.type(Sequencer, 'function'); - // @todo + + var r = new Runtime(); + var s = new Sequencer(r); + + t.type(s, 'object'); + t.ok(s instanceof Sequencer); + + t.type(s.stepThreads, 'function'); + t.type(s.stepThread, 'function'); + t.type(s.stepToBranch, 'function'); + t.type(s.stepToProcedure, 'function'); + t.type(s.retireThread, 'function'); + + t.end(); +}); + +var randomString = function () { + var top = Math.random().toString(36); + return top.substring(7); +}; + +var generateBlock = function (id) { + var block = {fields: Object, + id: id, + inputs: {}, + STEPS: Object, + block: 'fakeBlock', + name: 'fakeName', + next: null, + opcode: 'procedures_defnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + return block; +}; + +var generateBlockInput = function (id, next, inp) { + var block = {fields: Object, + id: id, + inputs: {SUBSTACK: {block: inp, name: 'SUBSTACK'}}, + STEPS: Object, + block: 'fakeBlock', + name: 'fakeName', + next: next, + opcode: 'procedures_defnoreturn', + mutation: {proccode: 'fakeCode'}, + parent: null, + shadow: false, + topLevel: true, + x: 0, + y: 0 + }; + return block; +}; + +var generateThread = function (runtime) { + var s = new Sprite(); + var rt = new RenderedTarget(s, runtime); + var th = new Thread(randomString()); + + var next = randomString(); + var inp = randomString(); + var name = th.topBlock; + + rt.blocks.createBlock(generateBlockInput(name, next, inp)); + th.pushStack(name); + rt.blocks.createBlock(generateBlock(inp)); + + for (var i = 0; i < 10; i++) { + name = next; + next = randomString(); + inp = randomString(); + + rt.blocks.createBlock(generateBlockInput(name, next, inp)); + th.pushStack(name); + rt.blocks.createBlock(generateBlock(inp)); + } + rt.blocks.createBlock(generateBlock(next)); + th.pushStack(next); + th.target = rt; + + runtime.threads.push(th); + + return th; +}; + +test('stepThread', function (t) { + var r = new Runtime(); + var s = new Sequencer(r); + var th = generateThread(r); + t.notEquals(th.status, Thread.STATUS_DONE); + s.stepThread(th); + t.strictEquals(th.status, Thread.STATUS_DONE); + th = generateThread(r); + th.status = Thread.STATUS_YIELD; + s.stepThread(th); + t.notEquals(th.status, Thread.STATUS_DONE); + th.status = Thread.STATUS_PROMISE_WAIT; + s.stepThread(th); + t.notEquals(th.status, Thread.STATUS_DONE); + + t.end(); +}); + +test('stepToBranch', function (t) { + var r = new Runtime(); + var s = new Sequencer(r); + var th = generateThread(r); + s.stepToBranch(th, 2, false); + t.strictEquals(th.peekStack(), null); + th.popStack(); + s.stepToBranch(th, 1, false); + t.strictEquals(th.peekStack(), null); + th.popStack(); + th.popStack(); + s.stepToBranch(th, 1, false); + t.notEquals(th.peekStack(), null); + + t.end(); +}); + +test('retireThread', function (t) { + var r = new Runtime(); + var s = new Sequencer(r); + var th = generateThread(r); + t.strictEquals(th.stack.length, 12); + s.retireThread(th); + t.strictEquals(th.stack.length, 0); + t.strictEquals(th.status, Thread.STATUS_DONE); + + t.end(); +}); + +test('stepToProcedure', function (t) { + var r = new Runtime(); + var s = new Sequencer(r); + var th = generateThread(r); + var expectedBlock = th.peekStack(); + s.stepToProcedure(th, ''); + t.strictEquals(th.peekStack(), expectedBlock); + s.stepToProcedure(th, 'faceCode'); + t.strictEquals(th.peekStack(), expectedBlock); + s.stepToProcedure(th, 'faceCode'); + th.target.blocks.getBlock(th.stack[th.stack.length - 4]).mutation.proccode = 'othercode'; + expectedBlock = th.stack[th.stack.length - 4]; + s.stepToProcedure(th, 'othercode'); + t.strictEquals(th.peekStack(), expectedBlock); + + + t.end(); +}); + +test('stepThreads', function (t) { + var r = new Runtime(); + r.currentStepTime = Infinity; + var s = new Sequencer(r); + t.strictEquals(s.stepThreads().length, 0); + generateThread(r); + t.strictEquals(r.threads.length, 1); + t.strictEquals(s.stepThreads().length, 0); + r.threads[0].status = Thread.STATUS_RUNNING; + t.strictEquals(s.stepThreads().length, 1); + t.end(); }); From 9f364b93eb850da9fa9713c5191b85d713c9806d Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sat, 18 Feb 2017 02:13:29 +0000 Subject: [PATCH 28/44] chore(package): update tap to version 10.1.2 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49d4c8043..459a971a0 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "scratch-render": "latest", "script-loader": "0.7.0", "stats.js": "0.17.0", - "tap": "10.1.1", + "tap": "10.1.2", "travis-after-all": "1.4.4", "webpack": "2.2.1", "webpack-dev-server": "2.3.0" From d7bff34a6602782698f0b9e83a7631ebfc31d22c Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 19 Feb 2017 01:19:16 +0000 Subject: [PATCH 29/44] chore(package): update tap to version 10.2.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 459a971a0..062243db6 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "scratch-render": "latest", "script-loader": "0.7.0", "stats.js": "0.17.0", - "tap": "10.1.2", + "tap": "10.2.0", "travis-after-all": "1.4.4", "webpack": "2.2.1", "webpack-dev-server": "2.3.0" From 0118a19cd52710b5c4a081e64c49295d67fa27d6 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Sun, 19 Feb 2017 22:15:28 +0000 Subject: [PATCH 30/44] chore(package): update webpack-dev-server to version 2.4.1 Closes #470 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 459a971a0..5f14710c9 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,6 @@ "tap": "10.1.2", "travis-after-all": "1.4.4", "webpack": "2.2.1", - "webpack-dev-server": "2.3.0" + "webpack-dev-server": "2.4.1" } } From 362a2312796e4fda1a3201241c5de671a2c86e5a Mon Sep 17 00:00:00 2001 From: griffpatch Date: Mon, 20 Feb 2017 08:20:21 +0000 Subject: [PATCH 31/44] bug - escape from conditional branches Execution bug: can't escape from conditional branches that end with a Promise-resolution-terminating command block (see #464) --- src/engine/execute.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 226a866a7..5d2734dde 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -233,8 +233,15 @@ var execute = function (sequencer, thread) { primitiveReportedValue.then(function (resolvedValue) { handleReport(resolvedValue); if (typeof resolvedValue === 'undefined') { - var popped = thread.popStack(); - var nextBlockId = thread.target.blocks.getNextBlock(popped); + do { + // In the case that the promise is the last block in the current thread stack + // We need to pop out repeatedly until we find the next block. + var popped = thread.popStack(); + if (popped === null) { + return; + } + var nextBlockId = thread.target.blocks.getNextBlock(popped); + } while (nextBlockId === null); thread.pushStack(nextBlockId); } else { thread.popStack(); From c5eb8ece1628cf0d142b27d76238a8551f6f40de Mon Sep 17 00:00:00 2001 From: griffpatch Date: Mon, 20 Feb 2017 09:40:32 +0000 Subject: [PATCH 32/44] Handle actual looping cases And it starts to get a little less elegant :/ Wondering if this should not be handled better in another part of the codebase? We don't want to be duplicating existing code stepping functionality locally at the end of the promise script really... What do you think? --- src/engine/execute.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 5d2734dde..7ff0203ec 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -241,7 +241,15 @@ var execute = function (sequencer, thread) { return; } var nextBlockId = thread.target.blocks.getNextBlock(popped); - } while (nextBlockId === null); + if (nextBlockId !== null) { + // A next block exists so break out this loop + break; + } + // Investigate the next block and if not in a loop, + // then repeat and pop the next item off the stack frame + var stackFrame = thread.peekStackFrame(); + } while (stackFrame !== null && !stackFrame.isLoop); + thread.pushStack(nextBlockId); } else { thread.popStack(); From 867d3c79f50144c082d1cad0fc71e6db4a1cf6bd Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Mon, 20 Feb 2017 16:37:01 +0000 Subject: [PATCH 33/44] chore(package): update eslint to version 3.16.0 https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 459a971a0..fd47e7fe3 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "adm-zip": "0.4.7", "babel-eslint": "7.1.1", "copy-webpack-plugin": "4.0.1", - "eslint": "3.15.0", + "eslint": "3.16.0", "eslint-config-scratch": "^3.1.0", "expose-loader": "0.7.1", "gh-pages": "0.12.0", From ed4b0080b6e379c3e19d9230cb683b9a9e774cf6 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 20 Feb 2017 17:13:40 -0500 Subject: [PATCH 34/44] Use caret ranges for dev dependencies These packages don't affect the output of the built package, so don't require a specific version. This should quiet Greenkeeper down a bit about our dependencies. --- package.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index f1f497ad7..3ba71fb0d 100644 --- a/package.json +++ b/package.json @@ -25,15 +25,15 @@ }, "devDependencies": { "adm-zip": "0.4.7", - "babel-eslint": "7.1.1", + "babel-eslint": "^7.1.1", "copy-webpack-plugin": "4.0.1", - "eslint": "3.16.0", + "eslint": "^3.16.0", "eslint-config-scratch": "^3.1.0", "expose-loader": "0.7.3", - "gh-pages": "0.12.0", - "highlightjs": "9.8.0", + "gh-pages": "^0.12.0", + "highlightjs": "^9.8.0", "htmlparser2": "3.9.2", - "json": "9.0.4", + "json": "^9.0.4", "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", "promise": "7.1.1", @@ -41,10 +41,10 @@ "scratch-blocks": "latest", "scratch-render": "latest", "script-loader": "0.7.0", - "stats.js": "0.17.0", - "tap": "10.2.0", - "travis-after-all": "1.4.4", + "stats.js": "^0.17.0", + "tap": "^10.2.0", + "travis-after-all": "^1.4.4", "webpack": "2.2.1", - "webpack-dev-server": "2.4.1" + "webpack-dev-server": "^2.4.1" } } From 99e1b242852102b04b1f4a5ee79402d0cc3e0304 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Tue, 14 Feb 2017 17:06:16 -0500 Subject: [PATCH 35/44] Add rudimentary toolbox filtering method --- src/virtual-machine.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 003c80e9f..52616e7c2 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -336,4 +336,23 @@ VirtualMachine.prototype.postSpriteInfo = function (data) { this.editingTarget.postSpriteInfo(data); }; +VirtualMachine.prototype.filterToolbox = function (toolboxDOM) { + var filteredToolbox = toolboxDOM.cloneNode(); + var category = toolboxDOM.firstElementChild; + while (category) { + var filteredCategory = category.cloneNode(); + var block = category.firstElementChild; + while (block) { + var opcode = block.getAttribute('type'); + if (opcode in this.runtime._primitives || opcode in this.runtime._hats) { + filteredCategory.appendChild(block.cloneNode(true)); + } + block = block.nextElementSibling; + } + if (filteredCategory.hasChildNodes()) filteredToolbox.appendChild(filteredCategory); + category = category.nextElementSibling; + } + return filteredToolbox; +}; + module.exports = VirtualMachine; From a9b495fced0d883ea62795d584b1cb5e11f9cfad Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 15 Feb 2017 15:24:48 -0500 Subject: [PATCH 36/44] Filter the playground toolbox --- src/playground/playground.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/playground/playground.js b/src/playground/playground.js index 9ee924122..f32e17ef9 100644 --- a/src/playground/playground.js +++ b/src/playground/playground.js @@ -59,6 +59,11 @@ window.onload = function () { }); window.workspace = workspace; + // Filter available blocks + var toolbox = vm.filterToolbox(workspace.options.languageTree); + // var toolbox = workspace.options.languageTree; + workspace.updateToolbox(toolbox); + // Attach scratch-blocks events to VM. workspace.addChangeListener(vm.blockListener); var flyoutWorkspace = workspace.getFlyout().getWorkspace(); From 390b2373e9efae553d2337ad1d356e7ca626b7fb Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 15 Feb 2017 19:16:37 -0500 Subject: [PATCH 37/44] Refactor filter methods into util --- src/util/filter-toolbox.js | 49 ++++++++++++++++++++++++++++++++++++++ src/virtual-machine.js | 29 ++++++++++------------ 2 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 src/util/filter-toolbox.js diff --git a/src/util/filter-toolbox.js b/src/util/filter-toolbox.js new file mode 100644 index 000000000..c4b18b41c --- /dev/null +++ b/src/util/filter-toolbox.js @@ -0,0 +1,49 @@ +/** + * Filter Blockly toolbox XML node containing blocks to only those with + * valid opcodes. Return a copy of the node with valid blocks. + * @param {HTMLElement} node Blockly toolbox XML node + * @param {Array.} opcodes Valid opcodes. Blocks producing other opcodes + * will be filtered. + * @returns {HTMLElement} filtered toolbox XML node + */ +var filterToolboxNode = function (node, opcodes) { + var filteredCategory = node.cloneNode(); + for (var block = node.firstElementChild; block; block = block.nextElementSibling) { + if (block.nodeName.toLowerCase() !== 'block') continue; + var opcode = block.getAttribute('type').toLowerCase(); + if (opcodes.indexOf(opcode) !== -1) { + filteredCategory.appendChild(block.cloneNode(true)); + } + } + return filteredCategory; +}; + +/** + * Filter Blockly toolbox XML and return a copy which only contains blocks with + * existent opcodes. Categories with no valid children will be removed. + * @param {HTMLElement} toolbox Blockly toolbox XML node + * @param {Array.} opcodes Valid opcodes. Blocks producing other opcodes + * will be filtered. + * @returns {HTMLElement} filtered toolbox XML node + */ +var filterToolbox = function (toolbox, opcodes) { + if (!toolbox.hasChildNodes()) return toolbox; + var filteredToolbox; + if (toolbox.firstElementChild.nodeName.toLowerCase() === 'category') { + filteredToolbox = toolbox.cloneNode(); + for ( + var category = toolbox.firstElementChild; + category; + category = category.nextElementSibling + ) { + if (category.nodeName.toLowerCase() !== 'category') continue; + var filteredCategory = filterToolboxNode(category, opcodes); + if (filteredCategory.hasChildNodes()) filteredToolbox.appendChild(filteredCategory); + } + } else { + filteredToolbox = filterToolboxNode(toolbox, opcodes); + } + return filteredToolbox; +}; + +module.exports = filterToolbox; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 52616e7c2..1f3bdfa1f 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -1,6 +1,7 @@ var EventEmitter = require('events'); var util = require('util'); +var filterToolbox = require('./util/filter-toolbox'); var Runtime = require('./engine/runtime'); var sb2import = require('./import/sb2import'); @@ -336,23 +337,17 @@ VirtualMachine.prototype.postSpriteInfo = function (data) { this.editingTarget.postSpriteInfo(data); }; -VirtualMachine.prototype.filterToolbox = function (toolboxDOM) { - var filteredToolbox = toolboxDOM.cloneNode(); - var category = toolboxDOM.firstElementChild; - while (category) { - var filteredCategory = category.cloneNode(); - var block = category.firstElementChild; - while (block) { - var opcode = block.getAttribute('type'); - if (opcode in this.runtime._primitives || opcode in this.runtime._hats) { - filteredCategory.appendChild(block.cloneNode(true)); - } - block = block.nextElementSibling; - } - if (filteredCategory.hasChildNodes()) filteredToolbox.appendChild(filteredCategory); - category = category.nextElementSibling; - } - return filteredToolbox; + +/** + * Filter Blockly toolbox XML and return a copy which only contains blocks with + * existent opcodes. Categories with no valid children will be removed. + * @param {HTMLElement} toolbox Blockly toolbox XML node + * @returns {HTMLElement} filtered toolbox XML node + */ +VirtualMachine.prototype.filterToolbox = function (toolbox) { + var opcodes = Object.keys(this.runtime._primitives) + .concat(Object.keys(this.runtime._hats)); + return filterToolbox(toolbox, opcodes); }; module.exports = VirtualMachine; From 6ea9b54539178229594bff4c7a93b2a420d97b93 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 27 Feb 2017 12:57:03 -0500 Subject: [PATCH 38/44] Add filterToolbox unit tests --- package.json | 1 + test/fixtures/.eslintrc.js | 5 + test/fixtures/toolboxes.js | 845 +++++++++++++++++++++++++++++++ test/unit/util_filter-toolbox.js | 22 + 4 files changed, 873 insertions(+) create mode 100644 test/fixtures/.eslintrc.js create mode 100644 test/fixtures/toolboxes.js create mode 100644 test/unit/util_filter-toolbox.js diff --git a/package.json b/package.json index 3ba71fb0d..4418452f5 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "gh-pages": "^0.12.0", "highlightjs": "^9.8.0", "htmlparser2": "3.9.2", + "jsdom": "^9.11.0", "json": "^9.0.4", "lodash.defaultsdeep": "4.6.0", "minilog": "3.1.0", diff --git a/test/fixtures/.eslintrc.js b/test/fixtures/.eslintrc.js new file mode 100644 index 000000000..8df47e667 --- /dev/null +++ b/test/fixtures/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'max-len': [0] + } +}; diff --git a/test/fixtures/toolboxes.js b/test/fixtures/toolboxes.js new file mode 100644 index 000000000..e8efe6f96 --- /dev/null +++ b/test/fixtures/toolboxes.js @@ -0,0 +1,845 @@ +var jsdom = require('jsdom').jsdom; +var categories = ''; +var simple = ''; +var empty = ''; +module.exports = { + categories: jsdom(categories).body.firstElementChild, + simple: jsdom(simple).body.firstElementChild, + empty: jsdom(empty).body.firstElementChild +}; diff --git a/test/unit/util_filter-toolbox.js b/test/unit/util_filter-toolbox.js new file mode 100644 index 000000000..4334b819d --- /dev/null +++ b/test/unit/util_filter-toolbox.js @@ -0,0 +1,22 @@ +var toolboxes = require('../fixtures/toolboxes'); +var test = require('tap').test; +var filterToolbox = require('../../src/util/filter-toolbox'); + +test('categories', function (t) { + var filteredToolbox = filterToolbox(toolboxes.categories, ['operator_random']); + t.strictEqual(filteredToolbox.children.length, 1); + t.strictEqual(filteredToolbox.firstElementChild.children.length, 1); + t.end(); +}); + +test('simple', function (t) { + var filteredToolbox = filterToolbox(toolboxes.simple, ['operator_random']); + t.strictEqual(filteredToolbox.children.length, 1); + t.end(); +}); + +test('empty', function (t) { + var filteredToolbox = filterToolbox(toolboxes.empty, ['operator_random']); + t.strictEqual(filteredToolbox.children.length, 0); + t.end(); +}); From 739c5deb63ab749982c5648a056f0ba7b42c7537 Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Wed, 1 Mar 2017 09:25:11 -0500 Subject: [PATCH 39/44] Show Categories that use custom code to load (variables, procedures) (#483) --- src/util/filter-toolbox.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/util/filter-toolbox.js b/src/util/filter-toolbox.js index c4b18b41c..54d59ced8 100644 --- a/src/util/filter-toolbox.js +++ b/src/util/filter-toolbox.js @@ -38,7 +38,11 @@ var filterToolbox = function (toolbox, opcodes) { ) { if (category.nodeName.toLowerCase() !== 'category') continue; var filteredCategory = filterToolboxNode(category, opcodes); - if (filteredCategory.hasChildNodes()) filteredToolbox.appendChild(filteredCategory); + if (filteredCategory.hasChildNodes() || + filteredCategory.hasAttribute('custom') + ) { + filteredToolbox.appendChild(filteredCategory); + } } } else { filteredToolbox = filterToolboxNode(toolbox, opcodes); From 0b2d05317760ec4ab98a85c3d61aa90f898c2d19 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 1 Mar 2017 12:57:46 -0500 Subject: [PATCH 40/44] Update tests to account for custom categories --- test/fixtures/toolboxes.js | 223 +++++++++++++++---------------- test/unit/util_filter-toolbox.js | 8 +- 2 files changed, 113 insertions(+), 118 deletions(-) diff --git a/test/fixtures/toolboxes.js b/test/fixtures/toolboxes.js index e8efe6f96..5f74c89df 100644 --- a/test/fixtures/toolboxes.js +++ b/test/fixtures/toolboxes.js @@ -724,120 +724,115 @@ var categories = ''; -var simple = ''; -var empty = ''; +var simple = ''; +var empty = ''; module.exports = { categories: jsdom(categories).body.firstElementChild, simple: jsdom(simple).body.firstElementChild, diff --git a/test/unit/util_filter-toolbox.js b/test/unit/util_filter-toolbox.js index 4334b819d..9c3ea073c 100644 --- a/test/unit/util_filter-toolbox.js +++ b/test/unit/util_filter-toolbox.js @@ -3,20 +3,20 @@ var test = require('tap').test; var filterToolbox = require('../../src/util/filter-toolbox'); test('categories', function (t) { - var filteredToolbox = filterToolbox(toolboxes.categories, ['operator_random']); - t.strictEqual(filteredToolbox.children.length, 1); + var filteredToolbox = filterToolbox(toolboxes.categories, ['motion_movesteps']); + t.strictEqual(filteredToolbox.children.length, 3); t.strictEqual(filteredToolbox.firstElementChild.children.length, 1); t.end(); }); test('simple', function (t) { - var filteredToolbox = filterToolbox(toolboxes.simple, ['operator_random']); + var filteredToolbox = filterToolbox(toolboxes.simple, ['motion_movesteps']); t.strictEqual(filteredToolbox.children.length, 1); t.end(); }); test('empty', function (t) { - var filteredToolbox = filterToolbox(toolboxes.empty, ['operator_random']); + var filteredToolbox = filterToolbox(toolboxes.empty, ['motion_movesteps']); t.strictEqual(filteredToolbox.children.length, 0); t.end(); }); From 8dadc42857ed390281eaa83779e6fff15abc771c Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Wed, 1 Mar 2017 18:49:17 -0500 Subject: [PATCH 41/44] Add draggable property to sprites This property should be managed by the VM so that it can be displayed in the sprite info panel in the GUI. --- src/import/sb2import.js | 3 +++ src/sprites/rendered-target.js | 22 ++++++++++++++++++++++ test/integration/complex.js | 1 + 3 files changed, 26 insertions(+) diff --git a/src/import/sb2import.js b/src/import/sb2import.js index 20669466e..39a1b1f15 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -104,6 +104,9 @@ var parseScratchObject = function (object, runtime, topLevel) { if (object.hasOwnProperty('direction')) { target.direction = object.direction; } + if (object.hasOwnProperty('isDraggable')) { + target.draggable = object.isDraggable; + } if (object.hasOwnProperty('scale')) { // SB2 stores as 1.0 = 100%; we use % in the VM. target.size = object.scale * 100; diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9c64b9aa0..049fbfd02 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -106,6 +106,12 @@ RenderedTarget.prototype.y = 0; */ RenderedTarget.prototype.direction = 90; +/** + * Whether the rendered target is draggable on the stage + * @type {boolean} + */ +RenderedTarget.prototype.draggable = false; + /** * Whether the rendered target is currently visible. * @type {boolean} @@ -229,6 +235,16 @@ RenderedTarget.prototype.setDirection = function (direction) { this.runtime.spriteInfoReport(this); }; +/** + * Set draggability; i.e., whether it's able to be dragged in the player + * @param {!boolean} draggable True if should be draggable. + */ +RenderedTarget.prototype.setDraggable = function (draggable) { + if (this.isStage) return; + this.draggable = !!draggable; + this.runtime.spriteInfoReport(this); +}; + /** * Set a say bubble. * @param {?string} type Type of say bubble: "say", "think", or null. @@ -431,6 +447,7 @@ RenderedTarget.prototype.updateAllDrawableProperties = function () { var props = { position: [this.x, this.y], direction: renderedDirectionScale.direction, + draggable: this.draggable, scale: renderedDirectionScale.scale, visible: this.visible, skin: costume.skin, @@ -656,6 +673,7 @@ RenderedTarget.prototype.makeClone = function () { newClone.x = this.x; newClone.y = this.y; newClone.direction = this.direction; + newClone.draggable = this.draggable; newClone.visible = this.visible; newClone.size = this.size; newClone.currentCostume = this.currentCostume; @@ -703,6 +721,9 @@ RenderedTarget.prototype.postSpriteInfo = function (data) { if (data.hasOwnProperty('direction')) { this.setDirection(data.direction); } + if (data.hasOwnProperty('draggable')) { + this.setDraggable(data.draggable); + } if (data.hasOwnProperty('rotationStyle')) { this.setRotationStyle(data.rotationStyle); } @@ -723,6 +744,7 @@ RenderedTarget.prototype.toJSON = function () { x: this.x, y: this.y, direction: this.direction, + draggable: this.draggable, costume: this.getCurrentCostume(), costumeCount: this.getCostumes().length, visible: this.visible, diff --git a/test/integration/complex.js b/test/integration/complex.js index 1ff1d384f..dea8d40bc 100644 --- a/test/integration/complex.js +++ b/test/integration/complex.js @@ -34,6 +34,7 @@ test('complex', function (t) { x: 0, y: 10, direction: 90, + draggable: true, rotationStyle: 'all around', visible: true }); From a4578ba0862f674686df2d534426f5c80ba421c4 Mon Sep 17 00:00:00 2001 From: Andrew Sliwinski Date: Thu, 2 Mar 2017 11:21:01 -0500 Subject: [PATCH 42/44] Default bitmapResolution to 1 if not provided in costume metadata (#485) * Default bitmapResolution to 1 if not provided in costume metadata. Resolves GH-484 --- src/sprites/rendered-target.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 9c64b9aa0..d934f72eb 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -428,16 +428,17 @@ RenderedTarget.prototype.updateAllDrawableProperties = function () { if (this.renderer) { var renderedDirectionScale = this._getRenderedDirectionAndScale(); var costume = this.sprite.costumes[this.currentCostume]; + var bitmapResolution = costume.bitmapResolution || 1; var props = { position: [this.x, this.y], direction: renderedDirectionScale.direction, scale: renderedDirectionScale.scale, visible: this.visible, skin: costume.skin, - costumeResolution: costume.bitmapResolution, + costumeResolution: bitmapResolution, rotationCenter: [ - costume.rotationCenterX / costume.bitmapResolution, - costume.rotationCenterY / costume.bitmapResolution + costume.rotationCenterX / bitmapResolution, + costume.rotationCenterY / bitmapResolution ] }; for (var effectName in this.effects) { From 11e28111c54d1b92eab8ec46b114f462f52a07b2 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Mon, 20 Feb 2017 16:22:13 -0500 Subject: [PATCH 43/44] Activate click hats on mouse up to match 2.0 --- src/io/mouse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/mouse.js b/src/io/mouse.js index bcd19087e..4a7361fa4 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -46,7 +46,7 @@ Mouse.prototype.postData = function (data) { } if (typeof data.isDown !== 'undefined') { this._isDown = data.isDown; - if (this._isDown) { + if (!this._isDown) { this._activateClickHats(data.x, data.y); } } From d0cd9cd8972ab93bc5d0241f2cd3bffaed7b8c59 Mon Sep 17 00:00:00 2001 From: Ray Schamp Date: Fri, 3 Mar 2017 09:35:57 -0500 Subject: [PATCH 44/44] Add methods for dragging and dropping sprites * Add `getTargetIdForDrawableId` to translate between renderer picks and VM targets * Add `startDrag` to stop sprite motion while dragging * Add `stopDrag` to return sprite motion after dragging --- src/engine/runtime.js | 12 ++++++++++++ src/sprites/rendered-target.js | 34 +++++++++++++++++++++++++++------ src/virtual-machine.js | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index a060d63a7..130c4ae95 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -793,6 +793,18 @@ Runtime.prototype.getSpriteTargetByName = function (spriteName) { } }; +/** + * Get a target by its drawable id. + * @param {number} drawableID drawable id of target to find + * @return {?Target} The target, if found + */ +Runtime.prototype.getTargetByDrawableId = function (drawableID) { + for (var i = 0; i < this.targets.length; i++) { + var target = this.targets[i]; + if (target.drawableID === drawableID) return target; + } +}; + /** * Update the clone counter to track how many clones are created. * @param {number} changeAmount How many clones have been created/destroyed. diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index d934f72eb..1e60cad48 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -33,6 +33,13 @@ var RenderedTarget = function (sprite, runtime) { */ this.drawableID = null; + /** + * Drag state of this rendered target. If true, x/y position can't be + * changed by blocks. + * @type {boolean} + */ + this.dragging = false; + /** * Map of current graphic effect values. * @type {!Object.} @@ -160,11 +167,11 @@ RenderedTarget.prototype.rotationStyle = ( * Set the X and Y coordinates. * @param {!number} x New X coordinate, in Scratch coordinates. * @param {!number} y New Y coordinate, in Scratch coordinates. + * @param {?boolean} force Force setting X/Y, in case of dragging */ -RenderedTarget.prototype.setXY = function (x, y) { - if (this.isStage) { - return; - } +RenderedTarget.prototype.setXY = function (x, y, force) { + if (this.isStage) return; + if (this.dragging && !force) return; var oldX = this.x; var oldY = this.y; if (this.renderer) { @@ -695,11 +702,12 @@ RenderedTarget.prototype.onStopAll = function () { * @param {object} data An object with sprite info data to set. */ RenderedTarget.prototype.postSpriteInfo = function (data) { + var force = data.hasOwnProperty('force') ? data.force : null; if (data.hasOwnProperty('x')) { - this.setXY(data.x, this.y); + this.setXY(data.x, this.y, force); } if (data.hasOwnProperty('y')) { - this.setXY(this.x, data.y); + this.setXY(this.x, data.y, force); } if (data.hasOwnProperty('direction')) { this.setDirection(data.direction); @@ -712,6 +720,20 @@ RenderedTarget.prototype.postSpriteInfo = function (data) { } }; +/** + * Put the sprite into the drag state. While in effect, setXY must be forced + */ +RenderedTarget.prototype.startDrag = function () { + this.dragging = true; +}; + +/** + * Remove the sprite from the drag state. + */ +RenderedTarget.prototype.stopDrag = function () { + this.dragging = false; +}; + /** * Serialize sprite info, used when emitting events about the sprite * @returns {object} sprite data as a simple object diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 1f3bdfa1f..9d8477c45 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -329,6 +329,41 @@ VirtualMachine.prototype.emitWorkspaceUpdate = function () { }); }; +/** + * Get a target id for a drawable id. Useful for interacting with the renderer + * @param {int} drawableId The drawable id to request the target id for + * @returns {?string} The target id, if found. Will also be null if the target found is the stage. + */ +VirtualMachine.prototype.getTargetIdForDrawableId = function (drawableId) { + var target = this.runtime.getTargetByDrawableId(drawableId); + if (target.hasOwnProperty('id') && target.hasOwnProperty('isStage') && !target.isStage) { + return target.id; + } + return null; +}; + +/** + * Put a target into a "drag" state, during which its X/Y positions will be unaffected + * by blocks. + * @param {string} targetId The id for the target to put into a drag state + */ +VirtualMachine.prototype.startDrag = function (targetId) { + var target = this.runtime.getTargetById(targetId); + if (target) { + target.startDrag(); + this.setEditingTarget(target.id); + } +}; + +/** + * Remove a target from a drag state, so blocks may begin affecting X/Y position again + * @param {string} targetId The id for the target to remove from the drag state + */ +VirtualMachine.prototype.stopDrag = function (targetId) { + var target = this.runtime.getTargetById(targetId); + if (target) target.stopDrag(); +}; + /** * Post/edit sprite info for the current editing target. * @param {object} data An object with sprite info data to set.