diff --git a/src/dispatch/shared-dispatch.js b/src/dispatch/shared-dispatch.js index d582dc170..85d38bde2 100644 --- a/src/dispatch/shared-dispatch.js +++ b/src/dispatch/shared-dispatch.js @@ -115,6 +115,12 @@ class SharedDispatch { _remoteTransferCall (provider, service, method, transfer, ...args) { return new Promise((resolve, reject) => { const responseId = this._storeCallbacks(resolve, reject); + + /** @TODO: remove this hack! this is just here so we don't try to send `util` to a worker */ + if ((args.length > 0) && (typeof args[args.length - 1].yield === 'function')) { + args.pop(); + } + if (transfer) { provider.postMessage({service, method, responseId, args}, transfer); } else { diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 1b0a9ddca..1ea4fb519 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -294,6 +294,14 @@ class Runtime extends EventEmitter { return 'MONITORS_UPDATE'; } + /** + * Event name for reporting that an extension was added. + * @const {string} + */ + static get EXTENSION_WAS_ADDED () { + return 'EXTENSION_WAS_ADDED'; + } + /** * How rapidly we try to step threads by default, in ms. */ @@ -371,10 +379,12 @@ class Runtime extends EventEmitter { for (const blockInfo of extensionInfo.blocks) { const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo); - const opcode = convertedBlock.json.id; + const opcode = convertedBlock.json.type; categoryInfo.blocks.push(convertedBlock); this._primitives[opcode] = convertedBlock.info.func; } + + this.emit(Runtime.EXTENSION_WAS_ADDED, categoryInfo.blocks); } /** @@ -387,10 +397,8 @@ class Runtime extends EventEmitter { _convertForScratchBlocks (blockInfo, categoryInfo) { const extendedOpcode = `${categoryInfo.id}.${blockInfo.opcode}`; const blockJSON = { - id: extendedOpcode, + type: extendedOpcode, inputsInline: true, - previousStatement: null, // null = available connection; undefined = hat block - nextStatement: null, // null = available connection; undefined = terminal category: categoryInfo.name, colour: categoryInfo.color1, colourSecondary: categoryInfo.color2, @@ -436,6 +444,10 @@ class Runtime extends EventEmitter { switch (blockInfo.blockType) { case BlockType.COMMAND: blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_SQUARE; + blockJSON.previousStatement = null; // null = available connection; undefined = hat + if (!blockInfo.isTerminal) { + blockJSON.nextStatement = null; // null = available connection; undefined = terminal + } break; case BlockType.REPORTER: blockJSON.output = 'String'; // TODO: distinguish number & string here? @@ -447,7 +459,7 @@ class Runtime extends EventEmitter { break; case BlockType.HAT: blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_SQUARE; - delete blockJSON.previousStatement; + blockJSON.nextStatement = null; // null = available connection; undefined = terminal break; case BlockType.CONDITIONAL: // Statement inputs get names like 'SUBSTACK', 'SUBSTACK2', 'SUBSTACK3', ... @@ -458,6 +470,8 @@ class Runtime extends EventEmitter { }; } blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_SQUARE; + blockJSON.previousStatement = null; // null = available connection; undefined = hat + blockJSON.nextStatement = null; // null = available connection; undefined = terminal break; } @@ -659,7 +673,7 @@ class Runtime extends EventEmitter { if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE) { const blockContainer = opts.target.blocks; const opcode = blockContainer.getOpcode(blockContainer.getBlock(topBlockId)); - + if (this.getIsEdgeActivatedHat(opcode) && this.threads[i].stackClick !== opts.stackClick) { // Allow edge activated hat thread stack click to coexist with // edge activated hat thread that runs every frame diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index e02234031..c7c1487df 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -65,10 +65,6 @@ class ExtensionManager { }); } - foo () { - return this.loadExtensionURL('extensions/example-extension.js'); - } - /** * Load an extension by URL * @param {string} extensionURL - the URL for the extension to load @@ -77,7 +73,7 @@ class ExtensionManager { loadExtensionURL (extensionURL) { return new Promise((resolve, reject) => { // If we `require` this at the global level it breaks non-webpack targets, including tests - const ExtensionWorker = require('worker-loader!./extension-worker'); + const ExtensionWorker = require('worker-loader?name=extension-worker.js!./extension-worker'); this.pendingExtensions.push({extensionURL, resolve, reject}); dispatch.addWorker(new ExtensionWorker()); diff --git a/src/extensions/example-extension.js b/src/extensions/example-extension.js index fa585f5e1..e9aeba53f 100644 --- a/src/extensions/example-extension.js +++ b/src/extensions/example-extension.js @@ -33,6 +33,28 @@ class ExampleExtension { // Required: the list of blocks implemented by this extension, // in the order intended for display. blocks: [ + { + opcode: 'example-noop', + blockType: Scratch.BlockType.COMMAND, + blockAllThreads: false, + text: 'do nothing', + func: 'noop' + }, + { + opcode: 'example-conditional', + blockType: Scratch.BlockType.CONDITIONAL, + branchCount: 4, + isTerminal: true, + blockAllThreads: false, + text: 'choose [BRANCH]', + arguments: { + BRANCH: { + type: Scratch.ArgumentType.NUMBER, + defaultValue: 1 + } + }, + func: 'noop' + }, { // Required: the machine-readable name of this operation. // This will appear in project JSON. Must not contain a '.' character. @@ -110,6 +132,18 @@ class ExampleExtension { // ['sprite', 'stage'] filter: ['someBlocks.wedo2', 'sprite', 'stage'] }, + { + opcode: 'example-Boolean', + blockType: Scratch.BlockType.BOOLEAN, + text: 'return true', + func: 'returnTrue' + }, + { + opcode: 'example-hat', + blockType: Scratch.BlockType.HAT, + text: 'after forever', + func: 'returnFalse' + }, { // Another block... } @@ -179,6 +213,17 @@ class ExampleExtension { return ['Letter ', args.LETTER_NUM, ' of ', args.TEXT, ' is ', result, '.'].join(''); } + + noop () { + } + + returnTrue () { + return true; + } + + returnFalse () { + return false; + } } Scratch.extensions.register(new ExampleExtension()); diff --git a/src/index.js b/src/index.js index f2d277902..a54deefbc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,8 @@ const VirtualMachine = require('./virtual-machine'); const CentralDispatch = require('./dispatch/central-dispatch'); -const ExtensionManager = require('./extension-support/extension-manager'); global.Scratch = global.Scratch || {}; global.Scratch.dispatch = CentralDispatch; -global.Scratch.extensions = new ExtensionManager(); module.exports = VirtualMachine; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 8334b3092..186607c37 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -1,6 +1,7 @@ const EventEmitter = require('events'); const centralDispatch = require('./dispatch/central-dispatch'); +const ExtensionManager = require('./extension-support/extension-manager'); const log = require('./util/log'); const Runtime = require('./engine/runtime'); const sb2 = require('./serialization/sb2'); @@ -63,6 +64,11 @@ class VirtualMachine extends EventEmitter { this.runtime.on(Runtime.MONITORS_UPDATE, monitorList => { this.emit(Runtime.MONITORS_UPDATE, monitorList); }); + this.runtime.on(Runtime.EXTENSION_WAS_ADDED, blocksInfo => { + this.emit(Runtime.EXTENSION_WAS_ADDED, blocksInfo); + }); + + this.extensionManager = new ExtensionManager(); this.blockListener = this.blockListener.bind(this); this.flyoutBlockListener = this.flyoutBlockListener.bind(this);