diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index ebf1951514..7324c2d026 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -193,13 +193,13 @@ class Scratch3ControlBlocks { } allAtOnce (args, util) { - // Since the "all at once" block is implemented for compatiblity with - // Scratch 2.0 projects, it behaves the same way it did in 2.0, which - // is to simply run the contained script (like "if 1 = 1"). - // (In early versions of Scratch 2.0, it would work the same way as - // "run without screen refresh" custom blocks do now, but this was - // removed before the release of 2.0.) + // In Scratch 3.0 and TurboWarp, this would simply + // run the contained substack. In Unsandboxed, + // we've reimplemented the intended functionality + // of running the stack all in one frame. + util.thread.peekStackFrame().warpMode = false; util.startBranch(1, false); + util.thread.peekStackFrame().warpMode = true; } } diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index a2a5ab4bd2..b730e5706b 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -25,10 +25,12 @@ class Scratch3OperatorsBlocks { operator_gt: this.gt, operator_and: this.and, operator_or: this.or, + operator_xor: this.xor, operator_not: this.not, operator_random: this.random, operator_join: this.join, operator_letter_of: this.letterOf, + operator_letters_of: this.lettersOf, operator_length: this.length, operator_contains: this.contains, operator_mod: this.mod, @@ -73,6 +75,10 @@ class Scratch3OperatorsBlocks { return Cast.toBoolean(args.OPERAND1) || Cast.toBoolean(args.OPERAND2); } + xor (args) { + return Cast.toBoolean(args.OPERAND1) !== Cast.toBoolean(args.OPERAND2); + } + not (args) { return !Cast.toBoolean(args.OPERAND); } @@ -107,6 +113,12 @@ class Scratch3OperatorsBlocks { return str.charAt(index); } + lettersOf (args) { + const index1 = Cast.toNumber(args.LETTER1) - 1; + const index2 = Cast.toNumber(args.LETTER2) - 1; + return str.slice(Math.max(index1, 1), Math.min(str.length, index2)); + } + length (args) { return Cast.toString(args.STRING).length; } diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index c3679fb41a..acd32ef2e0 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -663,7 +663,7 @@ class ScriptTreeGenerator { const blockInfo = this.getBlockInfo(block.opcode); if (blockInfo) { const type = blockInfo.info.blockType; - if (type === BlockType.REPORTER || type === BlockType.BOOLEAN) { + if (type === BlockType.REPORTER || type === BlockType.BOOLEAN || type === BlockType.INLINE) { return this.descendCompatLayer(block); } } @@ -694,15 +694,14 @@ class ScriptTreeGenerator { descendStackedBlock (block) { switch (block.opcode) { case 'control_all_at_once': - // In Scratch 3, this block behaves like "if 1 = 1" + // In Unsandboxed, attempts to run the script in 1 frame. return { - kind: 'control.if', + kind: 'control.allAtOnce', condition: { kind: 'constant', value: true }, - whenTrue: this.descendSubstack(block, 'SUBSTACK'), - whenFalse: [] + code: this.descendSubstack(block, 'SUBSTACK'), }; case 'control_clear_counter': return { @@ -1415,7 +1414,7 @@ class ScriptTreeGenerator { const blockInfo = this.getBlockInfo(block.opcode); const blockType = (blockInfo && blockInfo.info && blockInfo.info.blockType) || BlockType.COMMAND; const substacks = {}; - if (blockType === BlockType.CONDITIONAL || blockType === BlockType.LOOP) { + if (blockType === BlockType.CONDITIONAL || blockType === BlockType.LOOP || blockType === BlockType.INLINE) { for (const inputName in block.inputs) { if (!inputName.startsWith('SUBSTACK')) continue; const branchNum = inputName === 'SUBSTACK' ? 1 : +inputName.substring('SUBSTACK'.length); diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index ff07f3f0d5..b9a6c5e538 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -876,6 +876,13 @@ class JSGenerator { this.source += `}\n`; break; + case 'control.allAtOnce': + const previousWarp = this.isWarp; + this.isWarp = true; + this.descendStack(node.code, new Frame(false, 'control.allAtOnce')); + this.isWarp = previousWarp; + break; + case 'counter.clear': this.source += 'runtime.ext_scratch3_control._counter = 0;\n'; break; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 6fc794268f..d15574f835 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -123,6 +123,10 @@ const ArgumentTypeMap = (() => { fieldName: 'SOUND_MENU' } }; + map[ArgumentType.VARIABLE] = { + fieldType: 'field_variable', + fieldName: 'VARIABLE' + }; return map; })(); @@ -1219,14 +1223,15 @@ class Runtime extends EventEmitter { type: menuId, inputsInline: true, output: 'String', - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3, + colour: menuInfo.acceptText ? '#FFFFFF' : categoryInfo.color1, + colourSecondary: menuInfo.acceptText ? '#FFFFFF' : categoryInfo.color2, + colourTertiary: menuInfo.acceptText ? '#FFFFFF' : categoryInfo.color3, outputShape: menuInfo.acceptReporters ? ScratchBlocksConstants.OUTPUT_SHAPE_ROUND : ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE, args0: [ - { - type: 'field_dropdown', + {// to do: we could reimplement field_numberdropdown here really easily + type: menuInfo.acceptText ? + 'field_textdropdown' : 'field_dropdown', name: menuName, options: menuItems } @@ -1414,6 +1419,11 @@ class Runtime extends EventEmitter { blockJSON.nextStatement = null; // null = available connection; undefined = terminal } break; + case BlockType.INLINE: + blockInfo.branchCount = blockInfo.branchCount || 1; + blockJSON.output = blockInfo.allowDropAnywhere ? null : 'String'; // TODO: distinguish number & string here? + blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; + break; } const blockText = Array.isArray(blockInfo.text) ? blockInfo.text : [blockInfo.text]; @@ -1561,7 +1571,7 @@ class Runtime extends EventEmitter { } /** - * Helper for _convertPlaceholdes which handles inline images which are a specialized case of block "arguments". + * Helper for _convertPlaceholders which handles inline images which are a specialized case of block "arguments". * @param {object} argInfo Metadata about the inline image as specified by the extension * @return {object} JSON blob for a scratch-blocks image field. * @private @@ -1583,6 +1593,24 @@ class Runtime extends EventEmitter { }; } + /** + * Helper for _convertPlaceholders which handles variable fields which are a specialized case of block "arguments". + * @param {object} argInfo Metadata about the inline image as specified by the extension + * @return {object} JSON blob for a scratch-blocks variable field. + * @private + */ + _constructVariableJson (argInfo, placeholder) { + return { + type: 'field_variable', + name: placeholder, + variableTypes: + // eslint-disable-next-line max-len + argInfo.variableTypes ? (Array.isArray(argInfo.variableTypes) ? argInfo.variableTypes : [argInfo.variableTypes]) : [''], + variable: (argInfo.variableTypes === 'broadcast_msg') ? 'message1' : null, + filter: argInfo.filter ?? [] + }; + } + /** * Helper for _convertForScratchBlocks which handles linearization of argument placeholders. Called as a callback * from string#replace. In addition to the return value the JSON and XML items in the context will be filled. @@ -1610,6 +1638,8 @@ class Runtime extends EventEmitter { // check if this is not one of those cases. E.g. an inline image on a block. if (argTypeInfo.fieldType === 'field_image') { argJSON = this._constructInlineImageJson(argInfo); + } else if (argTypeInfo.fieldType === 'field_variable') { + argJSON = this._constructVariableJson(argInfo, placeholder); } else { // Construct input value diff --git a/src/extension-support/argument-type.js b/src/extension-support/argument-type.js index 2e8c13fff8..bf7fbdabb4 100644 --- a/src/extension-support/argument-type.js +++ b/src/extension-support/argument-type.js @@ -51,7 +51,12 @@ const ArgumentType = { /** * Name of sound in the current target */ - SOUND: 'sound' + SOUND: 'sound', + + /** + * Name of variable in the current specified target(s) + */ + VARIABLE: 'variable' }; module.exports = ArgumentType; diff --git a/src/extension-support/block-type.js b/src/extension-support/block-type.js index ecaf70754d..4680ef4af1 100644 --- a/src/extension-support/block-type.js +++ b/src/extension-support/block-type.js @@ -54,7 +54,13 @@ const BlockType = { /** * Arbitrary scratch-blocks XML. */ - XML: 'xml' + XML: 'xml', + + /** + * Specialized reporter block that allows for the insertion and evaluation + * of a substack. + */ + INLINE: 'inline' }; module.exports = BlockType; diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 5ec3979e0a..0f48912002 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -487,7 +487,8 @@ class ExtensionManager { }); if (!menuItems || menuItems.length < 1) { - throw new Error(`Extension menu returned no items: ${menuItemFunctionName}`); + console.warn(`Extension menu returned no items: ${menuItemFunctionName}`); + return ['']; } return menuItems; }