mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-01-11 10:39:56 -05:00
Track extension blocks in the Runtime
This commit is contained in:
parent
42255bbcfa
commit
403adb743c
4 changed files with 193 additions and 163 deletions
|
@ -1,9 +1,14 @@
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const Sequencer = require('./sequencer');
|
|
||||||
const Blocks = require('./blocks');
|
|
||||||
const Thread = require('./thread');
|
|
||||||
const {OrderedMap} = require('immutable');
|
const {OrderedMap} = require('immutable');
|
||||||
|
|
||||||
|
const ScratchBlocks = require('scratch-blocks');
|
||||||
|
|
||||||
|
const ArgumentType = require('../extension-support/argument-type');
|
||||||
|
const Blocks = require('./blocks');
|
||||||
|
const BlockType = require('../extension-support/block-type');
|
||||||
|
const Sequencer = require('./sequencer');
|
||||||
|
const Thread = require('./thread');
|
||||||
|
|
||||||
// Virtual I/O devices.
|
// Virtual I/O devices.
|
||||||
const Clock = require('../io/clock');
|
const Clock = require('../io/clock');
|
||||||
const DeviceManager = require('../io/deviceManager');
|
const DeviceManager = require('../io/deviceManager');
|
||||||
|
@ -24,6 +29,26 @@ const defaultBlockPackages = {
|
||||||
scratch3_wedo2: require('../blocks/scratch3_wedo2')
|
scratch3_wedo2: require('../blocks/scratch3_wedo2')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information used for converting Scratch argument types into scratch-blocks data.
|
||||||
|
* @type {object.<ArgumentType, {shadowType: string, fieldType: string}>}}
|
||||||
|
*/
|
||||||
|
const ArgumentTypeMap = (() => {
|
||||||
|
const map = {};
|
||||||
|
map[ArgumentType.NUMBER] = {
|
||||||
|
shadowType: 'math_number',
|
||||||
|
fieldType: 'NUM'
|
||||||
|
};
|
||||||
|
map[ArgumentType.STRING] = {
|
||||||
|
shadowType: 'text',
|
||||||
|
fieldType: 'TEXT'
|
||||||
|
};
|
||||||
|
map[ArgumentType.BOOLEAN] = {
|
||||||
|
shadowType: ''
|
||||||
|
};
|
||||||
|
return map;
|
||||||
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages targets, scripts, and the sequencer.
|
* Manages targets, scripts, and the sequencer.
|
||||||
* @constructor
|
* @constructor
|
||||||
|
@ -75,6 +100,13 @@ class Runtime extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
this._primitives = {};
|
this._primitives = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map to look up all block information by extended opcode.
|
||||||
|
* @type {Object.<string, {info:BlockInfo, json:object, xml:string}>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._blockInfo = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map to look up hat blocks' metadata.
|
* Map to look up hat blocks' metadata.
|
||||||
* Keys are opcode for hat, values are metadata objects.
|
* Keys are opcode for hat, values are metadata objects.
|
||||||
|
@ -320,6 +352,125 @@ class Runtime extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the primitives provided by an extension.
|
||||||
|
* @param {ExtensionInfo} extensionInfo - information about the extension (id, blocks, etc.)
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_registerExtensionPrimitives (extensionInfo) {
|
||||||
|
const categoryInfo = {
|
||||||
|
id: extensionInfo.id,
|
||||||
|
name: extensionInfo.name,
|
||||||
|
color1: '#FF6680',
|
||||||
|
color2: '#FF4D6A',
|
||||||
|
color3: '#FF3355'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const blockInfo of extensionInfo.blocks) {
|
||||||
|
const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo);
|
||||||
|
const opcode = convertedBlock.json.id;
|
||||||
|
this._blockInfo[opcode] = convertedBlock;
|
||||||
|
this._primitives[opcode] = convertedBlock.info.func;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert BlockInfo into scratch-blocks JSON & XML, and generate a proxy function.
|
||||||
|
* @param {BlockInfo} blockInfo - the block to convert
|
||||||
|
* @param {CategoryInfo} categoryInfo - the category for this block
|
||||||
|
* @returns {{info: BlockInfo, json: object, xml: string}} - the converted & original block information
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_convertForScratchBlocks (blockInfo, categoryInfo) {
|
||||||
|
const extendedOpcode = `${categoryInfo.id}.${blockInfo.opcode}`;
|
||||||
|
const blockJSON = {
|
||||||
|
id: 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,
|
||||||
|
colorTertiary: categoryInfo.color3,
|
||||||
|
args0: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputList = [];
|
||||||
|
|
||||||
|
// TODO: store this somewhere so that we can map args appropriately after translation.
|
||||||
|
// This maps an arg name to its relative position in the original (usually English) block text.
|
||||||
|
// When displaying a block in another language we'll need to run a `replace` action similar to the one below,
|
||||||
|
// but each `[ARG]` will need to be replaced with the number in this map instead of `args0.length`.
|
||||||
|
const argsMap = {};
|
||||||
|
|
||||||
|
blockJSON.message0 = blockInfo.text.replace(/\[(.+?)]/g, (match, placeholder) => {
|
||||||
|
|
||||||
|
// Sanitize the placeholder to ensure valid XML
|
||||||
|
placeholder = placeholder.replace(/[<"&]/, '_');
|
||||||
|
|
||||||
|
blockJSON.args0.push({
|
||||||
|
type: 'input_value',
|
||||||
|
name: placeholder
|
||||||
|
});
|
||||||
|
|
||||||
|
// scratch-blocks uses 1-based argument indexing
|
||||||
|
const argNum = blockJSON.args0.length;
|
||||||
|
argsMap[placeholder] = argNum;
|
||||||
|
|
||||||
|
const argInfo = blockInfo.arguments[placeholder] || {};
|
||||||
|
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
||||||
|
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ? '' : argInfo.defaultValue.toString());
|
||||||
|
inputList.push(`<value name="${placeholder
|
||||||
|
}"><shadow type="${argTypeInfo.shadowType
|
||||||
|
}"><field name="${argTypeInfo.fieldType
|
||||||
|
}">${defaultValue
|
||||||
|
}</field></shadow></value>`
|
||||||
|
);
|
||||||
|
|
||||||
|
return `%${argNum}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (blockInfo.blockType) {
|
||||||
|
case BlockType.COMMAND:
|
||||||
|
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_SQUARE;
|
||||||
|
break;
|
||||||
|
case BlockType.REPORTER:
|
||||||
|
blockJSON.output = 'String'; // TODO: distinguish number & string here?
|
||||||
|
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_ROUND;
|
||||||
|
break;
|
||||||
|
case BlockType.BOOLEAN:
|
||||||
|
blockJSON.output = 'Boolean';
|
||||||
|
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL;
|
||||||
|
break;
|
||||||
|
case BlockType.HAT:
|
||||||
|
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_SQUARE;
|
||||||
|
delete blockJSON.previousStatement;
|
||||||
|
break;
|
||||||
|
case BlockType.CONDITIONAL:
|
||||||
|
// Statement inputs get names like 'SUBSTACK', 'SUBSTACK2', 'SUBSTACK3', ...
|
||||||
|
for (let branchNum = 1; branchNum <= blockInfo.branchCount; ++branchNum) {
|
||||||
|
blockJSON[`args${branchNum}`] = {
|
||||||
|
type: 'input_statement',
|
||||||
|
name: `SUBSTACK${branchNum > 1 ? branchNum : ''}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_SQUARE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockInfo.isTerminal) {
|
||||||
|
delete blockJSON.nextStatement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockXML = `<block type="${extendedOpcode}">${inputList.join('')}</block>`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
info: blockInfo,
|
||||||
|
json: blockJSON,
|
||||||
|
xml: blockXML
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the function associated with the given opcode.
|
* Retrieve the function associated with the given opcode.
|
||||||
* @param {!string} opcode The opcode to look up.
|
* @param {!string} opcode The opcode to look up.
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
const centralDispatch = require('../dispatch/central-dispatch');
|
const dispatch = require('../dispatch/central-dispatch');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
|
|
||||||
const ArgumentType = require('./argument-type');
|
|
||||||
const BlockType = require('./block-type');
|
const BlockType = require('./block-type');
|
||||||
const ScratchBlocks = require('scratch-blocks');
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {object} ArgumentInfo - Information about an extension block argument
|
* @typedef {object} ArgumentInfo - Information about an extension block argument
|
||||||
|
@ -20,7 +18,7 @@ const ScratchBlocks = require('scratch-blocks');
|
||||||
* @property {Boolean|undefined} isTerminal - true if this block ends a stack (default: false)
|
* @property {Boolean|undefined} isTerminal - true if this block ends a stack (default: false)
|
||||||
* @property {Boolean|undefined} blockAllThreads - true if all threads must wait for this block to run (default: false)
|
* @property {Boolean|undefined} blockAllThreads - true if all threads must wait for this block to run (default: false)
|
||||||
* @property {object.<string,ArgumentInfo>|undefined} arguments - information about this block's arguments, if any
|
* @property {object.<string,ArgumentInfo>|undefined} arguments - information about this block's arguments, if any
|
||||||
* @property {string|undefined} func - the method implementing this block on the extension service (default: opcode)
|
* @property {string|Function|undefined} func - the method for this block on the extension service (default: opcode)
|
||||||
* @property {Array.<string>|undefined} filter - the list of targets for which this block should appear (default: all)
|
* @property {Array.<string>|undefined} filter - the list of targets for which this block should appear (default: all)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -32,26 +30,6 @@ const ScratchBlocks = require('scratch-blocks');
|
||||||
* @property {string} color3 - the tertiary color for this category, in '#rrggbb' format
|
* @property {string} color3 - the tertiary color for this category, in '#rrggbb' format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Information used for converting Scratch argument types into scratch-blocks data.
|
|
||||||
* @type {object.<ArgumentType, {shadowType: string, fieldType: string}>}}
|
|
||||||
*/
|
|
||||||
const ArgumentTypeMap = (() => {
|
|
||||||
const map = {};
|
|
||||||
map[ArgumentType.NUMBER] = {
|
|
||||||
shadowType: 'math_number',
|
|
||||||
fieldType: 'NUM'
|
|
||||||
};
|
|
||||||
map[ArgumentType.STRING] = {
|
|
||||||
shadowType: 'text',
|
|
||||||
fieldType: 'TEXT'
|
|
||||||
};
|
|
||||||
map[ArgumentType.BOOLEAN] = {
|
|
||||||
shadowType: ''
|
|
||||||
};
|
|
||||||
return map;
|
|
||||||
})();
|
|
||||||
|
|
||||||
class ExtensionManager {
|
class ExtensionManager {
|
||||||
constructor () {
|
constructor () {
|
||||||
/**
|
/**
|
||||||
|
@ -72,7 +50,9 @@ class ExtensionManager {
|
||||||
*/
|
*/
|
||||||
this.pendingExtensionURLs = [];
|
this.pendingExtensionURLs = [];
|
||||||
|
|
||||||
centralDispatch.setService('extensions', this);
|
dispatch.setService('extensions', this).catch(e => {
|
||||||
|
log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
foo () {
|
foo () {
|
||||||
|
@ -84,7 +64,7 @@ class ExtensionManager {
|
||||||
const ExtensionWorker = require('worker-loader!./extension-worker');
|
const ExtensionWorker = require('worker-loader!./extension-worker');
|
||||||
|
|
||||||
this.pendingExtensionURLs.push(extensionURL);
|
this.pendingExtensionURLs.push(extensionURL);
|
||||||
centralDispatch.addWorker(new ExtensionWorker());
|
dispatch.addWorker(new ExtensionWorker());
|
||||||
}
|
}
|
||||||
|
|
||||||
allocateWorker () {
|
allocateWorker () {
|
||||||
|
@ -94,29 +74,16 @@ class ExtensionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
registerExtensionService (serviceName) {
|
registerExtensionService (serviceName) {
|
||||||
centralDispatch.call(serviceName, 'getInfo').then(info => {
|
dispatch.call(serviceName, 'getInfo').then(info => {
|
||||||
this._registerExtensionInfo(serviceName, info);
|
this._registerExtensionInfo(serviceName, info);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_registerExtensionInfo (serviceName, extensionInfo) {
|
_registerExtensionInfo (serviceName, extensionInfo) {
|
||||||
const categoryInfo = {
|
extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo);
|
||||||
id: extensionInfo.id,
|
dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => {
|
||||||
name: extensionInfo.name,
|
log.error(`Failed to register primitives for extension on service ${serviceName}: ${JSON.stringify(e)}`);
|
||||||
color1: '#FF6680',
|
});
|
||||||
color2: '#FF4D6A',
|
|
||||||
color3: '#FF3355'
|
|
||||||
};
|
|
||||||
extensionInfo = this._sanitizeExtensionInfo(extensionInfo);
|
|
||||||
for (let blockInfo of extensionInfo.blocks) {
|
|
||||||
if (!(blockInfo.opcode && blockInfo.text)) {
|
|
||||||
log.error(`Ignoring malformed extension block: ${JSON.stringify(blockInfo)}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
blockInfo = this._sanitizeBlockInfo(blockInfo);
|
|
||||||
const convertedBlock = this._convertForScratchBlocks(blockInfo, serviceName, categoryInfo);
|
|
||||||
console.dir(convertedBlock);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -132,26 +99,37 @@ class ExtensionManager {
|
||||||
/**
|
/**
|
||||||
* Apply minor cleanup and defaults for optional extension fields.
|
* Apply minor cleanup and defaults for optional extension fields.
|
||||||
* TODO: make the ID unique in cases where two copies of the same extension are loaded.
|
* TODO: make the ID unique in cases where two copies of the same extension are loaded.
|
||||||
|
* @param {string} serviceName - the name of the service hosting this extension block
|
||||||
* @param {ExtensionInfo} extensionInfo - the extension info to be sanitized
|
* @param {ExtensionInfo} extensionInfo - the extension info to be sanitized
|
||||||
* @returns {ExtensionInfo} - a new extension info object with cleaned-up values
|
* @returns {ExtensionInfo} - a new extension info object with cleaned-up values
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_sanitizeExtensionInfo (extensionInfo) {
|
_prepareExtensionInfo (serviceName, extensionInfo) {
|
||||||
extensionInfo = Object.assign({}, extensionInfo);
|
extensionInfo = Object.assign({}, extensionInfo);
|
||||||
extensionInfo.id = this._sanitizeID(extensionInfo.id);
|
extensionInfo.id = this._sanitizeID(extensionInfo.id);
|
||||||
extensionInfo.name = extensionInfo.name || extensionInfo.id;
|
extensionInfo.name = extensionInfo.name || extensionInfo.id;
|
||||||
extensionInfo.blocks = extensionInfo.blocks || [];
|
extensionInfo.blocks = extensionInfo.blocks || [];
|
||||||
extensionInfo.targetTypes = extensionInfo.targetTypes || [];
|
extensionInfo.targetTypes = extensionInfo.targetTypes || [];
|
||||||
|
extensionInfo.blocks = extensionInfo.blocks.reduce((result, blockInfo) => {
|
||||||
|
try {
|
||||||
|
result.push(this._prepareBlockInfo(serviceName, blockInfo));
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: more meaningful error reporting
|
||||||
|
log.error(`Skipping malformed block: ${e}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
return extensionInfo;
|
return extensionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply defaults for optional block fields.
|
* Apply defaults for optional block fields.
|
||||||
|
* @param {string} serviceName - the name of the service hosting this extension block
|
||||||
* @param {BlockInfo} blockInfo - the block info from the extension
|
* @param {BlockInfo} blockInfo - the block info from the extension
|
||||||
* @returns {BlockInfo} - a new block info object which has values for all relevant optional fields.
|
* @returns {BlockInfo} - a new block info object which has values for all relevant optional fields.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_sanitizeBlockInfo (blockInfo) {
|
_prepareBlockInfo (serviceName, blockInfo) {
|
||||||
blockInfo = Object.assign({}, {
|
blockInfo = Object.assign({}, {
|
||||||
blockType: BlockType.COMMAND,
|
blockType: BlockType.COMMAND,
|
||||||
terminal: false,
|
terminal: false,
|
||||||
|
@ -160,120 +138,9 @@ class ExtensionManager {
|
||||||
}, blockInfo);
|
}, blockInfo);
|
||||||
blockInfo.opcode = this._sanitizeID(blockInfo.opcode);
|
blockInfo.opcode = this._sanitizeID(blockInfo.opcode);
|
||||||
blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
|
blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
|
||||||
|
blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func);
|
||||||
return blockInfo;
|
return blockInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert BlockInfo into scratch-blocks JSON & XML, and generate a proxy function.
|
|
||||||
* @param {BlockInfo} blockInfo - the block to convert
|
|
||||||
* @param {string} serviceName - the name of the service hosting this extension
|
|
||||||
* @param {CategoryInfo} categoryInfo - the category for this block
|
|
||||||
* @returns {{json: object, xml: string, blockFunction: Function}} - the converted block information
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_convertForScratchBlocks (blockInfo, serviceName, categoryInfo) {
|
|
||||||
const extendedOpcode = `${categoryInfo.id}.${blockInfo.opcode}`;
|
|
||||||
const blockJSON = {
|
|
||||||
id: 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,
|
|
||||||
colorTertiary: categoryInfo.color3,
|
|
||||||
args0: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputList = [];
|
|
||||||
|
|
||||||
// TODO: store this somewhere so that we can map args appropriately after translation.
|
|
||||||
// This maps an arg name to its relative position in the original (usually English) block text.
|
|
||||||
// When displaying a block in another language we'll need to run a `replace` action similar to the one below,
|
|
||||||
// but each `[ARG]` will need to be replaced with the number in this map instead of `args0.length`.
|
|
||||||
const argsMap = {};
|
|
||||||
|
|
||||||
blockJSON.message0 = blockInfo.text.replace(/\[(.+?)]/g, (match, placeholder) => {
|
|
||||||
|
|
||||||
// Sanitize the placeholder to ensure valid XML
|
|
||||||
placeholder = placeholder.replace(/[<"&]/, '_');
|
|
||||||
|
|
||||||
blockJSON.args0.push({
|
|
||||||
type: 'input_value',
|
|
||||||
name: placeholder
|
|
||||||
});
|
|
||||||
|
|
||||||
// scratch-blocks uses 1-based argument indexing
|
|
||||||
const argNum = blockJSON.args0.length;
|
|
||||||
argsMap[placeholder] = argNum;
|
|
||||||
|
|
||||||
const argInfo = blockInfo.arguments[placeholder] || {};
|
|
||||||
const argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
|
||||||
const defaultValue = (typeof argInfo.defaultValue === 'undefined' ? '' : argInfo.defaultValue.toString());
|
|
||||||
inputList.push(
|
|
||||||
`<value name="${placeholder}">` +
|
|
||||||
`<shadow type="${argTypeInfo.shadowType}">` +
|
|
||||||
`<field name="${argTypeInfo.fieldType}">${defaultValue}</field>` +
|
|
||||||
`</shadow>` +
|
|
||||||
`</value>`
|
|
||||||
);
|
|
||||||
|
|
||||||
return `%${argNum}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (blockInfo.blockType) {
|
|
||||||
case BlockType.COMMAND:
|
|
||||||
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_SQUARE;
|
|
||||||
break;
|
|
||||||
case BlockType.REPORTER:
|
|
||||||
blockJSON.output = 'String'; // TODO: distinguish number & string here?
|
|
||||||
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_ROUND;
|
|
||||||
break;
|
|
||||||
case BlockType.BOOLEAN:
|
|
||||||
blockJSON.output = 'Boolean';
|
|
||||||
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL;
|
|
||||||
break;
|
|
||||||
case BlockType.HAT:
|
|
||||||
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_SQUARE;
|
|
||||||
delete blockJSON.previousStatement;
|
|
||||||
break;
|
|
||||||
case BlockType.CONDITIONAL:
|
|
||||||
// Statement inputs get names like 'SUBSTACK', 'SUBSTACK2', 'SUBSTACK3', ...
|
|
||||||
for (let branchNum = 1; branchNum <= blockInfo.branchCount; ++branchNum) {
|
|
||||||
blockJSON[`args${branchNum}`] = {
|
|
||||||
type: 'input_statement',
|
|
||||||
name: `SUBSTACK${branchNum > 1 ? branchNum : ''}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
blockJSON.outputShape = ScratchBlocks.OUTPUT_SHAPE_SQUARE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blockInfo.isTerminal) {
|
|
||||||
delete blockJSON.nextStatement;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blockXML = `<block type="${extendedOpcode}">${inputList.join('')}</block>`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
json: blockJSON,
|
|
||||||
xml: blockXML,
|
|
||||||
blockFunction: this._extensionProxy.bind(this, serviceName, blockInfo.opcode)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run an opcode by proxying the call to an extension service.
|
|
||||||
* @param {string} serviceName - the name of the service hosting the extension
|
|
||||||
* @param {string} opcode - the opcode to run, also the name of the method on the extension service
|
|
||||||
* @param {object} blockArgs - the arguments provided to the block
|
|
||||||
* @returns {Promise} - a promise which will resolve after the block function executes. If the block function
|
|
||||||
* returns a value, this promise will resolve to that value.
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
_extensionProxy (serviceName, opcode, blockArgs) {
|
|
||||||
return centralDispatch.call(serviceName, opcode, blockArgs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ExtensionManager;
|
module.exports = ExtensionManager;
|
||||||
|
|
|
@ -5,7 +5,7 @@ class ExampleExtension {
|
||||||
getInfo () {
|
getInfo () {
|
||||||
return {
|
return {
|
||||||
// Required: the machine-readable name of this extension.
|
// Required: the machine-readable name of this extension.
|
||||||
// Will be used as the extension's namespace.
|
// Will be used as the extension's namespace. Must not contain a '.' character.
|
||||||
id: 'someBlocks',
|
id: 'someBlocks',
|
||||||
|
|
||||||
// Optional: the human-readable name of this extension as string.
|
// Optional: the human-readable name of this extension as string.
|
||||||
|
@ -35,7 +35,7 @@ class ExampleExtension {
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
// Required: the machine-readable name of this operation.
|
// Required: the machine-readable name of this operation.
|
||||||
// This will appear in project JSON.
|
// This will appear in project JSON. Must not contain a '.' character.
|
||||||
opcode: 'myReporter', // becomes 'someBlocks.myReporter'
|
opcode: 'myReporter', // becomes 'someBlocks.myReporter'
|
||||||
|
|
||||||
// Required: the kind of block we're defining, from a predefined list:
|
// Required: the kind of block we're defining, from a predefined list:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
const centralDispatch = require('./dispatch/central-dispatch');
|
||||||
const log = require('./util/log');
|
const log = require('./util/log');
|
||||||
const Runtime = require('./engine/runtime');
|
const Runtime = require('./engine/runtime');
|
||||||
const sb2 = require('./serialization/sb2');
|
const sb2 = require('./serialization/sb2');
|
||||||
|
@ -24,6 +25,10 @@ class VirtualMachine extends EventEmitter {
|
||||||
* @type {!Runtime}
|
* @type {!Runtime}
|
||||||
*/
|
*/
|
||||||
this.runtime = new Runtime();
|
this.runtime = new Runtime();
|
||||||
|
centralDispatch.setService('runtime', this.runtime).catch(e => {
|
||||||
|
log.error(`Failed to register runtime service: ${JSON.stringify(e)}`);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The "currently editing"/selected target ID for the VM.
|
* The "currently editing"/selected target ID for the VM.
|
||||||
* Block events from any Blockly workspace are routed to this target.
|
* Block events from any Blockly workspace are routed to this target.
|
||||||
|
@ -131,6 +136,13 @@ class VirtualMachine extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the categorized list of all blocks currently available in the VM.
|
||||||
|
*/
|
||||||
|
getBlocks () {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Post I/O data to the virtual devices.
|
* Post I/O data to the virtual devices.
|
||||||
* @param {?string} device Name of virtual I/O device.
|
* @param {?string} device Name of virtual I/O device.
|
||||||
|
|
Loading…
Reference in a new issue