From 6aebf22158cc42401fbd97e1fdd9996e2f51a04e Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 11 Feb 2024 20:24:32 +0000 Subject: [PATCH 01/12] Text Dropdowns (vm Part) (#1) Exposes the new textdropdowns to extensions. To do: Find a logical way to implement number dropdowns. --- src/engine/runtime.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 6fc794268f..d9afa9b127 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1219,14 +1219,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 } From 398f0dcac8d4e4243ce745019174775a94cc1e28 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 11 Feb 2024 20:26:59 +0000 Subject: [PATCH 02/12] Add BlockType.INLINE (#2) Add the "Inline" BlockType for extensions. Might add the Inline block as an official block in the distant future. --- src/compiler/irgen.js | 4 ++-- src/engine/runtime.js | 5 +++++ src/extension-support/block-type.js | 8 +++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index c3679fb41a..880aaa82ea 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); } } @@ -1415,7 +1415,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/engine/runtime.js b/src/engine/runtime.js index d9afa9b127..76d4270085 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1415,6 +1415,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]; 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; From fc02f89dbfd405ecb75302f2f3bf869f527ff6e0 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 11 Feb 2024 20:47:13 +0000 Subject: [PATCH 03/12] "all at once" reimplementation (ScratchBlocks Part) (#3) Reimplements the deprecated "all at once" block. Changes a single comment. --- src/blocks/scratch3_control.js | 12 ++++++------ src/compiler/jsgen.js | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) 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/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; From cd3bb5e74fcdd60897b8098afd185861dbbea7bd Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 12 Feb 2024 00:49:11 +0000 Subject: [PATCH 04/12] Fix allAtOnce in compiler. --- src/compiler/irgen.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 880aaa82ea..d5c8e15040 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -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: [] + this.descendSubstack(block, 'SUBSTACK'), }; case 'control_clear_counter': return { From 1b4127d824a75330713649d4939a3281ecc73a81 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:30:52 +0000 Subject: [PATCH 05/12] Fix allAtOnce in compiler (again) --- src/compiler/irgen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index d5c8e15040..294e051bbc 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -701,7 +701,7 @@ class ScriptTreeGenerator { kind: 'constant', value: true }, - this.descendSubstack(block, 'SUBSTACK'), + do: this.descendSubstack(block, 'SUBSTACK'), }; case 'control_clear_counter': return { From 81def271a39a05c31fb7a2cfc95927c41b5d8dd4 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 12 Feb 2024 01:37:37 +0000 Subject: [PATCH 06/12] Fix allAtOnce in compiler (for the final time) --- src/compiler/irgen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 294e051bbc..acd32ef2e0 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -701,7 +701,7 @@ class ScriptTreeGenerator { kind: 'constant', value: true }, - do: this.descendSubstack(block, 'SUBSTACK'), + code: this.descendSubstack(block, 'SUBSTACK'), }; case 'control_clear_counter': return { From 5b432b377679e2ee93239cca963bebf3e872d8a6 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 12 Feb 2024 02:35:57 +0000 Subject: [PATCH 07/12] Update extension-manager.js (#4) --- src/extension-support/extension-manager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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; } From 5a1d7c58529dcfccd9a0be6df3dc592396a698ea Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 12 Feb 2024 06:16:48 +0000 Subject: [PATCH 08/12] ArgumentType.VARIABLE (vm part) (#5) The ScratchBlocks part will come later. For now, filter will go unused. --- src/engine/runtime.js | 24 ++++++++++++++++++++++++ src/extension-support/argument-type.js | 7 ++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 76d4270085..a5ee3562c6 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; })(); @@ -1589,6 +1593,24 @@ class Runtime extends EventEmitter { }; } + /** + * Helper for _convertPlaceholdes 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. @@ -1616,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; From d2c123fd12db2ba81a50e41f75a29ffc4e4006a7 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:39:07 +0000 Subject: [PATCH 09/12] Allow custom variable declaration --- src/engine/runtime.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index a5ee3562c6..5f5dffc26b 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1571,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 @@ -1594,19 +1594,20 @@ class Runtime extends EventEmitter { } /** - * Helper for _convertPlaceholdes which handles variable fields which are a specialized case of block "arguments". + * 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) { + const variable = argInfo.variable; 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, + variable: variable ?? (argInfo.variableTypes === 'broadcast_msg') ? 'message1' : null; filter: argInfo.filter ?? [] }; } From fdebbe0254713069925816e7f814a5a212acb94b Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 12 Feb 2024 21:17:22 +0000 Subject: [PATCH 10/12] Remove semicolon --- src/engine/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 5f5dffc26b..028d156d45 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1607,7 +1607,7 @@ class Runtime extends EventEmitter { variableTypes: // eslint-disable-next-line max-len argInfo.variableTypes ? (Array.isArray(argInfo.variableTypes) ? argInfo.variableTypes : [argInfo.variableTypes]) : [''], - variable: variable ?? (argInfo.variableTypes === 'broadcast_msg') ? 'message1' : null; + variable: variable ?? (argInfo.variableTypes === 'broadcast_msg') ? 'message1' : null, filter: argInfo.filter ?? [] }; } From 44cb44936dda755be6234bb0310d3c1244e2e50a Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 12 Feb 2024 23:08:27 +0000 Subject: [PATCH 11/12] Remove variable default name --- src/engine/runtime.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 028d156d45..d15574f835 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1600,14 +1600,13 @@ class Runtime extends EventEmitter { * @private */ _constructVariableJson (argInfo, placeholder) { - const variable = argInfo.variable; return { type: 'field_variable', name: placeholder, variableTypes: // eslint-disable-next-line max-len argInfo.variableTypes ? (Array.isArray(argInfo.variableTypes) ? argInfo.variableTypes : [argInfo.variableTypes]) : [''], - variable: variable ?? (argInfo.variableTypes === 'broadcast_msg') ? 'message1' : null, + variable: (argInfo.variableTypes === 'broadcast_msg') ? 'message1' : null, filter: argInfo.filter ?? [] }; } From dd55a1839dd852fc28deb03665f370eed0642a48 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 13 Feb 2024 01:42:02 +0000 Subject: [PATCH 12/12] Update scratch3_operators.js --- src/blocks/scratch3_operators.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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; }