diff --git a/playground/index.html b/playground/index.html
index bbfc80324..e8b980141 100644
--- a/playground/index.html
+++ b/playground/index.html
@@ -404,7 +404,7 @@
-
+
diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js
new file mode 100644
index 000000000..8d43c4c92
--- /dev/null
+++ b/src/blocks/scratch3_procedures.js
@@ -0,0 +1,32 @@
+function Scratch3ProcedureBlocks(runtime) {
+ /**
+ * The runtime instantiating this block package.
+ * @type {Runtime}
+ */
+ this.runtime = runtime;
+}
+
+/**
+ * Retrieve the block primitives implemented by this package.
+ * @return {Object.} Mapping of opcode to Function.
+ */
+Scratch3ProcedureBlocks.prototype.getPrimitives = function() {
+ return {
+ 'procedures_defnoreturn': this.defNoReturn,
+ 'procedures_callnoreturn': this.callNoReturn
+ };
+};
+
+Scratch3ProcedureBlocks.prototype.defNoReturn = function () {
+ // No-op: execute the blocks.
+};
+
+Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) {
+ if (!util.stackFrame.executed) {
+ var procedureName = args.mutation.name;
+ util.stackFrame.executed = true;
+ util.startProcedure(procedureName);
+ }
+};
+
+module.exports = Scratch3ProcedureBlocks;
diff --git a/src/engine/adapter.js b/src/engine/adapter.js
index 26f52be90..327aae5e3 100644
--- a/src/engine/adapter.js
+++ b/src/engine/adapter.js
@@ -1,3 +1,4 @@
+var mutationAdapter = require('./mutation-adapter');
var html = require('htmlparser2');
/**
@@ -138,6 +139,9 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
// Link next block to this block.
block.next = childBlockNode.attribs.id;
break;
+ case 'mutation':
+ block.mutation = mutationAdapter(xmlChild);
+ break;
}
}
}
diff --git a/src/engine/blocks.js b/src/engine/blocks.js
index 99f52477d..f96c916ad 100644
--- a/src/engine/blocks.js
+++ b/src/engine/blocks.js
@@ -1,4 +1,5 @@
var adapter = require('./adapter');
+var mutationAdapter = require('./mutation-adapter');
var xmlEscape = require('../util/xml-escape');
/**
@@ -116,6 +117,16 @@ Blocks.prototype.getInputs = function (id) {
return inputs;
};
+/**
+ * Get mutation data for a block.
+ * @param {?string} id ID of block to query.
+ * @return {!Object} Mutation for the block.
+ */
+Blocks.prototype.getMutation = function (id) {
+ if (typeof this._blocks[id] === 'undefined') return null;
+ return this._blocks[id].mutation;
+};
+
/**
* Get the top-level script for a given block.
* @param {?string} id ID of block to query.
@@ -130,6 +141,23 @@ Blocks.prototype.getTopLevelScript = function (id) {
return block.id;
};
+/**
+ * Get the procedure definition for a given name.
+ * @param {?string} name Name of procedure to query.
+ * @return {?string} ID of procedure definition.
+ */
+Blocks.prototype.getProcedureDefinition = function (name) {
+ for (var id in this._blocks) {
+ var block = this._blocks[id];
+ if ((block.opcode == 'procedures_defnoreturn' ||
+ block.opcode == 'procedures_defreturn') &&
+ block.fields['NAME'].value == name) {
+ return id;
+ }
+ }
+ return null;
+};
+
// ---------------------------------------------------------------------
/**
@@ -226,12 +254,16 @@ Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) {
*/
Blocks.prototype.changeBlock = function (args) {
// Validate
- if (args.element !== 'field') return;
+ if (args.element !== 'field' && args.element !== 'mutation') return;
if (typeof this._blocks[args.id] === 'undefined') return;
- if (typeof this._blocks[args.id].fields[args.name] === 'undefined') return;
- // Update block value
- this._blocks[args.id].fields[args.name].value = args.value;
+ if (args.element == 'field') {
+ // Update block value
+ if (!this._blocks[args.id].fields[args.name]) return;
+ this._blocks[args.id].fields[args.name].value = args.value;
+ } else if (args.element == 'mutation') {
+ this._blocks[args.id].mutation = mutationAdapter(args.value);
+ }
};
/**
@@ -355,6 +387,10 @@ Blocks.prototype.blockToXML = function (blockId) {
' type="' + block.opcode + '"' +
xy +
'>';
+ // Add any mutation. Must come before inputs.
+ if (block.mutation) {
+ xmlString += this.mutationToXML(block.mutation);
+ }
// Add any inputs on this block.
for (var input in block.inputs) {
var blockInput = block.inputs[input];
@@ -389,6 +425,25 @@ Blocks.prototype.blockToXML = function (blockId) {
return xmlString;
};
+/**
+ * Recursively encode a mutation object to XML.
+ * @param {!Object} mutation Object representing a mutation.
+ * @return {string} XML string representing a mutation.
+ */
+Blocks.prototype.mutationToXML = function (mutation) {
+ var mutationString = '<' + mutation.tagName;
+ for (var prop in mutation) {
+ if (prop == 'children' || prop == 'tagName') continue;
+ mutationString += ' ' + prop + '="' + mutation[prop] + '"';
+ }
+ mutationString += '>';
+ for (var i = 0; i < mutation.children.length; i++) {
+ mutationString += this.mutationToXML(mutation.children[i]);
+ }
+ mutationString += '' + mutation.tagName + '>';
+ return mutationString;
+};
+
// ---------------------------------------------------------------------
/**
diff --git a/src/engine/execute.js b/src/engine/execute.js
index 29116d5e5..59b4a3cbf 100644
--- a/src/engine/execute.js
+++ b/src/engine/execute.js
@@ -132,6 +132,12 @@ var execute = function (sequencer, thread) {
argValues[inputName] = currentStackFrame.reported[inputName];
}
+ // Add any mutation to args (e.g., for procedures).
+ var mutation = target.blocks.getMutation(currentBlockId);
+ if (mutation) {
+ argValues.mutation = mutation;
+ }
+
// If we've gotten this far, all of the input blocks are evaluated,
// and `argValues` is fully populated. So, execute the block primitive.
// First, clear `currentStackFrame.reported`, so any subsequent execution
@@ -155,6 +161,9 @@ var execute = function (sequencer, thread) {
startBranch: function (branchNum) {
sequencer.stepToBranch(thread, branchNum);
},
+ startProcedure: function (procedureName) {
+ sequencer.stepToProcedure(thread, procedureName);
+ },
startHats: function(requestedHat, opt_matchFields, opt_target) {
return (
runtime.startHats(requestedHat, opt_matchFields, opt_target)
diff --git a/src/engine/mutation-adapter.js b/src/engine/mutation-adapter.js
new file mode 100644
index 000000000..12dc123e1
--- /dev/null
+++ b/src/engine/mutation-adapter.js
@@ -0,0 +1,39 @@
+var html = require('htmlparser2');
+
+/**
+ * Adapter between mutator XML or DOM and block representation which can be
+ * used by the Scratch runtime.
+ * @param {(Object|string)} mutation Mutation XML string or DOM.
+ * @return {Object} Object representing the mutation.
+ */
+module.exports = function (mutation) {
+ var mutationParsed;
+ // Check if the mutation is already parsed; if not, parse it.
+ if (typeof mutation === 'object') {
+ mutationParsed = mutation;
+ } else {
+ mutationParsed = html.parseDOM(mutation)[0];
+ }
+ return mutatorTagToObject(mutationParsed);
+};
+
+/**
+ * Convert a part of a mutation DOM to a mutation VM object, recursively.
+ * @param {Object} dom DOM object for mutation tag.
+ * @return {Object} Object representing useful parts of this mutation.
+ */
+function mutatorTagToObject (dom) {
+ var obj = Object.create(null);
+ obj.tagName = dom.name;
+ obj.children = [];
+ for (var prop in dom.attribs) {
+ if (prop == 'xmlns') continue;
+ obj[prop] = dom.attribs[prop];
+ }
+ for (var i = 0; i < dom.children.length; i++) {
+ obj.children.push(
+ mutatorTagToObject(dom.children[i])
+ );
+ }
+ return obj;
+}
diff --git a/src/engine/runtime.js b/src/engine/runtime.js
index 75067be3d..18cffd8ec 100644
--- a/src/engine/runtime.js
+++ b/src/engine/runtime.js
@@ -15,7 +15,8 @@ var defaultBlockPackages = {
'scratch3_motion': require('../blocks/scratch3_motion'),
'scratch3_operators': require('../blocks/scratch3_operators'),
'scratch3_sensing': require('../blocks/scratch3_sensing'),
- 'scratch3_data': require('../blocks/scratch3_data')
+ 'scratch3_data': require('../blocks/scratch3_data'),
+ 'scratch3_procedures': require('../blocks/scratch3_procedures')
};
/**
diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js
index 690ab61a7..85575c804 100644
--- a/src/engine/sequencer.js
+++ b/src/engine/sequencer.js
@@ -124,6 +124,16 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
}
};
+/**
+ * Step a procedure.
+ * @param {!Thread} thread Thread object to step to procedure.
+ * @param {!string} procedureName Name of procedure defined in this target.
+ */
+Sequencer.prototype.stepToProcedure = function (thread, procedureName) {
+ var definition = thread.target.blocks.getProcedureDefinition(procedureName);
+ thread.pushStack(definition);
+};
+
/**
* Step a thread into an input reporter, and manage its status appropriately.
* @param {!Thread} thread Thread object to step to reporter.
diff --git a/src/import/sb2import.js b/src/import/sb2import.js
index 79e135a7b..f633de9c0 100644
--- a/src/import/sb2import.js
+++ b/src/import/sb2import.js
@@ -327,6 +327,14 @@ function parseBlock (sb2block) {
};
}
}
+ // Special cases to generate mutations.
+ if (oldOpcode == 'call') {
+ activeBlock.mutation = {
+ tagName: 'mutation',
+ children: [],
+ name: sb2block[1]
+ };
+ }
return activeBlock;
}
diff --git a/src/import/sb2specmap.js b/src/import/sb2specmap.js
index 693d82f81..76e9683e1 100644
--- a/src/import/sb2specmap.js
+++ b/src/import/sb2specmap.js
@@ -1374,15 +1374,20 @@ var specMap = {
]
},
'procDef':{
- 'opcode':'proc_def',
- 'argMap':[]
+ 'opcode':'procedures_defnoreturn',
+ 'argMap':[
+ {
+ 'type':'field',
+ 'fieldName':'NAME'
+ }
+ ]
},
'getParam':{
'opcode':'proc_param',
'argMap':[]
},
'call':{
- 'opcode':'proc_call',
+ 'opcode':'procedures_callnoreturn',
'argMap':[]
}
};