Spaces:
Runtime error
Runtime error
| /** | |
| * @fileoverview | |
| * Partial implementation of an SB2 JSON importer. | |
| * Parses provided JSON and then generates all needed | |
| * scratch-vm runtime structures. | |
| */ | |
| 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'); | |
| const StringUtil = require('../util/string-util'); | |
| const MathUtil = require('../util/math-util'); | |
| const specMap = require('./sb2_specmap'); | |
| const Comment = require('../engine/comment'); | |
| const Variable = require('../engine/variable'); | |
| const MonitorRecord = require('../engine/monitor-record'); | |
| const StageLayering = require('../engine/stage-layering'); | |
| const ScratchXUtilities = require('../extension-support/tw-scratchx-utilities'); | |
| const {loadCostume} = require('../import/load-costume.js'); | |
| const {loadSound} = require('../import/load-sound.js'); | |
| const {deserializeCostume, deserializeSound} = require('./deserialize-assets.js'); | |
| // Constants used during deserialization of an SB2 file | |
| const CORE_EXTENSIONS = [ | |
| 'argument', | |
| 'control', | |
| 'data', | |
| 'event', | |
| 'looks', | |
| 'math', | |
| 'motion', | |
| 'operator', | |
| 'procedures', | |
| 'sensing', | |
| 'sound' | |
| ]; | |
| // Adjust script coordinates to account for | |
| // larger block size in scratch-blocks. | |
| // @todo: Determine more precisely the right formulas here. | |
| const WORKSPACE_X_SCALE = 1.5; | |
| const WORKSPACE_Y_SCALE = 2.2; | |
| // By examining ScratchX projects, we've found that ScratchX can use either "\u001f" or "." | |
| // to separate the extension name from the extension method opcode eg. "Text To Speech.say" | |
| // eslint-disable-next-line no-control-regex | |
| const SCRATCHX_OPCODE_SEPARATOR = /\u001f|\./; | |
| /** | |
| * @param {string} opcode | |
| * @returns {boolean} | |
| */ | |
| const isPossiblyScratchXBlock = opcode => SCRATCHX_OPCODE_SEPARATOR.test(opcode); | |
| /** | |
| * @param {string} opcode | |
| * @returns {string} | |
| */ | |
| const mapScratchXOpcode = opcode => { | |
| const [extensionName, extensionMethod] = opcode.split(SCRATCHX_OPCODE_SEPARATOR); | |
| const newOpcodeBase = ScratchXUtilities.generateExtensionId(extensionName); | |
| return `${newOpcodeBase}_${extensionMethod}`; | |
| }; | |
| /** | |
| * @param {object} block | |
| * @returns {object} | |
| */ | |
| const mapScratchXBlock = block => { | |
| const opcode = block[0]; | |
| const argumentCount = block.length - 1; | |
| const args = []; | |
| for (let i = 0; i < argumentCount; i++) { | |
| args.push({ | |
| type: 'input', | |
| inputOp: 'text', | |
| inputName: ScratchXUtilities.argumentIndexToId(i) | |
| }); | |
| } | |
| return { | |
| opcode: mapScratchXOpcode(opcode), | |
| argMap: args | |
| }; | |
| }; | |
| /** | |
| * 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'; | |
| } else if (argType === 'b') { | |
| arg.inputOp = 'boolean'; | |
| } | |
| argMap.push(arg); | |
| } | |
| } | |
| return argMap; | |
| }; | |
| /** | |
| * 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 | |
| }; | |
| /** | |
| * 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. | |
| * @param {Function} addBroadcastMsg function to update broadcast message name map | |
| * @param {Function} getVariableId function to retreive a variable's ID based on name | |
| * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. | |
| * @param {ParseState} parseState - info on the state of parsing beyond the current block. | |
| * @param {object<int, Comment>} comments - Comments from sb2 project that need to be attached to blocks. | |
| * They are indexed in this object by the sb2 flattened block list index indicating | |
| * which block they should attach to. | |
| * @param {int} commentIndex The current index of the top block in this list if it were in a flattened | |
| * list of all blocks for the target | |
| * @return {Array<Array.<object>|int>} Tuple where first item is the Scratch VM-format block list, and | |
| * second item is the updated comment index | |
| */ | |
| const parseBlockList = function (blockList, addBroadcastMsg, getVariableId, extensions, parseState, comments, | |
| commentIndex) { | |
| 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 | |
| const parsedBlockAndComments = parseBlock(block, addBroadcastMsg, getVariableId, | |
| extensions, parseState, comments, commentIndex); | |
| const parsedBlock = parsedBlockAndComments[0]; | |
| // Update commentIndex | |
| commentIndex = parsedBlockAndComments[1]; | |
| if (!parsedBlock) continue; | |
| if (previousBlock) { | |
| parsedBlock.parent = previousBlock.id; | |
| previousBlock.next = parsedBlock.id; | |
| } | |
| previousBlock = parsedBlock; | |
| resultingList.push(parsedBlock); | |
| } | |
| return [resultingList, commentIndex]; | |
| }; | |
| /** | |
| * 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. | |
| * @param {Function} addBroadcastMsg function to update broadcast message name map | |
| * @param {Function} getVariableId function to retreive a variable's ID based on name | |
| * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. | |
| * @param {object} comments Comments that need to be attached to the blocks that need to be parsed | |
| */ | |
| const parseScripts = function (scripts, blocks, addBroadcastMsg, getVariableId, extensions, comments) { | |
| // Keep track of the index of the current script being | |
| // parsed in order to attach block comments correctly | |
| let scriptIndexForComment = 0; | |
| for (let i = 0; i < scripts.length; i++) { | |
| const script = scripts[i]; | |
| const scriptX = script[0]; | |
| const scriptY = script[1]; | |
| const blockList = script[2]; | |
| const parseState = {}; | |
| const [parsedBlockList, newCommentIndex] = parseBlockList(blockList, addBroadcastMsg, getVariableId, extensions, | |
| parseState, comments, scriptIndexForComment); | |
| scriptIndexForComment = newCommentIndex; | |
| if (parsedBlockList[0]) { | |
| parsedBlockList[0].x = scriptX * WORKSPACE_X_SCALE; | |
| parsedBlockList[0].y = scriptY * WORKSPACE_Y_SCALE; | |
| 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]); | |
| } | |
| } | |
| }; | |
| /** | |
| * 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, type) => `${targetId}-${StringUtil.replaceUnsafeChars(name)}-${type}`; | |
| return function (targetId, topLevel) { | |
| // Reset the global variable map if topLevel | |
| if (topLevel) globalVariableNameMap = {}; | |
| return function (name, type) { | |
| if (topLevel) { // Store the name/id pair in the globalVariableNameMap | |
| globalVariableNameMap[`${name}-${type}`] = namer(targetId, name, type); | |
| return globalVariableNameMap[`${name}-${type}`]; | |
| } | |
| // Not top-level, so first check the global name map | |
| if (globalVariableNameMap[`${name}-${type}`]) return globalVariableNameMap[`${name}-${type}`]; | |
| return namer(targetId, name, type); | |
| }; | |
| }; | |
| }()); | |
| const globalBroadcastMsgStateGenerator = (function () { | |
| let broadcastMsgNameMap = {}; | |
| const allBroadcastFields = []; | |
| const emptyStringName = uid(); | |
| return function (topLevel) { | |
| if (topLevel) broadcastMsgNameMap = {}; | |
| return { | |
| broadcastMsgMapUpdater: function (name, field) { | |
| name = name.toLowerCase(); | |
| if (name === '') { | |
| name = emptyStringName; | |
| } | |
| broadcastMsgNameMap[name] = `broadcastMsgId-${StringUtil.replaceUnsafeChars(name)}`; | |
| allBroadcastFields.push(field); | |
| return broadcastMsgNameMap[name]; | |
| }, | |
| globalBroadcastMsgs: broadcastMsgNameMap, | |
| allBroadcastFields: allBroadcastFields, | |
| emptyMsgName: emptyStringName | |
| }; | |
| }; | |
| }()); | |
| /** | |
| * Parse a single monitor object and create all its in-memory VM objects. | |
| * | |
| * It is important that monitors are parsed last, | |
| * - after all sprite targets have finished parsing, and | |
| * - after the rest of the stage has finished parsing. | |
| * | |
| * It is specifically important that all the scripts in the project | |
| * have been parsed and all the relevant targets exist, have uids, | |
| * and have their variables initialized. | |
| * Calling this function before these things are true, will result in | |
| * undefined behavior. | |
| * @param {!object} object - From-JSON "Monitor object" | |
| * @param {!Runtime} runtime - (in/out) Runtime object to load monitor info into. | |
| * @param {!Array.<Target>} targets - Targets have already been parsed. | |
| * @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here. | |
| */ | |
| const parseMonitorObject = (object, runtime, targets, extensions) => { | |
| // If we can't find the block in the spec map, ignore it. | |
| // This happens for things like Lego Wedo 1.0 monitors. | |
| const mapped = specMap[object.cmd]; | |
| if (!mapped) { | |
| log.warn(`Could not find monitor block with opcode: ${object.cmd}`); | |
| return; | |
| } | |
| // In scratch 2.0, there are two monitors that now correspond to extension | |
| // blocks (tempo and video motion/direction). In the case of the | |
| // video motion/direction block, this reporter is not monitorable in Scratch 3.0. | |
| // In the case of the tempo block, we should import it and load the music extension | |
| // only when the monitor is actually visible. | |
| const opcode = specMap[object.cmd].opcode; | |
| const extIndex = opcode.indexOf('_'); | |
| const extID = opcode.substring(0, extIndex); | |
| if (extID === 'videoSensing') { | |
| return; | |
| } else if (CORE_EXTENSIONS.indexOf(extID) === -1 && extID !== '' && | |
| !extensions.extensionIDs.has(extID) && !object.visible) { | |
| // Don't import this monitor if it refers to a non-core extension that | |
| // doesn't exist anywhere else in the project and it isn't visible. | |
| // This should only apply to the tempo block at this point since | |
| // there are no other sb2 blocks that are now extension monitors. | |
| return; | |
| } | |
| let target = null; | |
| // List blocks don't come in with their target name set. | |
| // Find the target by searching for a target with matching variable name/type. | |
| if (!object.hasOwnProperty('target')) { | |
| for (let i = 0; i < targets.length; i++) { | |
| const currTarget = targets[i]; | |
| const listVariables = Object.keys(currTarget.variables).filter(key => { | |
| const variable = currTarget.variables[key]; | |
| return variable.type === Variable.LIST_TYPE && variable.name === object.listName; | |
| }); | |
| if (listVariables.length > 0) { | |
| target = currTarget; // Keep this target for later use | |
| object.target = currTarget.getName(); // Set target name to normalize with other monitors | |
| } | |
| } | |
| } | |
| // Get the target for this monitor, if not gotten above. | |
| target = target || targets.filter(t => t.getName() === object.target)[0]; | |
| if (!target) throw new Error('Cannot create monitor for target that cannot be found by name'); | |
| // Create var id getter to make block naming/parsing easier, variables already created. | |
| const getVariableId = generateVariableIdGetter(target.id, false); | |
| // eslint-disable-next-line no-use-before-define | |
| const [block, _] = parseBlock( | |
| [object.cmd, object.param], // Scratch 2 monitor blocks only have one param. | |
| null, // `addBroadcastMsg`, not needed for monitor blocks. | |
| getVariableId, | |
| extensions, | |
| {}, | |
| null, // `comments`, not needed for monitor blocks | |
| null // `commentIndex`, not needed for monitor blocks | |
| ); | |
| // Monitor blocks have special IDs to match the toolbox obtained from the getId | |
| // function in the runtime.monitorBlocksInfo. Variable monitors, however, | |
| // get their IDs from the variable id they reference. | |
| if (object.cmd === 'getVar:') { | |
| block.id = getVariableId(object.param, Variable.SCALAR_TYPE); | |
| } else if (object.cmd === 'contentsOfList:') { | |
| block.id = getVariableId(object.param, Variable.LIST_TYPE); | |
| } else if (runtime.monitorBlockInfo.hasOwnProperty(block.opcode)) { | |
| block.id = runtime.monitorBlockInfo[block.opcode].getId(target.id, block.fields); | |
| } else { | |
| // If the opcode can't be found in the runtime monitorBlockInfo, | |
| // then default to using the block opcode as the id instead. | |
| // This is for extension monitors, and assumes that extension monitors | |
| // cannot be sprite specific. | |
| block.id = block.opcode; | |
| } | |
| // Block needs a targetId if it is targetting something other than the stage | |
| block.targetId = target.isStage ? null : target.id; | |
| // Property required for running monitored blocks. | |
| block.isMonitored = object.visible; | |
| const existingMonitorBlock = runtime.monitorBlocks._blocks[block.id]; | |
| if (existingMonitorBlock) { | |
| // A monitor block already exists if the toolbox has been loaded and | |
| // the monitor block is not target specific (because the block gets recycled). | |
| // Update the existing block with the relevant monitor information. | |
| existingMonitorBlock.isMonitored = object.visible; | |
| existingMonitorBlock.targetId = block.targetId; | |
| } else { | |
| // Blocks can be created with children, flatten and add to monitorBlocks. | |
| const newBlocks = flatten([block]); | |
| for (let i = 0; i < newBlocks.length; i++) { | |
| runtime.monitorBlocks.createBlock(newBlocks[i]); | |
| } | |
| } | |
| // Convert numbered mode into strings for better understandability. | |
| switch (object.mode) { | |
| case 1: | |
| object.mode = 'default'; | |
| break; | |
| case 2: | |
| object.mode = 'large'; | |
| break; | |
| case 3: | |
| object.mode = 'slider'; | |
| break; | |
| } | |
| // Create a monitor record for the runtime's monitorState | |
| runtime.requestAddMonitor(MonitorRecord({ | |
| id: block.id, | |
| targetId: block.targetId, | |
| spriteName: block.targetId ? object.target : null, | |
| opcode: block.opcode, | |
| params: runtime.monitorBlocks._getBlockParams(block), | |
| value: '', | |
| mode: object.mode, | |
| sliderMin: object.sliderMin, | |
| sliderMax: object.sliderMax, | |
| isDiscrete: object.isDiscrete, | |
| x: object.x, | |
| y: object.y, | |
| width: object.width, | |
| height: object.height, | |
| visible: object.visible | |
| })); | |
| }; | |
| /** | |
| * Parse the assets of a single "Scratch object" and load them. This | |
| * preprocesses objects to support loading the data for those assets over a | |
| * network while the objects are further processed into Blocks, Sprites, and a | |
| * list of needed Extensions. | |
| * @param {!object} object - From-JSON "Scratch object:" sprite, stage, watcher. | |
| * @param {!Runtime} runtime - Runtime object to load all structures into. | |
| * @param {boolean} topLevel - Whether this is the top-level object (stage). | |
| * @param {?object} zip - Optional zipped assets for local file import | |
| * @return {?{costumePromises:Array.<Promise>,soundPromises:Array.<Promise>,soundBank:SoundBank,children:object}} | |
| * Object of arrays of promises and child objects for asset objects used in | |
| * Sprites. As well as a SoundBank for the sound assets. null for unsupported | |
| * objects. | |
| */ | |
| const parseScratchAssets = function (object, runtime, topLevel, zip) { | |
| if (!object.hasOwnProperty('objName')) { | |
| // Skip parsing monitors. Or any other objects missing objName. | |
| return null; | |
| } | |
| const assets = { | |
| costumePromises: [], | |
| soundPromises: [], | |
| soundBank: runtime.audioEngine && runtime.audioEngine.createBank(), | |
| children: [] | |
| }; | |
| // Costumes from JSON. | |
| const costumePromises = assets.costumePromises; | |
| if (object.hasOwnProperty('costumes')) { | |
| for (let i = 0; i < object.costumes.length; i++) { | |
| const costumeSource = object.costumes[i]; | |
| const bitmapResolution = costumeSource.bitmapResolution || 1; | |
| const costume = { | |
| name: costumeSource.costumeName, | |
| bitmapResolution: bitmapResolution, | |
| rotationCenterX: topLevel ? 240 * bitmapResolution : costumeSource.rotationCenterX, | |
| rotationCenterY: topLevel ? 180 * bitmapResolution : costumeSource.rotationCenterY, | |
| // 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, | |
| skinId: null | |
| }; | |
| const md5ext = costumeSource.baseLayerMD5; | |
| const idParts = StringUtil.splitFirst(md5ext, '.'); | |
| const md5 = idParts[0]; | |
| let ext; | |
| if (idParts.length === 2 && idParts[1]) { | |
| ext = idParts[1]; | |
| } else { | |
| // Default to 'png' if baseLayerMD5 is not formatted correctly | |
| ext = 'png'; | |
| // Fix costume md5 for later | |
| costume.md5 = `${costume.md5}.${ext}`; | |
| } | |
| costume.dataFormat = ext; | |
| costume.assetId = md5; | |
| if (costumeSource.textLayerMD5) { | |
| costume.textLayerMD5 = StringUtil.splitFirst(costumeSource.textLayerMD5, '.')[0]; | |
| } | |
| // 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}`; | |
| const textLayerFileName = costumeSource.textLayerID ? `${costumeSource.textLayerID}.png` : null; | |
| costumePromises.push(deserializeCostume(costume, runtime, zip, assetFileName, textLayerFileName) | |
| .then(() => loadCostume(costume.md5, costume, runtime, 2 /* optVersion */)) | |
| ); | |
| } | |
| } | |
| // Sounds from JSON | |
| const {soundBank, soundPromises} = assets; | |
| if (object.hasOwnProperty('sounds')) { | |
| for (let s = 0; s < object.sounds.length; s++) { | |
| const soundSource = object.sounds[s]; | |
| const sound = { | |
| name: soundSource.soundName, | |
| format: soundSource.format, | |
| rate: soundSource.rate, | |
| sampleCount: soundSource.sampleCount, | |
| // 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') | |
| md5: soundSource.md5, | |
| data: null | |
| }; | |
| 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, soundBank)) | |
| ); | |
| } | |
| } | |
| // The stage will have child objects; recursively process them. | |
| const childrenAssets = assets.children; | |
| if (object.children) { | |
| for (let m = 0; m < object.children.length; m++) { | |
| childrenAssets.push(parseScratchAssets(object.children[m], runtime, false, zip)); | |
| } | |
| } | |
| return assets; | |
| }; | |
| /** | |
| * Parse a single "Scratch object" and create all its in-memory VM objects. | |
| * 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). | |
| * @param {?object} zip - Optional zipped assets for local file import | |
| * @param {object} assets - Promises for assets of this scratch object grouped | |
| * into costumes and sounds | |
| * @return {!Promise.<Array.<Target>>} Promise for the loaded targets when ready, or null for unsupported objects. | |
| */ | |
| const parseScratchObject = function (object, runtime, extensions, topLevel, zip, assets) { | |
| if (!object.hasOwnProperty('objName')) { | |
| if (object.hasOwnProperty('listName')) { | |
| // Shim these objects so they can be processed as monitors | |
| object.cmd = 'contentsOfList:'; | |
| object.param = object.listName; | |
| object.mode = 'list'; | |
| } | |
| // Defer parsing monitors until targets are all parsed | |
| object.deferredMonitor = true; | |
| return Promise.resolve(object); | |
| } | |
| // Blocks container for this object. | |
| const blocks = new Blocks(runtime); | |
| // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite. | |
| const sprite = new Sprite(blocks, runtime); | |
| // Sprite/stage name from JSON. | |
| if (object.hasOwnProperty('objName')) { | |
| if (topLevel && object.objName !== 'Stage') { | |
| for (const child of object.children) { | |
| if (!child.hasOwnProperty('objName') && child.target === object.objName) { | |
| child.target = 'Stage'; | |
| } | |
| } | |
| object.objName = 'Stage'; | |
| } | |
| sprite.name = object.objName; | |
| } | |
| // Costumes from JSON. | |
| const costumePromises = assets.costumePromises; | |
| // Sounds from JSON | |
| const {soundBank, soundPromises} = assets; | |
| // Create the first clone, and load its run-state from JSON. | |
| const target = sprite.createClone(topLevel ? StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER); | |
| const getVariableId = generateVariableIdGetter(target.id, topLevel); | |
| const globalBroadcastMsgObj = globalBroadcastMsgStateGenerator(topLevel); | |
| const addBroadcastMsg = globalBroadcastMsgObj.broadcastMsgMapUpdater; | |
| // Load target properties from JSON. | |
| if (object.hasOwnProperty('variables')) { | |
| for (let j = 0; j < object.variables.length; j++) { | |
| const variable = object.variables[j]; | |
| // A variable is a cloud variable if: | |
| // - the project says it's a cloud variable, and | |
| // - it's a stage variable, and | |
| // - the runtime can support another cloud variable | |
| const isCloud = variable.isPersistent && topLevel && runtime.canAddCloudVariable(); | |
| const newVariable = new Variable( | |
| getVariableId(variable.name, Variable.SCALAR_TYPE), | |
| variable.name, | |
| Variable.SCALAR_TYPE, | |
| isCloud | |
| ); | |
| if (isCloud) runtime.addCloudVariable(); | |
| newVariable.value = variable.value; | |
| target.variables[newVariable.id] = newVariable; | |
| } | |
| } | |
| // If included, parse any and all comments on the object (this includes top-level | |
| // workspace comments as well as comments attached to specific blocks) | |
| const blockComments = {}; | |
| if (object.hasOwnProperty('scriptComments')) { | |
| const comments = object.scriptComments.map(commentDesc => { | |
| const [ | |
| commentX, | |
| commentY, | |
| commentWidth, | |
| commentHeight, | |
| commentFullSize, | |
| flattenedBlockIndex, | |
| commentText | |
| ] = commentDesc; | |
| const isBlockComment = commentDesc[5] >= 0; | |
| const newComment = new Comment( | |
| null, // generate a new id for this comment | |
| commentText, // text content of sb2 comment | |
| // Only serialize x & y position of comment if it's a workspace comment | |
| // If it's a block comment, we'll let scratch-blocks handle positioning | |
| isBlockComment ? null : commentX * WORKSPACE_X_SCALE, | |
| isBlockComment ? null : commentY * WORKSPACE_Y_SCALE, | |
| commentWidth * WORKSPACE_X_SCALE, | |
| commentHeight * WORKSPACE_Y_SCALE, | |
| !commentFullSize | |
| ); | |
| if (isBlockComment) { | |
| // commentDesc[5] refers to the index of the block that this | |
| // comment is attached to -- in a flattened version of the | |
| // scripts array. | |
| // If commentDesc[5] is -1, this is a workspace comment (we don't need to do anything | |
| // extra at this point), otherwise temporarily save the flattened script array | |
| // index as the blockId property of the new comment. We will | |
| // change this to refer to the actual block id of the corresponding | |
| // block when that block gets created | |
| newComment.blockId = flattenedBlockIndex; | |
| // Add this comment to the block comments object with its script index | |
| // as the key | |
| if (blockComments.hasOwnProperty(flattenedBlockIndex)) { | |
| blockComments[flattenedBlockIndex].push(newComment); | |
| } else { | |
| blockComments[flattenedBlockIndex] = [newComment]; | |
| } | |
| } | |
| return newComment; | |
| }); | |
| // Add all the comments that were just created to the target.comments, | |
| // referenced by id | |
| comments.forEach(comment => { | |
| target.comments[comment.id] = comment; | |
| }); | |
| } | |
| // If included, parse any and all scripts/blocks on the object. | |
| if (object.hasOwnProperty('scripts')) { | |
| parseScripts(object.scripts, blocks, addBroadcastMsg, getVariableId, extensions, blockComments); | |
| } | |
| // If there are any comments referring to a numerical block ID, make them | |
| // workspace comments. These are comments that were originally created as | |
| // block comments, detached from the block, and then had the associated | |
| // block deleted. | |
| // These comments should be imported as workspace comments | |
| // by making their blockIDs (which currently refer to non-existing blocks) | |
| // null (See #1452). | |
| for (const commentIndex in blockComments) { | |
| const currBlockComments = blockComments[commentIndex]; | |
| currBlockComments.forEach(c => { | |
| if (typeof c.blockId === 'number') { | |
| c.blockId = null; | |
| } | |
| }); | |
| } | |
| // Update stage specific blocks (e.g. sprite clicked <=> stage clicked) | |
| blocks.updateTargetSpecificBlocks(topLevel); // topLevel = isStage | |
| if (object.hasOwnProperty('lists')) { | |
| for (let k = 0; k < object.lists.length; k++) { | |
| const list = object.lists[k]; | |
| const newVariable = new Variable( | |
| getVariableId(list.listName, Variable.LIST_TYPE), | |
| list.listName, | |
| Variable.LIST_TYPE, | |
| false | |
| ); | |
| newVariable.value = list.contents; | |
| target.variables[newVariable.id] = newVariable; | |
| } | |
| } | |
| if (object.hasOwnProperty('scratchX')) { | |
| target.x = object.scratchX; | |
| } | |
| if (object.hasOwnProperty('scratchY')) { | |
| target.y = object.scratchY; | |
| } | |
| 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; | |
| } | |
| if (object.hasOwnProperty('visible')) { | |
| target.visible = object.visible; | |
| } | |
| if (object.hasOwnProperty('currentCostumeIndex')) { | |
| // Current costume index can sometimes be a floating | |
| // point number, use Math.floor to come up with an appropriate index | |
| // and clamp it to the actual number of costumes the object has for good measure. | |
| target.currentCostume = MathUtil.clamp(Math.floor(object.currentCostumeIndex), 0, object.costumes.length - 1); | |
| } | |
| if (object.hasOwnProperty('rotationStyle')) { | |
| if (object.rotationStyle === 'none') { | |
| target.rotationStyle = RenderedTarget.ROTATION_STYLE_NONE; | |
| } else if (object.rotationStyle === 'leftRight') { | |
| target.rotationStyle = RenderedTarget.ROTATION_STYLE_LEFT_RIGHT; | |
| } else if (object.rotationStyle === 'upDown') { | |
| target.rotationStyle = RenderedTarget.ROTATION_STYLE_UP_DOWN; | |
| } else if (object.rotationStyle === 'lookAt') { | |
| target.rotationStyle = RenderedTarget.ROTATION_STYLE_LOOK_AT; | |
| } else if (object.rotationStyle === 'normal') { | |
| target.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND; | |
| } | |
| } | |
| if (object.hasOwnProperty('tempoBPM')) { | |
| target.tempo = object.tempoBPM; | |
| } | |
| 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; | |
| } else { | |
| target.videoState = RenderedTarget.VIDEO_STATE.OFF; | |
| } | |
| } | |
| } | |
| if (object.hasOwnProperty('indexInLibrary')) { | |
| // Temporarily store the 'indexInLibrary' property from the sb2 file | |
| // so that we can correctly order sprites in the target pane. | |
| // This will be deleted after we are done parsing and ordering the targets list. | |
| target.targetPaneOrder = object.indexInLibrary; | |
| } | |
| target.isStage = topLevel; | |
| Promise.all(costumePromises).then(costumes => { | |
| sprite.costumes = costumes; | |
| }); | |
| Promise.all(soundPromises).then(sounds => { | |
| sprite.sounds = sounds; | |
| // Make sure if soundBank is undefined, sprite.soundBank is then null. | |
| sprite.soundBank = soundBank || null; | |
| }); | |
| // The stage will have child objects; recursively process them. | |
| const childrenPromises = []; | |
| if (object.children) { | |
| for (let m = 0; m < object.children.length; m++) { | |
| childrenPromises.push( | |
| parseScratchObject(object.children[m], runtime, extensions, false, zip, assets.children[m]) | |
| ); | |
| } | |
| } | |
| // Parse extension list from ScratchX projects. | |
| if (topLevel) { | |
| const savedExtensions = object.info && object.info.savedExtensions; | |
| if (Array.isArray(savedExtensions)) { | |
| for (const extension of savedExtensions) { | |
| const id = ScratchXUtilities.generateExtensionId(extension.extensionName); | |
| const url = extension.javascriptURL; | |
| extensions.extensionURLs.set(id, url); | |
| } | |
| } | |
| } | |
| return Promise.all( | |
| costumePromises.concat(soundPromises) | |
| ).then(() => | |
| Promise.all( | |
| childrenPromises | |
| ).then(children => { | |
| // Need create broadcast msgs as variables after | |
| // all other targets have finished processing. | |
| if (target.isStage) { | |
| const allBroadcastMsgs = globalBroadcastMsgObj.globalBroadcastMsgs; | |
| const allBroadcastMsgFields = globalBroadcastMsgObj.allBroadcastFields; | |
| const oldEmptyMsgName = globalBroadcastMsgObj.emptyMsgName; | |
| if (allBroadcastMsgs[oldEmptyMsgName]) { | |
| // Find a fresh 'messageN' | |
| let currIndex = 1; | |
| while (allBroadcastMsgs[`message${currIndex}`]) { | |
| currIndex += 1; | |
| } | |
| const newEmptyMsgName = `message${currIndex}`; | |
| // 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; | |
| } | |
| } | |
| } | |
| // Traverse the broadcast message name map and create | |
| // broadcast messages as variables on the stage (which is this | |
| // target). | |
| 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; | |
| } | |
| } | |
| let targets = [target]; | |
| const deferredMonitors = []; | |
| for (let n = 0; n < children.length; n++) { | |
| if (children[n]) { | |
| if (children[n].deferredMonitor) { | |
| deferredMonitors.push(children[n]); | |
| } else { | |
| targets = targets.concat(children[n]); | |
| } | |
| } | |
| } | |
| // It is important that monitors are parsed last | |
| // - after all sprite targets have finished parsing | |
| // - and this is the last thing that happens in the stage parsing | |
| // It is specifically important that all the scripts in the project | |
| // have been parsed and all the relevant targets exist, have uids, | |
| // and have their variables initialized. | |
| for (let n = 0; n < deferredMonitors.length; n++) { | |
| parseMonitorObject(deferredMonitors[n], runtime, targets, extensions); | |
| } | |
| return targets; | |
| }) | |
| ); | |
| }; | |
| const reorderParsedTargets = function (targets) { | |
| // Reorder parsed targets based on the temporary targetPaneOrder property | |
| // and then delete it. | |
| const reordered = targets.map((t, index) => { | |
| t.layerOrder = index; | |
| return t; | |
| }).sort((a, b) => a.targetPaneOrder - b.targetPaneOrder); | |
| // Delete the temporary target pane ordering since we shouldn't need it anymore. | |
| reordered.forEach(t => { | |
| delete t.targetPaneOrder; | |
| }); | |
| return reordered; | |
| }; | |
| /** | |
| * Top-level handler. Parse provided JSON, | |
| * and process the top-level object (the stage object). | |
| * @param {!object} json SB2-format JSON to load. | |
| * @param {!Runtime} runtime Runtime object to load all structures into. | |
| * @param {boolean=} optForceSprite If set, treat as sprite (Sprite2). | |
| * @param {?object} zip Optional zipped assets for local file import | |
| * @return {Promise.<ImportedProject>} Promise that resolves to the loaded targets when ready. | |
| */ | |
| const sb2import = function (json, runtime, optForceSprite, zip) { | |
| const extensions = { | |
| extensionIDs: new Set(), | |
| extensionURLs: new Map() | |
| }; | |
| return Promise.resolve(parseScratchAssets(json, runtime, !optForceSprite, zip)) | |
| // Force this promise to wait for the next loop in the js tick. Let | |
| // storage have some time to send off asset requests. | |
| .then(assets => Promise.resolve(assets)) | |
| .then(assets => ( | |
| parseScratchObject(json, runtime, extensions, !optForceSprite, zip, assets) | |
| )) | |
| .then(reorderParsedTargets) | |
| .then(targets => ({ | |
| targets, | |
| extensions | |
| })); | |
| }; | |
| /** | |
| * 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) { | |
| if (opcode && isPossiblyScratchXBlock(opcode)) { | |
| return mapScratchXBlock(block); | |
| } | |
| log.warn(`Couldn't find SB2 block: ${opcode}`); | |
| return null; | |
| } | |
| if (typeof mapped === 'function') { | |
| return mapped(block); | |
| } | |
| return mapped; | |
| }; | |
| /** | |
| * Parse a single SB2 JSON-formatted block and its children. | |
| * @param {!object} sb2block SB2 JSON-formatted block. | |
| * @param {Function} addBroadcastMsg function to update broadcast message name map | |
| * @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. | |
| * @param {ParseState} parseState - info on the state of parsing beyond the current block. | |
| * @param {object<int, Comment>} comments - Comments from sb2 project that need to be attached to blocks. | |
| * They are indexed in this object by the sb2 flattened block list index indicating | |
| * which block they should attach to. | |
| * @param {int} commentIndex The comment index for the block to be parsed if it were in a flattened | |
| * list of all blocks for the target | |
| * @return {Array.<object|int>} Tuple where first item is the Scratch VM-format block (or null if unsupported object), | |
| * and second item is the updated comment index (after this block and its children are parsed) | |
| */ | |
| const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extensions, parseState, comments, commentIndex) { | |
| const commentsForParsedBlock = (comments && typeof commentIndex === 'number' && !isNaN(commentIndex)) ? | |
| comments[commentIndex] : null; | |
| const blockMetadata = specMapBlock(sb2block); | |
| if (!blockMetadata) { | |
| // No block opcode found, exclude this block, increment the commentIndex, | |
| // make all block comments into workspace comments and send them to zero/zero | |
| // to prevent serialization issues. | |
| if (commentsForParsedBlock) { | |
| commentsForParsedBlock.forEach(comment => { | |
| comment.blockId = null; | |
| comment.x = comment.y = 0; | |
| }); | |
| } | |
| return [null, commentIndex + 1]; | |
| } | |
| const oldOpcode = sb2block[0]; | |
| // If the block is from an extension, record it. | |
| const index = blockMetadata.opcode.indexOf('_'); | |
| const prefix = blockMetadata.opcode.substring(0, index); | |
| if (CORE_EXTENSIONS.indexOf(prefix) === -1) { | |
| if (prefix !== '') extensions.extensionIDs.add(prefix); | |
| } | |
| // Block skeleton. | |
| const activeBlock = { | |
| 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. | |
| next: null, // Next block. | |
| shadow: false, // No shadow blocks in an SB2 by default. | |
| children: [] // Store any generated children, flattened in `flatten`. | |
| }; | |
| // Attach any comments to this block.. | |
| if (commentsForParsedBlock) { | |
| // Attach only the last comment to the block, make all others workspace comments | |
| activeBlock.comment = commentsForParsedBlock[commentsForParsedBlock.length - 1].id; | |
| commentsForParsedBlock.forEach(comment => { | |
| if (comment.id === activeBlock.comment) { | |
| comment.blockId = activeBlock.id; | |
| } else { | |
| // All other comments don't get a block ID and are sent back to zero. | |
| // This is important, because if they have `null` x/y, serialization breaks. | |
| comment.blockId = null; | |
| comment.x = comment.y = 0; | |
| } | |
| }); | |
| } | |
| commentIndex++; | |
| const parentExpectedArg = parseState.expectedArg; | |
| // For a procedure call, generate argument map from proc string. | |
| if (oldOpcode === 'call') { | |
| blockMetadata.argMap = parseProcedureArgMap(sb2block[1]); | |
| } | |
| // 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. | |
| for (let i = 0; i < blockMetadata.argMap.length; i++) { | |
| const expectedArg = blockMetadata.argMap[i]; | |
| const providedArg = sb2block[i + 1]; // (i = 0 is opcode) | |
| // Whether the input is obscuring a shadow. | |
| let shadowObscured = false; | |
| // Positional argument is an input. | |
| if (expectedArg.type === 'input') { | |
| // Create a new block and input metadata. | |
| const inputUid = uid(); | |
| activeBlock.inputs[expectedArg.inputName] = { | |
| name: expectedArg.inputName, | |
| block: null, | |
| shadow: null | |
| }; | |
| if (typeof providedArg === 'object' && providedArg) { | |
| // Block or block list occupies the input. | |
| let innerBlocks; | |
| parseState.expectedArg = expectedArg; | |
| if (typeof providedArg[0] === 'object' && providedArg[0]) { | |
| // Block list occupies the input. | |
| [innerBlocks, commentIndex] = parseBlockList(providedArg, addBroadcastMsg, getVariableId, | |
| extensions, parseState, comments, commentIndex); | |
| } else { | |
| // Single block occupies the input. | |
| const parsedBlockDesc = parseBlock(providedArg, addBroadcastMsg, getVariableId, extensions, | |
| parseState, comments, commentIndex); | |
| innerBlocks = parsedBlockDesc[0] ? [parsedBlockDesc[0]] : []; | |
| // Update commentIndex | |
| commentIndex = parsedBlockDesc[1]; | |
| } | |
| parseState.expectedArg = parentExpectedArg; | |
| // Check if innerBlocks is not an empty list. | |
| // An empty list indicates that all the inner blocks from the sb2 have | |
| // unknown opcodes and have been skipped. | |
| if (innerBlocks.length > 0) { | |
| let previousBlock = null; | |
| for (let j = 0; j < innerBlocks.length; j++) { | |
| if (j === 0) { | |
| innerBlocks[j].parent = activeBlock.id; | |
| } else { | |
| innerBlocks[j].parent = previousBlock; | |
| } | |
| previousBlock = innerBlocks[j].id; | |
| } | |
| activeBlock.inputs[expectedArg.inputName].block = ( | |
| innerBlocks[0].id | |
| ); | |
| activeBlock.children = ( | |
| activeBlock.children.concat(innerBlocks) | |
| ); | |
| } | |
| // Obscures any shadow. | |
| shadowObscured = true; | |
| } | |
| // Generate a shadow block to occupy the input. | |
| if (!expectedArg.inputOp) { | |
| // Undefined inputOp. inputOp should always be defined for inputs. | |
| log.warn(`Unknown input operation for input ${expectedArg.inputName} of opcode ${activeBlock.opcode}.`); | |
| continue; | |
| } | |
| if (expectedArg.inputOp === 'boolean' || expectedArg.inputOp === 'substack') { | |
| // 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. | |
| let fieldValue = providedArg; | |
| // Shadows' field names match the input name, except for these: | |
| let fieldName = expectedArg.inputName; | |
| if (expectedArg.inputOp === 'math_number' || | |
| expectedArg.inputOp === 'math_whole_number' || | |
| expectedArg.inputOp === 'math_positive_number' || | |
| expectedArg.inputOp === 'math_integer' || | |
| expectedArg.inputOp === 'math_angle') { | |
| fieldName = 'NUM'; | |
| // Fields are given Scratch 2.0 default values if obscured. | |
| if (shadowObscured) { | |
| fieldValue = 10; | |
| } | |
| } else if (expectedArg.inputOp === 'text') { | |
| fieldName = 'TEXT'; | |
| if (shadowObscured) { | |
| fieldValue = ''; | |
| } | |
| } else if (expectedArg.inputOp === 'colour_picker') { | |
| // Convert SB2 color to hex. | |
| fieldValue = Color.decimalToHex(providedArg); | |
| fieldName = 'COLOUR'; | |
| if (shadowObscured) { | |
| fieldValue = '#990000'; | |
| } | |
| } else if (expectedArg.inputOp === 'event_broadcast_menu') { | |
| fieldName = 'BROADCAST_OPTION'; | |
| if (shadowObscured) { | |
| fieldValue = ''; | |
| } | |
| } else if (expectedArg.inputOp === 'sensing_of_object_menu') { | |
| if (shadowObscured) { | |
| fieldValue = '_stage_'; | |
| } else if (fieldValue === 'Stage') { | |
| fieldValue = '_stage_'; | |
| } | |
| } else if (expectedArg.inputOp === 'note') { | |
| if (shadowObscured) { | |
| fieldValue = 60; | |
| } | |
| } else if (expectedArg.inputOp === 'music.menu.DRUM') { | |
| if (shadowObscured) { | |
| fieldValue = 1; | |
| } | |
| } else if (expectedArg.inputOp === 'music.menu.INSTRUMENT') { | |
| if (shadowObscured) { | |
| fieldValue = 1; | |
| } | |
| } else if (expectedArg.inputOp === 'videoSensing.menu.ATTRIBUTE') { | |
| if (shadowObscured) { | |
| fieldValue = 'motion'; | |
| } | |
| } else if (expectedArg.inputOp === 'videoSensing.menu.SUBJECT') { | |
| if (shadowObscured) { | |
| fieldValue = 'this sprite'; | |
| } | |
| } else if (expectedArg.inputOp === 'videoSensing.menu.VIDEO_STATE') { | |
| if (shadowObscured) { | |
| fieldValue = 'on'; | |
| } | |
| } else if (shadowObscured) { | |
| // Filled drop-down menu. | |
| fieldValue = ''; | |
| } | |
| const fields = {}; | |
| fields[fieldName] = { | |
| name: fieldName, | |
| value: fieldValue | |
| }; | |
| // 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') { | |
| // 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; | |
| fields[fieldName].variableType = expectedArg.variableType; | |
| } | |
| activeBlock.children.push({ | |
| id: inputUid, | |
| opcode: expectedArg.inputOp, | |
| inputs: {}, | |
| fields: fields, | |
| next: null, | |
| topLevel: false, | |
| parent: activeBlock.id, | |
| shadow: true | |
| }); | |
| activeBlock.inputs[expectedArg.inputName].shadow = inputUid; | |
| // If no block occupying the input, alias to the shadow. | |
| if (!activeBlock.inputs[expectedArg.inputName].block) { | |
| activeBlock.inputs[expectedArg.inputName].block = inputUid; | |
| } | |
| } else if (expectedArg.type === 'field') { | |
| // Add as a field on this block. | |
| activeBlock.fields[expectedArg.fieldName] = { | |
| name: expectedArg.fieldName, | |
| value: providedArg | |
| }; | |
| if (expectedArg.fieldName === 'CURRENTMENU') { | |
| // In 3.0, the field value of the `sensing_current` block | |
| // is in all caps. | |
| activeBlock.fields[expectedArg.fieldName].value = providedArg.toUpperCase(); | |
| if (providedArg === 'day of week') { | |
| activeBlock.fields[expectedArg.fieldName].value = 'DAYOFWEEK'; | |
| } | |
| } | |
| if (expectedArg.fieldName === 'VARIABLE') { | |
| // Add `id` property to variable fields | |
| activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg, Variable.SCALAR_TYPE); | |
| } else if (expectedArg.fieldName === 'LIST') { | |
| // Add `id` property to variable fields | |
| activeBlock.fields[expectedArg.fieldName].id = getVariableId(providedArg, Variable.LIST_TYPE); | |
| } else if (expectedArg.fieldName === 'BROADCAST_OPTION') { | |
| // 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; | |
| } | |
| const varType = expectedArg.variableType; | |
| if (typeof varType === 'string') { | |
| activeBlock.fields[expectedArg.fieldName].variableType = varType; | |
| } | |
| } | |
| } | |
| // Updates for blocks that have new menus (e.g. in Looks) | |
| switch (oldOpcode) { | |
| case 'comeToFront': | |
| activeBlock.fields.FRONT_BACK = { | |
| name: 'FRONT_BACK', | |
| value: 'front' | |
| }; | |
| break; | |
| case 'goBackByLayers:': | |
| activeBlock.fields.FORWARD_BACKWARD = { | |
| name: 'FORWARD_BACKWARD', | |
| value: 'backward' | |
| }; | |
| 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; | |
| case 'costumeName': | |
| activeBlock.fields.NUMBER_NAME = { | |
| name: 'NUMBER_NAME', | |
| value: 'name' | |
| }; | |
| break; | |
| } | |
| // Special cases to generate mutations. | |
| if (oldOpcode === 'stopScripts') { | |
| // Mutation for stop block: if the argument is 'other scripts', | |
| // the block needs a next connection. | |
| if (sb2block[1] === 'other scripts in sprite' || | |
| sb2block[1] === 'other scripts in stage') { | |
| activeBlock.mutation = { | |
| tagName: 'mutation', | |
| hasnext: 'true', | |
| children: [] | |
| }; | |
| } | |
| } else if (oldOpcode === 'procDef') { | |
| // Mutation for procedure definition: | |
| // store all 2.0 proc data. | |
| const procData = sb2block.slice(1); | |
| // Create a new block and input metadata. | |
| const inputUid = uid(); | |
| const inputName = 'custom_block'; | |
| activeBlock.inputs[inputName] = { | |
| name: inputName, | |
| block: inputUid, | |
| shadow: inputUid | |
| }; | |
| activeBlock.children = [{ | |
| id: inputUid, | |
| opcode: 'procedures_prototype', | |
| 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'] | |
| argumentids: JSON.stringify(parseProcedureArgIds(procData[0])), | |
| argumentdefaults: JSON.stringify(procData[2]), // e.g., [1, 'abc'] | |
| warp: procData[3], // Warp mode, e.g., true/false. | |
| children: [] | |
| } | |
| }]; | |
| } else if (oldOpcode === 'call') { | |
| // Mutation for procedure call: | |
| // string for proc code (e.g., "abc %n %b %s"). | |
| activeBlock.mutation = { | |
| tagName: 'mutation', | |
| children: [], | |
| proccode: sb2block[1], | |
| argumentids: JSON.stringify(parseProcedureArgIds(sb2block[1])) | |
| }; | |
| } else if (oldOpcode === 'getParam') { | |
| let returnCode = sb2block[2]; | |
| // Ensure the returnCode is "b" if used in a boolean input. | |
| if (parentExpectedArg && parentExpectedArg.inputOp === 'boolean' && returnCode !== 'b') { | |
| returnCode = 'b'; | |
| } | |
| // Assign correct opcode based on the block shape. | |
| switch (returnCode) { | |
| case 'r': | |
| activeBlock.opcode = 'argument_reporter_string_number'; | |
| break; | |
| case 'b': | |
| activeBlock.opcode = 'argument_reporter_boolean'; | |
| break; | |
| } | |
| } | |
| return [activeBlock, commentIndex]; | |
| }; | |
| module.exports = { | |
| deserialize: sb2import | |
| }; | |