2016-08-31 13:56:05 -04:00
|
|
|
/**
|
|
|
|
* @fileoverview
|
|
|
|
* Partial implementation of an SB2 JSON importer.
|
|
|
|
* Parses provided JSON and then generates all needed
|
|
|
|
* scratch-vm runtime structures.
|
|
|
|
*/
|
|
|
|
|
2017-04-17 15:10:04 -04:00
|
|
|
const Blocks = require('../engine/blocks');
|
|
|
|
const RenderedTarget = require('../sprites/rendered-target');
|
|
|
|
const Sprite = require('../sprites/sprite');
|
|
|
|
const Color = require('../util/color');
|
|
|
|
const log = require('../util/log');
|
|
|
|
const uid = require('../util/uid');
|
2018-03-25 18:14:30 -04:00
|
|
|
const StringUtil = require('../util/string-util');
|
2017-04-26 11:44:53 -04:00
|
|
|
const specMap = require('./sb2_specmap');
|
2017-04-17 15:10:04 -04:00
|
|
|
const Variable = require('../engine/variable');
|
2016-08-31 13:56:05 -04:00
|
|
|
|
2017-09-11 09:42:16 -04:00
|
|
|
const {loadCostume} = require('../import/load-costume.js');
|
|
|
|
const {loadSound} = require('../import/load-sound.js');
|
2018-03-25 18:14:30 -04:00
|
|
|
const {deserializeCostume, deserializeSound} = require('./deserialize-assets.js');
|
2017-03-27 15:04:44 -04:00
|
|
|
|
2017-04-17 17:15:19 -04:00
|
|
|
/**
|
|
|
|
* Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n")
|
|
|
|
* into an argument map. This allows us to provide the expected inputs
|
|
|
|
* to a mutated procedure call.
|
|
|
|
* @param {string} procCode Scratch 2.0 procedure string.
|
|
|
|
* @return {object} Argument map compatible with those in sb2specmap.
|
|
|
|
*/
|
|
|
|
const parseProcedureArgMap = function (procCode) {
|
|
|
|
const argMap = [
|
|
|
|
{} // First item in list is op string.
|
|
|
|
];
|
|
|
|
const INPUT_PREFIX = 'input';
|
|
|
|
let inputCount = 0;
|
|
|
|
// Split by %n, %b, %s.
|
|
|
|
const parts = procCode.split(/(?=[^\\]%[nbs])/);
|
|
|
|
for (let i = 0; i < parts.length; i++) {
|
|
|
|
const part = parts[i].trim();
|
|
|
|
if (part.substring(0, 1) === '%') {
|
|
|
|
const argType = part.substring(1, 2);
|
|
|
|
const arg = {
|
|
|
|
type: 'input',
|
|
|
|
inputName: INPUT_PREFIX + (inputCount++)
|
|
|
|
};
|
|
|
|
if (argType === 'n') {
|
|
|
|
arg.inputOp = 'math_number';
|
|
|
|
} else if (argType === 's') {
|
|
|
|
arg.inputOp = 'text';
|
|
|
|
}
|
|
|
|
argMap.push(arg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return argMap;
|
|
|
|
};
|
|
|
|
|
2017-11-02 17:03:12 -04:00
|
|
|
/**
|
|
|
|
* Generate a list of "argument IDs" for procdefs and caller mutations.
|
|
|
|
* IDs just end up being `input0`, `input1`, ... which is good enough.
|
|
|
|
* @param {string} procCode Scratch 2.0 procedure string.
|
|
|
|
* @return {Array.<string>} Array of argument id strings.
|
|
|
|
*/
|
|
|
|
const parseProcedureArgIds = function (procCode) {
|
|
|
|
return parseProcedureArgMap(procCode)
|
|
|
|
.map(arg => arg.inputName)
|
|
|
|
.filter(name => name); // Filter out unnamed inputs which are labels
|
2017-11-02 17:14:00 -04:00
|
|
|
};
|
2017-11-02 17:03:12 -04:00
|
|
|
|
2017-04-17 17:15:19 -04:00
|
|
|
/**
|
|
|
|
* Flatten a block tree into a block list.
|
|
|
|
* Children are temporarily stored on the `block.children` property.
|
|
|
|
* @param {Array.<object>} blocks list generated by `parseBlockList`.
|
|
|
|
* @return {Array.<object>} Flattened list to be passed to `blocks.createBlock`.
|
|
|
|
*/
|
|
|
|
const flatten = function (blocks) {
|
|
|
|
let finalBlocks = [];
|
|
|
|
for (let i = 0; i < blocks.length; i++) {
|
|
|
|
const block = blocks[i];
|
|
|
|
finalBlocks.push(block);
|
|
|
|
if (block.children) {
|
|
|
|
finalBlocks = finalBlocks.concat(flatten(block.children));
|
|
|
|
}
|
|
|
|
delete block.children;
|
|
|
|
}
|
|
|
|
return finalBlocks;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse any list of blocks from SB2 JSON into a list of VM-format blocks.
|
|
|
|
* Could be used to parse a top-level script,
|
|
|
|
* a list of blocks in a branch (e.g., in forever),
|
|
|
|
* or a list of blocks in an argument (e.g., move [pick random...]).
|
|
|
|
* @param {Array.<object>} blockList SB2 JSON-format block list.
|
2017-12-01 10:29:32 -05:00
|
|
|
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
2017-07-17 12:02:48 -04:00
|
|
|
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
2017-11-03 14:17:16 -04:00
|
|
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
2017-04-17 17:15:19 -04:00
|
|
|
* @return {Array.<object>} Scratch VM-format block list.
|
|
|
|
*/
|
2017-12-01 10:29:32 -05:00
|
|
|
const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, extensions) {
|
2017-04-17 17:15:19 -04:00
|
|
|
const resultingList = [];
|
|
|
|
let previousBlock = null; // For setting next.
|
|
|
|
for (let i = 0; i < blockList.length; i++) {
|
|
|
|
const block = blockList[i];
|
|
|
|
// eslint-disable-next-line no-use-before-define
|
2017-12-01 10:29:32 -05:00
|
|
|
const parsedBlock = parseBlock(block, addBroadcastMsg, getVariableId, extensions);
|
2017-04-17 17:15:19 -04:00
|
|
|
if (typeof parsedBlock === 'undefined') continue;
|
|
|
|
if (previousBlock) {
|
|
|
|
parsedBlock.parent = previousBlock.id;
|
|
|
|
previousBlock.next = parsedBlock.id;
|
|
|
|
}
|
|
|
|
previousBlock = parsedBlock;
|
|
|
|
resultingList.push(parsedBlock);
|
|
|
|
}
|
|
|
|
return resultingList;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse a Scratch object's scripts into VM blocks.
|
|
|
|
* This should only handle top-level scripts that include X, Y coordinates.
|
|
|
|
* @param {!object} scripts Scripts object from SB2 JSON.
|
|
|
|
* @param {!Blocks} blocks Blocks object to load parsed blocks into.
|
2017-12-01 10:29:32 -05:00
|
|
|
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
2017-07-17 12:02:48 -04:00
|
|
|
* @param {Function} getVariableId function to retreive a variable's ID based on name
|
2017-11-03 14:17:16 -04:00
|
|
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
2017-04-17 17:15:19 -04:00
|
|
|
*/
|
2017-12-01 10:29:32 -05:00
|
|
|
const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, extensions) {
|
2017-04-17 17:15:19 -04:00
|
|
|
for (let i = 0; i < scripts.length; i++) {
|
|
|
|
const script = scripts[i];
|
|
|
|
const scriptX = script[0];
|
|
|
|
const scriptY = script[1];
|
|
|
|
const blockList = script[2];
|
2017-12-01 10:29:32 -05:00
|
|
|
const parsedBlockList = parseBlockList(blockList, addBroadcastMsg, getVariableId, extensions);
|
2017-04-17 17:15:19 -04:00
|
|
|
if (parsedBlockList[0]) {
|
|
|
|
// Adjust script coordinates to account for
|
|
|
|
// larger block size in scratch-blocks.
|
|
|
|
// @todo: Determine more precisely the right formulas here.
|
|
|
|
parsedBlockList[0].x = scriptX * 1.5;
|
|
|
|
parsedBlockList[0].y = scriptY * 2.2;
|
|
|
|
parsedBlockList[0].topLevel = true;
|
|
|
|
parsedBlockList[0].parent = null;
|
|
|
|
}
|
|
|
|
// Flatten children and create add the blocks.
|
|
|
|
const convertedBlocks = flatten(parsedBlockList);
|
|
|
|
for (let j = 0; j < convertedBlocks.length; j++) {
|
|
|
|
blocks.createBlock(convertedBlocks[j]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-07-17 12:02:48 -04:00
|
|
|
/**
|
|
|
|
* Create a callback for assigning fixed IDs to imported variables
|
|
|
|
* Generator stores the global variable mapping in a closure
|
|
|
|
* @param {!string} targetId the id of the target to scope the variable to
|
|
|
|
* @return {string} variable ID
|
|
|
|
*/
|
|
|
|
const generateVariableIdGetter = (function () {
|
|
|
|
let globalVariableNameMap = {};
|
|
|
|
const namer = (targetId, name) => `${targetId}-${name}`;
|
|
|
|
return function (targetId, topLevel) {
|
|
|
|
// Reset the global variable map if topLevel
|
|
|
|
if (topLevel) globalVariableNameMap = {};
|
|
|
|
return function (name) {
|
|
|
|
if (topLevel) { // Store the name/id pair in the globalVariableNameMap
|
|
|
|
globalVariableNameMap[name] = namer(targetId, name);
|
|
|
|
return globalVariableNameMap[name];
|
|
|
|
}
|
|
|
|
// Not top-level, so first check the global name map
|
|
|
|
if (globalVariableNameMap[name]) return globalVariableNameMap[name];
|
|
|
|
return namer(targetId, name);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}());
|
|
|
|
|
2017-12-01 10:29:32 -05:00
|
|
|
const globalBroadcastMsgStateGenerator = (function () {
|
|
|
|
let broadcastMsgNameMap = {};
|
2017-12-15 14:00:53 -05:00
|
|
|
const allBroadcastFields = [];
|
2017-12-13 17:15:18 -05:00
|
|
|
const emptyStringName = uid();
|
2017-12-01 10:29:32 -05:00
|
|
|
return function (topLevel) {
|
|
|
|
if (topLevel) broadcastMsgNameMap = {};
|
|
|
|
return {
|
2017-12-15 14:00:53 -05:00
|
|
|
broadcastMsgMapUpdater: function (name, field) {
|
2017-12-13 17:15:18 -05:00
|
|
|
name = name.toLowerCase();
|
|
|
|
if (name === '') {
|
|
|
|
name = emptyStringName;
|
|
|
|
}
|
2017-12-01 11:27:54 -05:00
|
|
|
broadcastMsgNameMap[name] = `broadcastMsgId-${name}`;
|
2017-12-15 14:00:53 -05:00
|
|
|
allBroadcastFields.push(field);
|
|
|
|
return broadcastMsgNameMap[name];
|
2017-12-01 10:29:32 -05:00
|
|
|
},
|
2017-12-15 11:02:26 -05:00
|
|
|
globalBroadcastMsgs: broadcastMsgNameMap,
|
2017-12-15 14:00:53 -05:00
|
|
|
allBroadcastFields: allBroadcastFields,
|
2017-12-15 11:02:26 -05:00
|
|
|
emptyMsgName: emptyStringName
|
2017-12-01 10:29:32 -05:00
|
|
|
};
|
|
|
|
};
|
|
|
|
}());
|
|
|
|
|
2016-08-31 13:56:05 -04:00
|
|
|
/**
|
|
|
|
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
2017-11-03 14:17:16 -04:00
|
|
|
* TODO: parse the "info" section, especially "savedExtensions"
|
|
|
|
* @param {!object} object - From-JSON "Scratch object:" sprite, stage, watcher.
|
|
|
|
* @param {!Runtime} runtime - Runtime object to load all structures into.
|
|
|
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
|
|
|
* @param {boolean} topLevel - Whether this is the top-level object (stage).
|
2018-03-25 18:14:30 -04:00
|
|
|
* @param {?object} zip - Optional zipped assets for local file import
|
2017-11-03 14:17:16 -04:00
|
|
|
* @return {!Promise.<Array.<Target>>} Promise for the loaded targets when ready, or null for unsupported objects.
|
2016-08-31 13:56:05 -04:00
|
|
|
*/
|
2018-03-25 18:14:30 -04:00
|
|
|
const parseScratchObject = function (object, runtime, extensions, topLevel, zip) {
|
2016-08-31 13:56:05 -04:00
|
|
|
if (!object.hasOwnProperty('objName')) {
|
|
|
|
// Watcher/monitor - skip this object until those are implemented in VM.
|
|
|
|
// @todo
|
2017-04-18 11:55:38 -04:00
|
|
|
return Promise.resolve(null);
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
|
|
|
// Blocks container for this object.
|
2017-04-17 15:10:04 -04:00
|
|
|
const blocks = new Blocks();
|
2016-08-31 13:56:05 -04:00
|
|
|
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
|
2017-04-17 15:10:04 -04:00
|
|
|
const sprite = new Sprite(blocks, runtime);
|
2016-08-31 13:56:05 -04:00
|
|
|
// Sprite/stage name from JSON.
|
|
|
|
if (object.hasOwnProperty('objName')) {
|
|
|
|
sprite.name = object.objName;
|
|
|
|
}
|
|
|
|
// Costumes from JSON.
|
2017-04-17 15:10:04 -04:00
|
|
|
const costumePromises = [];
|
2016-08-31 13:56:05 -04:00
|
|
|
if (object.hasOwnProperty('costumes')) {
|
2017-04-17 15:10:04 -04:00
|
|
|
for (let i = 0; i < object.costumes.length; i++) {
|
|
|
|
const costumeSource = object.costumes[i];
|
|
|
|
const costume = {
|
2017-01-27 13:44:48 -05:00
|
|
|
name: costumeSource.costumeName,
|
|
|
|
bitmapResolution: costumeSource.bitmapResolution || 1,
|
|
|
|
rotationCenterX: costumeSource.rotationCenterX,
|
|
|
|
rotationCenterY: costumeSource.rotationCenterY,
|
2018-03-21 16:51:40 -04:00
|
|
|
// TODO we eventually want this next property to be called
|
|
|
|
// md5ext to reflect what it actually contains, however this
|
|
|
|
// will be a very extensive change across many repositories
|
|
|
|
// and should be done carefully and altogether
|
|
|
|
md5: costumeSource.baseLayerMD5,
|
2017-01-27 13:44:48 -05:00
|
|
|
skinId: null
|
|
|
|
};
|
2018-03-25 18:14:30 -04:00
|
|
|
const md5ext = costumeSource.baseLayerMD5;
|
|
|
|
const idParts = StringUtil.splitFirst(md5ext, '.');
|
|
|
|
const md5 = idParts[0];
|
|
|
|
const ext = idParts[1].toLowerCase();
|
|
|
|
costume.dataFormat = ext;
|
|
|
|
costume.assetId = md5;
|
|
|
|
// If there is no internet connection, or if the asset is not in storage
|
|
|
|
// for some reason, and we are doing a local .sb2 import, (e.g. zip is provided)
|
|
|
|
// the file name of the costume should be the baseLayerID followed by the file ext
|
|
|
|
const assetFileName = `${costumeSource.baseLayerID}.${ext}`;
|
|
|
|
costumePromises.push(deserializeCostume(costume, runtime, zip, assetFileName)
|
|
|
|
.then(() => loadCostume(costume.md5, costume, runtime)));
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
|
|
|
}
|
2016-09-28 16:42:25 -04:00
|
|
|
// Sounds from JSON
|
2017-04-19 17:54:52 -04:00
|
|
|
const soundPromises = [];
|
2016-10-13 11:35:52 -04:00
|
|
|
if (object.hasOwnProperty('sounds')) {
|
2017-04-17 15:10:04 -04:00
|
|
|
for (let s = 0; s < object.sounds.length; s++) {
|
|
|
|
const soundSource = object.sounds[s];
|
|
|
|
const sound = {
|
2017-01-27 13:44:48 -05:00
|
|
|
name: soundSource.soundName,
|
|
|
|
format: soundSource.format,
|
|
|
|
rate: soundSource.rate,
|
|
|
|
sampleCount: soundSource.sampleCount,
|
2018-03-21 16:51:40 -04:00
|
|
|
// TODO we eventually want this next property to be called
|
|
|
|
// md5ext to reflect what it actually contains, however this
|
|
|
|
// will be a very extensive change across many repositories
|
|
|
|
// and should be done carefully and altogether
|
|
|
|
// (for example, the audio engine currently relies on this
|
|
|
|
// property to be named 'md5')
|
2017-01-27 13:44:48 -05:00
|
|
|
md5: soundSource.md5,
|
|
|
|
data: null
|
|
|
|
};
|
2018-03-25 18:14:30 -04:00
|
|
|
const md5ext = soundSource.md5;
|
|
|
|
const idParts = StringUtil.splitFirst(md5ext, '.');
|
|
|
|
const md5 = idParts[0];
|
|
|
|
const ext = idParts[1].toLowerCase();
|
|
|
|
sound.dataFormat = ext;
|
|
|
|
sound.assetId = md5;
|
|
|
|
// If there is no internet connection, or if the asset is not in storage
|
|
|
|
// for some reason, and we are doing a local .sb2 import, (e.g. zip is provided)
|
|
|
|
// the file name of the sound should be the soundID (provided from the project.json)
|
|
|
|
// followed by the file ext
|
|
|
|
const assetFileName = `${soundSource.soundID}.${ext}`;
|
|
|
|
soundPromises.push(deserializeSound(sound, runtime, zip, assetFileName)
|
|
|
|
.then(() => loadSound(sound, runtime)));
|
2016-09-28 16:42:25 -04:00
|
|
|
}
|
|
|
|
}
|
2017-07-17 12:02:48 -04:00
|
|
|
|
2016-08-31 13:56:05 -04:00
|
|
|
// Create the first clone, and load its run-state from JSON.
|
2017-04-19 17:54:52 -04:00
|
|
|
const target = sprite.createClone();
|
2017-04-18 09:27:58 -04:00
|
|
|
|
2017-07-17 12:02:48 -04:00
|
|
|
const getVariableId = generateVariableIdGetter(target.id, topLevel);
|
|
|
|
|
2017-12-01 10:29:32 -05:00
|
|
|
const globalBroadcastMsgObj = globalBroadcastMsgStateGenerator(topLevel);
|
|
|
|
const addBroadcastMsg = globalBroadcastMsgObj.broadcastMsgMapUpdater;
|
|
|
|
|
2016-09-21 16:38:33 -04:00
|
|
|
// Load target properties from JSON.
|
|
|
|
if (object.hasOwnProperty('variables')) {
|
2017-04-17 15:10:04 -04:00
|
|
|
for (let j = 0; j < object.variables.length; j++) {
|
|
|
|
const variable = object.variables[j];
|
2017-06-15 17:29:15 -04:00
|
|
|
const newVariable = new Variable(
|
2017-07-17 12:02:48 -04:00
|
|
|
getVariableId(variable.name),
|
2016-09-21 16:38:33 -04:00
|
|
|
variable.name,
|
2017-11-13 16:55:57 -05:00
|
|
|
Variable.SCALAR_TYPE,
|
2016-09-21 16:38:33 -04:00
|
|
|
variable.isPersistent
|
|
|
|
);
|
2017-11-09 17:19:34 -05:00
|
|
|
newVariable.value = variable.value;
|
2017-06-15 17:29:15 -04:00
|
|
|
target.variables[newVariable.id] = newVariable;
|
2016-09-21 16:38:33 -04:00
|
|
|
}
|
|
|
|
}
|
2017-07-17 12:02:48 -04:00
|
|
|
|
|
|
|
// If included, parse any and all scripts/blocks on the object.
|
|
|
|
if (object.hasOwnProperty('scripts')) {
|
2017-12-01 10:29:32 -05:00
|
|
|
parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions);
|
2017-07-17 12:02:48 -04:00
|
|
|
}
|
|
|
|
|
2018-03-20 09:50:57 -04:00
|
|
|
// Update stage specific blocks (e.g. sprite clicked <=> stage clicked)
|
|
|
|
blocks.updateTargetSpecificBlocks(topLevel); // topLevel = isStage
|
|
|
|
|
2016-09-21 16:38:33 -04:00
|
|
|
if (object.hasOwnProperty('lists')) {
|
2017-04-17 15:10:04 -04:00
|
|
|
for (let k = 0; k < object.lists.length; k++) {
|
|
|
|
const list = object.lists[k];
|
2016-09-21 16:38:33 -04:00
|
|
|
// @todo: monitor properties.
|
2017-11-09 17:19:34 -05:00
|
|
|
const newVariable = new Variable(
|
|
|
|
getVariableId(list.listName),
|
2016-09-21 16:38:33 -04:00
|
|
|
list.listName,
|
2017-11-13 16:55:57 -05:00
|
|
|
Variable.LIST_TYPE,
|
2017-11-15 09:17:20 -05:00
|
|
|
false
|
2016-09-21 16:38:33 -04:00
|
|
|
);
|
2017-11-09 17:19:34 -05:00
|
|
|
newVariable.value = list.contents;
|
|
|
|
target.variables[newVariable.id] = newVariable;
|
2016-09-21 16:38:33 -04:00
|
|
|
}
|
|
|
|
}
|
2016-09-19 14:40:01 -04:00
|
|
|
if (object.hasOwnProperty('scratchX')) {
|
2016-08-31 13:56:05 -04:00
|
|
|
target.x = object.scratchX;
|
|
|
|
}
|
2016-09-19 14:40:01 -04:00
|
|
|
if (object.hasOwnProperty('scratchY')) {
|
2016-08-31 13:56:05 -04:00
|
|
|
target.y = object.scratchY;
|
|
|
|
}
|
2016-09-19 14:40:01 -04:00
|
|
|
if (object.hasOwnProperty('direction')) {
|
2016-08-31 13:56:05 -04:00
|
|
|
target.direction = object.direction;
|
|
|
|
}
|
2017-03-01 18:49:17 -05:00
|
|
|
if (object.hasOwnProperty('isDraggable')) {
|
|
|
|
target.draggable = object.isDraggable;
|
|
|
|
}
|
2016-09-19 14:40:01 -04:00
|
|
|
if (object.hasOwnProperty('scale')) {
|
2016-08-31 13:56:05 -04:00
|
|
|
// SB2 stores as 1.0 = 100%; we use % in the VM.
|
|
|
|
target.size = object.scale * 100;
|
|
|
|
}
|
2016-09-19 14:40:01 -04:00
|
|
|
if (object.hasOwnProperty('visible')) {
|
2016-08-31 13:56:05 -04:00
|
|
|
target.visible = object.visible;
|
|
|
|
}
|
2016-09-19 14:40:01 -04:00
|
|
|
if (object.hasOwnProperty('currentCostumeIndex')) {
|
2016-09-28 16:43:12 -04:00
|
|
|
target.currentCostume = Math.round(object.currentCostumeIndex);
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
2016-09-28 17:09:04 -04:00
|
|
|
if (object.hasOwnProperty('rotationStyle')) {
|
2016-10-23 17:55:31 -04:00
|
|
|
if (object.rotationStyle === 'none') {
|
2016-10-26 11:19:43 -04:00
|
|
|
target.rotationStyle = RenderedTarget.ROTATION_STYLE_NONE;
|
2016-10-23 17:55:31 -04:00
|
|
|
} else if (object.rotationStyle === 'leftRight') {
|
2016-10-26 11:19:43 -04:00
|
|
|
target.rotationStyle = RenderedTarget.ROTATION_STYLE_LEFT_RIGHT;
|
2016-10-23 17:55:31 -04:00
|
|
|
} else if (object.rotationStyle === 'normal') {
|
2016-10-26 11:19:43 -04:00
|
|
|
target.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND;
|
2016-09-28 17:09:04 -04:00
|
|
|
}
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
2018-02-27 11:48:23 -05:00
|
|
|
if (object.hasOwnProperty('tempoBPM')) {
|
|
|
|
target.tempo = object.tempoBPM;
|
|
|
|
}
|
2018-04-03 16:36:58 -04:00
|
|
|
if (object.hasOwnProperty('videoAlpha')) {
|
|
|
|
// SB2 stores alpha as opacity, where 1.0 is opaque.
|
|
|
|
// We convert to a percentage, and invert it so 100% is full transparency.
|
|
|
|
target.videoTransparency = 100 - (100 * object.videoAlpha);
|
|
|
|
}
|
|
|
|
if (object.hasOwnProperty('info')) {
|
|
|
|
if (object.info.hasOwnProperty('videoOn')) {
|
|
|
|
if (object.info.videoOn) {
|
|
|
|
target.videoState = RenderedTarget.VIDEO_STATE.ON;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-04-18 09:27:58 -04:00
|
|
|
|
2016-09-08 09:40:27 -04:00
|
|
|
target.isStage = topLevel;
|
2017-04-18 09:27:58 -04:00
|
|
|
|
2017-04-19 17:54:52 -04:00
|
|
|
Promise.all(costumePromises).then(costumes => {
|
2017-04-03 15:58:13 -04:00
|
|
|
sprite.costumes = costumes;
|
2017-01-27 13:44:48 -05:00
|
|
|
});
|
2017-04-18 09:27:58 -04:00
|
|
|
|
2017-04-19 17:54:52 -04:00
|
|
|
Promise.all(soundPromises).then(sounds => {
|
2017-04-18 09:27:58 -04:00
|
|
|
sprite.sounds = sounds;
|
|
|
|
});
|
|
|
|
|
2016-08-31 13:56:05 -04:00
|
|
|
// The stage will have child objects; recursively process them.
|
2017-04-19 17:54:52 -04:00
|
|
|
const childrenPromises = [];
|
2016-08-31 13:56:05 -04:00
|
|
|
if (object.children) {
|
2017-04-19 17:54:52 -04:00
|
|
|
for (let m = 0; m < object.children.length; m++) {
|
2018-03-25 18:14:30 -04:00
|
|
|
childrenPromises.push(parseScratchObject(object.children[m], runtime, extensions, false, zip));
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
|
|
|
}
|
2017-04-18 09:27:58 -04:00
|
|
|
|
2017-04-19 17:54:52 -04:00
|
|
|
return Promise.all(
|
|
|
|
costumePromises.concat(soundPromises)
|
|
|
|
).then(() =>
|
|
|
|
Promise.all(
|
|
|
|
childrenPromises
|
|
|
|
).then(children => {
|
2017-12-01 10:29:32 -05:00
|
|
|
// Need create broadcast msgs as variables after
|
|
|
|
// all other targets have finished processing.
|
|
|
|
if (target.isStage) {
|
|
|
|
const allBroadcastMsgs = globalBroadcastMsgObj.globalBroadcastMsgs;
|
2017-12-15 14:00:53 -05:00
|
|
|
const allBroadcastMsgFields = globalBroadcastMsgObj.allBroadcastFields;
|
|
|
|
const oldEmptyMsgName = globalBroadcastMsgObj.emptyMsgName;
|
|
|
|
if (allBroadcastMsgs[oldEmptyMsgName]) {
|
|
|
|
// Find a fresh 'messageN'
|
2017-12-15 11:02:26 -05:00
|
|
|
let currIndex = 1;
|
|
|
|
while (allBroadcastMsgs[`message${currIndex}`]) {
|
|
|
|
currIndex += 1;
|
|
|
|
}
|
|
|
|
const newEmptyMsgName = `message${currIndex}`;
|
2017-12-15 14:00:53 -05:00
|
|
|
// Add the new empty message name to the broadcast message
|
|
|
|
// name map, and assign it the old id.
|
|
|
|
// Then, delete the old entry in map.
|
|
|
|
allBroadcastMsgs[newEmptyMsgName] = allBroadcastMsgs[oldEmptyMsgName];
|
|
|
|
delete allBroadcastMsgs[oldEmptyMsgName];
|
|
|
|
// Now update all the broadcast message fields with
|
|
|
|
// the new empty message name.
|
|
|
|
for (let i = 0; i < allBroadcastMsgFields.length; i++) {
|
|
|
|
if (allBroadcastMsgFields[i].value === '') {
|
|
|
|
allBroadcastMsgFields[i].value = newEmptyMsgName;
|
2017-12-15 11:02:26 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-12-15 14:00:53 -05:00
|
|
|
// Traverse the broadcast message name map and create
|
|
|
|
// broadcast messages as variables on the stage (which is this
|
|
|
|
// target).
|
2017-12-01 10:29:32 -05:00
|
|
|
for (const msgName in allBroadcastMsgs) {
|
|
|
|
const msgId = allBroadcastMsgs[msgName];
|
|
|
|
const newMsg = new Variable(
|
|
|
|
msgId,
|
|
|
|
msgName,
|
|
|
|
Variable.BROADCAST_MESSAGE_TYPE,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
target.variables[newMsg.id] = newMsg;
|
|
|
|
}
|
|
|
|
}
|
2017-04-19 17:54:52 -04:00
|
|
|
let targets = [target];
|
|
|
|
for (let n = 0; n < children.length; n++) {
|
2017-04-18 09:27:58 -04:00
|
|
|
targets = targets.concat(children[n]);
|
|
|
|
}
|
|
|
|
return targets;
|
2017-04-19 17:54:52 -04:00
|
|
|
})
|
|
|
|
);
|
2016-10-23 17:55:31 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Top-level handler. Parse provided JSON,
|
|
|
|
* and process the top-level object (the stage object).
|
2017-04-27 17:49:57 -04:00
|
|
|
* @param {!object} json SB2-format JSON to load.
|
2016-10-23 17:55:31 -04:00
|
|
|
* @param {!Runtime} runtime Runtime object to load all structures into.
|
2017-02-01 15:59:50 -05:00
|
|
|
* @param {boolean=} optForceSprite If set, treat as sprite (Sprite2).
|
2018-03-25 18:14:30 -04:00
|
|
|
* @param {?object} zip Optional zipped assets for local file import
|
2017-11-03 14:17:16 -04:00
|
|
|
* @return {Promise.<ImportedProject>} Promise that resolves to the loaded targets when ready.
|
2016-10-23 17:55:31 -04:00
|
|
|
*/
|
2018-03-25 18:14:30 -04:00
|
|
|
const sb2import = function (json, runtime, optForceSprite, zip) {
|
2017-11-03 14:17:16 -04:00
|
|
|
const extensions = {
|
|
|
|
extensionIDs: new Set(),
|
|
|
|
extensionURLs: new Map()
|
|
|
|
};
|
2018-03-25 18:14:30 -04:00
|
|
|
return parseScratchObject(json, runtime, extensions, !optForceSprite, zip)
|
2017-11-03 14:17:16 -04:00
|
|
|
.then(targets => ({
|
|
|
|
targets,
|
|
|
|
extensions
|
|
|
|
}));
|
2016-10-23 17:55:31 -04:00
|
|
|
};
|
2016-08-31 13:56:05 -04:00
|
|
|
|
2018-04-05 15:39:16 -04:00
|
|
|
/**
|
|
|
|
* Given the sb2 block, inspect the specmap for a translation method or object.
|
|
|
|
* @param {!object} block a sb2 formatted block
|
|
|
|
* @return {object} specmap block to parse this opcode
|
|
|
|
*/
|
|
|
|
const specMapBlock = function (block) {
|
|
|
|
const opcode = block[0];
|
|
|
|
const mapped = opcode && specMap[opcode];
|
|
|
|
if (!mapped) {
|
|
|
|
log.warn(`Couldn't find SB2 block: ${opcode}`);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (typeof mapped === 'function') {
|
|
|
|
return mapped(block);
|
|
|
|
}
|
|
|
|
return mapped;
|
|
|
|
};
|
|
|
|
|
2016-08-31 13:56:05 -04:00
|
|
|
/**
|
|
|
|
* Parse a single SB2 JSON-formatted block and its children.
|
2017-02-01 15:59:50 -05:00
|
|
|
* @param {!object} sb2block SB2 JSON-formatted block.
|
2017-12-01 10:29:32 -05:00
|
|
|
* @param {Function} addBroadcastMsg function to update broadcast message name map
|
2017-11-03 14:17:16 -04:00
|
|
|
* @param {Function} getVariableId function to retrieve a variable's ID based on name
|
|
|
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
|
|
|
* @return {object} Scratch VM format block, or null if unsupported object.
|
2016-08-31 13:56:05 -04:00
|
|
|
*/
|
2017-12-01 10:29:32 -05:00
|
|
|
const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extensions) {
|
2018-04-05 15:39:16 -04:00
|
|
|
const blockMetadata = specMapBlock(sb2block);
|
|
|
|
if (!blockMetadata) {
|
|
|
|
return;
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
2018-04-05 15:39:16 -04:00
|
|
|
const oldOpcode = sb2block[0];
|
2017-11-03 14:17:16 -04:00
|
|
|
// If the block is from an extension, record it.
|
|
|
|
const dotIndex = blockMetadata.opcode.indexOf('.');
|
|
|
|
if (dotIndex >= 0) {
|
|
|
|
const extension = blockMetadata.opcode.substring(0, dotIndex);
|
|
|
|
extensions.extensionIDs.add(extension);
|
|
|
|
}
|
2016-08-31 13:56:05 -04:00
|
|
|
// Block skeleton.
|
2017-04-17 15:10:04 -04:00
|
|
|
const activeBlock = {
|
2016-08-31 13:56:05 -04:00
|
|
|
id: uid(), // Generate a new block unique ID.
|
|
|
|
opcode: blockMetadata.opcode, // Converted, e.g. "motion_movesteps".
|
|
|
|
inputs: {}, // Inputs to this block and the blocks they point to.
|
|
|
|
fields: {}, // Fields on this block and their values.
|
2016-09-06 10:55:52 -04:00
|
|
|
next: null, // Next block.
|
2016-08-31 13:56:05 -04:00
|
|
|
shadow: false, // No shadow blocks in an SB2 by default.
|
|
|
|
children: [] // Store any generated children, flattened in `flatten`.
|
|
|
|
};
|
2016-10-13 13:11:26 -04:00
|
|
|
// For a procedure call, generate argument map from proc string.
|
2016-10-23 17:55:31 -04:00
|
|
|
if (oldOpcode === 'call') {
|
2016-10-13 13:11:26 -04:00
|
|
|
blockMetadata.argMap = parseProcedureArgMap(sb2block[1]);
|
|
|
|
}
|
2016-08-31 13:56:05 -04:00
|
|
|
// Look at the expected arguments in `blockMetadata.argMap.`
|
|
|
|
// The basic problem here is to turn positional SB2 arguments into
|
|
|
|
// non-positional named Scratch VM arguments.
|
2017-04-17 15:10:04 -04:00
|
|
|
for (let i = 0; i < blockMetadata.argMap.length; i++) {
|
|
|
|
const expectedArg = blockMetadata.argMap[i];
|
|
|
|
const providedArg = sb2block[i + 1]; // (i = 0 is opcode)
|
2016-09-06 10:55:52 -04:00
|
|
|
// Whether the input is obscuring a shadow.
|
2017-04-17 15:10:04 -04:00
|
|
|
let shadowObscured = false;
|
2016-08-31 13:56:05 -04:00
|
|
|
// Positional argument is an input.
|
2016-10-23 17:55:31 -04:00
|
|
|
if (expectedArg.type === 'input') {
|
2016-08-31 13:56:05 -04:00
|
|
|
// Create a new block and input metadata.
|
2017-04-17 15:10:04 -04:00
|
|
|
const inputUid = uid();
|
2016-08-31 13:56:05 -04:00
|
|
|
activeBlock.inputs[expectedArg.inputName] = {
|
|
|
|
name: expectedArg.inputName,
|
2016-09-06 10:55:52 -04:00
|
|
|
block: null,
|
|
|
|
shadow: null
|
2016-08-31 13:56:05 -04:00
|
|
|
};
|
2016-10-23 17:55:31 -04:00
|
|
|
if (typeof providedArg === 'object' && providedArg) {
|
2016-08-31 13:56:05 -04:00
|
|
|
// Block or block list occupies the input.
|
2017-04-17 17:15:19 -04:00
|
|
|
let innerBlocks;
|
2016-10-23 17:55:31 -04:00
|
|
|
if (typeof providedArg[0] === 'object' && providedArg[0]) {
|
2016-08-31 13:56:05 -04:00
|
|
|
// Block list occupies the input.
|
2017-12-01 10:29:32 -05:00
|
|
|
innerBlocks = parseBlockList(providedArg, addBroadcastMsg, getVariableId, extensions);
|
2016-08-31 13:56:05 -04:00
|
|
|
} else {
|
|
|
|
// Single block occupies the input.
|
2017-12-01 10:29:32 -05:00
|
|
|
innerBlocks = [parseBlock(providedArg, addBroadcastMsg, getVariableId, extensions)];
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
2017-04-17 15:10:04 -04:00
|
|
|
let previousBlock = null;
|
|
|
|
for (let j = 0; j < innerBlocks.length; j++) {
|
2016-10-23 17:55:31 -04:00
|
|
|
if (j === 0) {
|
2016-10-17 16:44:33 -04:00
|
|
|
innerBlocks[j].parent = activeBlock.id;
|
|
|
|
} else {
|
|
|
|
innerBlocks[j].parent = previousBlock;
|
|
|
|
}
|
|
|
|
previousBlock = innerBlocks[j].id;
|
2016-09-08 09:40:53 -04:00
|
|
|
}
|
2016-09-06 10:55:52 -04:00
|
|
|
// Obscures any shadow.
|
|
|
|
shadowObscured = true;
|
|
|
|
activeBlock.inputs[expectedArg.inputName].block = (
|
|
|
|
innerBlocks[0].id
|
|
|
|
);
|
2016-08-31 13:56:05 -04:00
|
|
|
activeBlock.children = (
|
|
|
|
activeBlock.children.concat(innerBlocks)
|
|
|
|
);
|
2016-09-06 10:55:52 -04:00
|
|
|
}
|
|
|
|
// Generate a shadow block to occupy the input.
|
|
|
|
if (!expectedArg.inputOp) {
|
|
|
|
// No editable shadow input; e.g., for a boolean.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Each shadow has a field generated for it automatically.
|
|
|
|
// Value to be filled in the field.
|
2017-04-17 15:10:04 -04:00
|
|
|
let fieldValue = providedArg;
|
2016-09-06 10:55:52 -04:00
|
|
|
// Shadows' field names match the input name, except for these:
|
2017-04-17 15:10:04 -04:00
|
|
|
let fieldName = expectedArg.inputName;
|
2016-10-23 17:55:31 -04:00
|
|
|
if (expectedArg.inputOp === 'math_number' ||
|
|
|
|
expectedArg.inputOp === 'math_whole_number' ||
|
|
|
|
expectedArg.inputOp === 'math_positive_number' ||
|
|
|
|
expectedArg.inputOp === 'math_integer' ||
|
|
|
|
expectedArg.inputOp === 'math_angle') {
|
2016-09-06 10:55:52 -04:00
|
|
|
fieldName = 'NUM';
|
|
|
|
// Fields are given Scratch 2.0 default values if obscured.
|
|
|
|
if (shadowObscured) {
|
|
|
|
fieldValue = 10;
|
|
|
|
}
|
2016-10-23 17:55:31 -04:00
|
|
|
} else if (expectedArg.inputOp === 'text') {
|
2016-09-06 10:55:52 -04:00
|
|
|
fieldName = 'TEXT';
|
|
|
|
if (shadowObscured) {
|
|
|
|
fieldValue = '';
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
2016-10-23 17:55:31 -04:00
|
|
|
} else if (expectedArg.inputOp === 'colour_picker') {
|
2016-09-06 11:46:10 -04:00
|
|
|
// Convert SB2 color to hex.
|
2016-09-12 10:58:50 -04:00
|
|
|
fieldValue = Color.decimalToHex(providedArg);
|
2016-09-06 11:46:10 -04:00
|
|
|
fieldName = 'COLOUR';
|
2016-09-06 10:55:52 -04:00
|
|
|
if (shadowObscured) {
|
|
|
|
fieldValue = '#990000';
|
|
|
|
}
|
2017-12-13 17:15:18 -05:00
|
|
|
} else if (expectedArg.inputOp === 'event_broadcast_menu') {
|
|
|
|
fieldName = 'BROADCAST_OPTION';
|
|
|
|
if (shadowObscured) {
|
|
|
|
fieldValue = '';
|
|
|
|
}
|
2018-02-26 17:28:20 -05:00
|
|
|
} else if (expectedArg.inputOp === 'music.menu.DRUM') {
|
|
|
|
if (shadowObscured) {
|
|
|
|
fieldValue = 1;
|
|
|
|
}
|
|
|
|
} else if (expectedArg.inputOp === 'music.menu.INSTRUMENT') {
|
|
|
|
if (shadowObscured) {
|
|
|
|
fieldValue = 1;
|
|
|
|
}
|
2018-03-26 15:32:05 -04:00
|
|
|
} else if (expectedArg.inputOp === 'videoSensing.menu.MOTION_DIRECTION') {
|
|
|
|
if (shadowObscured) {
|
|
|
|
fieldValue = 1;
|
|
|
|
} else if (fieldValue === 'motion') {
|
|
|
|
fieldValue = 1;
|
|
|
|
} else if (fieldValue === 'direction') {
|
|
|
|
fieldValue = 2;
|
|
|
|
}
|
|
|
|
} else if (expectedArg.inputOp === 'videoSensing.menu.STAGE_SPRITE') {
|
|
|
|
if (shadowObscured) {
|
|
|
|
fieldValue = 2;
|
|
|
|
} else if (fieldValue === 'Stage') {
|
|
|
|
fieldValue = 1;
|
|
|
|
} else if (fieldValue === 'this sprite') {
|
|
|
|
fieldValue = 2;
|
|
|
|
}
|
2016-10-26 10:26:56 -04:00
|
|
|
} else if (shadowObscured) {
|
|
|
|
// Filled drop-down menu.
|
|
|
|
fieldValue = '';
|
2016-09-06 10:55:52 -04:00
|
|
|
}
|
2017-04-17 15:10:04 -04:00
|
|
|
const fields = {};
|
2016-09-06 10:55:52 -04:00
|
|
|
fields[fieldName] = {
|
2017-12-15 14:00:53 -05:00
|
|
|
name: fieldName,
|
|
|
|
value: fieldValue
|
2016-09-06 10:55:52 -04:00
|
|
|
};
|
2017-12-13 17:15:18 -05:00
|
|
|
// event_broadcast_menus have some extra properties to add to the
|
|
|
|
// field and a different value than the rest
|
|
|
|
if (expectedArg.inputOp === 'event_broadcast_menu') {
|
|
|
|
if (!shadowObscured) {
|
2017-12-15 14:00:53 -05:00
|
|
|
// Need to update the broadcast message name map with
|
|
|
|
// the value of this field.
|
|
|
|
// Also need to provide the fields[fieldName] object,
|
|
|
|
// so that we can later update its value property, e.g.
|
|
|
|
// if sb2 message name is empty string, we will later
|
|
|
|
// replace this field's value with messageN
|
|
|
|
// once we can traverse through all the existing message names
|
|
|
|
// and come up with a fresh messageN.
|
|
|
|
const broadcastId = addBroadcastMsg(fieldValue, fields[fieldName]);
|
|
|
|
fields[fieldName].id = broadcastId;
|
2017-12-13 17:15:18 -05:00
|
|
|
}
|
|
|
|
fields[fieldName].variableType = expectedArg.variableType;
|
|
|
|
}
|
2016-09-06 10:55:52 -04:00
|
|
|
activeBlock.children.push({
|
|
|
|
id: inputUid,
|
|
|
|
opcode: expectedArg.inputOp,
|
|
|
|
inputs: {},
|
|
|
|
fields: fields,
|
|
|
|
next: null,
|
|
|
|
topLevel: false,
|
2016-09-08 09:40:53 -04:00
|
|
|
parent: activeBlock.id,
|
2016-09-06 10:55:52 -04:00
|
|
|
shadow: true
|
|
|
|
});
|
|
|
|
activeBlock.inputs[expectedArg.inputName].shadow = inputUid;
|
2016-09-06 11:46:10 -04:00
|
|
|
// If no block occupying the input, alias to the shadow.
|
2016-09-06 10:55:52 -04:00
|
|
|
if (!activeBlock.inputs[expectedArg.inputName].block) {
|
|
|
|
activeBlock.inputs[expectedArg.inputName].block = inputUid;
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
2016-10-23 17:55:31 -04:00
|
|
|
} else if (expectedArg.type === 'field') {
|
2016-08-31 13:56:05 -04:00
|
|
|
// Add as a field on this block.
|
|
|
|
activeBlock.fields[expectedArg.fieldName] = {
|
|
|
|
name: expectedArg.fieldName,
|
|
|
|
value: providedArg
|
|
|
|
};
|
2017-07-17 12:02:48 -04:00
|
|
|
|
2017-11-13 14:24:30 -05:00
|
|
|
if (expectedArg.fieldName === 'VARIABLE' || expectedArg.fieldName === 'LIST') {
|
2017-07-17 12:02:48 -04:00
|
|
|
// Add `id` property to variable fields
|
|
|
|
activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg);
|
2017-12-01 10:29:32 -05:00
|
|
|
} else if (expectedArg.fieldName === 'BROADCAST_OPTION') {
|
2017-12-15 14:00:53 -05:00
|
|
|
// Add the name in this field to the broadcast msg name map.
|
|
|
|
// Also need to provide the fields[fieldName] object,
|
|
|
|
// so that we can later update its value property, e.g.
|
|
|
|
// if sb2 message name is empty string, we will later
|
|
|
|
// replace this field's value with messageN
|
|
|
|
// once we can traverse through all the existing message names
|
|
|
|
// and come up with a fresh messageN.
|
|
|
|
const broadcastId = addBroadcastMsg(providedArg, activeBlock.fields[expectedArg.fieldName]);
|
|
|
|
activeBlock.fields[expectedArg.fieldName].id = broadcastId;
|
2017-07-17 12:02:48 -04:00
|
|
|
}
|
2017-11-13 14:24:30 -05:00
|
|
|
const varType = expectedArg.variableType;
|
|
|
|
if (typeof varType === 'string') {
|
|
|
|
activeBlock.fields[expectedArg.fieldName].variableType = varType;
|
|
|
|
}
|
2016-08-31 13:56:05 -04:00
|
|
|
}
|
|
|
|
}
|
2017-12-28 14:52:06 -05:00
|
|
|
|
2018-01-04 10:24:13 -05:00
|
|
|
// Updates for blocks that have new menus (e.g. in Looks)
|
|
|
|
switch (oldOpcode) {
|
|
|
|
case 'comeToFront':
|
2017-12-28 14:52:06 -05:00
|
|
|
activeBlock.fields.FRONT_BACK = {
|
|
|
|
name: 'FRONT_BACK',
|
|
|
|
value: 'front'
|
|
|
|
};
|
2018-01-04 10:24:13 -05:00
|
|
|
break;
|
|
|
|
case 'goBackByLayers:':
|
2017-12-28 14:52:06 -05:00
|
|
|
activeBlock.fields.FORWARD_BACKWARD = {
|
|
|
|
name: 'FORWARD_BACKWARD',
|
|
|
|
value: 'backward'
|
|
|
|
};
|
2018-01-04 10:24:13 -05:00
|
|
|
break;
|
|
|
|
case 'backgroundIndex':
|
|
|
|
activeBlock.fields.NUMBER_NAME = {
|
|
|
|
name: 'NUMBER_NAME',
|
|
|
|
value: 'number'
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'sceneName':
|
|
|
|
activeBlock.fields.NUMBER_NAME = {
|
|
|
|
name: 'NUMBER_NAME',
|
|
|
|
value: 'name'
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
case 'costumeIndex':
|
|
|
|
activeBlock.fields.NUMBER_NAME = {
|
|
|
|
name: 'NUMBER_NAME',
|
|
|
|
value: 'number'
|
|
|
|
};
|
|
|
|
break;
|
2017-12-28 14:52:06 -05:00
|
|
|
}
|
|
|
|
|
2016-10-03 17:43:24 -04:00
|
|
|
// Special cases to generate mutations.
|
2016-10-23 17:55:31 -04:00
|
|
|
if (oldOpcode === 'stopScripts') {
|
2016-10-13 13:11:26 -04:00
|
|
|
// Mutation for stop block: if the argument is 'other scripts',
|
|
|
|
// the block needs a next connection.
|
2016-10-23 17:55:31 -04:00
|
|
|
if (sb2block[1] === 'other scripts in sprite' ||
|
|
|
|
sb2block[1] === 'other scripts in stage') {
|
2016-10-13 13:11:26 -04:00
|
|
|
activeBlock.mutation = {
|
|
|
|
tagName: 'mutation',
|
|
|
|
hasnext: 'true',
|
|
|
|
children: []
|
|
|
|
};
|
|
|
|
}
|
2016-10-23 17:55:31 -04:00
|
|
|
} else if (oldOpcode === 'procDef') {
|
2016-10-13 13:11:26 -04:00
|
|
|
// Mutation for procedure definition:
|
|
|
|
// store all 2.0 proc data.
|
2017-04-17 15:10:04 -04:00
|
|
|
const procData = sb2block.slice(1);
|
2017-10-06 15:34:32 -04:00
|
|
|
// Create a new block and input metadata.
|
|
|
|
const inputUid = uid();
|
|
|
|
const inputName = 'custom_block';
|
|
|
|
activeBlock.inputs[inputName] = {
|
|
|
|
name: inputName,
|
|
|
|
block: inputUid,
|
|
|
|
shadow: inputUid
|
2016-10-13 13:11:26 -04:00
|
|
|
};
|
2017-10-06 15:34:32 -04:00
|
|
|
activeBlock.children = [{
|
|
|
|
id: inputUid,
|
2017-11-16 14:17:08 -05:00
|
|
|
opcode: 'procedures_prototype',
|
2017-10-06 15:34:32 -04:00
|
|
|
inputs: {},
|
|
|
|
fields: {},
|
|
|
|
next: null,
|
|
|
|
shadow: true,
|
|
|
|
children: [],
|
|
|
|
mutation: {
|
|
|
|
tagName: 'mutation',
|
|
|
|
proccode: procData[0], // e.g., "abc %n %b %s"
|
|
|
|
argumentnames: JSON.stringify(procData[1]), // e.g. ['arg1', 'arg2']
|
2017-11-02 17:03:12 -04:00
|
|
|
argumentids: JSON.stringify(parseProcedureArgIds(procData[0])),
|
2017-10-06 15:34:32 -04:00
|
|
|
argumentdefaults: JSON.stringify(procData[2]), // e.g., [1, 'abc']
|
|
|
|
warp: procData[3], // Warp mode, e.g., true/false.
|
|
|
|
children: []
|
|
|
|
}
|
|
|
|
}];
|
2016-10-23 17:55:31 -04:00
|
|
|
} else if (oldOpcode === 'call') {
|
2016-10-13 13:11:26 -04:00
|
|
|
// Mutation for procedure call:
|
|
|
|
// string for proc code (e.g., "abc %n %b %s").
|
|
|
|
activeBlock.mutation = {
|
|
|
|
tagName: 'mutation',
|
|
|
|
children: [],
|
2017-11-02 17:03:12 -04:00
|
|
|
proccode: sb2block[1],
|
|
|
|
argumentids: JSON.stringify(parseProcedureArgIds(sb2block[1]))
|
2016-10-13 13:11:26 -04:00
|
|
|
};
|
2016-10-23 17:55:31 -04:00
|
|
|
} else if (oldOpcode === 'getParam') {
|
2017-11-16 14:17:08 -05:00
|
|
|
// Assign correct opcode based on the block shape.
|
|
|
|
switch (sb2block[2]) {
|
|
|
|
case 'r':
|
|
|
|
activeBlock.opcode = 'argument_reporter_string_number';
|
|
|
|
break;
|
|
|
|
case 'b':
|
|
|
|
activeBlock.opcode = 'argument_reporter_boolean';
|
|
|
|
break;
|
|
|
|
}
|
2016-10-03 17:43:24 -04:00
|
|
|
}
|
2016-08-31 13:56:05 -04:00
|
|
|
return activeBlock;
|
2016-10-23 17:55:31 -04:00
|
|
|
};
|
2016-08-31 13:56:05 -04:00
|
|
|
|
2016-12-30 10:19:58 -05:00
|
|
|
module.exports = {
|
|
|
|
deserialize: sb2import
|
|
|
|
};
|