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 001/202] 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 6fc794268f7..d9afa9b127d 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 002/202] 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 c3679fb41a7..880aaa82eaf 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 d9afa9b127d..76d42700857 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 ecaf70754dc..4680ef4af10 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 003/202] "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 ebf1951514e..7324c2d0266 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 ff07f3f0d57..b9a6c5e538b 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 004/202] 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 880aaa82eaf..d5c8e150400 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 005/202] 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 d5c8e150400..294e051bbc8 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 006/202] 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 294e051bbc8..acd32ef2e04 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 007/202] 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 5ec3979e0ae..0f489120022 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 008/202] 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 76d42700857..a5ee3562c66 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 2e8c13fff84..bf7fbdabb41 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 009/202] 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 a5ee3562c66..5f5dffc26bd 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 010/202] 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 5f5dffc26bd..028d156d45f 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 011/202] 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 028d156d45f..d15574f8351 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 0e8c4f52d074635425e8581029d1fdacace1c08a Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 13 Feb 2024 01:56:02 +0000 Subject: [PATCH 012/202] Make some compatibility changes for ArgumentType.VARIABLE (#6) * Update runtime.js * Update execute.js * Update blocks.js --- src/engine/blocks.js | 3 +-- src/engine/execute.js | 6 +----- src/engine/runtime.js | 3 ++- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 71ace3a205f..ccc0fa06ed8 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -685,8 +685,7 @@ class Blocks { // Update block value if (!block.fields[args.name]) return; - if (args.name === 'VARIABLE' || args.name === 'LIST' || - args.name === 'BROADCAST_OPTION') { + if (typeof block.fields[args.name].variableTypes !== 'undefined') { // Get variable name using the id in args.value. const variable = this.runtime.getEditingTarget().lookupVariableById(args.value); if (variable) { diff --git a/src/engine/execute.js b/src/engine/execute.js index 7e94ee9a36d..663a1d34af2 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -311,11 +311,7 @@ class BlockCached { // Store the static fields onto _argValues. for (const fieldName in fields) { - if ( - fieldName === 'VARIABLE' || - fieldName === 'LIST' || - fieldName === 'BROADCAST_OPTION' - ) { + if (typepof fields[fieldName].variableType !== 'undefined') { this._argValues[fieldName] = { id: fields[fieldName].id, name: fields[fieldName].value diff --git a/src/engine/runtime.js b/src/engine/runtime.js index d15574f8351..7dcd514f1f6 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1606,7 +1606,8 @@ class Runtime extends EventEmitter { variableTypes: // eslint-disable-next-line max-len argInfo.variableTypes ? (Array.isArray(argInfo.variableTypes) ? argInfo.variableTypes : [argInfo.variableTypes]) : [''], - variable: (argInfo.variableTypes === 'broadcast_msg') ? 'message1' : null, + // TO DO: default to an existing variable. + variable: argInfo.variable ?? (argInfo.variableTypes === 'broadcast_msg') ? 'message1' : null, filter: argInfo.filter ?? [] }; } From 14698f813e89dbd00e642c0df861b77fd3260043 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 13 Feb 2024 01:57:06 +0000 Subject: [PATCH 013/202] Typo --- src/engine/execute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 663a1d34af2..c5bd08bf601 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -311,7 +311,7 @@ class BlockCached { // Store the static fields onto _argValues. for (const fieldName in fields) { - if (typepof fields[fieldName].variableType !== 'undefined') { + if (typepof fields[fieldName].variableTypes !== 'undefined') { this._argValues[fieldName] = { id: fields[fieldName].id, name: fields[fieldName].value From 7df93a01ad1a3f9f228358a4b86594e52e9dc7c5 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 13 Feb 2024 02:53:10 +0000 Subject: [PATCH 014/202] Typo --- src/engine/execute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index c5bd08bf601..10c20615ad6 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -311,7 +311,7 @@ class BlockCached { // Store the static fields onto _argValues. for (const fieldName in fields) { - if (typepof fields[fieldName].variableTypes !== 'undefined') { + if (typeof fields[fieldName].variableTypes !== 'undefined') { this._argValues[fieldName] = { id: fields[fieldName].id, name: fields[fieldName].value From f0830c584b40322c5488823abad5e46dee5a2ed7 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:10:48 +0000 Subject: [PATCH 015/202] variableTypes > variableType --- src/engine/execute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/execute.js b/src/engine/execute.js index 10c20615ad6..531c1b3ced3 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -311,7 +311,7 @@ class BlockCached { // Store the static fields onto _argValues. for (const fieldName in fields) { - if (typeof fields[fieldName].variableTypes !== 'undefined') { + if (typeof fields[fieldName].variableType !== 'undefined') { this._argValues[fieldName] = { id: fields[fieldName].id, name: fields[fieldName].value From 0bbdb3e6dbcccf20c4849610242ca1e3088cd792 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:11:28 +0000 Subject: [PATCH 016/202] variableTypes > variableType --- src/engine/blocks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/blocks.js b/src/engine/blocks.js index ccc0fa06ed8..bb397832cae 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -685,7 +685,7 @@ class Blocks { // Update block value if (!block.fields[args.name]) return; - if (typeof block.fields[args.name].variableTypes !== 'undefined') { + if (typeof block.fields[args.name].variableType !== 'undefined') { // Get variable name using the id in args.value. const variable = this.runtime.getEditingTarget().lookupVariableById(args.value); if (variable) { From a90754dea7877ef5adbd1ea0a368ee3c44361080 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 13 Feb 2024 22:27:17 +0000 Subject: [PATCH 017/202] is TurboWarp? > is Unsandboxed? (#9) --- src/blocks/scratch3_procedures.js | 2 +- src/compiler/irgen.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js index 679fdf128e8..1f55d32bf16 100644 --- a/src/blocks/scratch3_procedures.js +++ b/src/blocks/scratch3_procedures.js @@ -122,7 +122,7 @@ class Scratch3ProcedureBlocks { if (util.target.runtime.compilerOptions.enabled && lowercaseValue === 'is compiled?') { return true; } - if (lowercaseValue === 'is turbowarp?') { + if (lowercaseValue === 'is unsandboxed?') { return true; } // When the parameter is not found in the most recent procedure diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index acd32ef2e04..1fe9a8efc21 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -210,7 +210,7 @@ class ScriptTreeGenerator { const name = block.fields.VALUE.value; const index = this.script.arguments.lastIndexOf(name); if (index === -1) { - if (name.toLowerCase() === 'is compiled?' || name.toLowerCase() === 'is turbowarp?') { + if (name.toLowerCase() === 'is compiled?' || name.toLowerCase() === 'is unsandboxed?') { return { kind: 'constant', value: true From 175df9d3b062ba445b2b21b0ad81bf89c3fd331d Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 13 Feb 2024 23:02:10 +0000 Subject: [PATCH 018/202] Update Unsandboxed blocks --- src/extensions/tw/index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/extensions/tw/index.js b/src/extensions/tw/index.js index 9485ff006cf..465c1b60347 100644 --- a/src/extensions/tw/index.js +++ b/src/extensions/tw/index.js @@ -7,7 +7,7 @@ const Cast = require('../../util/cast'); const iconURI = `data:image/svg+xml;base64,${btoa('')}`; /** - * Class for TurboWarp blocks + * Class for Unsandboxed blocks * @constructor */ class TurboWarpBlocks { @@ -25,13 +25,13 @@ class TurboWarpBlocks { getInfo () { return { id: 'tw', - name: 'TurboWarp', - color1: '#ff4c4c', - color2: '#e64444', - color3: '#c73a3a', + name: 'Unsandboxed', + color1: '#66757f', + color2: '#5c6a73', + color3: '#525e66', docsURI: 'https://docs.turbowarp.org/blocks', - menuIconURI: iconURI, - blockIconURI: iconURI, + // menuIconURI: iconURI, + // blockIconURI: iconURI, blocks: [ { opcode: 'getLastKeyPressed', From 5e3bcc7c09186a66612cebd27abc4f00fcdf7c28 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 14 Feb 2024 01:00:56 +0000 Subject: [PATCH 019/202] Allow Interpolation to be enabled on individual targets (#10) --- src/engine/runtime.js | 9 +++++++-- src/engine/tw-interpolate.js | 3 ++- src/sprites/rendered-target.js | 7 +++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 7dcd514f1f6..e06292979d1 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2520,7 +2520,12 @@ class Runtime extends EventEmitter { * inactive threads after each iteration. */ _step () { - if (this.interpolationEnabled) { + let targetHasInterpolation = false; + for (const target of this.targets) { + if (target.interpolation) targetHasInterpolation = true; + } + + if (this.interpolationEnabled || targetHasInterpolation) { interpolate.setupInitialState(this); } @@ -2600,7 +2605,7 @@ class Runtime extends EventEmitter { this.profiler.reportFrames(); } - if (this.interpolationEnabled) { + if (this.interpolationEnabled || targetHasInterpolation) { this._lastStepTime = Date.now(); } } diff --git a/src/engine/tw-interpolate.js b/src/engine/tw-interpolate.js index 3556dffd224..ce7d1580180 100644 --- a/src/engine/tw-interpolate.js +++ b/src/engine/tw-interpolate.js @@ -46,7 +46,8 @@ const interpolate = (runtime, time) => { // interpolationData is the initial state at the start of the frame (time 0) // the state on the target itself is the state at the end of the frame (time 1) const interpolationData = target.interpolationData; - if (!interpolationData) { + const interpolationEnabled = target.interpolation; + if !(interpolationData || interpolationEnabled) { continue; } diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index eeb029f3874..26e04469f6e 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -167,6 +167,7 @@ class RenderedTarget extends Target { this.onTargetMoved = null; this.onTargetVisualChange = null; + this.interpolation = false; this.interpolationData = null; } @@ -1066,6 +1067,12 @@ class RenderedTarget extends Target { this.dragging = false; } + /** + * Set whether the sprite is interpolated. + */ + setInterpolation(interpolated) { + this.interpolation = interpolated; + } /** * Serialize sprite info, used when emitting events about the sprite From 682b5e83d04d9214be197c371bce3b2a5d3bf14a Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 14 Feb 2024 01:24:46 +0000 Subject: [PATCH 020/202] illegal --- src/engine/tw-interpolate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/tw-interpolate.js b/src/engine/tw-interpolate.js index ce7d1580180..71bfa381e90 100644 --- a/src/engine/tw-interpolate.js +++ b/src/engine/tw-interpolate.js @@ -47,7 +47,7 @@ const interpolate = (runtime, time) => { // the state on the target itself is the state at the end of the frame (time 1) const interpolationData = target.interpolationData; const interpolationEnabled = target.interpolation; - if !(interpolationData || interpolationEnabled) { + if (!(interpolationData || interpolationEnabled)) { continue; } From ee2bbb480f4c665e351eab385a091e7d7d2c0c09 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 14 Feb 2024 02:44:40 +0000 Subject: [PATCH 021/202] Add acceptReporters to (some) argument types (#11) --- src/engine/runtime.js | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index e06292979d1..c71c863e445 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -130,6 +130,23 @@ const ArgumentTypeMap = (() => { return map; })(); +const FieldTypeMap = (() => { + const map = {}; + map[ArgumentType.ANGLE] = { + fieldName: "field_angle", + }; + map[ArgumentType.NUMBER] = { + fieldName: "field_number", + }; + map[ArgumentType.STRING] = { + fieldName: "field_input", + }; + map[ArgumentType.NOTE] = { + fieldName: "field_note", + }; + return map; +})(); + /** * A pair of functions used to manage the cloud variable limit, * to be used when adding (or attempting to add) or removing a cloud variable. @@ -1226,8 +1243,7 @@ class Runtime extends EventEmitter { 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, + outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND, args0: [ {// to do: we could reimplement field_numberdropdown here really easily type: menuInfo.acceptText ? @@ -1639,7 +1655,7 @@ 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') { + } else if (argTypeInfo.fieldType === 'field_variable' &&) { argJSON = this._constructVariableJson(argInfo, placeholder); } else { // Construct input value @@ -1666,7 +1682,15 @@ class Runtime extends EventEmitter { let fieldName; if (argInfo.menu) { const menuInfo = context.categoryInfo.menuInfo[argInfo.menu]; - if (menuInfo.acceptReporters) { + + let acceptReporters = false; + if (typeof argInfo.acceptReporters !== "undefined") { + acceptReporters = argInfo.acceptReporters; + } else if (typeof menuInfo.acceptReporters !== "undefined") { + acceptReporters = menuInfo.acceptReporters; + } + + if (acceptReporters) { valueName = placeholder; shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id); fieldName = argInfo.menu; @@ -1677,6 +1701,11 @@ class Runtime extends EventEmitter { shadowType = null; fieldName = placeholder; } + } else if (argInfo.acceptReporters === false && FieldTypeMap[argInfo.type]) { + argJSON.type = FieldTypeMap[argInfo.type].fieldName; + valueName = null; + shadowType = null; + fieldName = placeholder; } else { valueName = placeholder; shadowType = (argTypeInfo.shadow && argTypeInfo.shadow.type) || null; From db687f37be2eaadc442cc9beb422c056b7938a75 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 14 Feb 2024 02:59:48 +0000 Subject: [PATCH 022/202] add number dropdowns --- src/engine/runtime.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index c71c863e445..ba6d2a4e449 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1234,20 +1234,23 @@ class Runtime extends EventEmitter { _buildMenuForScratchBlocks (menuName, menuInfo, categoryInfo) { const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id); const menuItems = this._convertMenuItems(menuInfo.items); + const acceptInput = (menuInfo.acceptText || menuInfo.acceptNumber); + const type = menuInfo.acceptText ? + 'field_textdropdown' : menuInfo.acceptNumber ? + 'field_numberdropdown' : 'field_dropdown'; return { json: { message0: '%1', type: menuId, inputsInline: true, output: 'String', - colour: menuInfo.acceptText ? '#FFFFFF' : categoryInfo.color1, - colourSecondary: menuInfo.acceptText ? '#FFFFFF' : categoryInfo.color2, - colourTertiary: menuInfo.acceptText ? '#FFFFFF' : categoryInfo.color3, + colour: acceptInput ? '#FFFFFF' : categoryInfo.color1, + colourSecondary: acceptInput ? '#FFFFFF' : categoryInfo.color2, + colourTertiary: acceptInput ? '#FFFFFF' : categoryInfo.color3, outputShape: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND, args0: [ - {// to do: we could reimplement field_numberdropdown here really easily - type: menuInfo.acceptText ? - 'field_textdropdown' : 'field_dropdown', + { + type, name: menuName, options: menuItems } From 4151c5bd2e9b839e4c3fc8adfa078e0b804ef3ae Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 14 Feb 2024 03:14:56 +0000 Subject: [PATCH 023/202] fix typo --- 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 ba6d2a4e449..acbc383152e 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1658,7 +1658,7 @@ 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' &&) { + } else if (argTypeInfo.fieldType === 'field_variable') { argJSON = this._constructVariableJson(argInfo, placeholder); } else { // Construct input value From 776515fe2b324d3f1601da49960b6f8247aa0e1f Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 14 Feb 2024 03:27:03 +0000 Subject: [PATCH 024/202] Fix conditions for this --- src/engine/tw-interpolate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/tw-interpolate.js b/src/engine/tw-interpolate.js index 71bfa381e90..785af8238c2 100644 --- a/src/engine/tw-interpolate.js +++ b/src/engine/tw-interpolate.js @@ -47,7 +47,7 @@ const interpolate = (runtime, time) => { // the state on the target itself is the state at the end of the frame (time 1) const interpolationData = target.interpolationData; const interpolationEnabled = target.interpolation; - if (!(interpolationData || interpolationEnabled)) { + if (!interpolationData || !interpolationEnabled)) { continue; } From 8035d0886c912b150e55cd863b86b08cb61a4e3c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 14 Feb 2024 03:31:43 +0000 Subject: [PATCH 025/202] typo --- src/engine/tw-interpolate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/tw-interpolate.js b/src/engine/tw-interpolate.js index 785af8238c2..4396c79df4b 100644 --- a/src/engine/tw-interpolate.js +++ b/src/engine/tw-interpolate.js @@ -47,7 +47,7 @@ const interpolate = (runtime, time) => { // the state on the target itself is the state at the end of the frame (time 1) const interpolationData = target.interpolationData; const interpolationEnabled = target.interpolation; - if (!interpolationData || !interpolationEnabled)) { + if (!interpolationData || !interpolationEnabled) { continue; } From 8c0bc4098e240dc6419913c4bfbe68fdc55e93e8 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 15 Feb 2024 01:46:22 +0000 Subject: [PATCH 026/202] Remove limits and fencing by default --- src/engine/runtime.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index acbc383152e..b933efd0bc0 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -473,8 +473,8 @@ class Runtime extends EventEmitter { this.runtimeOptions = { maxClones: Runtime.MAX_CLONES, - miscLimits: true, - fencing: true + miscLimits: false, + fencing: false }; this.compilerOptions = { From 5ae81cdccd3fb3edb48d2fb45f693c3bc08ac455 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 15 Feb 2024 05:19:30 +0000 Subject: [PATCH 027/202] Add new blocks + Overhaul palette (vm part) (#7) --- src/blocks/scratch3_event.js | 6 ++++++ src/blocks/scratch3_looks.js | 7 +++++++ src/blocks/scratch3_motion.js | 24 ++++++++++++++++++++++++ src/blocks/scratch3_operators.js | 13 +++++++++++++ src/compiler/compat-blocks.js | 2 ++ src/compiler/irgen.js | 13 +++++++++++++ src/compiler/jsgen.js | 8 ++++++++ 7 files changed, 73 insertions(+) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index 10ceb10cc3d..4992ad1a5b5 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -24,6 +24,7 @@ class Scratch3EventBlocks { */ getPrimitives () { return { + event_when: this.when, event_whentouchingobject: this.touchingObject, event_broadcast: this.broadcast, event_broadcastandwait: this.broadcastAndWait, @@ -62,6 +63,11 @@ class Scratch3EventBlocks { }; } + when (args, util) { + const condition = Cast.toBoolean(args.CONDITION); + return condition; + } + touchingObject (args, util) { return util.target.isTouchingObject(args.TOUCHINGOBJECTMENU); } diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js index e84e0f83893..ba13dd7ba2e 100644 --- a/src/blocks/scratch3_looks.js +++ b/src/blocks/scratch3_looks.js @@ -298,6 +298,7 @@ class Scratch3LooksBlocks { looks_changeeffectby: this.changeEffect, looks_seteffectto: this.setEffect, looks_cleargraphiceffects: this.clearEffects, + looks_effect: this.getEffect, looks_changesizeby: this.changeSize, looks_setsizeto: this.setSize, looks_changestretchby: () => {}, // legacy no-op blocks @@ -560,6 +561,12 @@ class Scratch3LooksBlocks { util.target.clearEffects(); } + getEffect (args, util) { + const effect = Cast.toString(args.EFFECT).toLowerCase(); + const value = util.target.effects[effect]; + return Cast.toNumber(value); + } + changeSize (args, util) { const change = Cast.toNumber(args.CHANGE); util.target.setSize(util.target.size + change); diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index 057d73db124..72da61b3cb6 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -20,10 +20,12 @@ class Scratch3MotionBlocks { motion_movesteps: this.moveSteps, motion_gotoxy: this.goToXY, motion_goto: this.goTo, + motion_changebyxy: this.changeByXY, motion_turnright: this.turnRight, motion_turnleft: this.turnLeft, motion_pointindirection: this.pointInDirection, motion_pointtowards: this.pointTowards, + motion_pointtowards: this.pointTowardsXY, motion_glidesecstoxy: this.glide, motion_glideto: this.glideTo, motion_ifonedgebounce: this.ifOnEdgeBounce, @@ -35,6 +37,7 @@ class Scratch3MotionBlocks { motion_xposition: this.getX, motion_yposition: this.getY, motion_direction: this.getDirection, + motion_rotationstyle: this.getRotationStyle, // Legacy no-op blocks: motion_scroll_right: () => {}, motion_scroll_up: () => {}, @@ -57,6 +60,10 @@ class Scratch3MotionBlocks { motion_direction: { isSpriteSpecific: true, getId: targetId => `${targetId}_direction` + }, + motion_rotationstyle: { + isSpriteSpecific: true, + getId: targetId => `${targetId}_rotationstyle` } }; } @@ -78,6 +85,12 @@ class Scratch3MotionBlocks { util.target.setXY(x, y); } + changeByXY (args, util) { + const dx = Cast.toNumber(args.X); + const dy = Cast.toNumber(args.Y); + util.target.setXY(util.target.x + dx, util.target.y + dy); + } + getTargetXY (targetName, util) { let targetX = 0; let targetY = 0; @@ -144,6 +157,13 @@ class Scratch3MotionBlocks { util.target.setDirection(direction); } + pointTowardsXY (args, util) { + const dx = Cast.toNumber(args.X) - util.target.x; + const dy = Cast.toNumber(args.Y) - util.target.y; + const direction = 90 - MathUtil.radToDeg(Math.atan2(dy, dx)); + util.target.setDirection(direction); + } + glide (args, util) { if (util.stackFrame.timer) { const timeElapsed = util.stackFrame.timer.timeElapsed(); @@ -281,6 +301,10 @@ class Scratch3MotionBlocks { return util.target.direction; } + getRotationStyle (args, util) { + return util.target.rotationStyle; + } + // This corresponds to snapToInteger in Scratch 2 limitPrecision (coordinate) { const rounded = Math.round(coordinate); diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index a2a5ab4bd2b..068a7e23312 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,13 @@ class Scratch3OperatorsBlocks { return str.charAt(index); } + lettersOf (args) { + const index1 = Cast.toNumber(args.LETTER1) - 1; + const index2 = Cast.toNumber(args.LETTER2) - 1; + const str = Cast.toString(args.STRING); + 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/compat-blocks.js b/src/compiler/compat-blocks.js index 1c9d8f5ae9a..51094bedbaf 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -18,7 +18,9 @@ const stacked = [ 'motion_glidesecstoxy', 'motion_glideto', 'motion_goto', + 'motion_changebyxy', 'motion_pointtowards', + 'motion_pointtowardsxy', 'motion_scroll_right', 'motion_scroll_up', 'sensing_askandwait', diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 1fe9a8efc21..f62c8c2ba41 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -366,6 +366,13 @@ class ScriptTreeGenerator { letter: this.descendInputOfBlock(block, 'LETTER'), string: this.descendInputOfBlock(block, 'STRING') }; + case 'operator_letters_of': + return { + kind: 'op.lettersOf', + left: this.descendInputOfBlock(block, 'LETTER1'), + right: this.descendInputOfBlock(block, 'LETTER2'), + string: this.descendInputOfBlock(block, 'STRING') + }; case 'operator_lt': return { kind: 'op.less', @@ -461,6 +468,12 @@ class ScriptTreeGenerator { left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; + case 'operator_xor': + return { + kind: 'op.xor', + left: this.descendInputOfBlock(block, 'OPERAND1'), + right: this.descendInputOfBlock(block, 'OPERAND2') + }; case 'operator_random': { const from = this.descendInputOfBlock(block, 'FROM'); const to = this.descendInputOfBlock(block, 'TO'); diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index b9a6c5e538b..e11df633355 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -472,6 +472,8 @@ class JSGenerator { case 'list.length': return new TypedInput(`${this.referenceVariable(node.list)}.value.length`, TYPE_NUMBER); + case 'looks.effect': + return new TypedInput('Math.round(target)', TYPE_NUMBER); case 'looks.size': return new TypedInput('Math.round(target.size)', TYPE_NUMBER); case 'looks.backdropName': @@ -485,6 +487,8 @@ class JSGenerator { case 'motion.direction': return new TypedInput('target.direction', TYPE_NUMBER); + case 'motion.rotationStyle': + return new TypedInput('target.rotationStyle', TYPE_NUMBER); case 'motion.x': return new TypedInput('limitPrecision(target.x)', TYPE_NUMBER); case 'motion.y': @@ -593,6 +597,8 @@ class JSGenerator { } case 'op.letterOf': return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); + case 'op.lettersOf': + return new TypedInput(`((${this.descendInput(node.string).asString()}).substring(${this.descendInput(node.letter1).asNumber()}, ${this.descendInput(node.letter2).asNumber()}) || "")`, TYPE_STRING); case 'op.ln': // Needs to be marked as NaN because Math.log(-1) == NaN return new TypedInput(`Math.log(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER_NAN); @@ -610,6 +616,8 @@ class JSGenerator { return new TypedInput(`!${this.descendInput(node.operand).asBoolean()}`, TYPE_BOOLEAN); case 'op.or': return new TypedInput(`(${this.descendInput(node.left).asBoolean()} || ${this.descendInput(node.right).asBoolean()})`, TYPE_BOOLEAN); + case 'op.xor': + return new TypedInput(`(${this.descendInput(node.left).asBoolean()} !== ${this.descendInput(node.right).asBoolean()})`, TYPE_BOOLEAN); case 'op.random': if (node.useInts) { // Both inputs are ints, so we know neither are NaN From 0f45f9cc7612644ed20a5dd72b3495123bfa6fb2 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 15 Feb 2024 06:38:13 +0000 Subject: [PATCH 028/202] Fix new blocks (Part 1/?) (#12) --- src/blocks/scratch3_event.js | 4 ++++ src/blocks/scratch3_motion.js | 6 +++--- src/blocks/scratch3_operators.js | 6 +++--- src/compiler/jsgen.js | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index 4992ad1a5b5..cf598829f43 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -37,6 +37,10 @@ class Scratch3EventBlocks { event_whenflagclicked: { restartExistingThreads: true }, + event_when: { + restartExistingThreads: false, + edgeActivated: true + }, event_whenkeypressed: { restartExistingThreads: false }, diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index 72da61b3cb6..9a962dd0e70 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -25,7 +25,7 @@ class Scratch3MotionBlocks { motion_turnleft: this.turnLeft, motion_pointindirection: this.pointInDirection, motion_pointtowards: this.pointTowards, - motion_pointtowards: this.pointTowardsXY, + motion_pointtowardsxy: this.pointTowardsXY, motion_glidesecstoxy: this.glide, motion_glideto: this.glideTo, motion_ifonedgebounce: this.ifOnEdgeBounce, @@ -85,7 +85,7 @@ class Scratch3MotionBlocks { util.target.setXY(x, y); } - changeByXY (args, util) { + changeByXY (args, util) { // used by compiler const dx = Cast.toNumber(args.X); const dy = Cast.toNumber(args.Y); util.target.setXY(util.target.x + dx, util.target.y + dy); @@ -157,7 +157,7 @@ class Scratch3MotionBlocks { util.target.setDirection(direction); } - pointTowardsXY (args, util) { + pointTowardsXY (args, util) { // used by compiler const dx = Cast.toNumber(args.X) - util.target.x; const dy = Cast.toNumber(args.Y) - util.target.y; const direction = 90 - MathUtil.radToDeg(Math.atan2(dy, dx)); diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 068a7e23312..9c67754cfc7 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -114,10 +114,10 @@ class Scratch3OperatorsBlocks { } lettersOf (args) { - const index1 = Cast.toNumber(args.LETTER1) - 1; - const index2 = Cast.toNumber(args.LETTER2) - 1; + const index1 = Cast.toNumber(args.LETTER1); + const index2 = Cast.toNumber(args.LETTER2); const str = Cast.toString(args.STRING); - return str.slice(Math.max(index1, 1), Math.min(str.length, index2)); + return str.slice(Math.max(index1, 1) - 1, Math.min(str.length, index2)); } length (args) { diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index e11df633355..cbe199caa18 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -598,7 +598,7 @@ class JSGenerator { case 'op.letterOf': return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); case 'op.lettersOf': - return new TypedInput(`((${this.descendInput(node.string).asString()}).substring(${this.descendInput(node.letter1).asNumber()}, ${this.descendInput(node.letter2).asNumber()}) || "")`, TYPE_STRING); + return new TypedInput(`((${this.descendInput(node.string).asString()}).substring(${this.descendInput(node.left).asNumber() - 1}, ${this.descendInput(node.right).asNumber() - 1}) || "")`, TYPE_STRING); case 'op.ln': // Needs to be marked as NaN because Math.log(-1) == NaN return new TypedInput(`Math.log(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER_NAN); From efefa58f23e7bcbfb396bf0c53fde0026b032c1e Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 15 Feb 2024 07:28:50 +0000 Subject: [PATCH 029/202] Fix new blocks (Part 2/2) (#13) --- src/compiler/irgen.js | 9 +++++++++ src/compiler/jsgen.js | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index f62c8c2ba41..f1e2c380bfd 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -299,7 +299,16 @@ class ScriptTreeGenerator { return { kind: 'looks.size' }; + case 'looks_effect': + return { + kind: 'looks.effect', + effect: this.descendInputOfBlock(block, 'EFFECT') + }; + case 'motion_rotationstyle': + return { + kind: 'motion.rotationStyle' + }; case 'motion_direction': return { kind: 'motion.direction' diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index cbe199caa18..ad18e610336 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -473,7 +473,7 @@ class JSGenerator { return new TypedInput(`${this.referenceVariable(node.list)}.value.length`, TYPE_NUMBER); case 'looks.effect': - return new TypedInput('Math.round(target)', TYPE_NUMBER); + return new TypedInput(`Math.round(target.effects[${this.referenceVariable(node.effect)}])`, TYPE_NUMBER); case 'looks.size': return new TypedInput('Math.round(target.size)', TYPE_NUMBER); case 'looks.backdropName': @@ -598,7 +598,7 @@ class JSGenerator { case 'op.letterOf': return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); case 'op.lettersOf': - return new TypedInput(`((${this.descendInput(node.string).asString()}).substring(${this.descendInput(node.left).asNumber() - 1}, ${this.descendInput(node.right).asNumber() - 1}) || "")`, TYPE_STRING); + return new TypedInput(`((${this.descendInput(node.string).asString()}).substring(${this.descendInput(node.left).asNumber() - 1}, ${this.descendInput(node.right).asNumber()}) || "")`, TYPE_STRING); case 'op.ln': // Needs to be marked as NaN because Math.log(-1) == NaN return new TypedInput(`Math.log(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER_NAN); From 6bcd397d7f9c0e40d9bfada27bbfbdcb4fa3a6f9 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 15 Feb 2024 18:17:10 +0000 Subject: [PATCH 030/202] Add in-built camera (part 1) --- src/engine/runtime.js | 52 ++++++++++++++++++++++++++++++++++++++++++ src/io/mouse.js | 4 ++-- src/virtual-machine.js | 10 ++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index b933efd0bc0..3998b84b5f7 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -471,6 +471,9 @@ class Runtime extends EventEmitter { this.stageWidth = Runtime.STAGE_WIDTH; this.stageHeight = Runtime.STAGE_HEIGHT; + this.cameraX = Runtime.CAMERA_X; + this.cameraY = Runtime.CAMERA_Y; + this.runtimeOptions = { maxClones: Runtime.MAX_CLONES, miscLimits: false, @@ -569,6 +572,24 @@ class Runtime extends EventEmitter { return 360; } + /** + * X position of camera. + * @const {number} + */ + static get CAMERA_X () { + // tw: camera is set per-runtime, this is only the initial value + return 0; + } + + /** + * Y position of camera. + * @const {number} + */ + static get CAMERA_Y () { + // tw: camera is set per-runtime, this is only the initial value + return 0; + } + /** * Event name for glowing a script. * @const {string} @@ -666,6 +687,14 @@ class Runtime extends EventEmitter { return 'STAGE_SIZE_CHANGED'; } + /** + * Event name for camera moving. + * @const {string} + */ + static get CAMERA_MOVED () { + return 'CAMERA_MOVED'; + } + /** * Event name for compiler errors. * @const {string} @@ -2778,6 +2807,29 @@ class Runtime extends EventEmitter { this.emit(Runtime.STAGE_SIZE_CHANGED, width, height); } + /** + * Change X and Y of the camera. This will also inform the renderer of the new camera position. + * @param {number} x New camera x + * @param {number} y New camera y + */ + setCameraXY(x, y) { + if (this.cameraX !== x || this.cameraY !== y) { + const deltaX = x - this.cameraX; + const deltaY = y - this.cameraY; + // Preserve monitor location relative to the center of the stage + + this.cameraX = x; + this.cameraY = y; + if (this.renderer) { + this.renderer.setCameraPosition( + x, + y + ); + } + } + this.emit(Runtime.CAMERA_MOVED, x, y); + } + // eslint-disable-next-line no-unused-vars setInEditor (inEditor) { // no-op diff --git a/src/io/mouse.js b/src/io/mouse.js index 169e6af0b67..7ef2551cd9b 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -69,7 +69,7 @@ class Mouse { this.runtime.stageWidth * ((data.x / data.canvasWidth) - 0.5), -(this.runtime.stageWidth / 2), (this.runtime.stageWidth / 2) - ); + ) + this.runtime.cameraX; } if (typeof data.y === 'number') { this._clientY = data.y; @@ -77,7 +77,7 @@ class Mouse { -this.runtime.stageHeight * ((data.y / data.canvasHeight) - 0.5), -(this.runtime.stageHeight / 2), (this.runtime.stageHeight / 2) - ); + ) + this.runtime.cameraY; } if (typeof data.isDown !== 'undefined') { // If no button specified, default to left button for compatibility diff --git a/src/virtual-machine.js b/src/virtual-machine.js index e1a1fcdea9e..8905face5c5 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -187,6 +187,9 @@ class VirtualMachine extends EventEmitter { this.runtime.on(Runtime.STAGE_SIZE_CHANGED, (width, height) => { this.emit(Runtime.STAGE_SIZE_CHANGED, width, height); }); + this.runtime.on(Runtime.CAMERA_MOVED, (x, y) => { + this.emit(Runtime.CAMERA_MOVED, x, y); + }); this.runtime.on(Runtime.COMPILE_ERROR, (target, error) => { this.emit(Runtime.COMPILE_ERROR, target, error); }); @@ -306,6 +309,10 @@ class VirtualMachine extends EventEmitter { this.runtime.setStageSize(width, height); } + setCameraXY (x, y) { + this.runtime.setCameraXY(x, y); + } + setInEditor (inEditor) { this.runtime.setInEditor(inEditor); } @@ -508,6 +515,9 @@ class VirtualMachine extends EventEmitter { log.error(`Failed to fetch project with id: ${id}`); return null; } + // usb: switch to 480 when loading from Scratch + // (will change automatically if options tell otherwise) + vm.setStageSize(480, 360); return vm.loadProject(projectAsset.data); }); } From fb3ba9c062483a6e94c4e3297a095ba191471209 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 15 Feb 2024 18:24:51 +0000 Subject: [PATCH 031/202] Update jsgen.js --- src/compiler/jsgen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index ad18e610336..0a4b6854a31 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -473,7 +473,7 @@ class JSGenerator { return new TypedInput(`${this.referenceVariable(node.list)}.value.length`, TYPE_NUMBER); case 'looks.effect': - return new TypedInput(`Math.round(target.effects[${this.referenceVariable(node.effect)}])`, TYPE_NUMBER); + return new TypedInput(`Math.round(target.effects[${sanitize(node.effect)}])`, TYPE_NUMBER); case 'looks.size': return new TypedInput('Math.round(target.size)', TYPE_NUMBER); case 'looks.backdropName': From 3904b7fe7279357ecb40e060a2c87e09371b450b Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 15 Feb 2024 23:26:16 +0000 Subject: [PATCH 032/202] Update jsgen.js --- src/compiler/jsgen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 0a4b6854a31..0583e721478 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -473,7 +473,7 @@ class JSGenerator { return new TypedInput(`${this.referenceVariable(node.list)}.value.length`, TYPE_NUMBER); case 'looks.effect': - return new TypedInput(`Math.round(target.effects[${sanitize(node.effect)}])`, TYPE_NUMBER); + return new TypedInput(`Math.round(target.effects["${sanitize(node.effect)}"] || 0)`, TYPE_NUMBER); case 'looks.size': return new TypedInput('Math.round(target.size)', TYPE_NUMBER); case 'looks.backdropName': From 9e73f53028c16682d89bcd81a7171ff53ff36f0c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 00:55:59 +0000 Subject: [PATCH 033/202] Incorporate all camera properties (#15) --- src/engine/runtime.js | 68 ++++++++++++++++++++++++++++++++---------- src/virtual-machine.js | 4 +-- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 3998b84b5f7..2f6e60b04eb 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -471,8 +471,12 @@ class Runtime extends EventEmitter { this.stageWidth = Runtime.STAGE_WIDTH; this.stageHeight = Runtime.STAGE_HEIGHT; - this.cameraX = Runtime.CAMERA_X; - this.cameraY = Runtime.CAMERA_Y; + this.camera = { + x: Runtime.CAMERA_X, + y: Runtime.CAMERA_Y, + direction: Runtime.CAMERA_DIRECTION, + zoom: Runtime.CAMERA_ZOOM + } this.runtimeOptions = { maxClones: Runtime.MAX_CLONES, @@ -581,6 +585,24 @@ class Runtime extends EventEmitter { return 0; } + /** + * Initial rotation of camera. + * @const {number} + */ + static get CAMERA_DIRECTION () { + // tw: camera is set per-runtime, this is only the initial value + return 90; + } + + /** + * Initial zoom of camera. + * @const {number} + */ + static get CAMERA_ZOOM () { + // tw: camera is set per-runtime, this is only the initial value + return 100; + } + /** * Y position of camera. * @const {number} @@ -691,8 +713,8 @@ class Runtime extends EventEmitter { * Event name for camera moving. * @const {string} */ - static get CAMERA_MOVED () { - return 'CAMERA_MOVED'; + static get CAMREA_NEEDS_UPDATE () { + return 'CAMERA_NEEDS_UPDATE'; } /** @@ -2808,26 +2830,40 @@ class Runtime extends EventEmitter { } /** - * Change X and Y of the camera. This will also inform the renderer of the new camera position. + * Change all values of the camera. This will also inform the renderer of the new camera position. * @param {number} x New camera x * @param {number} y New camera y + * @param {number} direction New camera direction + * @param {number} zoom New camera zoom */ - setCameraXY(x, y) { - if (this.cameraX !== x || this.cameraY !== y) { - const deltaX = x - this.cameraX; - const deltaY = y - this.cameraY; - // Preserve monitor location relative to the center of the stage + setCamera(x, y, direction, zoom) { + if ( + this.camera.x !== x || + this.camera.y !== y || + this.camera.direction !== direction || + this.camera.zoom !== zoom + ) { + this.camera.x = x ?? this.camera.x; + this.camera.y = y ?? this.camera.y; + this.camera.direction = direction ?? this.camera.direction; + this.camera.zoom = zoom ?? this.camera.zoom; - this.cameraX = x; - this.cameraY = y; if (this.renderer) { - this.renderer.setCameraPosition( - x, - y + this.renderer.updateCamera( + this.camera.x, + this.camera.y, + this.camera.direction, + this.camera.zoom, ); } } - this.emit(Runtime.CAMERA_MOVED, x, y); + this.emit( + Runtime.CAMERA_NEEDS_UPDATE, + this.camera.x, + this.camera.y, + this.camera.direction, + this.camera.zoom + ); } // eslint-disable-next-line no-unused-vars diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 8905face5c5..67678f36279 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -187,8 +187,8 @@ class VirtualMachine extends EventEmitter { this.runtime.on(Runtime.STAGE_SIZE_CHANGED, (width, height) => { this.emit(Runtime.STAGE_SIZE_CHANGED, width, height); }); - this.runtime.on(Runtime.CAMERA_MOVED, (x, y) => { - this.emit(Runtime.CAMERA_MOVED, x, y); + this.runtime.on(Runtime.CAMERA_NEEDS_UPDATE, (x, y, direction, zoom) => { + this.emit(Runtime.CAMERA_NEEDS_UPDATE, x, y, direction, zoom); }); this.runtime.on(Runtime.COMPILE_ERROR, (target, error) => { this.emit(Runtime.COMPILE_ERROR, target, error); From c7f03bae03ea2800457748b8a9b2efe29a719e11 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 02:52:10 +0000 Subject: [PATCH 034/202] Update runtime.js --- 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 2f6e60b04eb..7edd5d1dc9e 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2849,7 +2849,7 @@ class Runtime extends EventEmitter { this.camera.zoom = zoom ?? this.camera.zoom; if (this.renderer) { - this.renderer.updateCamera( + this.renderer._updateCamera ( this.camera.x, this.camera.y, this.camera.direction, From 926504614ca9836b3ff21c0b7206f840002bd864 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 03:58:38 +0000 Subject: [PATCH 035/202] Update virtual-machine.js --- src/virtual-machine.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 67678f36279..360c671021e 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -309,8 +309,8 @@ class VirtualMachine extends EventEmitter { this.runtime.setStageSize(width, height); } - setCameraXY (x, y) { - this.runtime.setCameraXY(x, y); + setCamera (x, y, direction, zoom) { + this.runtime.setCamera(x, y, direction, zoom); } setInEditor (inEditor) { From 61e8f5e4b23d59b5d7ad549870494477fc75d9a3 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 05:28:52 +0000 Subject: [PATCH 036/202] Camera support (#16) * Remove bubble bounds * Change mouse transforms --- src/blocks/scratch3_looks.js | 3 ++- src/io/mouse.js | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js index ba13dd7ba2e..bd72ab1c1dc 100644 --- a/src/blocks/scratch3_looks.js +++ b/src/blocks/scratch3_looks.js @@ -171,7 +171,8 @@ class Scratch3LooksBlocks { bottom: target.y }; } - const stageSize = this.runtime.renderer.getNativeSize(); + // usb: remove bounds to support camera + const stageSize = [Infinity, Infinity]; const stageBounds = { left: -stageSize[0] / 2, right: stageSize[0] / 2, diff --git a/src/io/mouse.js b/src/io/mouse.js index 7ef2551cd9b..f0fa7d9ab8c 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -70,6 +70,23 @@ class Mouse { -(this.runtime.stageWidth / 2), (this.runtime.stageWidth / 2) ) + this.runtime.cameraX; + // usb: transform based on camera + this._clientX = this.runtime.rendeer.translateX( + this._clientX, + true, + 1, + true, + this._clientY, + -1 + ); + this._scratchX = this.runtime.renderer.translateX( + this._scratchX, + false, + 1, + true, + this._scratchY, + 1 + ); } if (typeof data.y === 'number') { this._clientY = data.y; @@ -78,6 +95,23 @@ class Mouse { -(this.runtime.stageHeight / 2), (this.runtime.stageHeight / 2) ) + this.runtime.cameraY; + // usb: transform based on camera + this._clientY = this.runtime.rendeer.translateY( + this.clientY, + true, + 1, + true, + this.clientX, + -1 + ); + this._scratchY = this.runtime.renderer.translateY( + this._scratchY, + false, + 1, + true, + this._scratchX, + 1 + ); } if (typeof data.isDown !== 'undefined') { // If no button specified, default to left button for compatibility From 9e98bf266e2d14d07e1d9a7662913f609ddf3b61 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 05:42:16 +0000 Subject: [PATCH 037/202] Fix typos + old transform --- src/io/mouse.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/io/mouse.js b/src/io/mouse.js index f0fa7d9ab8c..e9de3dad035 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -69,9 +69,9 @@ class Mouse { this.runtime.stageWidth * ((data.x / data.canvasWidth) - 0.5), -(this.runtime.stageWidth / 2), (this.runtime.stageWidth / 2) - ) + this.runtime.cameraX; + ); // usb: transform based on camera - this._clientX = this.runtime.rendeer.translateX( + this._clientX = this.runtime.renderer.translateX( this._clientX, true, 1, @@ -94,9 +94,9 @@ class Mouse { -this.runtime.stageHeight * ((data.y / data.canvasHeight) - 0.5), -(this.runtime.stageHeight / 2), (this.runtime.stageHeight / 2) - ) + this.runtime.cameraY; + ); // usb: transform based on camera - this._clientY = this.runtime.rendeer.translateY( + this._clientY = this.runtime.renderer.translateY( this.clientY, true, 1, From 81180169f4718a76a1182bfbdbe6626820f47b0c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 06:08:30 +0000 Subject: [PATCH 038/202] Fix mouse positions (#17) --- src/io/mouse.js | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/io/mouse.js b/src/io/mouse.js index e9de3dad035..3bd2a78f037 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -64,23 +64,24 @@ class Mouse { */ postData (data) { if (typeof data.x === 'number') { - this._clientX = data.x; - this._scratchX = MathUtil.clamp( - this.runtime.stageWidth * ((data.x / data.canvasWidth) - 0.5), - -(this.runtime.stageWidth / 2), - (this.runtime.stageWidth / 2) - ); // usb: transform based on camera this._clientX = this.runtime.renderer.translateX( - this._clientX, + data.x, true, 1, true, this._clientY, -1 ); + const scratchX = MathUtil.clamp( + this.runtime.stageWidth * ((data.x / data.canvasWidth) - 0.5), + -(this.runtime.stageWidth / 2), + (this.runtime.stageWidth / 2) + ); + + // usb: transform based on camera this._scratchX = this.runtime.renderer.translateX( - this._scratchX, + scratchX, false, 1, true, @@ -89,23 +90,24 @@ class Mouse { ); } if (typeof data.y === 'number') { - this._clientY = data.y; - this._scratchY = MathUtil.clamp( - -this.runtime.stageHeight * ((data.y / data.canvasHeight) - 0.5), - -(this.runtime.stageHeight / 2), - (this.runtime.stageHeight / 2) - ); // usb: transform based on camera this._clientY = this.runtime.renderer.translateY( - this.clientY, + data.y, true, 1, true, this.clientX, -1 ); + const scratchY = MathUtil.clamp( + -this.runtime.stageHeight * ((data.y / data.canvasHeight) - 0.5), + -(this.runtime.stageHeight / 2), + (this.runtime.stageHeight / 2) + ); + + // usb: transform based on camera this._scratchY = this.runtime.renderer.translateY( - this._scratchY, + scratchY, false, 1, true, From e96f1628e848cce6c2ed54138e7444476e6538fa Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 06:30:03 +0000 Subject: [PATCH 039/202] More mouse fixes --- src/io/mouse.js | 57 +++++++++++-------------------------------------- 1 file changed, 12 insertions(+), 45 deletions(-) diff --git a/src/io/mouse.js b/src/io/mouse.js index 3bd2a78f037..56b3bc1a8c7 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -65,55 +65,20 @@ class Mouse { postData (data) { if (typeof data.x === 'number') { // usb: transform based on camera - this._clientX = this.runtime.renderer.translateX( - data.x, - true, - 1, - true, - this._clientY, - -1 - ); - const scratchX = MathUtil.clamp( + this._clientX = data.x; + this._scratchX = MathUtil.clamp( this.runtime.stageWidth * ((data.x / data.canvasWidth) - 0.5), -(this.runtime.stageWidth / 2), (this.runtime.stageWidth / 2) ); - - // usb: transform based on camera - this._scratchX = this.runtime.renderer.translateX( - scratchX, - false, - 1, - true, - this._scratchY, - 1 - ); } if (typeof data.y === 'number') { - // usb: transform based on camera - this._clientY = this.runtime.renderer.translateY( - data.y, - true, - 1, - true, - this.clientX, - -1 - ); - const scratchY = MathUtil.clamp( + this._clientY = data.y; + this._scratchY = MathUtil.clamp( -this.runtime.stageHeight * ((data.y / data.canvasHeight) - 0.5), -(this.runtime.stageHeight / 2), (this.runtime.stageHeight / 2) ); - - // usb: transform based on camera - this._scratchY = this.runtime.renderer.translateY( - scratchY, - false, - 1, - true, - this._scratchX, - 1 - ); } if (typeof data.isDown !== 'undefined') { // If no button specified, default to left button for compatibility @@ -156,7 +121,7 @@ class Mouse { * @return {number} Non-clamped X position of the mouse cursor. */ getClientX () { - return this._clientX; + return this.runtime.renderer.translateX(this._clientX, true, 1, true, this._clientY, -1); } /** @@ -164,7 +129,7 @@ class Mouse { * @return {number} Non-clamped Y position of the mouse cursor. */ getClientY () { - return this._clientY; + return this.runtime.renderer.translateY(this._clientY, true, -1, true, this._clientX, 1); } /** @@ -172,10 +137,11 @@ class Mouse { * @return {number} Clamped and integer rounded X position of the mouse cursor. */ getScratchX () { + const x = this.runtime.renderer.translateX(this._scratchX, false, 1, true, this._scratchY, 1); if (this.runtime.runtimeOptions.miscLimits) { - return Math.round(this._scratchX); + return Math.round(x); } - return roundToThreeDecimals(this._scratchX); + return roundToThreeDecimals(x); } /** @@ -183,10 +149,11 @@ class Mouse { * @return {number} Clamped and integer rounded Y position of the mouse cursor. */ getScratchY () { + const y = this.runtime.renderer.translateY(this._scratchY, false, 1, true, this._scratchX, 1); if (this.runtime.runtimeOptions.miscLimits) { - return Math.round(this._scratchY); + return Math.round(y); } - return roundToThreeDecimals(this._scratchY); + return roundToThreeDecimals(y); } /** From 51e7576a9a8d979c387a6aa7a550a957cac8eab4 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 08:30:46 +0000 Subject: [PATCH 040/202] fix mouse again --- src/io/mouse.js | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/io/mouse.js b/src/io/mouse.js index 56b3bc1a8c7..53264f52e55 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -121,7 +121,9 @@ class Mouse { * @return {number} Non-clamped X position of the mouse cursor. */ getClientX () { - return this.runtime.renderer.translateX(this._clientX, true, 1, true, this._clientY, -1); + return this.runtime.renderer.translateX( + this._clientX, true, 1, true, this._clientY, -1 + ); } /** @@ -129,7 +131,9 @@ class Mouse { * @return {number} Non-clamped Y position of the mouse cursor. */ getClientY () { - return this.runtime.renderer.translateY(this._clientY, true, -1, true, this._clientX, 1); + return this.runtime.renderer.translateY( + this._clientY, true, -1, true, this._clientX, 1 + ); } /** @@ -137,11 +141,11 @@ class Mouse { * @return {number} Clamped and integer rounded X position of the mouse cursor. */ getScratchX () { - const x = this.runtime.renderer.translateX(this._scratchX, false, 1, true, this._scratchY, 1); - if (this.runtime.runtimeOptions.miscLimits) { - return Math.round(x); - } - return roundToThreeDecimals(x); + const x = this.roundValue(this._scratchX); + const y = this.roundValue(this._scratchY); + return this.runtime.renderer.translateX( + x, false, 1, true, y, 1 + ); } /** @@ -149,11 +153,23 @@ class Mouse { * @return {number} Clamped and integer rounded Y position of the mouse cursor. */ getScratchY () { - const y = this.runtime.renderer.translateY(this._scratchY, false, 1, true, this._scratchX, 1); + const x = this.roundValue(this._scratchX); + const y = this.roundValue(this._scratchY); + return this.runtime.renderer.translateY( + y, false, 1, true, x, 1 + ); + } + + /** + * Round a mouse coordinate in relation to miscLimits. + * @param {number} float The mouse value that needs to be rounded. + * @return {number} Clamped and integer rounded Y position of the mouse cursor. + */ + roundValue (float) { if (this.runtime.runtimeOptions.miscLimits) { - return Math.round(y); + return Math.round(float); } - return roundToThreeDecimals(y); + return roundToThreeDecimals(float); } /** From 74c4dea6872a39d80d76b5bc9740389ab05f6a5c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:47:45 +0000 Subject: [PATCH 041/202] Create camera.js --- src/blocks/camera.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/blocks/camera.js diff --git a/src/blocks/camera.js b/src/blocks/camera.js new file mode 100644 index 00000000000..ebc4390570f --- /dev/null +++ b/src/blocks/camera.js @@ -0,0 +1,31 @@ +const Cast = require('../util/cast'); +const MathUtil = require('../util/math-util'); +const Timer = require('../util/timer'); + +class Scratch3CameraBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + camera_movetoxy: this.moveToXY + }; + } + + goToXY (args, util) { + const x = Cast.toNumber(args.X); + const y = Cast.toNumber(args.Y); + this.runtime.setCamera(x, y); + } +} + +module.exports = Scratch3CameraBlocks; From 5d507d7aec04ffbb52460dfd29c5ab0d1db76ecc Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:48:08 +0000 Subject: [PATCH 042/202] Rename camera.js to scratch3_camera.js --- src/blocks/{camera.js => scratch3_camera.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/blocks/{camera.js => scratch3_camera.js} (100%) diff --git a/src/blocks/camera.js b/src/blocks/scratch3_camera.js similarity index 100% rename from src/blocks/camera.js rename to src/blocks/scratch3_camera.js From df012145f07ba9547e38b976423164b403488510 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:49:11 +0000 Subject: [PATCH 043/202] Update compat-blocks.js --- src/compiler/compat-blocks.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index 51094bedbaf..7a546fa0ec4 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -6,6 +6,9 @@ // Please keep these lists alphabetical. const stacked = [ + // usb to do: compile these when working + 'camera_movetoxy', + 'looks_changestretchby', 'looks_hideallsprites', 'looks_say', From b4c618281cc15e1b22bebe53e32dd9061905e52b Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:50:31 +0000 Subject: [PATCH 044/202] Update runtime.js --- src/engine/runtime.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 7edd5d1dc9e..9d24b9fe7c1 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -43,6 +43,7 @@ const defaultBlockPackages = { scratch3_operators: require('../blocks/scratch3_operators'), scratch3_sound: require('../blocks/scratch3_sound'), scratch3_sensing: require('../blocks/scratch3_sensing'), + scratch3_camera: require('../blocks/scratch3_camera'), scratch3_data: require('../blocks/scratch3_data'), scratch3_procedures: require('../blocks/scratch3_procedures') }; From 0e6eef3ab0c286d26cd4b2e861f24cbafbf2fe54 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:22:49 +0000 Subject: [PATCH 045/202] wrong name --- src/blocks/scratch3_camera.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/scratch3_camera.js b/src/blocks/scratch3_camera.js index ebc4390570f..81e1b6d359d 100644 --- a/src/blocks/scratch3_camera.js +++ b/src/blocks/scratch3_camera.js @@ -21,7 +21,7 @@ class Scratch3CameraBlocks { }; } - goToXY (args, util) { + moveToXY (args, util) { const x = Cast.toNumber(args.X); const y = Cast.toNumber(args.Y); this.runtime.setCamera(x, y); From 53804e064115928861456a447bed4ec8f07dc86c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:59:58 +0000 Subject: [PATCH 046/202] Add camera blocks to vm (#18) * Update scratch3_camera.js * Update compat-blocks.js --- src/blocks/scratch3_camera.js | 58 ++++++++++++++++++++++++++++++++++- src/compiler/compat-blocks.js | 7 +++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/blocks/scratch3_camera.js b/src/blocks/scratch3_camera.js index 81e1b6d359d..3c3880c255a 100644 --- a/src/blocks/scratch3_camera.js +++ b/src/blocks/scratch3_camera.js @@ -11,13 +11,31 @@ class Scratch3CameraBlocks { this.runtime = runtime; } + getMonitored () { + return { + camera_xposition: { + getId: () => 'xposition' + }, + camera_yposition: { + getId: () => 'yposition' + } + }; + } + /** * Retrieve the block primitives implemented by this package. * @return {object.} Mapping of opcode to Function. */ getPrimitives () { return { - camera_movetoxy: this.moveToXY + camera_movetoxy: this.moveToXY, + camera_changebyxy: this.changeByXY, + camera_setx: this.setX, + camera_changex: this.changeX, + camera_sety: this.setY, + camera_changey: this.changeY, + camera_xposition: this.getCameraX, + camera_yposition: this.getCameraY }; } @@ -26,6 +44,44 @@ class Scratch3CameraBlocks { const y = Cast.toNumber(args.Y); this.runtime.setCamera(x, y); } + + changeByXY (args, util) { + const x = Cast.toNumber(args.X); + const y = Cast.toNumber(args.Y); + const newX = x + this.runtime.camera.x; + const newY = y + this.runtime.camera.y; + this.runtime.setCamera(x, y); + } + + setX (args, util) { + const x = Cast.toNumber(args.X); + this.runtime.setCamera(x); + } + + changeX (args, util) { + const x = Cast.toNumber(args.X); + const newX = x + this.runtime.camera.x; + this.runtime.setCamera(x); + } + + setY (args, util) { + const y = Cast.toNumber(args.Y); + this.runtime.setCamera(null, y); + } + + changeY (args, util) { + const y = Cast.toNumber(args.Y); + const newY = y + this.runtime.camera.y; + this.runtime.setCamera(null, y); + } + + getCameraX (args, util) { + return this.runtime.camera.x; + } + + getCameraY (args, util) { + return this.runtime.camera.y; + } } module.exports = Scratch3CameraBlocks; diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index 7a546fa0ec4..409eadeb5fe 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -8,6 +8,13 @@ const stacked = [ // usb to do: compile these when working 'camera_movetoxy', + 'camera_changebyxy', + 'camera_setx', + 'camera_changex', + 'camera_sety', + 'camera_changey', + 'camera_xposition', + 'camera_yposition', 'looks_changestretchby', 'looks_hideallsprites', From d89117c88cf16c8386e23a3a4ee1589a2bd81df3 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:21:34 +0000 Subject: [PATCH 047/202] Fix camera in vm --- src/blocks/scratch3_camera.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/blocks/scratch3_camera.js b/src/blocks/scratch3_camera.js index 3c3880c255a..0b25153c10e 100644 --- a/src/blocks/scratch3_camera.js +++ b/src/blocks/scratch3_camera.js @@ -50,7 +50,7 @@ class Scratch3CameraBlocks { const y = Cast.toNumber(args.Y); const newX = x + this.runtime.camera.x; const newY = y + this.runtime.camera.y; - this.runtime.setCamera(x, y); + this.runtime.setCamera(newX, newY); } setX (args, util) { @@ -61,7 +61,7 @@ class Scratch3CameraBlocks { changeX (args, util) { const x = Cast.toNumber(args.X); const newX = x + this.runtime.camera.x; - this.runtime.setCamera(x); + this.runtime.setCamera(newX); } setY (args, util) { @@ -72,7 +72,7 @@ class Scratch3CameraBlocks { changeY (args, util) { const y = Cast.toNumber(args.Y); const newY = y + this.runtime.camera.y; - this.runtime.setCamera(null, y); + this.runtime.setCamera(null, newY); } getCameraX (args, util) { From 6bf14ab318a255ad6f84a451638907634b32848a Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:21:50 +0000 Subject: [PATCH 048/202] Update compat-blocks.js --- src/compiler/compat-blocks.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index 409eadeb5fe..b36eb2c8aff 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -13,8 +13,6 @@ const stacked = [ 'camera_changex', 'camera_sety', 'camera_changey', - 'camera_xposition', - 'camera_yposition', 'looks_changestretchby', 'looks_hideallsprites', From f880b1255e94515e9943522836d8745a8e72315e Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:23:05 +0000 Subject: [PATCH 049/202] Update jsgen.js --- src/compiler/jsgen.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 0583e721478..9e30e55f8be 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -744,6 +744,11 @@ class JSGenerator { case 'sensing.year': return new TypedInput(`(new Date().getFullYear())`, TYPE_NUMBER); + case 'camera.x': + return new TypedInput('runtime.camera.x', TYPE_NUMBER); + case 'camera.y': + return new TypedInput('runtime.camera.y', TYPE_NUMBER); + case 'timer.get': return new TypedInput('runtime.ioDevices.clock.projectTimer()', TYPE_NUMBER); From 3a2ed58bf3af0853282de45548da7fa05f069417 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 19:23:50 +0000 Subject: [PATCH 050/202] Update irgen.js --- src/compiler/irgen.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index f1e2c380bfd..d0e513e489c 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -662,6 +662,15 @@ class ScriptTreeGenerator { kind: 'sensing.username' }; + case 'camera_xposition': + return { + kind: 'camera.x' + }; + case 'camera_yposition': + return { + kind: 'camera.y' + }; + case 'sound_sounds_menu': // This menu is special compared to other menus -- it actually has an opcode function. return { From 98d5ac0ef5f5ed1956abed83273f8a5e15b2fab2 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:27:49 +0000 Subject: [PATCH 051/202] expose some more utils --- src/util/usb-util.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/util/usb-util.js diff --git a/src/util/usb-util.js b/src/util/usb-util.js new file mode 100644 index 00000000000..eced66e6f5a --- /dev/null +++ b/src/util/usb-util.js @@ -0,0 +1,23 @@ +const Base64Util = require('../util/base64-util'); +const Cast = require('../util/cast'); +const Clone = require('../util/clone'); +const Color = require('../util/color'); +const MathUtil = require('../util/math-util'); +const StringUtil = require('../util/string-util'); +const Timer = require('../util/timer'); +const uid = require('../util/uid'); +const xmlEscape = require('../util/xml-escape'); + +const Util = { + Base64Util, + Cast, + Clone, + Color, + MathUtil, + StringUtil, + Timer, + uid, + xmlEscape +} + +module.exports = Util; From 2906f32b34b097afebc256e69542a8ba340c39c3 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:28:37 +0000 Subject: [PATCH 052/202] Update tw-extension-api-common.js --- src/extension-support/tw-extension-api-common.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/extension-support/tw-extension-api-common.js b/src/extension-support/tw-extension-api-common.js index e2ac0b74d0f..dcc782ff407 100644 --- a/src/extension-support/tw-extension-api-common.js +++ b/src/extension-support/tw-extension-api-common.js @@ -2,12 +2,14 @@ const ArgumentType = require('./argument-type'); const BlockType = require('./block-type'); const TargetType = require('./target-type'); const Cast = require('../util/cast'); +const Util = require('../util/usb-util'); const Scratch = { ArgumentType, BlockType, TargetType, - Cast + Cast, + Util }; module.exports = Scratch; From ddab02ecb3c8be66f0f7779709bc8ffcf42333f7 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 00:58:35 +0000 Subject: [PATCH 053/202] Add getEffect for target --- src/sprites/rendered-target.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 26e04469f6e..263de56fda1 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -413,6 +413,16 @@ class RenderedTarget extends Target { } } + /** + * Get a particular graphic effect value. + * @param {!string} effectName Name of effect (see `RenderedTarget.prototype.effects`). + * @return {!number} Numerical magnitude of effect. + */ + getEffect (effectName) { // used by compiler + if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) return 0; + return this.effects[effectName]; + } + /** * Clear all graphic effects on this rendered target. */ From fde332e3fe055b0864822bb1ab2416bd589435a0 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 00:59:26 +0000 Subject: [PATCH 054/202] Update scratch3_looks.js --- src/blocks/scratch3_looks.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js index bd72ab1c1dc..93e40c2bcd7 100644 --- a/src/blocks/scratch3_looks.js +++ b/src/blocks/scratch3_looks.js @@ -564,8 +564,7 @@ class Scratch3LooksBlocks { getEffect (args, util) { const effect = Cast.toString(args.EFFECT).toLowerCase(); - const value = util.target.effects[effect]; - return Cast.toNumber(value); + return util.target.getEffect(effect); } changeSize (args, util) { From 7c0bd3d2478393f6a91f47467eae5158c4003f84 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 01:00:59 +0000 Subject: [PATCH 055/202] adjustment --- 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 d0e513e489c..f496b22af6b 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -301,7 +301,7 @@ class ScriptTreeGenerator { }; case 'looks_effect': return { - kind: 'looks.effect', + kind: 'looks.getEffect', effect: this.descendInputOfBlock(block, 'EFFECT') }; From f3fc285159c280459355d0c0750fbd1e232bc431 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 01:02:19 +0000 Subject: [PATCH 056/202] Update jsgen.js --- src/compiler/jsgen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 9e30e55f8be..f3324389e6a 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -473,7 +473,7 @@ class JSGenerator { return new TypedInput(`${this.referenceVariable(node.list)}.value.length`, TYPE_NUMBER); case 'looks.effect': - return new TypedInput(`Math.round(target.effects["${sanitize(node.effect)}"] || 0)`, TYPE_NUMBER); + return new TypedInput(`target.getEffect(${sanitize(node.effect)})`, TYPE_NUMBER); case 'looks.size': return new TypedInput('Math.round(target.size)', TYPE_NUMBER); case 'looks.backdropName': From 20f8f5079bcf2bb6cdcd436a4a087ee3adf99b30 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 05:40:53 +0000 Subject: [PATCH 057/202] reset camera when runtime is disposed --- src/engine/runtime.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 9d24b9fe7c1..bf8e435c4e6 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2402,6 +2402,13 @@ class Runtime extends EventEmitter { this.ioDevices.cloud.clear(); + this.camera = { + x: Runtime.CAMERA_X, + y: Runtime.CAMERA_Y, + direction: Runtime.CAMERA_DIRECTION, + zoom: Runtime.CAMERA_ZOOM + } + // Reset runtime cloud data info const newCloudDataManager = cloudDataManager(this.cloudOptions); this.hasCloudData = newCloudDataManager.hasCloudVariables; From 3f80f949b699b355b9281d0d628a64d61f7dd5fb Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 06:15:43 +0000 Subject: [PATCH 058/202] Add interpolation to the camera (#19) --- src/engine/runtime.js | 20 +++++++++------ src/engine/tw-interpolate.js | 47 +++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index bf8e435c4e6..26f6329119b 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -476,7 +476,9 @@ class Runtime extends EventEmitter { x: Runtime.CAMERA_X, y: Runtime.CAMERA_Y, direction: Runtime.CAMERA_DIRECTION, - zoom: Runtime.CAMERA_ZOOM + zoom: Runtime.CAMERA_ZOOM, + enabled: true, + interpolationData: null } this.runtimeOptions = { @@ -2389,6 +2391,15 @@ class Runtime extends EventEmitter { this.emit(Runtime.RUNTIME_DISPOSED); this.ioDevices.clock.resetProjectTimer(); this.fontManager.clear(); + + this.camera = { + x: Runtime.CAMERA_X, + y: Runtime.CAMERA_Y, + direction: Runtime.CAMERA_DIRECTION, + zoom: Runtime.CAMERA_ZOOM, + enabled: true, + interpolationData: null + } // @todo clear out extensions? turboMode? etc. // *********** Cloud ******************* @@ -2402,13 +2413,6 @@ class Runtime extends EventEmitter { this.ioDevices.cloud.clear(); - this.camera = { - x: Runtime.CAMERA_X, - y: Runtime.CAMERA_Y, - direction: Runtime.CAMERA_DIRECTION, - zoom: Runtime.CAMERA_ZOOM - } - // Reset runtime cloud data info const newCloudDataManager = cloudDataManager(this.cloudOptions); this.hasCloudData = newCloudDataManager.hasCloudVariables; diff --git a/src/engine/tw-interpolate.js b/src/engine/tw-interpolate.js index 4396c79df4b..17310dc66f9 100644 --- a/src/engine/tw-interpolate.js +++ b/src/engine/tw-interpolate.js @@ -4,7 +4,31 @@ */ const setupInitialState = runtime => { const renderer = runtime.renderer; + const camera = runtime.camera; + // camera state + if (camera.enabled) { + // x, y, direction, zoom + if (renderer && camera.interpolationData) { + renderer._updateCamera( + camera.x, + camera.y, + camera.direction, + camera.zoom + ); + } + + camera.interpolationData = { + x: camera.x, + y: camera.y, + direction: camera.direction, + zoom: camera.zoom + } + } else { + camera.interpolationData = null; + } + + // target state(s) for (const target of runtime.targets) { const directionAndScale = target._getRenderedDirectionAndScale(); @@ -16,7 +40,7 @@ const setupInitialState = runtime => { renderer.updateDrawableEffect(drawableID, 'ghost', target.effects.ghost); } - if (target.visible && !target.isStage) { + if (target.visible && !target.isStage && target.interpolation) { target.interpolationData = { x: target.x, y: target.y, @@ -42,6 +66,27 @@ const interpolate = (runtime, time) => { return; } + // camera state + if (camera.enabled && camera.interpolationData) { + const interpolationData = camera.interpolationData; + + // Position interpolation. + const xDistance = camera.x - interpolationData.x; + const yDistance = camera.y - interpolationData.y; + const absoluteXDistance = Math.abs(xDistance); + const absoluteYDistance = Math.abs(yDistance); + if (absoluteXDistance > 0.1 || absoluteYDistance > 0.1) { + // Large movements are likely intended to be instantaneous. + const distance = Math.sqrt((absoluteXDistance ** 2) + (absoluteYDistance ** 2)); + if (distance < 50) { + const newX = interpolationData.x + (xDistance * time); + const newY = interpolationData.y + (yDistance * time); + renderer._updateCamera(newX, newY, camera.direction, camera.zoom); + } + } + } + + // target state(s) for (const target of runtime.targets) { // interpolationData is the initial state at the start of the frame (time 0) // the state on the target itself is the state at the end of the frame (time 1) From e0ddba2dd23608595418929257a032e79afa5557 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 06:16:43 +0000 Subject: [PATCH 059/202] define camera --- src/engine/tw-interpolate.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/engine/tw-interpolate.js b/src/engine/tw-interpolate.js index 17310dc66f9..bdb26fe3251 100644 --- a/src/engine/tw-interpolate.js +++ b/src/engine/tw-interpolate.js @@ -62,6 +62,8 @@ const setupInitialState = runtime => { */ const interpolate = (runtime, time) => { const renderer = runtime.renderer; + const camera = runtime.camera; + if (!renderer) { return; } From 974b57af567d7214c41e06e0b75852db7b6f6ea7 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 19:00:02 +0000 Subject: [PATCH 060/202] Rewrite camera (#20) --- src/blocks/scratch3_camera.js | 18 +++-- src/compiler/jsgen.js | 2 +- src/engine/camera.js | 136 +++++++++++++++++++++++++++++++++ src/engine/runtime.js | 99 ++---------------------- src/engine/tw-interpolate.js | 7 +- src/sprites/rendered-target.js | 8 -- src/virtual-machine.js | 7 -- 7 files changed, 156 insertions(+), 121 deletions(-) create mode 100644 src/engine/camera.js diff --git a/src/blocks/scratch3_camera.js b/src/blocks/scratch3_camera.js index 0b25153c10e..b010cb50c70 100644 --- a/src/blocks/scratch3_camera.js +++ b/src/blocks/scratch3_camera.js @@ -9,6 +9,8 @@ class Scratch3CameraBlocks { * @type {Runtime} */ this.runtime = runtime; + + this.camera = runtime.camera; } getMonitored () { @@ -42,7 +44,7 @@ class Scratch3CameraBlocks { moveToXY (args, util) { const x = Cast.toNumber(args.X); const y = Cast.toNumber(args.Y); - this.runtime.setCamera(x, y); + this.camera.setXY(x, y); } changeByXY (args, util) { @@ -50,37 +52,37 @@ class Scratch3CameraBlocks { const y = Cast.toNumber(args.Y); const newX = x + this.runtime.camera.x; const newY = y + this.runtime.camera.y; - this.runtime.setCamera(newX, newY); + this.camera.setXY(newX, newY); } setX (args, util) { const x = Cast.toNumber(args.X); - this.runtime.setCamera(x); + this.camera.setXY(x, this.camera.y); } changeX (args, util) { const x = Cast.toNumber(args.X); const newX = x + this.runtime.camera.x; - this.runtime.setCamera(newX); + this.camera.setXY(newX, this.camera.y); } setY (args, util) { const y = Cast.toNumber(args.Y); - this.runtime.setCamera(null, y); + this.camera.setXY(this.camera.x, y); } changeY (args, util) { const y = Cast.toNumber(args.Y); const newY = y + this.runtime.camera.y; - this.runtime.setCamera(null, newY); + this.camera.setXY(this.camera.x, newY); } getCameraX (args, util) { - return this.runtime.camera.x; + return this.camera.x; } getCameraY (args, util) { - return this.runtime.camera.y; + return this.camera.y; } } diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index f3324389e6a..49ee53383fe 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -472,7 +472,7 @@ class JSGenerator { case 'list.length': return new TypedInput(`${this.referenceVariable(node.list)}.value.length`, TYPE_NUMBER); - case 'looks.effect': + case 'looks.getEffect': return new TypedInput(`target.getEffect(${sanitize(node.effect)})`, TYPE_NUMBER); case 'looks.size': return new TypedInput('Math.round(target.size)', TYPE_NUMBER); diff --git a/src/engine/camera.js b/src/engine/camera.js new file mode 100644 index 00000000000..2fb7e8759c3 --- /dev/null +++ b/src/engine/camera.js @@ -0,0 +1,136 @@ +const Cast = require('../util/cast'); +const MathUtil = require('../util/math-util'); + +/** + * @fileoverview + * The camera is an arbitrary object used to + * describe properties of the renderer projection. + */ + +/** + * Camera: instance of a camera object on the stage. + */ +class Camera { + constructor(runtime) { + this.runtime = runtime; + + this.renderer = runtime.renderer; + + /** + * Scratch X coordinate. Currently should range from -240 to 240. + * @type {Number} + */ + this.x = 0; + + /** + * Scratch Y coordinate. Currently should range from -180 to 180. + * @type {number} + */ + this.y = 0; + + /** + * Scratch direction. Currently should range from -179 to 180. + * @type {number} + */ + this.direction = 90; + + /** + * Zoom of camera as a percentage. Similar to size. + * @type {number} + */ + this.zoom = 100; + + /** + * Determines whether the camera values will affect the projection. + * @type {boolean} + */ + this.enabled = true; + + /** + * Interpolation data used by tw-interpolate. + */ + this.interpolationData = null; + } + + /** + * Event name for the camera updating. + * @const {string} + */ + static get CAMERA_NEEDS_UPDATE () { + return 'CAMERA_NEEDS_UPDATE'; + } + + /** + * Set the X and Y values of the camera. + * @param x The x coordinate. + * @param y The y coordinate. + */ + setXY(x, y) { + this.x = Cast.toNumber(x); + this.y = Cast.toNumber(y); + + this.emitCameraUpdate(); + } + + /** + * Set the zoom of the camera. + * @param zoom The new zoom value. + */ + setZoom(zoom) { + this.zoom = Cast.toNumber(zoom); + if (this.runtime.runtimeOptions.miscLimits) { + this.zoom = MathUtil.clamp(this.zoom, 10, 300); + } + + this.emitCameraUpdate(); + } + + /** + * Point the camera towards a given direction. + * @param direction Direction to point the camera. + */ + setDirection(direction) { + if (!isFinite(direction)) return; + + this.direction = MathUtil.wrapClamp(direction, -179, 180); + + this.emitCameraUpdate(); + } + + /** + * Set whether the camera will affect the projection. + * @param enabled The new enabled state. + */ + setEnabled(enabled) { + this.enabled = enabled; + } + + /** + * Tell the renderer to update the rendered camera state. + */ + emitCameraUpdate() { + if (!this.renderer) return; + + this.renderer._updateCamera( + this.x, + this.y, + this.direction, + this.zoom + ) + + this.emit(Camera.CAMERA_UPDATE); + } + + /** + * Reset all camera properties. + */ + reset() { + this.x = 0; + this.y = 0; + this.direction = 90; + this.zoom = 100; + this.enabled = true; + } +} + +module.exports = Camera; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 26f6329119b..79622071432 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -50,6 +50,7 @@ const defaultBlockPackages = { const interpolate = require('./tw-interpolate'); const FrameLoop = require('./tw-frame-loop'); +const Camera = require('./camera'); const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69']; @@ -472,14 +473,7 @@ class Runtime extends EventEmitter { this.stageWidth = Runtime.STAGE_WIDTH; this.stageHeight = Runtime.STAGE_HEIGHT; - this.camera = { - x: Runtime.CAMERA_X, - y: Runtime.CAMERA_Y, - direction: Runtime.CAMERA_DIRECTION, - zoom: Runtime.CAMERA_ZOOM, - enabled: true, - interpolationData: null - } + this.camera = new Camera(this); this.runtimeOptions = { maxClones: Runtime.MAX_CLONES, @@ -579,42 +573,6 @@ class Runtime extends EventEmitter { return 360; } - /** - * X position of camera. - * @const {number} - */ - static get CAMERA_X () { - // tw: camera is set per-runtime, this is only the initial value - return 0; - } - - /** - * Initial rotation of camera. - * @const {number} - */ - static get CAMERA_DIRECTION () { - // tw: camera is set per-runtime, this is only the initial value - return 90; - } - - /** - * Initial zoom of camera. - * @const {number} - */ - static get CAMERA_ZOOM () { - // tw: camera is set per-runtime, this is only the initial value - return 100; - } - - /** - * Y position of camera. - * @const {number} - */ - static get CAMERA_Y () { - // tw: camera is set per-runtime, this is only the initial value - return 0; - } - /** * Event name for glowing a script. * @const {string} @@ -712,14 +670,6 @@ class Runtime extends EventEmitter { return 'STAGE_SIZE_CHANGED'; } - /** - * Event name for camera moving. - * @const {string} - */ - static get CAMREA_NEEDS_UPDATE () { - return 'CAMERA_NEEDS_UPDATE'; - } - /** * Event name for compiler errors. * @const {string} @@ -2392,14 +2342,7 @@ class Runtime extends EventEmitter { this.ioDevices.clock.resetProjectTimer(); this.fontManager.clear(); - this.camera = { - x: Runtime.CAMERA_X, - y: Runtime.CAMERA_Y, - direction: Runtime.CAMERA_DIRECTION, - zoom: Runtime.CAMERA_ZOOM, - enabled: true, - interpolationData: null - } + this.camera.reset(); // @todo clear out extensions? turboMode? etc. // *********** Cloud ******************* @@ -2842,40 +2785,10 @@ class Runtime extends EventEmitter { } /** - * Change all values of the camera. This will also inform the renderer of the new camera position. - * @param {number} x New camera x - * @param {number} y New camera y - * @param {number} direction New camera direction - * @param {number} zoom New camera zoom + * Get the current instance of the camera. */ - setCamera(x, y, direction, zoom) { - if ( - this.camera.x !== x || - this.camera.y !== y || - this.camera.direction !== direction || - this.camera.zoom !== zoom - ) { - this.camera.x = x ?? this.camera.x; - this.camera.y = y ?? this.camera.y; - this.camera.direction = direction ?? this.camera.direction; - this.camera.zoom = zoom ?? this.camera.zoom; - - if (this.renderer) { - this.renderer._updateCamera ( - this.camera.x, - this.camera.y, - this.camera.direction, - this.camera.zoom, - ); - } - } - this.emit( - Runtime.CAMERA_NEEDS_UPDATE, - this.camera.x, - this.camera.y, - this.camera.direction, - this.camera.zoom - ); + getCamera() { + return this.camera; } // eslint-disable-next-line no-unused-vars diff --git a/src/engine/tw-interpolate.js b/src/engine/tw-interpolate.js index bdb26fe3251..bf63f2d079b 100644 --- a/src/engine/tw-interpolate.js +++ b/src/engine/tw-interpolate.js @@ -17,7 +17,7 @@ const setupInitialState = runtime => { camera.zoom ); } - + camera.interpolationData = { x: camera.x, y: camera.y, @@ -40,7 +40,7 @@ const setupInitialState = runtime => { renderer.updateDrawableEffect(drawableID, 'ghost', target.effects.ghost); } - if (target.visible && !target.isStage && target.interpolation) { + if (target.visible && !target.isStage) { target.interpolationData = { x: target.x, y: target.y, @@ -93,8 +93,7 @@ const interpolate = (runtime, time) => { // interpolationData is the initial state at the start of the frame (time 0) // the state on the target itself is the state at the end of the frame (time 1) const interpolationData = target.interpolationData; - const interpolationEnabled = target.interpolation; - if (!interpolationData || !interpolationEnabled) { + if (!interpolationData) { continue; } diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 263de56fda1..d64e47a649b 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -167,7 +167,6 @@ class RenderedTarget extends Target { this.onTargetMoved = null; this.onTargetVisualChange = null; - this.interpolation = false; this.interpolationData = null; } @@ -1077,13 +1076,6 @@ class RenderedTarget extends Target { this.dragging = false; } - /** - * Set whether the sprite is interpolated. - */ - setInterpolation(interpolated) { - this.interpolation = interpolated; - } - /** * Serialize sprite info, used when emitting events about the sprite * @returns {object} Sprite data as a simple object diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 360c671021e..baf0cf67fcd 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -187,9 +187,6 @@ class VirtualMachine extends EventEmitter { this.runtime.on(Runtime.STAGE_SIZE_CHANGED, (width, height) => { this.emit(Runtime.STAGE_SIZE_CHANGED, width, height); }); - this.runtime.on(Runtime.CAMERA_NEEDS_UPDATE, (x, y, direction, zoom) => { - this.emit(Runtime.CAMERA_NEEDS_UPDATE, x, y, direction, zoom); - }); this.runtime.on(Runtime.COMPILE_ERROR, (target, error) => { this.emit(Runtime.COMPILE_ERROR, target, error); }); @@ -309,10 +306,6 @@ class VirtualMachine extends EventEmitter { this.runtime.setStageSize(width, height); } - setCamera (x, y, direction, zoom) { - this.runtime.setCamera(x, y, direction, zoom); - } - setInEditor (inEditor) { this.runtime.setInEditor(inEditor); } From fda91734ee6adf33cd0f70c053d1adb17ddf9be2 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 19:01:28 +0000 Subject: [PATCH 061/202] oops --- src/engine/camera.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/camera.js b/src/engine/camera.js index 2fb7e8759c3..c4901ffc137 100644 --- a/src/engine/camera.js +++ b/src/engine/camera.js @@ -56,8 +56,8 @@ class Camera { * Event name for the camera updating. * @const {string} */ - static get CAMERA_NEEDS_UPDATE () { - return 'CAMERA_NEEDS_UPDATE'; + static get CAMERA_UPDATE () { + return 'CAMERA_UPDATE'; } /** From 4e49a061ca549d2cb972934f1e837d584e3af438 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 20:44:21 +0000 Subject: [PATCH 062/202] Next batch (#21) * Update scratch3_camera.js * Update camera.js * Update jsgen.js * Update irgen.js * Update compat-blocks.js --- src/blocks/scratch3_camera.js | 18 ++++++++---------- src/compiler/compat-blocks.js | 1 + src/compiler/irgen.js | 5 ----- src/compiler/jsgen.js | 2 -- src/engine/camera.js | 12 +++++------- 5 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/blocks/scratch3_camera.js b/src/blocks/scratch3_camera.js index b010cb50c70..29fe16def24 100644 --- a/src/blocks/scratch3_camera.js +++ b/src/blocks/scratch3_camera.js @@ -9,8 +9,6 @@ class Scratch3CameraBlocks { * @type {Runtime} */ this.runtime = runtime; - - this.camera = runtime.camera; } getMonitored () { @@ -44,7 +42,7 @@ class Scratch3CameraBlocks { moveToXY (args, util) { const x = Cast.toNumber(args.X); const y = Cast.toNumber(args.Y); - this.camera.setXY(x, y); + this.runtime.camera.setXY(x, y); } changeByXY (args, util) { @@ -52,37 +50,37 @@ class Scratch3CameraBlocks { const y = Cast.toNumber(args.Y); const newX = x + this.runtime.camera.x; const newY = y + this.runtime.camera.y; - this.camera.setXY(newX, newY); + this.runtime.camera.setXY(newX, newY); } setX (args, util) { const x = Cast.toNumber(args.X); - this.camera.setXY(x, this.camera.y); + this.runtime.camera.setXY(x, this.runtime.camera.y); } changeX (args, util) { const x = Cast.toNumber(args.X); const newX = x + this.runtime.camera.x; - this.camera.setXY(newX, this.camera.y); + this.runtime.camera.setXY(newX, this.runtime.camera.y); } setY (args, util) { const y = Cast.toNumber(args.Y); - this.camera.setXY(this.camera.x, y); + this.runtime.camera.setXY(this.runtime.camera.x, y); } changeY (args, util) { const y = Cast.toNumber(args.Y); const newY = y + this.runtime.camera.y; - this.camera.setXY(this.camera.x, newY); + this.runtime.camera.setXY(this.runtime.camera.x, newY); } getCameraX (args, util) { - return this.camera.x; + return this.runtime.camera.x; } getCameraY (args, util) { - return this.camera.y; + return this.runtime.camera.y; } } diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index b36eb2c8aff..26c6ec44424 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -44,6 +44,7 @@ const stacked = [ ]; const inputs = [ + 'looks_effect', 'motion_xscroll', 'motion_yscroll', 'sensing_loud', diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index f496b22af6b..5aae5cb1e1b 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -299,11 +299,6 @@ class ScriptTreeGenerator { return { kind: 'looks.size' }; - case 'looks_effect': - return { - kind: 'looks.getEffect', - effect: this.descendInputOfBlock(block, 'EFFECT') - }; case 'motion_rotationstyle': return { diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 49ee53383fe..85e6e56963d 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -472,8 +472,6 @@ class JSGenerator { case 'list.length': return new TypedInput(`${this.referenceVariable(node.list)}.value.length`, TYPE_NUMBER); - case 'looks.getEffect': - return new TypedInput(`target.getEffect(${sanitize(node.effect)})`, TYPE_NUMBER); case 'looks.size': return new TypedInput('Math.round(target.size)', TYPE_NUMBER); case 'looks.backdropName': diff --git a/src/engine/camera.js b/src/engine/camera.js index c4901ffc137..64cfddadfb2 100644 --- a/src/engine/camera.js +++ b/src/engine/camera.js @@ -1,3 +1,5 @@ +const EventEmitter = require('events'); + const Cast = require('../util/cast'); const MathUtil = require('../util/math-util'); @@ -10,12 +12,10 @@ const MathUtil = require('../util/math-util'); /** * Camera: instance of a camera object on the stage. */ -class Camera { +class Camera extends EventEmitter { constructor(runtime) { this.runtime = runtime; - this.renderer = runtime.renderer; - /** * Scratch X coordinate. Currently should range from -240 to 240. * @type {Number} @@ -109,16 +109,14 @@ class Camera { * Tell the renderer to update the rendered camera state. */ emitCameraUpdate() { - if (!this.renderer) return; + if (!this.runtime.renderer) return; - this.renderer._updateCamera( + this.runtime.renderer._updateCamera( this.x, this.y, this.direction, this.zoom ) - - this.emit(Camera.CAMERA_UPDATE); } /** From 82ea6c446c65b4c0193e1e8ca5fca5b495703294 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 20:51:25 +0000 Subject: [PATCH 063/202] call super --- src/engine/camera.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/engine/camera.js b/src/engine/camera.js index 64cfddadfb2..2b8a93fc3f0 100644 --- a/src/engine/camera.js +++ b/src/engine/camera.js @@ -14,6 +14,8 @@ const MathUtil = require('../util/math-util'); */ class Camera extends EventEmitter { constructor(runtime) { + super(); + this.runtime = runtime; /** From d937b28627c96189ce2492e1ba6525f05a602f89 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 17 Feb 2024 21:36:59 +0000 Subject: [PATCH 064/202] Wrap clamp the color effect value (#22) * Update rendered-target.js * Update scratch3_looks.js --- src/blocks/scratch3_looks.js | 3 +++ src/sprites/rendered-target.js | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js index 93e40c2bcd7..cf2833e20fa 100644 --- a/src/blocks/scratch3_looks.js +++ b/src/blocks/scratch3_looks.js @@ -314,6 +314,9 @@ class Scratch3LooksBlocks { getMonitored () { return { + looks_effect: { + getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_effect`, fields) + }, looks_size: { isSpriteSpecific: true, getId: targetId => `${targetId}_size` diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index d64e47a649b..ff22191a4c2 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -402,6 +402,12 @@ class RenderedTarget extends Target { */ setEffect (effectName, value) { // used by compiler if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) return; + + // "Infinity" is often used to get a monochrome effect. + if (effectName === 'color' && isFinite(value) { + value = MathUtil.wrapClamp(value, 0, 200); + } + this.effects[effectName] = value; if (this.renderer) { this.renderer.updateDrawableEffect(this.drawableID, effectName, value); From dd45612a80af5d9c9006b452c809ed5eaca8ebe6 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 18 Feb 2024 02:00:24 +0000 Subject: [PATCH 065/202] fix typo --- src/sprites/rendered-target.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index ff22191a4c2..eab59ca75bf 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -404,7 +404,7 @@ class RenderedTarget extends Target { if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) return; // "Infinity" is often used to get a monochrome effect. - if (effectName === 'color' && isFinite(value) { + if (effectName === 'color' && isFinite(value)) { value = MathUtil.wrapClamp(value, 0, 200); } From 5e636282e9e00f5d1c2f3ee50cfef3927e8dbc66 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 18 Feb 2024 20:03:57 +0000 Subject: [PATCH 066/202] Next batch (#23) * request redraw when the camera is moved * mark looks_effect as sprite-specific * transform _scratchX/Y instead of getScratchX/Y This'll fix the mouse position addon * transform "goTo"'s fencing in relation to the camera --- src/blocks/scratch3_looks.js | 1 + src/blocks/scratch3_motion.js | 8 ++++++++ src/engine/camera.js | 2 ++ src/io/mouse.js | 23 ++++++++++++----------- 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/blocks/scratch3_looks.js b/src/blocks/scratch3_looks.js index cf2833e20fa..9e2ca4d6f3f 100644 --- a/src/blocks/scratch3_looks.js +++ b/src/blocks/scratch3_looks.js @@ -315,6 +315,7 @@ class Scratch3LooksBlocks { getMonitored () { return { looks_effect: { + isSpriteSpecific: true, getId: (targetId, fields) => getMonitorIdForBlockWithArgs(`${targetId}_effect`, fields) }, looks_size: { diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index 9a962dd0e70..ab2833fa5a7 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -102,6 +102,14 @@ class Scratch3MotionBlocks { const stageHeight = this.runtime.stageHeight; targetX = Math.round(stageWidth * (Math.random() - 0.5)); targetY = Math.round(stageHeight * (Math.random() - 0.5)); + + // usb: transform based on camera + targetX = this.runtime.renderer.translateX( + targetX, false, 1, true, targetY, 1 + ); + targetY = this.runtime.renderer.translateY( + targetY, false, 1, true, targetX, 1 + ); } else { targetName = Cast.toString(targetName); const goToTarget = this.runtime.getSpriteTargetByName(targetName); diff --git a/src/engine/camera.js b/src/engine/camera.js index 2b8a93fc3f0..3f35004cb6e 100644 --- a/src/engine/camera.js +++ b/src/engine/camera.js @@ -119,6 +119,8 @@ class Camera extends EventEmitter { this.direction, this.zoom ) + + this.runtime.requestRedraw(); } /** diff --git a/src/io/mouse.js b/src/io/mouse.js index 53264f52e55..c88e20e7cc3 100644 --- a/src/io/mouse.js +++ b/src/io/mouse.js @@ -64,13 +64,17 @@ class Mouse { */ postData (data) { if (typeof data.x === 'number') { - // usb: transform based on camera this._clientX = data.x; this._scratchX = MathUtil.clamp( this.runtime.stageWidth * ((data.x / data.canvasWidth) - 0.5), -(this.runtime.stageWidth / 2), (this.runtime.stageWidth / 2) ); + + // usb: transform based on camera + this._scratchX = this.runtime.renderer.translateX( + this._scratchX, false, 1, true, this._scratchY, 1 + ); } if (typeof data.y === 'number') { this._clientY = data.y; @@ -79,6 +83,11 @@ class Mouse { -(this.runtime.stageHeight / 2), (this.runtime.stageHeight / 2) ); + + // usb: transform based on camera + this._scratchY = this.runtime.renderer.translateY( + this._scratchY, false, 1, true, this._scratchX, 1 + ); } if (typeof data.isDown !== 'undefined') { // If no button specified, default to left button for compatibility @@ -141,11 +150,7 @@ class Mouse { * @return {number} Clamped and integer rounded X position of the mouse cursor. */ getScratchX () { - const x = this.roundValue(this._scratchX); - const y = this.roundValue(this._scratchY); - return this.runtime.renderer.translateX( - x, false, 1, true, y, 1 - ); + return this.roundValue(this._scratchX); } /** @@ -153,11 +158,7 @@ class Mouse { * @return {number} Clamped and integer rounded Y position of the mouse cursor. */ getScratchY () { - const x = this.roundValue(this._scratchX); - const y = this.roundValue(this._scratchY); - return this.runtime.renderer.translateY( - y, false, 1, true, x, 1 - ); + return this.roundValue(this._scratchY); } /** From 426634740acd06f01cbb63ff1ad4cc061ef5b6c8 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:52:14 +0000 Subject: [PATCH 067/202] ArgumentType.LABEL (#8) --- src/engine/runtime.js | 7 +++++++ src/extension-support/argument-type.js | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 79622071432..36b423ff092 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -129,6 +129,10 @@ const ArgumentTypeMap = (() => { fieldType: 'field_variable', fieldName: 'VARIABLE' }; + map[ArgumentType.LABEL] = { + fieldType: 'field_label_serializable', + fieldName: 'LABEL' + }; return map; })(); @@ -1662,6 +1666,9 @@ 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_label') { + argJSON.type = 'field_label'; + argJSON.text = argInfo.text; } else if (argTypeInfo.fieldType === 'field_variable') { argJSON = this._constructVariableJson(argInfo, placeholder); } else { diff --git a/src/extension-support/argument-type.js b/src/extension-support/argument-type.js index bf7fbdabb41..f0f9ecbe279 100644 --- a/src/extension-support/argument-type.js +++ b/src/extension-support/argument-type.js @@ -56,7 +56,12 @@ const ArgumentType = { /** * Name of variable in the current specified target(s) */ - VARIABLE: 'variable' + VARIABLE: 'variable', + + /** + * A label text that can be dynamically changed + */ + LABEL: 'label' }; module.exports = ArgumentType; From 36175825b4151d1560be30ea2023cb603e9dbe53 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 22 Feb 2024 07:45:01 +0000 Subject: [PATCH 068/202] Hat parameters (#24) --- src/compiler/irgen.js | 16 ++++++++++++---- src/compiler/jsgen.js | 3 +++ src/engine/block-utility.js | 5 +++-- src/engine/runtime.js | 25 +++++++++++++++++++++++-- src/extension-support/argument-type.js | 7 ++++++- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 5aae5cb1e1b..e99184995ba 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -193,12 +193,20 @@ class ScriptTreeGenerator { kind: 'tw.lastKeyPressed' }; } - } - if (index === -1) { + + // probably a hat parameter. return { - kind: 'constant', - value: 0 + kind: 'args.parameter', + name: name }; + + // TODO: Removing this will put extra overhead on procedure + // arguments when used outside of define blocks. + // + // return { + // kind: 'constant', + // value: 0 + // }; } return { kind: 'args.stringNumber', diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 85e6e56963d..f247cc48cab 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -437,6 +437,9 @@ class JSGenerator { return new TypedInput(`toBoolean(p${node.index})`, TYPE_BOOLEAN); case 'args.stringNumber': return new TypedInput(`p${node.index}`, TYPE_UNKNOWN); + // hat parameter + case 'args.parameter': + return new TypedInput(`thread.getParam("${node.name}")`, TYPE_UNKNOWN); case 'compat': // Compatibility layer inputs never use flags. diff --git a/src/engine/block-utility.js b/src/engine/block-utility.js index 52384c5eca2..bb873d6da78 100644 --- a/src/engine/block-utility.js +++ b/src/engine/block-utility.js @@ -202,15 +202,16 @@ class BlockUtility { * @param {!string} requestedHat Opcode of hats to start. * @param {object=} optMatchFields Optionally, fields to match on the hat. * @param {Target=} optTarget Optionally, a target to restrict to. + * @param {Target=} optParams Optionally, parameters to push onto the hat. * @return {Array.} List of threads started by this function. */ - startHats (requestedHat, optMatchFields, optTarget) { + startHats (requestedHat, optMatchFields, optTarget, optParams) { // Store thread and sequencer to ensure we can return to the calling block's context. // startHats may execute further blocks and dirty the BlockUtility's execution context // and confuse the calling block when we return to it. const callerThread = this.thread; const callerSequencer = this.sequencer; - const result = this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget); + const result = this.sequencer.runtime.startHats(requestedHat, optMatchFields, optTarget, optParams); // Restore thread and sequencer to prior values before we return to the calling block. this.thread = callerThread; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 36b423ff092..3e2fe94d32e 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -133,6 +133,12 @@ const ArgumentTypeMap = (() => { fieldType: 'field_label_serializable', fieldName: 'LABEL' }; + map[ArgumentType.PARAMETER] = { + shadow: { + type: 'argument_reporter_string_number', + fieldName: 'VALUE' + } + }; return map; })(); @@ -1680,6 +1686,11 @@ class Runtime extends EventEmitter { name: placeholder }; + // TO DO: make it impossible to connect anything in here. + if (argInfo.type === ArgumentType.PARAMETER) { + argJSON.check = null; + } + const defaultValue = typeof argInfo.defaultValue === 'undefined' ? null : maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString(); @@ -2241,10 +2252,10 @@ class Runtime extends EventEmitter { * @param {!string} requestedHatOpcode Opcode of hats to start. * @param {object=} optMatchFields Optionally, fields to match on the hat. * @param {Target=} optTarget Optionally, a target to restrict to. + * @param {Target=} optParams Optionally, parameters to push onto the hat. * @return {Array.} List of threads started by this function. */ - startHats (requestedHatOpcode, - optMatchFields, optTarget) { + startHats (requestedHatOpcode, optMatchFields, optTarget, optParams) { if (!Object.prototype.hasOwnProperty.call(this._hats, requestedHatOpcode)) { // No known hat with this opcode. return; @@ -2307,6 +2318,16 @@ class Runtime extends EventEmitter { // Start the thread with this top block. newThreads.push(this._pushThread(topBlockId, target)); }, optTarget); + + // If there are "hat parameters", push them + if (optParams) { + newThreads.forEach(thread => { + for (const param in optParams) { + thread.pushParam(param, optParams.param); + } + }); + } + // For compatibility with Scratch 2, edge triggered hats need to be processed before // threads are stepped. See ScratchRuntime.as for original implementation newThreads.forEach(thread => { diff --git a/src/extension-support/argument-type.js b/src/extension-support/argument-type.js index f0f9ecbe279..6936a06a8dc 100644 --- a/src/extension-support/argument-type.js +++ b/src/extension-support/argument-type.js @@ -61,7 +61,12 @@ const ArgumentType = { /** * A label text that can be dynamically changed */ - LABEL: 'label' + LABEL: 'label', + + /** + * A reporter that can be defined using startHats. + */ + PARAMETER: 'parameter' }; module.exports = ArgumentType; From c478b46846560dfbcc9c1bd0807b2c8580a47134 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:04:38 +0000 Subject: [PATCH 069/202] Fix hat parameters (#25) * Fix hat parameters * missing semi --- src/compiler/irgen.js | 9 --------- src/compiler/jsgen.js | 3 +-- src/engine/runtime.js | 18 +++++++++--------- 3 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index e99184995ba..fa05c6a2578 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -194,19 +194,10 @@ class ScriptTreeGenerator { }; } - // probably a hat parameter. return { kind: 'args.parameter', name: name }; - - // TODO: Removing this will put extra overhead on procedure - // arguments when used outside of define blocks. - // - // return { - // kind: 'constant', - // value: 0 - // }; } return { kind: 'args.stringNumber', diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index f247cc48cab..1a38b47980f 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -437,9 +437,8 @@ class JSGenerator { return new TypedInput(`toBoolean(p${node.index})`, TYPE_BOOLEAN); case 'args.stringNumber': return new TypedInput(`p${node.index}`, TYPE_UNKNOWN); - // hat parameter case 'args.parameter': - return new TypedInput(`thread.getParam("${node.name}")`, TYPE_UNKNOWN); + return new TypedInput(`(thread.getParam("${node.name}") ?? 0)`, TYPE_UNKNOWN); case 'compat': // Compatibility layer inputs never use flags. diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 3e2fe94d32e..f0acc414b8e 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2319,15 +2319,6 @@ class Runtime extends EventEmitter { newThreads.push(this._pushThread(topBlockId, target)); }, optTarget); - // If there are "hat parameters", push them - if (optParams) { - newThreads.forEach(thread => { - for (const param in optParams) { - thread.pushParam(param, optParams.param); - } - }); - } - // For compatibility with Scratch 2, edge triggered hats need to be processed before // threads are stepped. See ScratchRuntime.as for original implementation newThreads.forEach(thread => { @@ -2342,8 +2333,17 @@ class Runtime extends EventEmitter { } else { execute(this.sequencer, thread); thread.goToNextBlock(); + + // If there are "hat parameters", push them + if (optParams) { + thread.initParams(); + for (const param in optParams) { + thread.pushParam(param, optParams[param]); + } + } } }); + return newThreads; } From 8fb72558be3f61f3f8704c542cd8ae36fddceccd Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 22 Feb 2024 19:08:06 +0000 Subject: [PATCH 070/202] push hat parameters after threads are set up --- src/engine/runtime.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index f0acc414b8e..9e6729719f1 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -2333,17 +2333,19 @@ class Runtime extends EventEmitter { } else { execute(this.sequencer, thread); thread.goToNextBlock(); - - // If there are "hat parameters", push them - if (optParams) { - thread.initParams(); - for (const param in optParams) { - thread.pushParam(param, optParams[param]); - } - } } }); + // If there are "hat parameters", push them + if (optParams) { + newThreads.forEach(thread => { + thread.initParams(); + for (const param in optParams) { + thread.pushParam(param, optParams[param]); + } + }); + } + return newThreads; } From 4f458eb60468d33ec9a531f0879dbfde90730980 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:51:48 +0000 Subject: [PATCH 071/202] Handle new dropdown options (#27) * Add looking rotation style * Add milliseconds to current --- src/blocks/scratch3_sensing.js | 1 + src/sprites/rendered-target.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index 15c071cb7f5..d5eba4a1792 100644 --- a/src/blocks/scratch3_sensing.js +++ b/src/blocks/scratch3_sensing.js @@ -253,6 +253,7 @@ class Scratch3SensingBlocks { case 'hour': return date.getHours(); case 'minute': return date.getMinutes(); case 'second': return date.getSeconds(); + case 'millisecond': return date.getMilliseconds(); } return 0; } diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index eab59ca75bf..73ab977358d 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -228,6 +228,14 @@ class RenderedTarget extends Target { return 'left-right'; } + /** + * Rotation style for "looking." + * @type {string} + */ + static get ROTATION_STYLE_LOOKING () { + return "looking"; + } + /** * Rotation style for "no rotation." * @type {string} @@ -303,6 +311,9 @@ class RenderedTarget extends Target { finalDirection = 90; const scaleFlip = (this.direction < 0) ? -1 : 1; finalScale = [scaleFlip * this.size, this.size]; + } else if (this.rotationStyle === RenderedTarget.ROTATION_LOOKING) { + const scaleFlip = (this.direction < 0) ? -1 : 1; + finalScale = [scaleFlip * this.size, this.size]; } return {direction: finalDirection, scale: finalScale}; } @@ -600,6 +611,8 @@ class RenderedTarget extends Target { this.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND; } else if (rotationStyle === RenderedTarget.ROTATION_STYLE_LEFT_RIGHT) { this.rotationStyle = RenderedTarget.ROTATION_STYLE_LEFT_RIGHT; + } else if (rotationStyle === RenderedTarget.ROTATION_STYLE_LOOKING) { + this.rotationStyle = RenderedTarget.ROTATION_STYLE_LOOKING; } if (this.renderer) { const {direction, scale} = this._getRenderedDirectionAndScale(); From a63276a8c44fbfa63db6345996371e5fbed66e3c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 23 Feb 2024 17:52:25 +0000 Subject: [PATCH 072/202] Fix new rotation style I swear I did this, but I must've only done it on my local server. Darn. --- src/sprites/rendered-target.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 73ab977358d..6aaccc249f3 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -311,9 +311,9 @@ class RenderedTarget extends Target { finalDirection = 90; const scaleFlip = (this.direction < 0) ? -1 : 1; finalScale = [scaleFlip * this.size, this.size]; - } else if (this.rotationStyle === RenderedTarget.ROTATION_LOOKING) { + } else if (this.rotationStyle === RenderedTarget.ROTATION_STYLE_LOOKING) { const scaleFlip = (this.direction < 0) ? -1 : 1; - finalScale = [scaleFlip * this.size, this.size]; + finalScale = [this.size, scaleFlip * this.size]; } return {direction: finalDirection, scale: finalScale}; } From 8fe383527ee9f37f3ee87de975ecbf5f89982dd2 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 24 Feb 2024 03:34:11 +0000 Subject: [PATCH 073/202] Support all the new operator/string blocks (#28) --- src/blocks/scratch3_operators.js | 79 +++++++------- src/blocks/scratch3_string.js | 181 +++++++++++++++++++++++++++++++ src/compiler/compat-blocks.js | 20 +++- 3 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 src/blocks/scratch3_string.js diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 9c67754cfc7..f710efb0355 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -20,20 +20,21 @@ class Scratch3OperatorsBlocks { operator_subtract: this.subtract, operator_multiply: this.multiply, operator_divide: this.divide, + operator_exponent: this.exponent, + operator_clamp: this.clamp, operator_lt: this.lt, + operator_lt_equals: this.ltEquals, operator_equals: this.equals, operator_gt: this.gt, + operator_gt_equals: this.gtEquals, 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, + operator_min: this.min, + operator_max: this.max, operator_round: this.round, operator_mathop: this.mathop }; @@ -55,10 +56,18 @@ class Scratch3OperatorsBlocks { return Cast.toNumber(args.NUM1) / Cast.toNumber(args.NUM2); } + exponent (args) { + return Cast.toNumber(args.NUM1) ** Cast.toNumber(args.NUM2); + } + lt (args) { return Cast.compare(args.OPERAND1, args.OPERAND2) < 0; } + ltEquals (args) { + return Cast.compare(args.OPERAND1, args.OPERAND2) <= 0; + } + equals (args) { return Cast.compare(args.OPERAND1, args.OPERAND2) === 0; } @@ -67,6 +76,10 @@ class Scratch3OperatorsBlocks { return Cast.compare(args.OPERAND1, args.OPERAND2) > 0; } + gtEquals (args) { + return Cast.compare(args.OPERAND1, args.OPERAND2) >= 0; + } + and (args) { return Cast.toBoolean(args.OPERAND1) && Cast.toBoolean(args.OPERAND2); } @@ -83,6 +96,30 @@ class Scratch3OperatorsBlocks { return !Cast.toBoolean(args.OPERAND); } + min (args) { + const n1 = Cast.toNumber(args.NUM1); + const n2 = Cast.toNumber(args.NUM2); + return Math.min(n1, n2); + } + + max (args) { + const n1 = Cast.toNumber(args.NUM1); + const n2 = Cast.toNumber(args.NUM2); + return Math.max(n1, n2); + } + + clamp (args) { + const n = Cast.toNumber(args.NUM); + const from = Cast.toNumber(args.FROM); + const to = Cast.toNumber(args.TO); + + if (from > to) { + return Math.min(Math.max(n, to), from); + } else { + return Math.min(Math.max(n, from), to); + } + } + random (args) { return this._random(args.FROM, args.TO); } @@ -99,38 +136,6 @@ class Scratch3OperatorsBlocks { return (Math.random() * (high - low)) + low; } - join (args) { - return Cast.toString(args.STRING1) + Cast.toString(args.STRING2); - } - - letterOf (args) { - const index = Cast.toNumber(args.LETTER) - 1; - const str = Cast.toString(args.STRING); - // Out of bounds? - if (index < 0 || index >= str.length) { - return ''; - } - return str.charAt(index); - } - - lettersOf (args) { - const index1 = Cast.toNumber(args.LETTER1); - const index2 = Cast.toNumber(args.LETTER2); - const str = Cast.toString(args.STRING); - return str.slice(Math.max(index1, 1) - 1, Math.min(str.length, index2)); - } - - length (args) { - return Cast.toString(args.STRING).length; - } - - contains (args) { - const format = function (string) { - return Cast.toString(string).toLowerCase(); - }; - return format(args.STRING1).includes(format(args.STRING2)); - } - mod (args) { const n = Cast.toNumber(args.NUM1); const modulus = Cast.toNumber(args.NUM2); diff --git a/src/blocks/scratch3_string.js b/src/blocks/scratch3_string.js new file mode 100644 index 00000000000..9f3aba87665 --- /dev/null +++ b/src/blocks/scratch3_string.js @@ -0,0 +1,181 @@ +const Cast = require('../util/cast.js'); +const MathUtil = require('../util/math-util.js'); + +class Scratch3OperatorsBlocks { + constructor (runtime) { + /** + * The runtime instantiating this block package. + * @type {Runtime} + */ + this.runtime = runtime; + } + + /** + * Retrieve the block primitives implemented by this package. + * @return {object.} Mapping of opcode to Function. + */ + getPrimitives () { + return { + operator_join: this.join, + operator_letter_of: this.letterOf, + operator_letters_of: this.lettersOf, + operator_length: this.length, + operator_contains: this.contains, + string_reverse: this.reverse, + string_repeat: this.repeat, + string_replace: this.replace, + string_item_split: this.itemSplit, + string_ternary: this.ternary, + string_convert: this.convertTo, + string_index_of: this.indexOf, + string_exactly: this.exactly, + string_is: this.stringIs + }; + } + + length (args) { + return Cast.toString(args.STRING).length; + } + + join (args) { + return Cast.toString(args.STRING1) + Cast.toString(args.STRING2); + } + + reverse (args) { + const str = Cast.toString(args.STRING); + + return str.split("").reverse().join(""); + } + + repeat (args) { + const str = Cast.toString(args.STRING); + const times = Cast.toNumber(args.NUMBER); + + return str.repeat(times); + } + + replace (args) { + const old = Cast.toString(args.REPLACE); + const replacer = Cast.toString(args.WITH); + const str = Cast.toString(args.STRING); + + return str.replace(new RegExp(old, "gi"), replacer); + } + + letterOf (args) { + const str = Cast.toString(args.STRING); + + if (args.LETTER === "_last_") { + args.LETTER = str.length - 1; + } else if (args.LETTER === "_random_") { + args.LETTER = Math.floor(Math.random()*str.length); + } else { + args.LETTER = Cast.toNumber(args.LETTER) - 1; + } + + const index = args.LETTER; + // Out of bounds? + if (index < 0 || index >= str.length) { + return ''; + } + + return str.charAt(index); + } + + lettersOf (args) { + const index1 = Cast.toNumber(args.LETTER1); + const index2 = Cast.toNumber(args.LETTER2); + const str = Cast.toString(args.STRING); + + return str.slice(Math.max(index1, 1) - 1, Math.min(str.length, index2)); + } + + itemSplit (args) { + const str = Cast.toString(args.STRING).toLowerCase(); + const split = Cast.toString(args.SPLIT).toLowerCase(); + + if (args.INDEX === "_last_") { + args.INDEX = str.length - 1; + } else if (args.INDEX === "_random_") { + args.INDEX = Math.floor(Math.random()*str.length); + } else { + args.INDEX = Cast.toNumber(args.INDEX) - 1; + } + + const index = args.INDEX; + return str.split(split)[index] ?? 0; + } + + ternary (args) { + const condition = Cast.toBoolean(args.CONDITION); + const str1 = Cast.toString(args.STRING1); + const str2 = Cast.toString(args.STRING2); + + return condition ? str1 : str2; + } + + convertTo (args) { + const str = Cast.toString(args.STRING); + const convert = Cast.toString(args.CONVERT).toLowerCase(); + + if (convert === "lowercase") { + return str.toLowerCase(); + } else { + return str.toUpperCase(); + } + } + + indexOf (args) { + const find = Cast.toString(args.STRING1).toLowerCase(); + const str = Cast.toString(args.STRING2).toLowerCase(); + + if (args.INDEX === "_last_") { + args.INDEX = str.length - 1; + } else if (args.INDEX === "_random_") { + args.INDEX = Math.floor(Math.random()*str.length); + } else { + args.INDEX = Cast.toNumber(args.INDEX) - 1; + } + + const index = args.INDEX; + + const length = find.length() - 1; + if (length > str) return 0; + + let occurences = []; + for (let i = 0; i > str.length(); i++) { + if (str.substring(i, i + length) === find) { + occurences.push(i); + } + } + + return occurences[index] ?? 0; + } + + contains (args) { + const format = function (string) { + return Cast.toString(string).toLowerCase(); + }; + return format(args.STRING1).includes(format(args.STRING2)); + } + + exactly (args) { + const str1 = args.STRING1; + const str2 = args.STRING2; + return str1 === str2; + } + + stringIs (args) { // usb + const str = Cast.toString(args.STRING); + const check = Cast.toString(args.CONVERT).toLowerCase(); + + if (check === "lowercase") { + return str.toLowerCase() === str; + } else { + return str.toUpperCase() === str; + } + } + +} + +module.exports = Scratch3OperatorsBlocks; diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index 26c6ec44424..1fcf5675728 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -40,7 +40,7 @@ const stacked = [ 'sound_playuntildone', 'sound_seteffectto', 'sound_setvolumeto', - 'sound_stopallsounds' + 'sound_stopallsounds', ]; const inputs = [ @@ -51,6 +51,24 @@ const inputs = [ 'sensing_loudness', 'sensing_userid', 'sound_volume' + + // USB TO DO: MUST SUPPORT COMPILER + 'operator_letter_of', + 'operator_clamp', + 'operator_exponent', + 'operator_gt_equals', + 'operator_lt_equals', + 'operator_min', + 'operator_max', + 'string_convert', + 'string_exactly', + 'string_index_of', + 'string_is', + 'string_item_split', + 'string_repeat', + 'string_replace', + 'string_reverse', + 'string_ternary', ]; module.exports = { From 085ea5367fccb4d9403e9eb3b9ba2d92602e4feb Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 24 Feb 2024 03:39:52 +0000 Subject: [PATCH 074/202] Typo --- src/compiler/compat-blocks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index 1fcf5675728..ca23580431b 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -50,7 +50,7 @@ const inputs = [ 'sensing_loud', 'sensing_loudness', 'sensing_userid', - 'sound_volume' + 'sound_volume', // USB TO DO: MUST SUPPORT COMPILER 'operator_letter_of', From a8a317df4188ea54762d732d6a60b3a24ec0845c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 24 Feb 2024 07:31:40 +0000 Subject: [PATCH 075/202] Support new blocks in compiler (#29) --- src/blocks/scratch3_string.js | 90 ++++++++++++++------------ src/compiler/compat-blocks.js | 18 ------ src/compiler/irgen.js | 115 +++++++++++++++++++++++++++++++--- src/compiler/jsgen.js | 43 +++++++++++++ src/engine/runtime.js | 1 + 5 files changed, 200 insertions(+), 67 deletions(-) diff --git a/src/blocks/scratch3_string.js b/src/blocks/scratch3_string.js index 9f3aba87665..674cbc6c136 100644 --- a/src/blocks/scratch3_string.js +++ b/src/blocks/scratch3_string.js @@ -1,7 +1,7 @@ const Cast = require('../util/cast.js'); const MathUtil = require('../util/math-util.js'); -class Scratch3OperatorsBlocks { +class Scratch3StringBlocks { constructor (runtime) { /** * The runtime instantiating this block package. @@ -41,20 +41,20 @@ class Scratch3OperatorsBlocks { return Cast.toString(args.STRING1) + Cast.toString(args.STRING2); } - reverse (args) { + reverse (args) { // usb const str = Cast.toString(args.STRING); return str.split("").reverse().join(""); } - repeat (args) { + repeat (args) { // usb const str = Cast.toString(args.STRING); const times = Cast.toNumber(args.NUMBER); return str.repeat(times); } - replace (args) { + replace (args) { // usb const old = Cast.toString(args.REPLACE); const replacer = Cast.toString(args.WITH); const str = Cast.toString(args.STRING); @@ -64,25 +64,28 @@ class Scratch3OperatorsBlocks { letterOf (args) { const str = Cast.toString(args.STRING); + return this._getLetterOf(str, args.LETTER); + } - if (args.LETTER === "_last_") { - args.LETTER = str.length - 1; - } else if (args.LETTER === "_random_") { - args.LETTER = Math.floor(Math.random()*str.length); + _getLetterOf (string, index) { // usb // used by compiler + // usb: we support some weird dropdowns now + if (index === "_last_") { + index = string.length - 1; + } else if (index === "_random_") { + index = Math.floor(Math.random()*string.length); } else { - args.LETTER = Cast.toNumber(args.LETTER) - 1; + index = Cast.toNumber(index) - 1; } - const index = args.LETTER; // Out of bounds? - if (index < 0 || index >= str.length) { + if (index < 0 || index >= string.length) { return ''; } - return str.charAt(index); + return string.charAt(index); } - lettersOf (args) { + lettersOf (args) { // usb const index1 = Cast.toNumber(args.LETTER1); const index2 = Cast.toNumber(args.LETTER2); const str = Cast.toString(args.STRING); @@ -90,23 +93,26 @@ class Scratch3OperatorsBlocks { return str.slice(Math.max(index1, 1) - 1, Math.min(str.length, index2)); } - itemSplit (args) { + itemSplit (args) { // usb const str = Cast.toString(args.STRING).toLowerCase(); const split = Cast.toString(args.SPLIT).toLowerCase(); - if (args.INDEX === "_last_") { - args.INDEX = str.length - 1; - } else if (args.INDEX === "_random_") { - args.INDEX = Math.floor(Math.random()*str.length); + return this._getIndexFromSplit(str, split, args.INDEX); + } + + _getIndexFromSplit (string, split, index) { // used by compiler + if (index === "_last_") { + index = string.length - 1; + } else if (index === "_random_") { + index = Math.floor(Math.random()*string.length); } else { - args.INDEX = Cast.toNumber(args.INDEX) - 1; + index = Cast.toNumber(index) - 1; } - const index = args.INDEX; - return str.split(split)[index] ?? 0; + return string.split(split)[index] ?? 0; } - ternary (args) { + ternary (args) { // usb const condition = Cast.toBoolean(args.CONDITION); const str1 = Cast.toString(args.STRING1); const str2 = Cast.toString(args.STRING2); @@ -114,37 +120,43 @@ class Scratch3OperatorsBlocks { return condition ? str1 : str2; } - convertTo (args) { + convertTo (args) { // usb const str = Cast.toString(args.STRING); const convert = Cast.toString(args.CONVERT).toLowerCase(); - if (convert === "lowercase") { - return str.toLowerCase(); + return this._convertString(str, convert); + } + + _convertString (string, textCase) { // used by compiler + if (textCase === "lowercase") { + return string.toLowerCase(); } else { - return str.toUpperCase(); + return string.toUpperCase(); } } - indexOf (args) { + indexOf (args) { // usb const find = Cast.toString(args.STRING1).toLowerCase(); const str = Cast.toString(args.STRING2).toLowerCase(); - if (args.INDEX === "_last_") { - args.INDEX = str.length - 1; - } else if (args.INDEX === "_random_") { - args.INDEX = Math.floor(Math.random()*str.length); + return this._getNumberIndex(find, str, args.INDEX); + } + + _getNumberIndex (find, string, index) { // used by compiler + if (index === "_last_") { + index = string.length - 1; + } else if (index === "_random_") { + index = Math.floor(Math.random()*string.length); } else { - args.INDEX = Cast.toNumber(args.INDEX) - 1; + index = Cast.toNumber(index) - 1; } - const index = args.INDEX; - const length = find.length() - 1; - if (length > str) return 0; + if (length > string) return 0; let occurences = []; - for (let i = 0; i > str.length(); i++) { - if (str.substring(i, i + length) === find) { + for (let i = 0; i > string.length(); i++) { + if (string.substring(i, i + length) === find) { occurences.push(i); } } @@ -159,7 +171,7 @@ class Scratch3OperatorsBlocks { return format(args.STRING1).includes(format(args.STRING2)); } - exactly (args) { + exactly (args) { // usb const str1 = args.STRING1; const str2 = args.STRING2; return str1 === str2; @@ -178,4 +190,4 @@ class Scratch3OperatorsBlocks { } -module.exports = Scratch3OperatorsBlocks; +module.exports = Scratch3StringBlocks; diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index ca23580431b..b3766cc8262 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -51,24 +51,6 @@ const inputs = [ 'sensing_loudness', 'sensing_userid', 'sound_volume', - - // USB TO DO: MUST SUPPORT COMPILER - 'operator_letter_of', - 'operator_clamp', - 'operator_exponent', - 'operator_gt_equals', - 'operator_lt_equals', - 'operator_min', - 'operator_max', - 'string_convert', - 'string_exactly', - 'string_index_of', - 'string_is', - 'string_item_split', - 'string_repeat', - 'string_replace', - 'string_reverse', - 'string_ternary', ]; module.exports = { diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index fa05c6a2578..ab3bc193506 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -226,6 +226,15 @@ class ScriptTreeGenerator { }; } + case 'camera_xposition': + return { + kind: 'camera.x' + }; + case 'camera_yposition': + return { + kind: 'camera.y' + }; + case 'control_get_counter': return { kind: 'counter.get' @@ -328,6 +337,13 @@ class ScriptTreeGenerator { left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; + case 'operator_clamp': + return { + kind: 'op.clamp', + num: this.descendInputOfBlock(block, 'NUM'), + left: this.descendInputOfBlock(block, 'FROM'), + right: this.descendInputOfBlock(block, 'TO') + }; case 'operator_contains': return { kind: 'op.contains', @@ -340,6 +356,12 @@ class ScriptTreeGenerator { left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; + case 'operator_exponent': + return { + kind: 'op.exponent', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; case 'operator_equals': return { kind: 'op.equals', @@ -352,6 +374,12 @@ class ScriptTreeGenerator { left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; + case 'operator_gt_equals': + return { + kind: 'op.greaterEqual', + left: this.descendInputOfBlock(block, 'OPERAND1'), + right: this.descendInputOfBlock(block, 'OPERAND2') + }; case 'operator_join': return { kind: 'op.join', @@ -382,6 +410,12 @@ class ScriptTreeGenerator { left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; + case 'operator_lt_equals': + return { + kind: 'op.lessEqual', + left: this.descendInputOfBlock(block, 'OPERAND1'), + right: this.descendInputOfBlock(block, 'OPERAND2') + }; case 'operator_mathop': { const value = this.descendInputOfBlock(block, 'NUM'); const operator = block.fields.OPERATOR.value.toLowerCase(); @@ -454,6 +488,18 @@ class ScriptTreeGenerator { left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; + case 'operator_min': + return { + kind: 'op.min', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; + case 'operator_max': + return { + kind: 'op.max', + left: this.descendInputOfBlock(block, 'NUM1'), + right: this.descendInputOfBlock(block, 'NUM2') + }; case 'operator_multiply': return { kind: 'op.multiply', @@ -471,12 +517,6 @@ class ScriptTreeGenerator { left: this.descendInputOfBlock(block, 'OPERAND1'), right: this.descendInputOfBlock(block, 'OPERAND2') }; - case 'operator_xor': - return { - kind: 'op.xor', - left: this.descendInputOfBlock(block, 'OPERAND1'), - right: this.descendInputOfBlock(block, 'OPERAND2') - }; case 'operator_random': { const from = this.descendInputOfBlock(block, 'FROM'); const to = this.descendInputOfBlock(block, 'TO'); @@ -555,6 +595,12 @@ class ScriptTreeGenerator { left: this.descendInputOfBlock(block, 'NUM1'), right: this.descendInputOfBlock(block, 'NUM2') }; + case 'operator_xor': + return { + kind: 'op.xor', + left: this.descendInputOfBlock(block, 'OPERAND1'), + right: this.descendInputOfBlock(block, 'OPERAND2') + }; case 'procedures_call': return this.descendProcedure(block); @@ -656,13 +702,62 @@ class ScriptTreeGenerator { kind: 'sensing.username' }; - case 'camera_xposition': + case 'string_convert': return { - kind: 'camera.x' + kind: 'str.convert', + left: this.descendInputOfBlock(block, 'STRING'), + right: this.descendInputOfBlock(block, 'CONVERT'), }; - case 'camera_yposition': + case 'string_exactly': return { - kind: 'camera.y' + kind: 'str.exactly', + left: this.descendInputOfBlock(block, 'STRING1'), + right: this.descendInputOfBlock(block, 'STRING2') + }; + case 'string_index_of': + return { + kind: 'str.index', + num: this.descendInputOfBlock(block, 'INDEX'), + left: this.descendInputOfBlock(block, 'STRING1'), + right: this.descendInputOfBlock(block, 'STRING2') + }; + case 'string_is': + return { + kind: 'str.is', + left: this.descendInputOfBlock(block, 'STRING'), + right: this.descendInputOfBlock(block, 'CONVERT') + }; + case 'string_item_split': + return { + kind: 'str.split', + num: this.descendInputOfBlock(block, 'INDEX'), + str: this.descendInputOfBlock(block, 'STRING'), + split: this.descendInputOfBlock(block, 'SPLIT') + }; + case 'string_repeat': + return { + kind: 'str.repeat', + str: this.descendInputOfBlock(block, 'STRING'), + num: this.descendInputOfBlock(block, 'NUMBER') + }; + case 'string_replace': + return { + kind: 'str.replace', + left: this.descendInputOfBlock(block, 'REPLACE'), + right: this.descendInputOfBlock(block, 'WITH'), + str: this.descendInputOfBlock(block, 'STRING') + }; + case 'string_reverse': + return { + kind: 'str.reverse', + str: this.descendInputOfBlock(block, 'STRING') + }; + case 'string_ternary': + return { + kind: 'str.convert', + operand: this.descendInputOfBlock(block, 'CONDITION'), + left: this.descendInputOfBlock(block, 'STRING1'), + right: this.descendInputOfBlock(block, 'STRING2') }; case 'sound_sounds_menu': diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 1a38b47980f..d817ac99ade 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -521,6 +521,8 @@ class JSGenerator { return new TypedInput(`((Math.atan(${this.descendInput(node.value).asNumber()}) * 180) / Math.PI)`, TYPE_NUMBER); case 'op.ceiling': return new TypedInput(`Math.ceil(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER); + case 'op.clamp': + return new TypedInput(`Math.min(Math.max(${this.descendInput(node.num).asNumber()}, ${this.descendInput(node.left).asNumber()}), ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER); case 'op.contains': return new TypedInput(`(${this.descendInput(node.string).asString()}.toLowerCase().indexOf(${this.descendInput(node.contains).asString()}.toLowerCase()) !== -1)`, TYPE_BOOLEAN); case 'op.cos': @@ -551,6 +553,8 @@ class JSGenerator { // No compile-time optimizations possible - use fallback method. return new TypedInput(`compareEqual(${left.asUnknown()}, ${right.asUnknown()})`, TYPE_BOOLEAN); } + case 'op.exponent': + return new TypedInput(`(${this.descendInput(node.left).asNumber()} ** ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER); case 'op.e^': return new TypedInput(`Math.exp(${this.descendInput(node.value).asNumber()})`, TYPE_NUMBER); case 'op.floor': @@ -573,6 +577,11 @@ class JSGenerator { // No compile-time optimizations possible - use fallback method. return new TypedInput(`compareGreaterThan(${left.asUnknown()}, ${right.asUnknown()})`, TYPE_BOOLEAN); } + case 'op.greaterEqual': { + const left = this.descendInput(node.left); + const right = this.descendInput(node.right); + return new TypedInput(`(${left.asUnknown()} >= ${right.asUnknown()})`, TYPE_BOOLEAN); + } case 'op.join': return new TypedInput(`(${this.descendInput(node.left).asString()} + ${this.descendInput(node.right).asString()})`, TYPE_STRING); case 'op.length': @@ -595,6 +604,11 @@ class JSGenerator { // No compile-time optimizations possible - use fallback method. return new TypedInput(`compareLessThan(${left.asUnknown()}, ${right.asUnknown()})`, TYPE_BOOLEAN); } + case 'op.lessEqual': { + const left = this.descendInput(node.left); + const right = this.descendInput(node.right); + return new TypedInput(`(${left.asUnknown()} <= ${right.asUnknown()})`, TYPE_BOOLEAN); + } case 'op.letterOf': return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); case 'op.lettersOf': @@ -609,6 +623,10 @@ class JSGenerator { this.descendedIntoModulo = true; // Needs to be marked as NaN because mod(0, 0) (and others) == NaN return new TypedInput(`mod(${this.descendInput(node.left).asNumber()}, ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN); + case 'op.min': + return new TypedInput(`Math.min(${this.descendInput(node.left).asNumber()}, ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER); + case 'op.max': + return new TypedInput(`Math.max(${this.descendInput(node.left).asNumber()}, ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER); case 'op.multiply': // Needs to be marked as NaN because Infinity * 0 === NaN return new TypedInput(`(${this.descendInput(node.left).asNumber()} * ${this.descendInput(node.right).asNumber()})`, TYPE_NUMBER_NAN); @@ -744,6 +762,31 @@ class JSGenerator { case 'sensing.year': return new TypedInput(`(new Date().getFullYear())`, TYPE_NUMBER); + case 'str.convert': + return new TypedInput(`runtime.ext_scratch3_string._convertString(${this.descendInput(node.left).asString()}, ${this.descendInput(node.right).asString()})`, TYPE_STRING); + case 'str.exactly': + return new TypedInput(`(${this.descendInput(node.left)} === ${this.descendInput(node.right)})`, TYPE_UNKNOWN); + case 'str.index': + return new TypedInput(`runtime.ext_scratch3_string._getNumberIndex(${this.descendInput(node.left).asString()}, ${this.descendInput(node.right).asString()}, ${this.descendInput(node.num).asNumber()})`, TYPE_NUMBER); + case 'str.is': { + const str = this.descendInput(node.left).asString(); + if (this.descendInput(node.right).asString().toLowerCase() === "uppercase") { + return new TypedInput(`${str.toUpperCase() === str}`, TYPE_BOOLEAN); + } else { + return new TypedInput(`${str.toLowerCase() === str}`, TYPE_BOOLEAN); + } + } + case 'str.split': + return new TypedInput(`runtime.ext_scratch3_string._getIndexFromSplit(${this.descendInput(node.str).asString()}, ${this.descendInput(node.split).asString()}, ${this.descendInput(node.num).asNumber()})`, TYPE_STRING); + case 'str.repeat': + return new TypedInput(`(${this.descendInput(node.str).asString()}.repeat(${this.descendInput(node.num).asNumber()}))`, TYPE_STRING); + case 'str.replace': + return new TypedInput(`${this.descendInput(node.str).asString()}.replace(new RegExp(${this.descendInput(node.left).asString()}, "gi"), ${this.descendInput(node.right).asString()})`, TYPE_STRING); + case 'str.reverse': + return new TypedInput(`${this.descendInput(node.str).asString()}.split("").reverse().join("");`, TYPE_STRING); + case 'str.ternary': + return new TypedInput(`(${this.descendInput(node.operand).asString()}) ? ${this.descendInput(node.left).asString()} : ${this.descendInput(node.right).asString()}`, TYPE_UNKNOWN); + case 'camera.x': return new TypedInput('runtime.camera.x', TYPE_NUMBER); case 'camera.y': diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 9e6729719f1..1132f2bae21 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -41,6 +41,7 @@ const defaultBlockPackages = { scratch3_looks: require('../blocks/scratch3_looks'), scratch3_motion: require('../blocks/scratch3_motion'), scratch3_operators: require('../blocks/scratch3_operators'), + scratch3_string: require('../blocks/scratch3_string'), scratch3_sound: require('../blocks/scratch3_sound'), scratch3_sensing: require('../blocks/scratch3_sensing'), scratch3_camera: require('../blocks/scratch3_camera'), From 9678facf8ed23e003850cf7bea19c30c4e85c506 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 24 Feb 2024 08:03:53 +0000 Subject: [PATCH 076/202] Various compiler fixes (#30) * Fix string operations in interpreter * fix blocks in compiler * fix convert value fields --- src/blocks/scratch3_string.js | 18 +++++++++--------- src/compiler/irgen.js | 4 ++-- src/compiler/jsgen.js | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/blocks/scratch3_string.js b/src/blocks/scratch3_string.js index 674cbc6c136..4fc4022c1f9 100644 --- a/src/blocks/scratch3_string.js +++ b/src/blocks/scratch3_string.js @@ -69,9 +69,9 @@ class Scratch3StringBlocks { _getLetterOf (string, index) { // usb // used by compiler // usb: we support some weird dropdowns now - if (index === "_last_") { + if (index.toLowerCase() === "last") { index = string.length - 1; - } else if (index === "_random_") { + } else if (index.toLowerCase() === "random") { index = Math.floor(Math.random()*string.length); } else { index = Cast.toNumber(index) - 1; @@ -101,9 +101,9 @@ class Scratch3StringBlocks { } _getIndexFromSplit (string, split, index) { // used by compiler - if (index === "_last_") { + if (index.toLowerCase() === "last") { index = string.length - 1; - } else if (index === "_random_") { + } else if (index.toLowerCase() === "random") { index = Math.floor(Math.random()*string.length); } else { index = Cast.toNumber(index) - 1; @@ -142,20 +142,20 @@ class Scratch3StringBlocks { return this._getNumberIndex(find, str, args.INDEX); } - _getNumberIndex (find, string, index) { // used by compiler - if (index === "_last_") { + _getNumberIndex (find, string, index) { // used by compile + if (index.toLowerCase() === "last") { index = string.length - 1; - } else if (index === "_random_") { + } else if (index.toLowerCase() === "random") { index = Math.floor(Math.random()*string.length); } else { index = Cast.toNumber(index) - 1; } - const length = find.length() - 1; + const length = find.length - 1; if (length > string) return 0; let occurences = []; - for (let i = 0; i > string.length(); i++) { + for (let i = 0; i > string.length; i++) { if (string.substring(i, i + length) === find) { occurences.push(i); } diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index ab3bc193506..6932bb9476a 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -706,7 +706,7 @@ class ScriptTreeGenerator { return { kind: 'str.convert', left: this.descendInputOfBlock(block, 'STRING'), - right: this.descendInputOfBlock(block, 'CONVERT'), + right: block.fields.CONVERT.value }; case 'string_exactly': return { @@ -725,7 +725,7 @@ class ScriptTreeGenerator { return { kind: 'str.is', left: this.descendInputOfBlock(block, 'STRING'), - right: this.descendInputOfBlock(block, 'CONVERT') + right: block.fields.CONVERT.value }; case 'string_item_split': return { diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index d817ac99ade..c7a2f058970 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -765,7 +765,7 @@ class JSGenerator { case 'str.convert': return new TypedInput(`runtime.ext_scratch3_string._convertString(${this.descendInput(node.left).asString()}, ${this.descendInput(node.right).asString()})`, TYPE_STRING); case 'str.exactly': - return new TypedInput(`(${this.descendInput(node.left)} === ${this.descendInput(node.right)})`, TYPE_UNKNOWN); + return new TypedInput(`(${this.descendInput(node.left).asUnknown()} === ${this.descendInput(node.right).asUnknown()})`, TYPE_UNKNOWN); case 'str.index': return new TypedInput(`runtime.ext_scratch3_string._getNumberIndex(${this.descendInput(node.left).asString()}, ${this.descendInput(node.right).asString()}, ${this.descendInput(node.num).asNumber()})`, TYPE_NUMBER); case 'str.is': { @@ -785,7 +785,7 @@ class JSGenerator { case 'str.reverse': return new TypedInput(`${this.descendInput(node.str).asString()}.split("").reverse().join("");`, TYPE_STRING); case 'str.ternary': - return new TypedInput(`(${this.descendInput(node.operand).asString()}) ? ${this.descendInput(node.left).asString()} : ${this.descendInput(node.right).asString()}`, TYPE_UNKNOWN); + return new TypedInput(`(${this.descendInput(node.operand).asBoolean()}) ? ${this.descendInput(node.left).asUnknown()} : ${this.descendInput(node.right).asUnknown()})`, TYPE_UNKNOWN); case 'camera.x': return new TypedInput('runtime.camera.x', TYPE_NUMBER); From fb6c7b18992d0a3944b3cc9e96603438898c30d4 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:47:46 +0000 Subject: [PATCH 077/202] More fixes for the compiler (#31) --- src/blocks/scratch3_string.js | 12 ++++++------ src/compiler/jsgen.js | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/blocks/scratch3_string.js b/src/blocks/scratch3_string.js index 4fc4022c1f9..a8bda69fea0 100644 --- a/src/blocks/scratch3_string.js +++ b/src/blocks/scratch3_string.js @@ -69,9 +69,9 @@ class Scratch3StringBlocks { _getLetterOf (string, index) { // usb // used by compiler // usb: we support some weird dropdowns now - if (index.toLowerCase() === "last") { + if (index === "last") { index = string.length - 1; - } else if (index.toLowerCase() === "random") { + } else if (index === "random") { index = Math.floor(Math.random()*string.length); } else { index = Cast.toNumber(index) - 1; @@ -101,9 +101,9 @@ class Scratch3StringBlocks { } _getIndexFromSplit (string, split, index) { // used by compiler - if (index.toLowerCase() === "last") { + if (index === "last") { index = string.length - 1; - } else if (index.toLowerCase() === "random") { + } else if (index === "random") { index = Math.floor(Math.random()*string.length); } else { index = Cast.toNumber(index) - 1; @@ -143,9 +143,9 @@ class Scratch3StringBlocks { } _getNumberIndex (find, string, index) { // used by compile - if (index.toLowerCase() === "last") { + if (index === "last") { index = string.length - 1; - } else if (index.toLowerCase() === "random") { + } else if (index === "random") { index = Math.floor(Math.random()*string.length); } else { index = Cast.toNumber(index) - 1; diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index c7a2f058970..4c427d7c7e5 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -763,14 +763,14 @@ class JSGenerator { return new TypedInput(`(new Date().getFullYear())`, TYPE_NUMBER); case 'str.convert': - return new TypedInput(`runtime.ext_scratch3_string._convertString(${this.descendInput(node.left).asString()}, ${this.descendInput(node.right).asString()})`, TYPE_STRING); + return new TypedInput(`runtime.ext_scratch3_string._convertString(${this.descendInput(node.left).asString()}, ${node.right})`, TYPE_STRING); case 'str.exactly': return new TypedInput(`(${this.descendInput(node.left).asUnknown()} === ${this.descendInput(node.right).asUnknown()})`, TYPE_UNKNOWN); case 'str.index': return new TypedInput(`runtime.ext_scratch3_string._getNumberIndex(${this.descendInput(node.left).asString()}, ${this.descendInput(node.right).asString()}, ${this.descendInput(node.num).asNumber()})`, TYPE_NUMBER); case 'str.is': { const str = this.descendInput(node.left).asString(); - if (this.descendInput(node.right).asString().toLowerCase() === "uppercase") { + if (node.right.toLowerCase() === "uppercase") { return new TypedInput(`${str.toUpperCase() === str}`, TYPE_BOOLEAN); } else { return new TypedInput(`${str.toLowerCase() === str}`, TYPE_BOOLEAN); @@ -785,7 +785,7 @@ class JSGenerator { case 'str.reverse': return new TypedInput(`${this.descendInput(node.str).asString()}.split("").reverse().join("");`, TYPE_STRING); case 'str.ternary': - return new TypedInput(`(${this.descendInput(node.operand).asBoolean()}) ? ${this.descendInput(node.left).asUnknown()} : ${this.descendInput(node.right).asUnknown()})`, TYPE_UNKNOWN); + return new TypedInput(`(${this.descendInput(node.operand).asBoolean()} ? ${this.descendInput(node.left).asString()} : ${this.descendInput(node.right).asString()})`, TYPE_UNKNOWN); case 'camera.x': return new TypedInput('runtime.camera.x', TYPE_NUMBER); From 6dfb24edf3040dc76cab1ae70beda278c069ccb0 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 25 Feb 2024 07:03:22 +0000 Subject: [PATCH 078/202] Incorporate new Object and Array types (#32) --- src/engine/runtime.js | 14 +++++++++ src/engine/scratch-blocks-constants.js | 12 ++++++-- src/extension-support/argument-type.js | 42 ++++++++++++++++---------- src/extension-support/block-type.js | 22 ++++++++++---- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 1132f2bae21..291d9229508 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -97,6 +97,12 @@ const ArgumentTypeMap = (() => { map[ArgumentType.BOOLEAN] = { check: 'Boolean' }; + map[ArgumentType.ARRAY] = { + check: 'Array' + }; + map[ArgumentType.OBJECT] = { + check: 'Object' + }; map[ArgumentType.MATRIX] = { shadow: { type: 'matrix', @@ -1458,6 +1464,14 @@ class Runtime extends EventEmitter { blockJSON.output = blockInfo.allowDropAnywhere ? null : 'String'; // TODO: distinguish number & string here? blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; break; + case BlockType.ARRAY: + blockJSON.output = 'Array'; + blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; + break; + case BlockType.OBJECT: + blockJSON.output = 'Object'; + blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_OBJECT; + break; } const blockText = Array.isArray(blockInfo.text) ? blockInfo.text : [blockInfo.text]; diff --git a/src/engine/scratch-blocks-constants.js b/src/engine/scratch-blocks-constants.js index ee558549705..5a942d06a55 100644 --- a/src/engine/scratch-blocks-constants.js +++ b/src/engine/scratch-blocks-constants.js @@ -12,16 +12,22 @@ const ScratchBlocksConstants = { OUTPUT_SHAPE_HEXAGONAL: 1, /** - * ENUM for output shape: rounded (numbers). + * ENUM for output shape: rounded (numbers and strings). * @const */ OUTPUT_SHAPE_ROUND: 2, /** - * ENUM for output shape: squared (any/all values; strings). + * ENUM for output shape: squared (arrays). * @const */ - OUTPUT_SHAPE_SQUARE: 3 + OUTPUT_SHAPE_SQUARE: 3, + + /** + * ENUM for output shape: object (objects). + * @const + */ + OUTPUT_SHAPE_OBJECT: 4 }; module.exports = ScratchBlocksConstants; diff --git a/src/extension-support/argument-type.js b/src/extension-support/argument-type.js index 6936a06a8dc..f8cf31a4e3a 100644 --- a/src/extension-support/argument-type.js +++ b/src/extension-support/argument-type.js @@ -8,6 +8,11 @@ const ArgumentType = { */ ANGLE: 'angle', + /** + * A representation of a JSON array. + */ + ARRAY: 'array', + /** * Boolean value with hexagonal placeholder */ @@ -19,14 +24,19 @@ const ArgumentType = { COLOR: 'color', /** - * Numeric value with text field + * Name of costume in the current target */ - NUMBER: 'number', + COSTUME: 'costume', /** - * String value with text field + * Inline image on block (as part of the label) */ - STRING: 'string', + IMAGE: 'image', + + /** + * A label text that can be dynamically changed + */ + LABEL: 'label', /** * String value with matrix field @@ -39,34 +49,34 @@ const ArgumentType = { NOTE: 'note', /** - * Inline image on block (as part of the label) + * Numeric value with text field */ - IMAGE: 'image', + NUMBER: 'number', /** - * Name of costume in the current target + * A representation of a JSON object. */ - COSTUME: 'costume', + OBJECT: 'Object', /** - * Name of sound in the current target + * A reporter that can be defined using startHats. */ - SOUND: 'sound', + PARAMETER: 'parameter', /** - * Name of variable in the current specified target(s) + * Name of sound in the current target */ - VARIABLE: 'variable', + SOUND: 'sound', /** - * A label text that can be dynamically changed + * String value with text field */ - LABEL: 'label', + STRING: 'string', /** - * A reporter that can be defined using startHats. + * Name of variable in the current specified target(s) */ - PARAMETER: 'parameter' + VARIABLE: 'variable', }; module.exports = ArgumentType; diff --git a/src/extension-support/block-type.js b/src/extension-support/block-type.js index 4680ef4af10..8c24b04dc9f 100644 --- a/src/extension-support/block-type.js +++ b/src/extension-support/block-type.js @@ -3,6 +3,16 @@ * @enum {string} */ const BlockType = { + /** + * Array reporter with a square shape. + */ + ARRAY: 'array', + + /** + * Object reporter with a lemon shape. + */ + OBJECT: 'Object', + /** * Boolean reporter with hexagonal shape */ @@ -40,6 +50,12 @@ const BlockType = { */ HAT: 'hat', + /** + * Specialized reporter block that allows for the insertion and evaluation + * of a substack. + */ + INLINE: 'inline', + /** * Specialized command block which may or may not run a child branch * If a child branch runs, the thread evaluates the loop block again. @@ -55,12 +71,6 @@ const BlockType = { * Arbitrary scratch-blocks XML. */ XML: 'xml', - - /** - * Specialized reporter block that allows for the insertion and evaluation - * of a substack. - */ - INLINE: 'inline' }; module.exports = BlockType; From e972ebf9b39b1a60a7974efbcb69b6bbdc07665e Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 25 Feb 2024 07:14:18 +0000 Subject: [PATCH 079/202] bare minimum compat (#33) --- 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 6932bb9476a..e667fdf0135 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -783,7 +783,7 @@ class ScriptTreeGenerator { const blockInfo = this.getBlockInfo(block.opcode); if (blockInfo) { const type = blockInfo.info.blockType; - if (type === BlockType.REPORTER || type === BlockType.BOOLEAN || type === BlockType.INLINE) { + if (type === BlockType.ARRAY || type === BlockType.OBJECT || type === BlockType.REPORTER || type === BlockType.BOOLEAN || type === BlockType.INLINE) { return this.descendCompatLayer(block); } } From 9ab989642b6390ec58d0d4c6c20648003252930e Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 1 Mar 2024 07:34:39 +0000 Subject: [PATCH 080/202] Incorporate more camera into the default blocks (#35) Hope no one named their sprite "_camera_". --- src/blocks/scratch3_motion.js | 6 ++++++ src/virtual-machine.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index ab2833fa5a7..5779ab59928 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -97,6 +97,9 @@ class Scratch3MotionBlocks { if (targetName === '_mouse_') { targetX = util.ioQuery('mouse', 'getScratchX'); targetY = util.ioQuery('mouse', 'getScratchY'); + } else if (args.TOWARDS === '_camera_') { + targetX = this.runtime.camera.x; + targetY = this.runtime.camera.y; } else if (targetName === '_random_') { const stageWidth = this.runtime.stageWidth; const stageHeight = this.runtime.stageHeight; @@ -148,6 +151,9 @@ class Scratch3MotionBlocks { if (args.TOWARDS === '_mouse_') { targetX = util.ioQuery('mouse', 'getScratchX'); targetY = util.ioQuery('mouse', 'getScratchY'); + } else if (args.TOWARDS === '_camera_') { + targetX = this.runtime.camera.x; + targetY = this.runtime.camera.y; } else if (args.TOWARDS === '_random_') { util.target.setDirection(Math.round(Math.random() * 360) - 180); return; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index baf0cf67fcd..28632017311 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -28,7 +28,7 @@ require('canvas-toBlob'); const {exportCostume} = require('./serialization/tw-costume-import-export'); const Base64Util = require('./util/base64-util'); -const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_']; +const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_', '_camera_']; const CORE_EXTENSIONS = [ // 'motion', From 79667d68e12d1c55c7084697a19a04c558f0970b Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 1 Mar 2024 08:21:55 +0000 Subject: [PATCH 081/202] fix an oversight --- src/blocks/scratch3_motion.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index 5779ab59928..3aa489b9588 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -97,7 +97,7 @@ class Scratch3MotionBlocks { if (targetName === '_mouse_') { targetX = util.ioQuery('mouse', 'getScratchX'); targetY = util.ioQuery('mouse', 'getScratchY'); - } else if (args.TOWARDS === '_camera_') { + } else if (targetName === '_camera_') { targetX = this.runtime.camera.x; targetY = this.runtime.camera.y; } else if (targetName === '_random_') { From 6dc9e312b14e8162bd168db81a4e6bc20ef75515 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 1 Mar 2024 08:29:01 +0000 Subject: [PATCH 082/202] add _camera_ to the "distance to" menu --- src/blocks/scratch3_sensing.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index d5eba4a1792..2da4eef0e3b 100644 --- a/src/blocks/scratch3_sensing.js +++ b/src/blocks/scratch3_sensing.js @@ -203,6 +203,9 @@ class Scratch3SensingBlocks { if (args.DISTANCETOMENU === '_mouse_') { targetX = util.ioQuery('mouse', 'getScratchX'); targetY = util.ioQuery('mouse', 'getScratchY'); + if (args.DISTANCETOMENU === '_camera_') { + targetX = this.runtime.camera.x; + targetY = this.runtime.camera.y; } else { args.DISTANCETOMENU = Cast.toString(args.DISTANCETOMENU); const distTarget = this.runtime.getSpriteTargetByName( From 350c27e9ecdcd5c40bdd72d15ec299fff9807262 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 1 Mar 2024 08:43:41 +0000 Subject: [PATCH 083/202] fix a silly syntax error --- src/blocks/scratch3_sensing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index 2da4eef0e3b..7152c62d406 100644 --- a/src/blocks/scratch3_sensing.js +++ b/src/blocks/scratch3_sensing.js @@ -203,7 +203,7 @@ class Scratch3SensingBlocks { if (args.DISTANCETOMENU === '_mouse_') { targetX = util.ioQuery('mouse', 'getScratchX'); targetY = util.ioQuery('mouse', 'getScratchY'); - if (args.DISTANCETOMENU === '_camera_') { + } else if (args.DISTANCETOMENU === '_camera_') { targetX = this.runtime.camera.x; targetY = this.runtime.camera.y; } else { From c6991b9ab9d781c2faf0786e08db8f6fd1d9136b Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 1 Mar 2024 09:06:07 +0000 Subject: [PATCH 084/202] Add an extra note for distance --- src/compiler/jsgen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 4c427d7c7e5..0bedd69cbd9 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -703,7 +703,7 @@ class JSGenerator { case 'sensing.daysSince2000': return new TypedInput('daysSince2000()', TYPE_NUMBER); case 'sensing.distance': - // TODO: on stages, this can be computed at compile time + // TODO: on stages and invalid values, this can be computed at compile time return new TypedInput(`distance(${this.descendInput(node.target).asString()})`, TYPE_NUMBER); case 'sensing.hour': return new TypedInput(`(new Date().getHours())`, TYPE_NUMBER); From 8657f0da6491ccd1ad43c4f71aca85846cf25a57 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 1 Mar 2024 09:07:53 +0000 Subject: [PATCH 085/202] Add extra distance calc to compiler too --- src/compiler/jsexecute.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/compiler/jsexecute.js b/src/compiler/jsexecute.js index 247b3e2c606..71f72b0350d 100644 --- a/src/compiler/jsexecute.js +++ b/src/compiler/jsexecute.js @@ -377,6 +377,9 @@ runtimeFunctions.distance = `const distance = menu => { if (menu === '_mouse_') { targetX = thread.target.runtime.ioDevices.mouse.getScratchX(); targetY = thread.target.runtime.ioDevices.mouse.getScratchY(); + } else if (menu === '_camera_') { + targetX = thread.target.runtime.camera.x; + targetY = thread.target.runtime.camera.y; } else { const distTarget = thread.target.runtime.getSpriteTargetByName(menu); if (!distTarget) return 10000; From f3612befcdf7ee188b4ebd3bd06a013ad9b2a035 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:51:43 +0000 Subject: [PATCH 086/202] Boring compiler compatibility for broken blocks (#36) --- src/blocks/scratch3_string.js | 38 ++++++++++++++++++---------------- src/compiler/compat-blocks.js | 6 ++++++ src/compiler/irgen.js | 39 ++++++----------------------------- src/compiler/jsgen.js | 12 ++--------- 4 files changed, 34 insertions(+), 61 deletions(-) diff --git a/src/blocks/scratch3_string.js b/src/blocks/scratch3_string.js index a8bda69fea0..59c70a3fb43 100644 --- a/src/blocks/scratch3_string.js +++ b/src/blocks/scratch3_string.js @@ -95,21 +95,23 @@ class Scratch3StringBlocks { itemSplit (args) { // usb const str = Cast.toString(args.STRING).toLowerCase(); - const split = Cast.toString(args.SPLIT).toLowerCase(); + const split = Cast.toString(args.SPLIT); - return this._getIndexFromSplit(str, split, args.INDEX); + return this._getItemFromSplit(str, split, args.INDEX); } - _getIndexFromSplit (string, split, index) { // used by compiler + _getItemFromSplit (string, split, index) { // used by compiler + const splitString = string.split(split); + if (index === "last") { - index = string.length - 1; + index = splitString.length - 1; } else if (index === "random") { - index = Math.floor(Math.random()*string.length); + index = Math.floor(Math.random()*splitString.length); } else { index = Cast.toNumber(index) - 1; } - return string.split(split)[index] ?? 0; + return splitString[index] ?? ""; } ternary (args) { // usb @@ -142,25 +144,25 @@ class Scratch3StringBlocks { return this._getNumberIndex(find, str, args.INDEX); } - _getNumberIndex (find, string, index) { // used by compile - if (index === "last") { - index = string.length - 1; - } else if (index === "random") { - index = Math.floor(Math.random()*string.length); - } else { - index = Cast.toNumber(index) - 1; - } - - const length = find.length - 1; - if (length > string) return 0; + _getNumberIndex (find, string, index) { // used by compiler + const length = find.length; + if (length > string.length) return 0; let occurences = []; - for (let i = 0; i > string.length; i++) { + for (let i = 0; i < string.length; i++) { if (string.substring(i, i + length) === find) { occurences.push(i); } } + if (index === "last") { + index = occurences.length - 1; + } else if (index === "random") { + index = Math.floor(Math.random()*occurences.length); + } else { + index = Cast.toNumber(index) - 1; + } + return occurences[index] ?? 0; } diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index b3766cc8262..9fffe2f7fb4 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -51,6 +51,12 @@ const inputs = [ 'sensing_loudness', 'sensing_userid', 'sound_volume', + + 'operator_letter_of', + 'string_item_split', + 'string_convert', + 'string_index_of', + 'string_ternary', ]; module.exports = { diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index e667fdf0135..6dfd0698769 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -391,12 +391,12 @@ class ScriptTreeGenerator { kind: 'op.length', string: this.descendInputOfBlock(block, 'STRING') }; - case 'operator_letter_of': - return { - kind: 'op.letterOf', - letter: this.descendInputOfBlock(block, 'LETTER'), - string: this.descendInputOfBlock(block, 'STRING') - }; +// case 'operator_letter_of': +// return { +// kind: 'op.letterOf', +// letter: this.descendInputOfBlock(block, 'LETTER'), +// string: this.descendInputOfBlock(block, 'STRING') +// }; case 'operator_letters_of': return { kind: 'op.lettersOf', @@ -702,38 +702,18 @@ class ScriptTreeGenerator { kind: 'sensing.username' }; - case 'string_convert': - return { - kind: 'str.convert', - left: this.descendInputOfBlock(block, 'STRING'), - right: block.fields.CONVERT.value - }; case 'string_exactly': return { kind: 'str.exactly', left: this.descendInputOfBlock(block, 'STRING1'), right: this.descendInputOfBlock(block, 'STRING2') }; - case 'string_index_of': - return { - kind: 'str.index', - num: this.descendInputOfBlock(block, 'INDEX'), - left: this.descendInputOfBlock(block, 'STRING1'), - right: this.descendInputOfBlock(block, 'STRING2') - }; case 'string_is': return { kind: 'str.is', left: this.descendInputOfBlock(block, 'STRING'), right: block.fields.CONVERT.value }; - case 'string_item_split': - return { - kind: 'str.split', - num: this.descendInputOfBlock(block, 'INDEX'), - str: this.descendInputOfBlock(block, 'STRING'), - split: this.descendInputOfBlock(block, 'SPLIT') - }; case 'string_repeat': return { kind: 'str.repeat', @@ -752,13 +732,6 @@ class ScriptTreeGenerator { kind: 'str.reverse', str: this.descendInputOfBlock(block, 'STRING') }; - case 'string_ternary': - return { - kind: 'str.convert', - operand: this.descendInputOfBlock(block, 'CONDITION'), - left: this.descendInputOfBlock(block, 'STRING1'), - right: this.descendInputOfBlock(block, 'STRING2') - }; case 'sound_sounds_menu': // This menu is special compared to other menus -- it actually has an opcode function. diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 0bedd69cbd9..fce7ca281f2 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -609,8 +609,8 @@ class JSGenerator { const right = this.descendInput(node.right); return new TypedInput(`(${left.asUnknown()} <= ${right.asUnknown()})`, TYPE_BOOLEAN); } - case 'op.letterOf': - return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); +// case 'op.letterOf': +// return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); case 'op.lettersOf': return new TypedInput(`((${this.descendInput(node.string).asString()}).substring(${this.descendInput(node.left).asNumber() - 1}, ${this.descendInput(node.right).asNumber()}) || "")`, TYPE_STRING); case 'op.ln': @@ -762,12 +762,8 @@ class JSGenerator { case 'sensing.year': return new TypedInput(`(new Date().getFullYear())`, TYPE_NUMBER); - case 'str.convert': - return new TypedInput(`runtime.ext_scratch3_string._convertString(${this.descendInput(node.left).asString()}, ${node.right})`, TYPE_STRING); case 'str.exactly': return new TypedInput(`(${this.descendInput(node.left).asUnknown()} === ${this.descendInput(node.right).asUnknown()})`, TYPE_UNKNOWN); - case 'str.index': - return new TypedInput(`runtime.ext_scratch3_string._getNumberIndex(${this.descendInput(node.left).asString()}, ${this.descendInput(node.right).asString()}, ${this.descendInput(node.num).asNumber()})`, TYPE_NUMBER); case 'str.is': { const str = this.descendInput(node.left).asString(); if (node.right.toLowerCase() === "uppercase") { @@ -776,16 +772,12 @@ class JSGenerator { return new TypedInput(`${str.toLowerCase() === str}`, TYPE_BOOLEAN); } } - case 'str.split': - return new TypedInput(`runtime.ext_scratch3_string._getIndexFromSplit(${this.descendInput(node.str).asString()}, ${this.descendInput(node.split).asString()}, ${this.descendInput(node.num).asNumber()})`, TYPE_STRING); case 'str.repeat': return new TypedInput(`(${this.descendInput(node.str).asString()}.repeat(${this.descendInput(node.num).asNumber()}))`, TYPE_STRING); case 'str.replace': return new TypedInput(`${this.descendInput(node.str).asString()}.replace(new RegExp(${this.descendInput(node.left).asString()}, "gi"), ${this.descendInput(node.right).asString()})`, TYPE_STRING); case 'str.reverse': return new TypedInput(`${this.descendInput(node.str).asString()}.split("").reverse().join("");`, TYPE_STRING); - case 'str.ternary': - return new TypedInput(`(${this.descendInput(node.operand).asBoolean()} ? ${this.descendInput(node.left).asString()} : ${this.descendInput(node.right).asString()})`, TYPE_UNKNOWN); case 'camera.x': return new TypedInput('runtime.camera.x', TYPE_NUMBER); From b24fe6a489cb1a35b67d08a1fd3f4eb30aff0240 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 8 Mar 2024 14:44:54 +0000 Subject: [PATCH 087/202] Fix minor oversight When changing field_label into field_label_serializable, I only did it for 1 of the 3 places that needed it. --- src/engine/runtime.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 291d9229508..db2594b4e9b 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1687,8 +1687,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_label') { - argJSON.type = 'field_label'; + } else if (argTypeInfo.fieldType === 'field_label_serializable') { + argJSON.type = 'field_label_serializable'; argJSON.text = argInfo.text; } else if (argTypeInfo.fieldType === 'field_variable') { argJSON = this._constructVariableJson(argInfo, placeholder); From 64b90a54b26ba4c29c9491b4e2dc51f110f7a187 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:12:22 +0000 Subject: [PATCH 088/202] Various changes - Don't hardcode width/height for inline images - Create constructor for label fields --- src/engine/runtime.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index db2594b4e9b..3a5f3ec6ee2 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1631,9 +1631,8 @@ class Runtime extends EventEmitter { return { type: 'field_image', src: argInfo.dataURI || '', - // TODO these probably shouldn't be hardcoded...? - width: 24, - height: 24, + width: argInfo.width || 24, + height: argInfo.height || 24, // Whether or not the inline image should be flipped horizontally // in RTL languages. Defaults to false, indicating that the // image will not be flipped. @@ -1643,7 +1642,7 @@ 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 + * @param {object} argInfo Metadata about the variable dropdown as specified by the extension * @return {object} JSON blob for a scratch-blocks variable field. * @private */ @@ -1660,6 +1659,20 @@ class Runtime extends EventEmitter { }; } + /** + * Helper for _convertPlaceholders which handles label fields which are a specialized case of block "arguments". + * @param {object} argInfo Metadata about the label as specified by the extension + * @return {object} JSON blob for a scratch-blocks label field. + * @private + */ + _constructLabelJson (argInfo, placeholder) { + return { + type: 'field_label_serializable', + name: placeholder, + text: argInfo.defaultValue, + }; + } + /** * 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. @@ -1688,8 +1701,7 @@ class Runtime extends EventEmitter { if (argTypeInfo.fieldType === 'field_image') { argJSON = this._constructInlineImageJson(argInfo); } else if (argTypeInfo.fieldType === 'field_label_serializable') { - argJSON.type = 'field_label_serializable'; - argJSON.text = argInfo.text; + argJSON = this._constructLabelJson(argInfo); } else if (argTypeInfo.fieldType === 'field_variable') { argJSON = this._constructVariableJson(argInfo, placeholder); } else { From 8e9a18bc1490450ae835b0374dda5efde9c541ca Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 21 Apr 2024 04:11:35 +0100 Subject: [PATCH 089/202] Update sb3.js --- src/serialization/sb3.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index b760e05bb08..7c0f90fbf40 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -789,6 +789,13 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {}) meta.origin = runtime.origin; } + // USB: A few mods have agreed to list our platform's name inside of the project json. + // We also add a couple more bits specific to Unsandboxed. + const platformMeta = Object.create(null); + platformMeta.name = "Unsandboxed"; + platformMeta.version = "unsandboxed-alpha"; + meta.platform = platformMeta; + // Attach full user agent string to metadata if available meta.agent = ''; // TW: Never include full user agent to slightly improve user privacy From 8ca425d3e946f0bf4c6b0bb346d0d73bcf42e972 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 21 Apr 2024 04:18:02 +0100 Subject: [PATCH 090/202] Update sb3.js --- src/serialization/sb3.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 7c0f90fbf40..14fb90d6ac2 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -793,7 +793,8 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {}) // We also add a couple more bits specific to Unsandboxed. const platformMeta = Object.create(null); platformMeta.name = "Unsandboxed"; - platformMeta.version = "unsandboxed-alpha"; + platformMeta.url = "https://unsandboxed.org/"; + platformMeta.version = "alpha"; meta.platform = platformMeta; // Attach full user agent string to metadata if available From 5555a85e5b1e929075504aa84585215ee68b5fe0 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 21 Apr 2024 21:22:14 +0100 Subject: [PATCH 091/202] Remove some unnecessary junk --- src/blocks/scratch3_camera.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/blocks/scratch3_camera.js b/src/blocks/scratch3_camera.js index 29fe16def24..399ff987528 100644 --- a/src/blocks/scratch3_camera.js +++ b/src/blocks/scratch3_camera.js @@ -1,6 +1,4 @@ const Cast = require('../util/cast'); -const MathUtil = require('../util/math-util'); -const Timer = require('../util/timer'); class Scratch3CameraBlocks { constructor (runtime) { @@ -39,13 +37,13 @@ class Scratch3CameraBlocks { }; } - moveToXY (args, util) { + moveToXY (args) { const x = Cast.toNumber(args.X); const y = Cast.toNumber(args.Y); this.runtime.camera.setXY(x, y); } - changeByXY (args, util) { + changeByXY (args) { const x = Cast.toNumber(args.X); const y = Cast.toNumber(args.Y); const newX = x + this.runtime.camera.x; @@ -53,33 +51,33 @@ class Scratch3CameraBlocks { this.runtime.camera.setXY(newX, newY); } - setX (args, util) { + setX (args) { const x = Cast.toNumber(args.X); this.runtime.camera.setXY(x, this.runtime.camera.y); } - changeX (args, util) { + changeX (args) { const x = Cast.toNumber(args.X); const newX = x + this.runtime.camera.x; this.runtime.camera.setXY(newX, this.runtime.camera.y); } - setY (args, util) { + setY (args) { const y = Cast.toNumber(args.Y); this.runtime.camera.setXY(this.runtime.camera.x, y); } - changeY (args, util) { + changeY (args) { const y = Cast.toNumber(args.Y); const newY = y + this.runtime.camera.y; this.runtime.camera.setXY(this.runtime.camera.x, newY); } - getCameraX (args, util) { + getCameraX () { return this.runtime.camera.x; } - getCameraY (args, util) { + getCameraY () { return this.runtime.camera.y; } } From 88d053d8f83047c50d8c4f7b2ce2e5e7f44e6770 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 21 Apr 2024 21:25:02 +0100 Subject: [PATCH 092/202] Update scratch3_event.js --- src/blocks/scratch3_event.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index cf598829f43..5bfad8217f7 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -67,7 +67,7 @@ class Scratch3EventBlocks { }; } - when (args, util) { + when (args) { const condition = Cast.toBoolean(args.CONDITION); return condition; } From c39ed7b836c7ed7d24a48c5cebf20667efde94b4 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 21 Apr 2024 21:28:10 +0100 Subject: [PATCH 093/202] Update scratch3_operators.js --- src/blocks/scratch3_operators.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index f710efb0355..3c0bbf1199d 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -115,9 +115,8 @@ class Scratch3OperatorsBlocks { if (from > to) { return Math.min(Math.max(n, to), from); - } else { - return Math.min(Math.max(n, from), to); } + return Math.min(Math.max(n, from), to); } random (args) { From c4e7ca7b54f2fdc50055ee3ea2ec5c8b6cd7ce81 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sun, 21 Apr 2024 21:35:16 +0100 Subject: [PATCH 094/202] Fix some formatting and unused stuff --- src/blocks/scratch3_string.js | 40 +++++++++++++++++------------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/blocks/scratch3_string.js b/src/blocks/scratch3_string.js index 59c70a3fb43..ca26bd78f5d 100644 --- a/src/blocks/scratch3_string.js +++ b/src/blocks/scratch3_string.js @@ -1,5 +1,4 @@ const Cast = require('../util/cast.js'); -const MathUtil = require('../util/math-util.js'); class Scratch3StringBlocks { constructor (runtime) { @@ -44,7 +43,9 @@ class Scratch3StringBlocks { reverse (args) { // usb const str = Cast.toString(args.STRING); - return str.split("").reverse().join(""); + return str.split('') + .reverse() + .join(''); } repeat (args) { // usb @@ -59,7 +60,7 @@ class Scratch3StringBlocks { const replacer = Cast.toString(args.WITH); const str = Cast.toString(args.STRING); - return str.replace(new RegExp(old, "gi"), replacer); + return str.replace(new RegExp(old, 'gi'), replacer); } letterOf (args) { @@ -69,10 +70,10 @@ class Scratch3StringBlocks { _getLetterOf (string, index) { // usb // used by compiler // usb: we support some weird dropdowns now - if (index === "last") { + if (index === 'last') { index = string.length - 1; - } else if (index === "random") { - index = Math.floor(Math.random()*string.length); + } else if (index === 'random') { + index = Math.floor(Math.random() * string.length); } else { index = Cast.toNumber(index) - 1; } @@ -103,15 +104,15 @@ class Scratch3StringBlocks { _getItemFromSplit (string, split, index) { // used by compiler const splitString = string.split(split); - if (index === "last") { + if (index === 'last') { index = splitString.length - 1; - } else if (index === "random") { - index = Math.floor(Math.random()*splitString.length); + } else if (index === 'random') { + index = Math.floor(Math.random() * splitString.length); } else { index = Cast.toNumber(index) - 1; } - return splitString[index] ?? ""; + return splitString[index] ?? ''; } ternary (args) { // usb @@ -130,11 +131,11 @@ class Scratch3StringBlocks { } _convertString (string, textCase) { // used by compiler - if (textCase === "lowercase") { + if (textCase === 'lowercase') { return string.toLowerCase(); - } else { - return string.toUpperCase(); } + + return string.toUpperCase(); } indexOf (args) { // usb @@ -148,17 +149,17 @@ class Scratch3StringBlocks { const length = find.length; if (length > string.length) return 0; - let occurences = []; + const occurences = []; for (let i = 0; i < string.length; i++) { if (string.substring(i, i + length) === find) { occurences.push(i); } } - if (index === "last") { + if (index === 'last') { index = occurences.length - 1; - } else if (index === "random") { - index = Math.floor(Math.random()*occurences.length); + } else if (index === 'random') { + index = Math.floor(Math.random() * occurences.length); } else { index = Cast.toNumber(index) - 1; } @@ -183,11 +184,10 @@ class Scratch3StringBlocks { const str = Cast.toString(args.STRING); const check = Cast.toString(args.CONVERT).toLowerCase(); - if (check === "lowercase") { + if (check === 'lowercase') { return str.toLowerCase() === str; - } else { - return str.toUpperCase() === str; } + return str.toUpperCase() === str; } } From 703d08cc11f9f17b644c891f7c632fbf42c0f176 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 22 Apr 2024 00:01:36 +0100 Subject: [PATCH 095/202] Update compat-blocks.js --- src/compiler/compat-blocks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index 9fffe2f7fb4..fe6825e04f0 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -40,7 +40,7 @@ const stacked = [ 'sound_playuntildone', 'sound_seteffectto', 'sound_setvolumeto', - 'sound_stopallsounds', + 'sound_stopallsounds' ]; const inputs = [ @@ -56,7 +56,7 @@ const inputs = [ 'string_item_split', 'string_convert', 'string_index_of', - 'string_ternary', + 'string_ternary' ]; module.exports = { From 7a4c69b3fe11fa0b0b4aa6abb4215a29d8bf1c0e Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 22 Apr 2024 00:02:38 +0100 Subject: [PATCH 096/202] Update irgen.js --- src/compiler/irgen.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 6dfd0698769..c3acc17422c 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -391,12 +391,12 @@ class ScriptTreeGenerator { kind: 'op.length', string: this.descendInputOfBlock(block, 'STRING') }; -// case 'operator_letter_of': -// return { -// kind: 'op.letterOf', -// letter: this.descendInputOfBlock(block, 'LETTER'), -// string: this.descendInputOfBlock(block, 'STRING') -// }; + // case 'operator_letter_of': + // return { + // kind: 'op.letterOf', + // letter: this.descendInputOfBlock(block, 'LETTER'), + // string: this.descendInputOfBlock(block, 'STRING') + // }; case 'operator_letters_of': return { kind: 'op.lettersOf', From aa6c005785b86c4e913c7d4388e8a834ac38584a Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 22 Apr 2024 00:03:52 +0100 Subject: [PATCH 097/202] Update irgen.js --- src/compiler/irgen.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index c3acc17422c..1ba316380d0 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -756,7 +756,12 @@ class ScriptTreeGenerator { const blockInfo = this.getBlockInfo(block.opcode); if (blockInfo) { const type = blockInfo.info.blockType; - if (type === BlockType.ARRAY || type === BlockType.OBJECT || type === BlockType.REPORTER || type === BlockType.BOOLEAN || type === BlockType.INLINE) { + if (type === BlockType.ARRAY + || type === BlockType.OBJECT + || type === BlockType.REPORTER + || type === BlockType.BOOLEAN + || type === BlockType.INLINE + ) { return this.descendCompatLayer(block); } } @@ -794,7 +799,7 @@ class ScriptTreeGenerator { kind: 'constant', value: true }, - code: this.descendSubstack(block, 'SUBSTACK'), + code: this.descendSubstack(block, 'SUBSTACK') }; case 'control_clear_counter': return { From 9cfd09105df93bde927ccabe23705d32c82ef9d1 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 22 Apr 2024 00:04:38 +0100 Subject: [PATCH 098/202] Update jsgen.js --- src/compiler/jsgen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index fce7ca281f2..9dbc3cd724b 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -609,8 +609,8 @@ class JSGenerator { const right = this.descendInput(node.right); return new TypedInput(`(${left.asUnknown()} <= ${right.asUnknown()})`, TYPE_BOOLEAN); } -// case 'op.letterOf': -// return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); + //case 'op.letterOf': + // return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); case 'op.lettersOf': return new TypedInput(`((${this.descendInput(node.string).asString()}).substring(${this.descendInput(node.left).asNumber() - 1}, ${this.descendInput(node.right).asNumber()}) || "")`, TYPE_STRING); case 'op.ln': From 7ec34b32a37f3beeb38eed0f224a977ba7ffa940 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 22 Apr 2024 00:07:56 +0100 Subject: [PATCH 099/202] Update jsgen.js --- src/compiler/jsgen.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 9dbc3cd724b..03402bbf795 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -766,11 +766,10 @@ class JSGenerator { return new TypedInput(`(${this.descendInput(node.left).asUnknown()} === ${this.descendInput(node.right).asUnknown()})`, TYPE_UNKNOWN); case 'str.is': { const str = this.descendInput(node.left).asString(); - if (node.right.toLowerCase() === "uppercase") { + if (node.right.toLowerCase() === 'uppercase') { return new TypedInput(`${str.toUpperCase() === str}`, TYPE_BOOLEAN); - } else { - return new TypedInput(`${str.toLowerCase() === str}`, TYPE_BOOLEAN); } + return new TypedInput(`${str.toLowerCase() === str}`, TYPE_BOOLEAN); } case 'str.repeat': return new TypedInput(`(${this.descendInput(node.str).asString()}.repeat(${this.descendInput(node.num).asNumber()}))`, TYPE_STRING); From 876c8a4f7826521ea06bb2072390fa59d077975c Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 22 Apr 2024 00:10:45 +0100 Subject: [PATCH 100/202] Update camera.js --- src/engine/camera.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/engine/camera.js b/src/engine/camera.js index 3f35004cb6e..44d7cc4aee8 100644 --- a/src/engine/camera.js +++ b/src/engine/camera.js @@ -67,7 +67,7 @@ class Camera extends EventEmitter { * @param x The x coordinate. * @param y The y coordinate. */ - setXY(x, y) { + setXY (x, y) { this.x = Cast.toNumber(x); this.y = Cast.toNumber(y); @@ -78,7 +78,7 @@ class Camera extends EventEmitter { * Set the zoom of the camera. * @param zoom The new zoom value. */ - setZoom(zoom) { + setZoom (zoom) { this.zoom = Cast.toNumber(zoom); if (this.runtime.runtimeOptions.miscLimits) { this.zoom = MathUtil.clamp(this.zoom, 10, 300); @@ -91,7 +91,7 @@ class Camera extends EventEmitter { * Point the camera towards a given direction. * @param direction Direction to point the camera. */ - setDirection(direction) { + setDirection (direction) { if (!isFinite(direction)) return; this.direction = MathUtil.wrapClamp(direction, -179, 180); @@ -103,14 +103,14 @@ class Camera extends EventEmitter { * Set whether the camera will affect the projection. * @param enabled The new enabled state. */ - setEnabled(enabled) { + setEnabled (enabled) { this.enabled = enabled; } /** * Tell the renderer to update the rendered camera state. */ - emitCameraUpdate() { + emitCameraUpdate () { if (!this.runtime.renderer) return; this.runtime.renderer._updateCamera( @@ -118,7 +118,7 @@ class Camera extends EventEmitter { this.y, this.direction, this.zoom - ) + ); this.runtime.requestRedraw(); } @@ -126,7 +126,7 @@ class Camera extends EventEmitter { /** * Reset all camera properties. */ - reset() { + reset () { this.x = 0; this.y = 0; this.direction = 90; From 3dbd30f85f886a939af8ae543f5a033bb4b129e3 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 27 Apr 2024 13:57:43 +0100 Subject: [PATCH 101/202] Expose more utils (#39) --- src/util/usb-util.js | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/util/usb-util.js b/src/util/usb-util.js index eced66e6f5a..a316e9cadbd 100644 --- a/src/util/usb-util.js +++ b/src/util/usb-util.js @@ -2,22 +2,46 @@ const Base64Util = require('../util/base64-util'); const Cast = require('../util/cast'); const Clone = require('../util/clone'); const Color = require('../util/color'); +const {fetchWithTimeout} = require('../util/fetch-with-timeout'); +const getMonitorID = require('../util/get-monitor-id'); +const JSONRPC = require('../util/get-monitor-id'); +const log = require('../util/log'); const MathUtil = require('../util/math-util'); +const maybeFormatMessage = require('../util/maybe-format-message'); +const newBlockIDs = require('../util/new-block-ids'); +const RateLimiter = require('../util/rateLimiter'); +const ScratchLinkWebsocket = require('../util/scratch-link-websocket'); const StringUtil = require('../util/string-util'); +const taskQueue = require('../util/task-queue'); const Timer = require('../util/timer'); +const AssetUtil = require('../util/tw-asset-util'); +const StaticFetch = require('../util/tw-static-fetch'); const uid = require('../util/uid'); +const VariableUtil = require('../util/variable-util'); const xmlEscape = require('../util/xml-escape'); const Util = { - Base64Util, - Cast, - Clone, - Color, - MathUtil, - StringUtil, - Timer, - uid, - xmlEscape -} + Base64Util, + Cast, + Clone, + Color, + fetchWithTimeout, + getMonitorID, + JSONRPC, + log, + MathUtil, + maybeFormatMessage, + newBlockIDs, + RateLimiter, + ScratchLinkWebsocket, + StringUtil, + taskQueue, + Timer, + AssetUtil, + StaticFetch, + uid, + VariableUtil, + xmlEscape +}; module.exports = Util; From 5cc38628d745aeae2658e16ab73befba2a1509f5 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 27 Apr 2024 15:01:03 +0100 Subject: [PATCH 102/202] Add BLE and BT to vm --- src/virtual-machine.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 28632017311..cfaf80efee6 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -21,6 +21,9 @@ const formatMessage = require('format-message'); const Variable = require('./engine/variable'); const newBlockIds = require('./util/new-block-ids'); +const BLE = require('./engine/ble'); +const BT = require('./io/bt'); + const {loadCostume} = require('./import/load-costume.js'); const {loadSound} = require('./import/load-sound.js'); const {serializeSounds, serializeCostumes} = require('./serialization/serialize-assets'); @@ -71,6 +74,11 @@ class VirtualMachine extends EventEmitter { log.error(`Failed to register runtime service: ${JSON.stringify(e)}`); }); + this.io = { + ble, + bt + } + /** * The "currently editing"/selected target ID for the VM. * Block events from any Blockly workspace are routed to this target. From 9688278f1bddeafbe932c74f4c385d32f9274099 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 27 Apr 2024 15:13:16 +0100 Subject: [PATCH 103/202] I might be an idiot --- src/virtual-machine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index cfaf80efee6..4709eeaa594 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -21,7 +21,7 @@ const formatMessage = require('format-message'); const Variable = require('./engine/variable'); const newBlockIds = require('./util/new-block-ids'); -const BLE = require('./engine/ble'); +const BLE = require('./io/ble'); const BT = require('./io/bt'); const {loadCostume} = require('./import/load-costume.js'); From af51643a79a60bf0b050992cfa40fab3ffe41b65 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Sat, 27 Apr 2024 15:17:15 +0100 Subject: [PATCH 104/202] Update virtual-machine.js --- src/virtual-machine.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 4709eeaa594..6ad65f30eb1 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -75,8 +75,8 @@ class VirtualMachine extends EventEmitter { }); this.io = { - ble, - bt + BLE, + BT } /** From bdac43dcac4754bc0654a367c3207e14fcafe927 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:55:21 +0100 Subject: [PATCH 105/202] Update tw-scratchx-compatibility-layer.js (#40) Co-authored-by: KyleKart <71104484+KyleKart@users.noreply.github.com> --- src/extension-support/tw-scratchx-compatibility-layer.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/extension-support/tw-scratchx-compatibility-layer.js b/src/extension-support/tw-scratchx-compatibility-layer.js index bceae527aae..7f4b8c72eb0 100644 --- a/src/extension-support/tw-scratchx-compatibility-layer.js +++ b/src/extension-support/tw-scratchx-compatibility-layer.js @@ -79,6 +79,11 @@ const parseScratchXArgument = (argument, defaultValue) => { const split = argument.split(/\.|:/); const menuName = split[1]; result.menu = menuName; + } else if (argument === 'b') { + result.type = ArgumentType.BOOLEAN; + if (!hasDefaultValue) { + result.defaultValue = ''; + } } else { throw new Error(`Unknown ScratchX argument type: ${argument}`); } From e3708b6bc1456d5c520c4fdd55f0d9a2579d7e4b Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Thu, 2 May 2024 10:06:07 +0100 Subject: [PATCH 106/202] Update sb3.js (#41) --- src/serialization/sb3.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 14fb90d6ac2..1f6d51ab034 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -44,6 +44,7 @@ const INPUT_DIFF_BLOCK_SHADOW = 3; // obscured shadow // Constants used during deserialization of an SB3 file const CORE_EXTENSIONS = [ 'argument', + 'camera', 'colour', 'control', 'data', @@ -54,6 +55,7 @@ const CORE_EXTENSIONS = [ 'operator', 'procedures', 'sensing', + 'string', 'sound' ]; From a827732cc1b72138322b0d3e20e493097abe68ed Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 17 May 2024 05:59:17 +0100 Subject: [PATCH 107/202] Reimplement MenuState (#43) * Add menuState * lint --- src/blocks/scratch3_operators.js | 4 +-- src/blocks/scratch3_procedures.js | 1 + src/blocks/scratch3_string.js | 39 +++++++++++----------- src/compiler/compat-blocks.js | 4 +-- src/compiler/irgen.js | 14 ++++---- src/compiler/jsgen.js | 10 +++--- src/engine/camera.js | 16 ++++----- src/engine/runtime.js | 18 +++++----- src/engine/tw-interpolate.js | 2 +- src/extension-support/argument-type.js | 2 +- src/extension-support/block-type.js | 2 +- src/extension-support/extension-manager.js | 25 +++++++++++--- src/serialization/sb3.js | 6 ++-- src/sprites/rendered-target.js | 2 +- src/virtual-machine.js | 2 +- 15 files changed, 83 insertions(+), 64 deletions(-) diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index f710efb0355..db1bb5df1f1 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -115,9 +115,9 @@ class Scratch3OperatorsBlocks { if (from > to) { return Math.min(Math.max(n, to), from); - } else { - return Math.min(Math.max(n, from), to); } + return Math.min(Math.max(n, from), to); + } random (args) { diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js index 1f55d32bf16..e8205b569d6 100644 --- a/src/blocks/scratch3_procedures.js +++ b/src/blocks/scratch3_procedures.js @@ -90,6 +90,7 @@ class Scratch3ProcedureBlocks { } util.startProcedure(procedureCode); + util.thread.tryCompile(); } return (args, util) { diff --git a/src/blocks/scratch3_string.js b/src/blocks/scratch3_string.js index 59c70a3fb43..783da2218f7 100644 --- a/src/blocks/scratch3_string.js +++ b/src/blocks/scratch3_string.js @@ -44,7 +44,8 @@ class Scratch3StringBlocks { reverse (args) { // usb const str = Cast.toString(args.STRING); - return str.split("").reverse().join(""); + return str.split('').reverse() + .join(''); } repeat (args) { // usb @@ -59,7 +60,7 @@ class Scratch3StringBlocks { const replacer = Cast.toString(args.WITH); const str = Cast.toString(args.STRING); - return str.replace(new RegExp(old, "gi"), replacer); + return str.replace(new RegExp(old, 'gi'), replacer); } letterOf (args) { @@ -69,10 +70,10 @@ class Scratch3StringBlocks { _getLetterOf (string, index) { // usb // used by compiler // usb: we support some weird dropdowns now - if (index === "last") { + if (index === 'last') { index = string.length - 1; - } else if (index === "random") { - index = Math.floor(Math.random()*string.length); + } else if (index === 'random') { + index = Math.floor(Math.random() * string.length); } else { index = Cast.toNumber(index) - 1; } @@ -103,15 +104,15 @@ class Scratch3StringBlocks { _getItemFromSplit (string, split, index) { // used by compiler const splitString = string.split(split); - if (index === "last") { + if (index === 'last') { index = splitString.length - 1; - } else if (index === "random") { - index = Math.floor(Math.random()*splitString.length); + } else if (index === 'random') { + index = Math.floor(Math.random() * splitString.length); } else { index = Cast.toNumber(index) - 1; } - return splitString[index] ?? ""; + return splitString[index] ?? ''; } ternary (args) { // usb @@ -130,11 +131,11 @@ class Scratch3StringBlocks { } _convertString (string, textCase) { // used by compiler - if (textCase === "lowercase") { + if (textCase === 'lowercase') { return string.toLowerCase(); - } else { - return string.toUpperCase(); } + return string.toUpperCase(); + } indexOf (args) { // usb @@ -148,17 +149,17 @@ class Scratch3StringBlocks { const length = find.length; if (length > string.length) return 0; - let occurences = []; + const occurences = []; for (let i = 0; i < string.length; i++) { if (string.substring(i, i + length) === find) { occurences.push(i); } } - if (index === "last") { + if (index === 'last') { index = occurences.length - 1; - } else if (index === "random") { - index = Math.floor(Math.random()*occurences.length); + } else if (index === 'random') { + index = Math.floor(Math.random() * occurences.length); } else { index = Cast.toNumber(index) - 1; } @@ -183,11 +184,11 @@ class Scratch3StringBlocks { const str = Cast.toString(args.STRING); const check = Cast.toString(args.CONVERT).toLowerCase(); - if (check === "lowercase") { + if (check === 'lowercase') { return str.toLowerCase() === str; - } else { - return str.toUpperCase() === str; } + return str.toUpperCase() === str; + } } diff --git a/src/compiler/compat-blocks.js b/src/compiler/compat-blocks.js index 9fffe2f7fb4..fe6825e04f0 100644 --- a/src/compiler/compat-blocks.js +++ b/src/compiler/compat-blocks.js @@ -40,7 +40,7 @@ const stacked = [ 'sound_playuntildone', 'sound_seteffectto', 'sound_setvolumeto', - 'sound_stopallsounds', + 'sound_stopallsounds' ]; const inputs = [ @@ -56,7 +56,7 @@ const inputs = [ 'string_item_split', 'string_convert', 'string_index_of', - 'string_ternary', + 'string_ternary' ]; module.exports = { diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 6dfd0698769..90ad0678881 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -391,12 +391,12 @@ class ScriptTreeGenerator { kind: 'op.length', string: this.descendInputOfBlock(block, 'STRING') }; -// case 'operator_letter_of': -// return { -// kind: 'op.letterOf', -// letter: this.descendInputOfBlock(block, 'LETTER'), -// string: this.descendInputOfBlock(block, 'STRING') -// }; + // case 'operator_letter_of': + // return { + // kind: 'op.letterOf', + // letter: this.descendInputOfBlock(block, 'LETTER'), + // string: this.descendInputOfBlock(block, 'STRING') + // }; case 'operator_letters_of': return { kind: 'op.lettersOf', @@ -794,7 +794,7 @@ class ScriptTreeGenerator { kind: 'constant', value: true }, - code: this.descendSubstack(block, 'SUBSTACK'), + code: this.descendSubstack(block, 'SUBSTACK') }; case 'control_clear_counter': return { diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index fce7ca281f2..786eeb67363 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -609,8 +609,8 @@ class JSGenerator { const right = this.descendInput(node.right); return new TypedInput(`(${left.asUnknown()} <= ${right.asUnknown()})`, TYPE_BOOLEAN); } -// case 'op.letterOf': -// return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); + // case 'op.letterOf': + // return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); case 'op.lettersOf': return new TypedInput(`((${this.descendInput(node.string).asString()}).substring(${this.descendInput(node.left).asNumber() - 1}, ${this.descendInput(node.right).asNumber()}) || "")`, TYPE_STRING); case 'op.ln': @@ -766,11 +766,11 @@ class JSGenerator { return new TypedInput(`(${this.descendInput(node.left).asUnknown()} === ${this.descendInput(node.right).asUnknown()})`, TYPE_UNKNOWN); case 'str.is': { const str = this.descendInput(node.left).asString(); - if (node.right.toLowerCase() === "uppercase") { + if (node.right.toLowerCase() === 'uppercase') { return new TypedInput(`${str.toUpperCase() === str}`, TYPE_BOOLEAN); - } else { - return new TypedInput(`${str.toLowerCase() === str}`, TYPE_BOOLEAN); } + return new TypedInput(`${str.toLowerCase() === str}`, TYPE_BOOLEAN); + } case 'str.repeat': return new TypedInput(`(${this.descendInput(node.str).asString()}.repeat(${this.descendInput(node.num).asNumber()}))`, TYPE_STRING); diff --git a/src/engine/camera.js b/src/engine/camera.js index 3f35004cb6e..691104ff4f1 100644 --- a/src/engine/camera.js +++ b/src/engine/camera.js @@ -13,7 +13,7 @@ const MathUtil = require('../util/math-util'); * Camera: instance of a camera object on the stage. */ class Camera extends EventEmitter { - constructor(runtime) { + constructor (runtime) { super(); this.runtime = runtime; @@ -67,7 +67,7 @@ class Camera extends EventEmitter { * @param x The x coordinate. * @param y The y coordinate. */ - setXY(x, y) { + setXY (x, y) { this.x = Cast.toNumber(x); this.y = Cast.toNumber(y); @@ -78,7 +78,7 @@ class Camera extends EventEmitter { * Set the zoom of the camera. * @param zoom The new zoom value. */ - setZoom(zoom) { + setZoom (zoom) { this.zoom = Cast.toNumber(zoom); if (this.runtime.runtimeOptions.miscLimits) { this.zoom = MathUtil.clamp(this.zoom, 10, 300); @@ -91,7 +91,7 @@ class Camera extends EventEmitter { * Point the camera towards a given direction. * @param direction Direction to point the camera. */ - setDirection(direction) { + setDirection (direction) { if (!isFinite(direction)) return; this.direction = MathUtil.wrapClamp(direction, -179, 180); @@ -103,14 +103,14 @@ class Camera extends EventEmitter { * Set whether the camera will affect the projection. * @param enabled The new enabled state. */ - setEnabled(enabled) { + setEnabled (enabled) { this.enabled = enabled; } /** * Tell the renderer to update the rendered camera state. */ - emitCameraUpdate() { + emitCameraUpdate () { if (!this.runtime.renderer) return; this.runtime.renderer._updateCamera( @@ -118,7 +118,7 @@ class Camera extends EventEmitter { this.y, this.direction, this.zoom - ) + ); this.runtime.requestRedraw(); } @@ -126,7 +126,7 @@ class Camera extends EventEmitter { /** * Reset all camera properties. */ - reset() { + reset () { this.x = 0; this.y = 0; this.direction = 90; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 3a5f3ec6ee2..d885461b724 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -152,16 +152,16 @@ const ArgumentTypeMap = (() => { const FieldTypeMap = (() => { const map = {}; map[ArgumentType.ANGLE] = { - fieldName: "field_angle", + fieldName: 'field_angle' }; map[ArgumentType.NUMBER] = { - fieldName: "field_number", + fieldName: 'field_number' }; map[ArgumentType.STRING] = { - fieldName: "field_input", + fieldName: 'field_input' }; map[ArgumentType.NOTE] = { - fieldName: "field_note", + fieldName: 'field_note' }; return map; })(); @@ -1258,7 +1258,7 @@ class Runtime extends EventEmitter { const acceptInput = (menuInfo.acceptText || menuInfo.acceptNumber); const type = menuInfo.acceptText ? 'field_textdropdown' : menuInfo.acceptNumber ? - 'field_numberdropdown' : 'field_dropdown'; + 'field_numberdropdown' : 'field_dropdown'; return { json: { message0: '%1', @@ -1669,7 +1669,7 @@ class Runtime extends EventEmitter { return { type: 'field_label_serializable', name: placeholder, - text: argInfo.defaultValue, + text: argInfo.defaultValue }; } @@ -1736,9 +1736,9 @@ class Runtime extends EventEmitter { const menuInfo = context.categoryInfo.menuInfo[argInfo.menu]; let acceptReporters = false; - if (typeof argInfo.acceptReporters !== "undefined") { + if (typeof argInfo.acceptReporters !== 'undefined') { acceptReporters = argInfo.acceptReporters; - } else if (typeof menuInfo.acceptReporters !== "undefined") { + } else if (typeof menuInfo.acceptReporters !== 'undefined') { acceptReporters = menuInfo.acceptReporters; } @@ -2844,7 +2844,7 @@ class Runtime extends EventEmitter { /** * Get the current instance of the camera. */ - getCamera() { + getCamera () { return this.camera; } diff --git a/src/engine/tw-interpolate.js b/src/engine/tw-interpolate.js index bf63f2d079b..deb06497c47 100644 --- a/src/engine/tw-interpolate.js +++ b/src/engine/tw-interpolate.js @@ -23,7 +23,7 @@ const setupInitialState = runtime => { y: camera.y, direction: camera.direction, zoom: camera.zoom - } + }; } else { camera.interpolationData = null; } diff --git a/src/extension-support/argument-type.js b/src/extension-support/argument-type.js index f8cf31a4e3a..eb97555c9b9 100644 --- a/src/extension-support/argument-type.js +++ b/src/extension-support/argument-type.js @@ -76,7 +76,7 @@ const ArgumentType = { /** * Name of variable in the current specified target(s) */ - VARIABLE: 'variable', + VARIABLE: 'variable' }; module.exports = ArgumentType; diff --git a/src/extension-support/block-type.js b/src/extension-support/block-type.js index 8c24b04dc9f..4800291cc80 100644 --- a/src/extension-support/block-type.js +++ b/src/extension-support/block-type.js @@ -70,7 +70,7 @@ const BlockType = { /** * Arbitrary scratch-blocks XML. */ - XML: 'xml', + XML: 'xml' }; module.exports = BlockType; diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 0f489120022..2ac61f8c588 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -430,6 +430,18 @@ class ExtensionManager { */ _prepareMenuInfo (serviceName, menus) { const menuNames = Object.getOwnPropertyNames(menus); + + // `makeGetItemsShim` is like `bind` except that it explicitly doesn't bind `this` + const makeGetItemsShim = (serviceObject, menuItemFunctionName) => { + const extensionManager = this; + return function () { + // scratch-blocks passes the menu as `this` when calling a menu's `items` function + const scratchBlocksMenuObject = this; // eslint-disable-line no-invalid-this + return extensionManager._getExtensionMenuItems( + serviceObject, menuItemFunctionName, scratchBlocksMenuObject); + }; + }; + for (let i = 0; i < menuNames.length; i++) { const menuName = menuNames[i]; let menuInfo = menus[menuName]; @@ -447,8 +459,7 @@ class ExtensionManager { if (typeof menuInfo.items === 'string') { const menuItemFunctionName = menuInfo.items; const serviceObject = dispatch.services[serviceName]; - // Bind the function here so we can pass a simple item generation function to Scratch Blocks later. - menuInfo.items = this._getExtensionMenuItems.bind(this, serviceObject, menuItemFunctionName); + menuInfo.items = makeGetItemsShim(serviceObject, menuItemFunctionName); } } return menus; @@ -458,19 +469,25 @@ class ExtensionManager { * Fetch the items for a particular extension menu, providing the target ID for context. * @param {object} extensionObject - the extension object providing the menu. * @param {string} menuItemFunctionName - the name of the menu function to call. + * @param {object} scratchBlocksMenuObject - the scratch-blocks menu object, for access to its current state. * @returns {Array} menu items ready for scratch-blocks. * @private */ - _getExtensionMenuItems (extensionObject, menuItemFunctionName) { + _getExtensionMenuItems (extensionObject, menuItemFunctionName, scratchBlocksMenuObject) { // Fetch the items appropriate for the target currently being edited. This assumes that menus only // collect items when opened by the user while editing a particular target. const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage(); const editingTargetID = editingTarget ? editingTarget.id : null; const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget); + // trim the menu state down to structured-copy-compatible properties that extensions might need + const menuState = { + selectedValue: scratchBlocksMenuObject.getValue() + }; + // TODO: Fix this to use dispatch.call when extensions are running in workers. const menuFunc = extensionObject[menuItemFunctionName]; - const menuItems = menuFunc.call(extensionObject, editingTargetID).map( + const menuItems = menuFunc.call(extensionObject, editingTargetID, menuState).map( item => { item = maybeFormatMessage(item, extensionMessageContext); switch (typeof item) { diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 1f6d51ab034..a8d3381bb9b 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -794,9 +794,9 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {}) // USB: A few mods have agreed to list our platform's name inside of the project json. // We also add a couple more bits specific to Unsandboxed. const platformMeta = Object.create(null); - platformMeta.name = "Unsandboxed"; - platformMeta.url = "https://unsandboxed.org/"; - platformMeta.version = "alpha"; + platformMeta.name = 'Unsandboxed'; + platformMeta.url = 'https://unsandboxed.org/'; + platformMeta.version = 'alpha'; meta.platform = platformMeta; // Attach full user agent string to metadata if available diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index 6aaccc249f3..680d6bab64c 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -233,7 +233,7 @@ class RenderedTarget extends Target { * @type {string} */ static get ROTATION_STYLE_LOOKING () { - return "looking"; + return 'looking'; } /** diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 6ad65f30eb1..ef280524793 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -77,7 +77,7 @@ class VirtualMachine extends EventEmitter { this.io = { BLE, BT - } + }; /** * The "currently editing"/selected target ID for the VM. From ecc638e4bed52e0a04bdfa71b8049aa1d9b0ee6a Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 17 May 2024 06:08:40 +0100 Subject: [PATCH 108/202] Update extension-manager.js (#44) --- src/extension-support/extension-manager.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 2ac61f8c588..e3370ac1c9a 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -482,7 +482,9 @@ class ExtensionManager { // trim the menu state down to structured-copy-compatible properties that extensions might need const menuState = { - selectedValue: scratchBlocksMenuObject.getValue() + selectedValue: scratchBlocksMenuObject.getValue(), + sourceBlock: scratchBlocksMenuObject.sourceBlock_, + menuObject: scratchBlocksMenuObject, }; // TODO: Fix this to use dispatch.call when extensions are running in workers. From 5fd6aec30cef4ce561af2b4c61995892d62f669a Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Fri, 17 May 2024 16:01:36 +0100 Subject: [PATCH 109/202] API for custom context menus (#45) * Create context-menu-context.js * Update extension-manager.js * Update tw-extension-api-common.js * Update extension-manager.js --- src/extension-support/context-menu-context.js | 25 +++++++++++++++++++ src/extension-support/extension-manager.js | 23 +++++++++++++++++ .../tw-extension-api-common.js | 2 ++ 3 files changed, 50 insertions(+) create mode 100644 src/extension-support/context-menu-context.js diff --git a/src/extension-support/context-menu-context.js b/src/extension-support/context-menu-context.js new file mode 100644 index 00000000000..5ad2e9cf0b3 --- /dev/null +++ b/src/extension-support/context-menu-context.js @@ -0,0 +1,25 @@ +/** + * Block argument types + * @enum {string} + */ +const ContextMenuContext = { + /** + * Indicates that the context menu item should appear regardless of whether + * the block is in the toolbox or on the main workspace. + */ + ALL: 'all', + + /** + * Indicates that the context menu item should only appear on a block + * if it is in the toolbox. + */ + TOOLBOX_ONLY: 'toolbox', + + /** + * Indicates that the context menu item should only appear on a block + * if it is on the main workspace. + */ + WORKSPACE_ONLY: 'workspace' +}; + +module.exports = ContextMenuContext; diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index e3370ac1c9a..754b004e9e9 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -382,6 +382,16 @@ class ExtensionManager { }); } + /** + * Modify the provided text as necessary to ensure that it may be used as an attribute value in valid XML. + * @param {string} text - the text to be sanitized + * @returns {string} - the sanitized text + * @private + */ + _sanitizeID (text) { + return text.toString().replace(/[<"&]/, '_'); + } + /** * Apply minor cleanup and defaults for optional extension fields. * TODO: make the ID unique in cases where two copies of the same extension are loaded. @@ -534,6 +544,19 @@ class ExtensionManager { }, blockInfo); blockInfo.text = blockInfo.text || blockInfo.opcode; + if (blockInfo.customContextMenu && blockInfo.customContextMenu.length > 0) { + // Replace all the string callback names of the context menu items + // with the actual function call. + blockInfo.customContextMenu = blockInfo.customContextMenu.map(contextMenuOption => { + if (typeof contextMenuOption.callback === 'string') { + const callbackName = this._sanitizeID(contextMenuOption.callback); + contextMenuOption.callback = args => + dispatch.call(serviceName, callbackName, args); + } + return contextMenuOption; + }); + } + switch (blockInfo.blockType) { case BlockType.EVENT: if (blockInfo.func) { diff --git a/src/extension-support/tw-extension-api-common.js b/src/extension-support/tw-extension-api-common.js index dcc782ff407..a0e49dc3f93 100644 --- a/src/extension-support/tw-extension-api-common.js +++ b/src/extension-support/tw-extension-api-common.js @@ -1,6 +1,7 @@ const ArgumentType = require('./argument-type'); const BlockType = require('./block-type'); const TargetType = require('./target-type'); +const ContextMenuContext = require('./context-menu-context'); const Cast = require('../util/cast'); const Util = require('../util/usb-util'); @@ -8,6 +9,7 @@ const Scratch = { ArgumentType, BlockType, TargetType, + ContextMenuContext, Cast, Util }; From abf565df01b494b0e8a66af6341730207ad29bde Mon Sep 17 00:00:00 2001 From: LilyMakesThings Date: Sun, 19 May 2024 13:41:31 +0100 Subject: [PATCH 110/202] pending monitors --- src/engine/blocks.js | 103 +++++++++++++++--- src/engine/runtime.js | 121 ++++++++++++++++++++- src/engine/target.js | 17 +++ src/extension-support/extension-manager.js | 2 +- src/util/string-util.js | 15 +++ src/virtual-machine.js | 3 + 6 files changed, 242 insertions(+), 19 deletions(-) diff --git a/src/engine/blocks.js b/src/engine/blocks.js index bb397832cae..e5b9410a113 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -477,6 +477,12 @@ class Blocks { if (e.isLocal && editingTarget && !editingTarget.isStage && !e.isCloud) { if (!editingTarget.lookupVariableById(e.varId)) { editingTarget.createVariable(e.varId, e.varName, e.varType); + + this.runtime.addPendingMonitor(e.varId); + + // TODO this should probably be batched + // (esp. if we receive multiple new var_creates in a row). + this.runtime.requestToolboxExtensionsUpdate(); this.emitProjectChanged(); } } else { @@ -492,6 +498,12 @@ class Blocks { } } stage.createVariable(e.varId, e.varName, e.varType, e.isCloud); + + this.runtime.addPendingMonitor(e.varId); + + // TODO same as above, this should probably be batched + // (esp. if we receive multiple new var_creates in a row). + this.runtime.requestToolboxExtensionsUpdate(); this.emitProjectChanged(); } break; @@ -655,6 +667,20 @@ class Blocks { this._addScript(block.id); } + // A block was just created, see if it had an associated pending monitor, + // if so, keep track of the monitor state change by mimicing the checkbox + // event from the flyout. Clear record of this block from + // the pending monitors list. + if (this === this.runtime.monitorBlocks && + this.runtime.getPendingMonitor(block.id)) { + this.changeBlock({ + id: block.id, // Monitor blocks for variables are the variable ID. + element: 'checkbox', // Mimic checkbox event from flyout. + value: true + }, this.runtime); + this.runtime.removePendingMonitor(block.id); + } + this.resetCache(); // A new block was actually added to the block container, @@ -937,6 +963,41 @@ class Blocks { this.emitProjectChanged(); } + getAllReferencesForVariable (variable) { + let fieldName; + let truncatedOpcode; + if (variable.type === Variable.SCALAR_TYPE) { + fieldName = 'VARIABLE'; + truncatedOpcode = 'variable'; + } else if (variable.type === Variable.LIST_TYPE) { + fieldName = 'LIST'; + truncatedOpcode = 'listcontents'; + } else { + // TODO handle broadcast messages later + return []; + } + + const variableBlocks = []; + for (const blockId in this._blocks) { + if (!this._blocks.hasOwnProperty(blockId)) continue; + const block = this._blocks[blockId]; + // Check for blocks with fields referencing variable/list, otherwise variable/list reporters + if (block.fields[fieldName] && + block.fields[fieldName].value === variable.name) { + // It's a block containing a variable field whose currently selected value + // matches the given variable name + variableBlocks.push(block); + } else if (block.mutation && + block.mutation.blockInfo && + block.mutation.blockInfo.opcode === truncatedOpcode && + block.mutation.blockInfo.text === variable.name) { + // It's a variable reporter whose name matches the given variable + variableBlocks.push(block); + } + } + return variableBlocks; + } + /** * Delete all blocks and their associated scripts. */ @@ -1222,25 +1283,33 @@ class Blocks { xmlString += ''; } } - // Add any fields on this block. - for (const field in block.fields) { - if (!Object.prototype.hasOwnProperty.call(block.fields, field)) continue; - const blockField = block.fields[field]; - xmlString += `${value}`; } - xmlString += `>${value}`; } + // Add blocks connected to the next connection. if (block.next) { xmlString += `${this.blockToXML(block.next, comments)}`; diff --git a/src/engine/runtime.js b/src/engine/runtime.js index d885461b724..e3e176a4929 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -23,6 +23,8 @@ const ScratchLinkWebSocket = require('../util/scratch-link-websocket'); const FontManager = require('./tw-font-manager'); const fetchWithTimeout = require('../util/fetch-with-timeout'); +const CORE_BLOCKS = []; + // Virtual I/O devices. const Clock = require('../io/clock'); const Cloud = require('../io/cloud'); @@ -367,6 +369,12 @@ class Runtime extends EventEmitter { */ this._prevMonitorState = OrderedMap({}); + /** + * Track any monitors to be added that don't have corresponding monitor blocks + * created yet. + */ + this._pendingMonitors = new Set(); + /** * Whether the project is in "turbo mode." * @type {Boolean} @@ -922,6 +930,10 @@ class Runtime extends EventEmitter { return 'BLOCKSINFO_UPDATE'; } + static get BLOCK_UPDATE () { + return 'BLOCK_UPDATE'; + } + /** * Event name when the runtime tick loop has been started. * @const {string} @@ -1146,6 +1158,7 @@ class Runtime extends EventEmitter { categoryInfo.customFieldTypes = {}; categoryInfo.menus = []; categoryInfo.menuInfo = {}; + categoryInfo.convertedMenuInfo = {}; for (const menuName in extensionInfo.menus) { if (Object.prototype.hasOwnProperty.call(extensionInfo.menus, menuName)) { @@ -1153,6 +1166,13 @@ class Runtime extends EventEmitter { const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuInfo, categoryInfo); categoryInfo.menus.push(convertedMenu); categoryInfo.menuInfo[menuName] = menuInfo; + // Use the convertedMenu and `menuInfo` to consolidate + // the information needed to layout the menu correctly + // on a dynamic extension block. + categoryInfo.convertedMenuInfo[menuName] = { + items: Array.isArray(menuInfo.items) ? convertedMenu.json.args0[0].options : menuInfo.items, + acceptReporters: menuInfo.acceptReporters || false + }; } } for (const fieldTypeName in extensionInfo.customFieldTypes) { @@ -1532,7 +1552,9 @@ class Runtime extends EventEmitter { const mutation = blockInfo.isDynamic ? `` : ''; const inputs = context.inputList.join(''); - const blockXML = `${mutation}${inputs}`; + const toolboxIdXml = blockInfo.isDynamic && blockInfo.paletteKey ? + ` id="${xmlEscape(blockInfo.paletteKey)}"` : ''; + const blockXML = `${mutation}${inputs}`; if (blockInfo.extensions) { for (const extension of blockInfo.extensions) { @@ -2393,6 +2415,7 @@ class Runtime extends EventEmitter { const emptyMonitorState = OrderedMap({}); if (!emptyMonitorState.equals(this._monitorState)) { this._monitorState = emptyMonitorState; + this._pendingMonitors.clear(); this.emit(Runtime.MONITORS_UPDATE, this._monitorState); } this.emit(Runtime.RUNTIME_DISPOSED); @@ -3410,6 +3433,10 @@ class Runtime extends EventEmitter { return varNames; } + isCoreExtension (categoryId) { + return CORE_BLOCKS.indexOf(categoryId) > -1; + } + /** * Get the label or label function for an opcode * @param {string} extendedOpcode - the opcode you want a label for @@ -3417,6 +3444,7 @@ class Runtime extends EventEmitter { * @property {string} category - the category for this opcode * @property {Function} [labelFn] - function to generate the label for this opcode * @property {string} [label] - the label for this opcode if `labelFn` is absent + * @deprecated */ getLabelForOpcode (extendedOpcode) { const [category, opcode] = StringUtil.splitFirst(extendedOpcode, '_'); @@ -3435,6 +3463,88 @@ class Runtime extends EventEmitter { }; } + /** + * Get the monitor label and color for a given toolbox block id. + * @param {string} id - the block id for a block with a monitor being requested + * @return {object} - object with label and color + * @property {string} color - the color for the monitor for the given block id + * @property {string} label - the label for the monitor for the given block id + */ + getMonitorLabelForBlock (id) { + // Having dynamic extension blocks makes it so that a single extension category can have + // multiple toolbox blocks with the same opcode. This previously wasn't possible. + // This means that we can no longer use just the opcode to look up the block and + // should instead look up the block using its unique id. + const block = this.flyoutBlocks.getBlock(id); + // If the block doesn't actually exist in the flyout, we should not + // be creating a label for it. + if (!block) return; + + // Look up category info from the block + const extendedOpcode = block.opcode; + const [category, opcode] = StringUtil.splitFirst(extendedOpcode, '_'); + if (!(category && opcode)) return; + + const categoryInfo = this._blockInfo.find(ci => ci.id === category); + if (!categoryInfo) return; + + const isDynamicExtensionBlock = + block.mutation && + block.mutation.blockInfo && + block.mutation.blockInfo.isDynamic; + let extensionBlockInfo; + if (isDynamicExtensionBlock) { + extensionBlockInfo = block.mutation.blockInfo; + } else { + const extensionBlockDesc = categoryInfo.blocks.find(b => b.info.opcode === opcode); + extensionBlockInfo = (extensionBlockDesc && extensionBlockDesc.info) || null; + } + if (!extensionBlockInfo) return; + + // TODO get rid of this when we update the core extension name + const temporaryCategory = category === 'data2' ? 'data' : category; + if (this.isCoreExtension(temporaryCategory)) { + return { + color: extensionBlockInfo.color1 || categoryInfo.color1, + label: extensionBlockInfo.text + }; + } + + // TODO: we may want to format the label in a locale-specific way. + return { + category: 'extension', // This assumes that all extensions have the same monitor color. + label: `${categoryInfo.name}: ${extensionBlockInfo.text}` + }; + } + + /** + * Keep track of the fact that a monitor has been requested + * for the given blockId, even though the block with the given id + * does not exist in the flyout yet. + * @param {string} blockId The id of the block with a pending monitor + */ + addPendingMonitor (blockId) { + this._pendingMonitors.add(blockId); + } + + /** + * Removes a block id from the pending monitors list (e.g. if it has + * been turned into an actual monitor). + * @param {string} blockId The id of the block with a pending monitor + */ + removePendingMonitor (blockId) { + this._pendingMonitors.delete(blockId); + } + + /** + * Checks whether the given block id has a pending monitor. + * @param {string} blockId The id of the block with a pending monitor + * @return {boolean} True if the block has a pending monitor, false otherwise. + */ + getPendingMonitor (blockId) { + return this._pendingMonitors.has(blockId); + } + /** * Create a new global variable avoiding conflicts with other variable names. * @param {string} variableName The desired variable name for the new global variable. @@ -3479,6 +3589,15 @@ class Runtime extends EventEmitter { this.emit(Runtime.BLOCKS_NEED_UPDATE); } + /** + * Emit an event that indicates that the given block got updated. + * @param {string} blockId The id of the block + * @param {ExtensionBlockMetadata} blockInfo The new block info + */ + updateBlockInfo (blockId, blockInfo) { + this.emit(Runtime.BLOCK_UPDATE, blockId, blockInfo); + } + /** * Emit an event that indicates that the toolbox extension blocks need updating. */ diff --git a/src/engine/target.js b/src/engine/target.js index e9cecfe0b0d..3f74128c619 100644 --- a/src/engine/target.js +++ b/src/engine/target.js @@ -137,6 +137,23 @@ class Target extends EventEmitter { return newVariable; } + /** + * Look up a variable object by name. + * Create a new variable with a generated ID if lookup fails. + * @param {string} name Name of the variable. + * @param {string} [type] Optional type of the variable (default: scalar). + * @return {!Variable} Variable object. + */ + lookupOrCreateVariableByNameAndType (name, type = Variable.SCALAR_TYPE) { + let variable = this.lookupVariableByNameAndType(name, type); + if (!variable) { + // No variable with this name exists - create it locally. + variable = new Variable(uid(), name, Variable.SCALAR_TYPE, false); + this.variables[variable.id] = variable; + } + return variable; + } + /** * Look up a broadcast message object with the given id and return it * if it exists. diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 754b004e9e9..0b97c625411 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -494,7 +494,7 @@ class ExtensionManager { const menuState = { selectedValue: scratchBlocksMenuObject.getValue(), sourceBlock: scratchBlocksMenuObject.sourceBlock_, - menuObject: scratchBlocksMenuObject, + state: scratchBlocksMenuObject, }; // TODO: Fix this to use dispatch.call when extensions are running in workers. diff --git a/src/util/string-util.js b/src/util/string-util.js index 6fc99530f96..a691531adea 100644 --- a/src/util/string-util.js +++ b/src/util/string-util.js @@ -89,6 +89,21 @@ class StringUtil { } }); } + + // Port over the string comparison utility from scratch-blocks + // ScratchBlocks.scratchBlocksUtils.compareStrings + /** + * Compare strings with natural number sorting. + * @param {string} str1 First input. + * @param {string} str2 Second input. + * @return {number} -1, 0, or 1 to signify greater than, equality, or less than. + */ + static compareStrings (str1, str2) { + return str1.localeCompare(str2, [], { + sensitivity: 'base', + numeric: true + }); + } } module.exports = StringUtil; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index ef280524793..9380755815a 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -144,6 +144,9 @@ class VirtualMachine extends EventEmitter { this.runtime.on(Runtime.BLOCKS_NEED_UPDATE, () => { this.emitWorkspaceUpdate(); }); + this.runtime.on(Runtime.BLOCK_UPDATE, (blockId, blockInfo) => { + this.emit(Runtime.BLOCK_UPDATE, blockId, blockInfo); + }); this.runtime.on(Runtime.TOOLBOX_EXTENSIONS_NEED_UPDATE, () => { this.extensionManager.refreshBlocks(); }); From 76f22f09626e8003a60a4fcff4a409b40057f0b5 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 27 May 2024 05:30:27 +0100 Subject: [PATCH 111/202] Add option to lock lists (#47) * list locking * Update variable.js * allow for locks to be serialized * Update sb3.js --- src/blocks/scratch3_data.js | 6 ++++++ src/compiler/jsgen.js | 25 ++++++++++++++++--------- src/engine/variable.js | 6 ++++-- src/serialization/sb3.js | 12 ++++++++++-- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/blocks/scratch3_data.js b/src/blocks/scratch3_data.js index 1a2e5b86eea..006e2f09bea 100644 --- a/src/blocks/scratch3_data.js +++ b/src/blocks/scratch3_data.js @@ -126,6 +126,8 @@ class Scratch3DataBlocks { addToList (args, util) { const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); + if (list.locked) return; + list.value.push(args.ITEM); list._monitorUpToDate = false; } @@ -133,6 +135,7 @@ class Scratch3DataBlocks { deleteOfList (args, util) { const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); + if (list.locked) return; const index = Cast.toListIndex(args.INDEX, list.value.length, true); if (index === Cast.LIST_INVALID) { return; @@ -147,6 +150,7 @@ class Scratch3DataBlocks { deleteAllOfList (args, util) { const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); + if (list.locked) return; list.value = []; return; } @@ -155,6 +159,7 @@ class Scratch3DataBlocks { const item = args.ITEM; const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); + if (list.locked) return; const index = Cast.toListIndex(args.INDEX, list.value.length + 1, false); if (index === Cast.LIST_INVALID) { return; @@ -167,6 +172,7 @@ class Scratch3DataBlocks { const item = args.ITEM; const list = util.target.lookupOrCreateList( args.LIST.id, args.LIST.name); + if (list.locked) return; const index = Cast.toListIndex(args.INDEX, list.value.length, false); if (index === Cast.LIST_INVALID) { return; diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index fc5e5ab0bb2..9faaae31d48 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -973,9 +973,12 @@ class JSGenerator { this.yielded(); break; + // TODO: Lists that are locked don't need their monitors updated. + // In fact, they don't need to be executed at all, since right + // now, locked is a static value that cannot be changed. case 'list.add': { const list = this.referenceVariable(node.list); - this.source += `${list}.value.push(${this.descendInput(node.item).asSafe()});\n`; + this.source += `if (!${list}.locked) ${list}.value.push(${this.descendInput(node.item).asSafe()});\n`; this.source += `${list}._monitorUpToDate = false;\n`; break; } @@ -984,12 +987,12 @@ class JSGenerator { const index = this.descendInput(node.index); if (index instanceof ConstantInput) { if (index.constantValue === 'last') { - this.source += `${list}.value.pop();\n`; + this.source += `if (!${list}.locked) ${list}.value.pop();\n`; this.source += `${list}._monitorUpToDate = false;\n`; break; } if (+index.constantValue === 1) { - this.source += `${list}.value.shift();\n`; + this.source += `if (!${list}.locked) ${list}.value.shift();\n`; this.source += `${list}._monitorUpToDate = false;\n`; break; } @@ -998,9 +1001,11 @@ class JSGenerator { this.source += `listDelete(${list}, ${index.asUnknown()});\n`; break; } - case 'list.deleteAll': - this.source += `${this.referenceVariable(node.list)}.value = [];\n`; + case 'list.deleteAll': { + const list = this.referenceVariable(node.list); + this.source += `if (!${list}.locked) ${list}.value = [];\n`; break; + } case 'list.hide': this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.list.id)}", element: "checkbox", value: false }, runtime);\n`; break; @@ -1009,16 +1014,18 @@ class JSGenerator { const index = this.descendInput(node.index); const item = this.descendInput(node.item); if (index instanceof ConstantInput && +index.constantValue === 1) { - this.source += `${list}.value.unshift(${item.asSafe()});\n`; + this.source += `if (!${list}.locked) ${list}.value.unshift(${item.asSafe()});\n`; this.source += `${list}._monitorUpToDate = false;\n`; break; } - this.source += `listInsert(${list}, ${index.asUnknown()}, ${item.asSafe()});\n`; + this.source += `if (!${list}.locked) listInsert(${list}, ${index.asUnknown()}, ${item.asSafe()});\n`; break; } - case 'list.replace': - this.source += `listReplace(${this.referenceVariable(node.list)}, ${this.descendInput(node.index).asUnknown()}, ${this.descendInput(node.item).asSafe()});\n`; + case 'list.replace': { + const list = this.referenceVariable(node.list); + this.source += `if (!${list}.locked) listReplace(${list}, ${this.descendInput(node.index).asUnknown()}, ${this.descendInput(node.item).asSafe()});\n`; break; + } case 'list.show': this.source += `runtime.monitorBlocks.changeBlock({ id: "${sanitize(node.list.id)}", element: "checkbox", value: true }, runtime);\n`; break; diff --git a/src/engine/variable.js b/src/engine/variable.js index 2cf45b50379..50b2e347c28 100644 --- a/src/engine/variable.js +++ b/src/engine/variable.js @@ -14,11 +14,12 @@ class Variable { * @param {boolean} isCloud Whether the variable is stored in the cloud. * @constructor */ - constructor (id, name, type, isCloud) { + constructor (id, name, type, isCloud, locked) { this.id = id || uid(); this.name = name; this.type = type; this.isCloud = isCloud; + this.locked = !!locked; switch (this.type) { case Variable.SCALAR_TYPE: this.value = 0; @@ -37,7 +38,8 @@ class Variable { toXML (isLocal) { isLocal = (isLocal === true); return `${xmlEscape(this.name)}`; + }" iscloud="${this.isCloud}" islocked="${this.locked + }">${xmlEscape(this.name)}`; } /** diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 97424915c93..0dcdec568af 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -536,7 +536,12 @@ const serializeVariables = function (variables) { continue; } if (v.type === Variable.LIST_TYPE) { - obj.lists[varId] = [v.name, makeSafeForJSON(v.value)]; + obj.lists[varId] = [ + v.name, + makeSafeForJSON(v.value), + false, + v.locked + ]; continue; } @@ -691,6 +696,7 @@ const serializeMonitors = function (monitors, runtime, extensions) { value: Array.isArray(monitorData.value) ? [] : 0, width: monitorData.width, height: monitorData.height, + locked: monitorData.locked, x: monitorData.x - xOffset, y: monitorData.y - yOffset, visible: monitorData.visible @@ -1249,7 +1255,8 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) { listId, list[0], Variable.LIST_TYPE, - false + false, + list[3], ); newList.value = list[1]; target.variables[newList.id] = newList; @@ -1438,6 +1445,7 @@ const deserializeMonitor = function (monitorData, runtime, targets, extensions) } else if (monitorData.opcode === 'data_listcontents') { const field = monitorBlock.fields.LIST; field.id = monitorData.id; + field.locked = monitorData = monitorData.locked; field.variableType = Variable.LIST_TYPE; } From 57c70b40eb40da2db57896fa92fe9388f2d8543a Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 27 May 2024 08:05:01 +0100 Subject: [PATCH 112/202] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b5286ec8a74..6421a3dd4cf 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "format-message": "6.2.1", "htmlparser2": "3.10.0", "immutable": "3.8.2", - "scratch-parser": "github:TurboWarp/scratch-parser#master", + "scratch-parser": "github:Unsandboxed/scratch-parser#master", "scratch-sb1-converter": "0.2.7", "scratch-translate-extension-languages": "0.0.20191118205314", "text-encoding": "0.7.0", From ab72f6a67c7ef22fce82ec10805b67062d6ad0c4 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 27 May 2024 17:44:13 +0100 Subject: [PATCH 113/202] remove locking from serialization Doing this until I can actually figure out how to get it to save. Might ask cubester or garbo. --- src/serialization/sb3.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 0dcdec568af..821a0f6095b 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -539,8 +539,6 @@ const serializeVariables = function (variables) { obj.lists[varId] = [ v.name, makeSafeForJSON(v.value), - false, - v.locked ]; continue; } @@ -696,7 +694,6 @@ const serializeMonitors = function (monitors, runtime, extensions) { value: Array.isArray(monitorData.value) ? [] : 0, width: monitorData.width, height: monitorData.height, - locked: monitorData.locked, x: monitorData.x - xOffset, y: monitorData.y - yOffset, visible: monitorData.visible @@ -1445,7 +1442,6 @@ const deserializeMonitor = function (monitorData, runtime, targets, extensions) } else if (monitorData.opcode === 'data_listcontents') { const field = monitorBlock.fields.LIST; field.id = monitorData.id; - field.locked = monitorData = monitorData.locked; field.variableType = Variable.LIST_TYPE; } From f947c9fbaeebadd6e9837037b04ca52f1c71bd12 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 27 May 2024 18:00:20 +0100 Subject: [PATCH 114/202] Update sb3.js --- src/serialization/sb3.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 821a0f6095b..8f2d37541a8 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -536,10 +536,7 @@ const serializeVariables = function (variables) { continue; } if (v.type === Variable.LIST_TYPE) { - obj.lists[varId] = [ - v.name, - makeSafeForJSON(v.value), - ]; + obj.lists[varId] = [v.name, makeSafeForJSON(v.value)]; continue; } @@ -1251,9 +1248,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) { const newList = new Variable( listId, list[0], - Variable.LIST_TYPE, - false, - list[3], + Variable.LIST_TYPE ); newList.value = list[1]; target.variables[newList.id] = newList; From f5b66e5b76ad02ad60ff9b06bd89e6476599e2a0 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Mon, 27 May 2024 18:36:52 +0100 Subject: [PATCH 115/202] Fix saving (#48) * Update sb3.js * Update monitor-record.js --- src/engine/monitor-record.js | 1 + src/serialization/sb3.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/engine/monitor-record.js b/src/engine/monitor-record.js index 1259d525290..a117f32bb1d 100644 --- a/src/engine/monitor-record.js +++ b/src/engine/monitor-record.js @@ -17,6 +17,7 @@ const MonitorRecord = Record({ y: null, width: 0, height: 0, + locked: false, visible: true }); diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 8f2d37541a8..c3e7e2e3d3d 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -699,6 +699,7 @@ const serializeMonitors = function (monitors, runtime, extensions) { serializedMonitor.sliderMin = monitorData.sliderMin; serializedMonitor.sliderMax = monitorData.sliderMax; serializedMonitor.isDiscrete = monitorData.isDiscrete; + serializedMonitor.locked = monitorData.locked; } return serializedMonitor; }) From 2c9059f85661eb531c8fd0b24935160011315f4a Mon Sep 17 00:00:00 2001 From: LilyMakesThings Date: Wed, 29 May 2024 04:02:15 +0100 Subject: [PATCH 116/202] rebuild --- scratch-parser | 1 + 1 file changed, 1 insertion(+) create mode 160000 scratch-parser diff --git a/scratch-parser b/scratch-parser new file mode 160000 index 00000000000..a6ba63bcc1f --- /dev/null +++ b/scratch-parser @@ -0,0 +1 @@ +Subproject commit a6ba63bcc1f4194a48d15b430c1e27687cfee456 From 30f937920cc91475e803276636e97ab3f81ead95 Mon Sep 17 00:00:00 2001 From: LilyMakesThings Date: Wed, 29 May 2024 04:03:25 +0100 Subject: [PATCH 117/202] remove submodule --- scratch-parser | 1 - 1 file changed, 1 deletion(-) delete mode 160000 scratch-parser diff --git a/scratch-parser b/scratch-parser deleted file mode 160000 index a6ba63bcc1f..00000000000 --- a/scratch-parser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a6ba63bcc1f4194a48d15b430c1e27687cfee456 From 90241aafdd4ad091e68d6790019ffd72c400baff Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 29 May 2024 08:21:59 +0100 Subject: [PATCH 118/202] something went horribly wrong here --- src/serialization/sb3.js | 48 +++++----------------------------------- 1 file changed, 5 insertions(+), 43 deletions(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index c3e7e2e3d3d..a8d3381bb9b 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -4,7 +4,6 @@ * JSON and then generates all needed scratch-vm runtime structures. */ -const Runtime = require('../engine/runtime'); const Blocks = require('../engine/blocks'); const Sprite = require('../sprites/sprite'); const Variable = require('../engine/variable'); @@ -699,7 +698,6 @@ const serializeMonitors = function (monitors, runtime, extensions) { serializedMonitor.sliderMin = monitorData.sliderMin; serializedMonitor.sliderMax = monitorData.sliderMax; serializedMonitor.isDiscrete = monitorData.isDiscrete; - serializedMonitor.locked = monitorData.locked; } return serializedMonitor; }) @@ -806,9 +804,6 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {}) // TW: Never include full user agent to slightly improve user privacy // if (typeof navigator !== 'undefined') meta.agent = navigator.userAgent; - // TW: Attach copy of platform information - meta.platform = Object.assign({}, runtime.platform); - // Assemble payload and return obj.meta = meta; @@ -1126,7 +1121,7 @@ const parseScratchAssets = function (object, runtime, zip) { // we're always loading the 'sb3' representation of the costume // any translation that needs to happen will happen in the process // of building up the costume object into an sb3 format - return runtime.wrapAssetRequest(() => deserializeCostume(costume, runtime, zip) + return runtime.wrapAssetRequest(deserializeCostume(costume, runtime, zip) .then(() => loadCostume(costumeMd5Ext, costume, runtime))); // Only attempt to load the costume after the deserialization // process has been completed @@ -1151,7 +1146,7 @@ const parseScratchAssets = function (object, runtime, zip) { // we're always loading the 'sb3' representation of the costume // any translation that needs to happen will happen in the process // of building up the costume object into an sb3 format - return runtime.wrapAssetRequest(() => deserializeSound(sound, runtime, zip) + return runtime.wrapAssetRequest(deserializeSound(sound, runtime, zip) .then(() => loadSound(sound, runtime, assets.soundBank))); // Only attempt to load the sound after the deserialization // process has been completed. @@ -1249,7 +1244,8 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) { const newList = new Variable( listId, list[0], - Variable.LIST_TYPE + Variable.LIST_TYPE, + false ); newList.value = list[1]; target.variables[newList.id] = newList; @@ -1482,36 +1478,6 @@ const replaceUnsafeCharsInVariableIds = function (targets) { return targets; }; -/** - * @param {object} json - * @param {Runtime} runtime - * @returns {void|Promise} Resolves when the user has acknowledged any compatibilities, if any exist. - */ -const checkPlatformCompatibility = (json, runtime) => { - if (!json.meta || !json.meta.platform) { - return; - } - - const projectPlatform = json.meta.platform.name; - if (projectPlatform === runtime.platform.name) { - return; - } - - let pending = runtime.listenerCount(Runtime.PLATFORM_MISMATCH); - if (pending === 0) { - return; - } - - return new Promise(resolve => { - runtime.emit(Runtime.PLATFORM_MISMATCH, json.meta.platform, () => { - pending--; - if (pending === 0) { - resolve(); - } - }); - }); -}; - /** * Deserialize the specified representation of a VM runtime and loads it into the provided runtime instance. * @param {object} json - JSON representation of a VM runtime. @@ -1520,9 +1486,7 @@ const checkPlatformCompatibility = (json, runtime) => { * @param {boolean} isSingleSprite - If true treat as single sprite, else treat as whole project * @returns {Promise.} Promise that resolves to the list of targets after the project is deserialized */ -const deserialize = async function (json, runtime, zip, isSingleSprite) { - await checkPlatformCompatibility(json, runtime); - +const deserialize = function (json, runtime, zip, isSingleSprite) { const extensions = { extensionIDs: new Set(), extensionURLs: new Map() @@ -1530,10 +1494,8 @@ const deserialize = async function (json, runtime, zip, isSingleSprite) { // Store the origin field (e.g. project originated at CSFirst) so that we can save it again. if (json.meta && json.meta.origin) { - // eslint-disable-next-line require-atomic-updates runtime.origin = json.meta.origin; } else { - // eslint-disable-next-line require-atomic-updates runtime.origin = null; } From b04cdf958c858c17526207034827cdc1d168e3b8 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 29 May 2024 08:28:39 +0100 Subject: [PATCH 119/202] Update sb3.js --- src/serialization/sb3.js | 47 +++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index a8d3381bb9b..8f2d37541a8 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -4,6 +4,7 @@ * JSON and then generates all needed scratch-vm runtime structures. */ +const Runtime = require('../engine/runtime'); const Blocks = require('../engine/blocks'); const Sprite = require('../sprites/sprite'); const Variable = require('../engine/variable'); @@ -804,6 +805,9 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {}) // TW: Never include full user agent to slightly improve user privacy // if (typeof navigator !== 'undefined') meta.agent = navigator.userAgent; + // TW: Attach copy of platform information + meta.platform = Object.assign({}, runtime.platform); + // Assemble payload and return obj.meta = meta; @@ -1121,7 +1125,7 @@ const parseScratchAssets = function (object, runtime, zip) { // we're always loading the 'sb3' representation of the costume // any translation that needs to happen will happen in the process // of building up the costume object into an sb3 format - return runtime.wrapAssetRequest(deserializeCostume(costume, runtime, zip) + return runtime.wrapAssetRequest(() => deserializeCostume(costume, runtime, zip) .then(() => loadCostume(costumeMd5Ext, costume, runtime))); // Only attempt to load the costume after the deserialization // process has been completed @@ -1146,7 +1150,7 @@ const parseScratchAssets = function (object, runtime, zip) { // we're always loading the 'sb3' representation of the costume // any translation that needs to happen will happen in the process // of building up the costume object into an sb3 format - return runtime.wrapAssetRequest(deserializeSound(sound, runtime, zip) + return runtime.wrapAssetRequest(() => deserializeSound(sound, runtime, zip) .then(() => loadSound(sound, runtime, assets.soundBank))); // Only attempt to load the sound after the deserialization // process has been completed. @@ -1244,8 +1248,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) { const newList = new Variable( listId, list[0], - Variable.LIST_TYPE, - false + Variable.LIST_TYPE ); newList.value = list[1]; target.variables[newList.id] = newList; @@ -1478,6 +1481,36 @@ const replaceUnsafeCharsInVariableIds = function (targets) { return targets; }; +/** + * @param {object} json + * @param {Runtime} runtime + * @returns {void|Promise} Resolves when the user has acknowledged any compatibilities, if any exist. + */ +const checkPlatformCompatibility = (json, runtime) => { + if (!json.meta || !json.meta.platform) { + return; + } + + const projectPlatform = json.meta.platform.name; + if (projectPlatform === runtime.platform.name) { + return; + } + + let pending = runtime.listenerCount(Runtime.PLATFORM_MISMATCH); + if (pending === 0) { + return; + } + + return new Promise(resolve => { + runtime.emit(Runtime.PLATFORM_MISMATCH, json.meta.platform, () => { + pending--; + if (pending === 0) { + resolve(); + } + }); + }); +}; + /** * Deserialize the specified representation of a VM runtime and loads it into the provided runtime instance. * @param {object} json - JSON representation of a VM runtime. @@ -1486,7 +1519,9 @@ const replaceUnsafeCharsInVariableIds = function (targets) { * @param {boolean} isSingleSprite - If true treat as single sprite, else treat as whole project * @returns {Promise.} Promise that resolves to the list of targets after the project is deserialized */ -const deserialize = function (json, runtime, zip, isSingleSprite) { +const deserialize = async function (json, runtime, zip, isSingleSprite) { + await checkPlatformCompatibility(json, runtime); + const extensions = { extensionIDs: new Set(), extensionURLs: new Map() @@ -1494,8 +1529,10 @@ const deserialize = function (json, runtime, zip, isSingleSprite) { // Store the origin field (e.g. project originated at CSFirst) so that we can save it again. if (json.meta && json.meta.origin) { + // eslint-disable-next-line require-atomic-updates runtime.origin = json.meta.origin; } else { + // eslint-disable-next-line require-atomic-updates runtime.origin = null; } From 784c08b1e4033eda89b3795752bfb6ab2f735693 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 29 May 2024 08:29:30 +0100 Subject: [PATCH 120/202] Update tw-platform.js --- src/engine/tw-platform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/engine/tw-platform.js b/src/engine/tw-platform.js index ee6a2e2d627..3ebad74837f 100644 --- a/src/engine/tw-platform.js +++ b/src/engine/tw-platform.js @@ -2,6 +2,6 @@ // This can be accessed externally on `vm.runtime.platform` module.exports = { - name: 'TurboWarp', - url: 'https://turbowarp.org/' + name: 'Unsandboxed', + url: 'https://unsandboxed.org/' }; From da14a00452082e1d433fd01c229423830910912f Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 29 May 2024 08:51:12 +0100 Subject: [PATCH 121/202] Update sb3.js --- src/serialization/sb3.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 8f2d37541a8..1b1309562ff 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -792,14 +792,6 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {}) meta.origin = runtime.origin; } - // USB: A few mods have agreed to list our platform's name inside of the project json. - // We also add a couple more bits specific to Unsandboxed. - const platformMeta = Object.create(null); - platformMeta.name = 'Unsandboxed'; - platformMeta.url = 'https://unsandboxed.org/'; - platformMeta.version = 'alpha'; - meta.platform = platformMeta; - // Attach full user agent string to metadata if available meta.agent = ''; // TW: Never include full user agent to slightly improve user privacy From 17c7a9cd3cef0aef172532e73aff155a7fcbc0b3 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 29 May 2024 09:26:29 +0100 Subject: [PATCH 122/202] random change to start workflow i know this isnt smart --- src/serialization/sb3.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 1b1309562ff..9437b7cd756 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -1351,7 +1351,7 @@ const deserializeMonitor = function (monitorData, runtime, targets, extensions) } // Get information about this monitor, if it exists, given the monitor's opcode. - // This will be undefined for extension blocks + // This will be undefined for extension blocks. const monitorBlockInfo = runtime.monitorBlockInfo[monitorData.opcode]; // Due to a bug (see https://github.com/scratchfoundation/scratch-vm/pull/2322), renamed list monitors may have been serialized From 0ea11652ff4edafed49852ea96e4db2f3dac546e Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 29 May 2024 17:15:08 +0100 Subject: [PATCH 123/202] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6421a3dd4cf..b5286ec8a74 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "format-message": "6.2.1", "htmlparser2": "3.10.0", "immutable": "3.8.2", - "scratch-parser": "github:Unsandboxed/scratch-parser#master", + "scratch-parser": "github:TurboWarp/scratch-parser#master", "scratch-sb1-converter": "0.2.7", "scratch-translate-extension-languages": "0.0.20191118205314", "text-encoding": "0.7.0", From 2a255558f59297ccbce4cb41524681a403184d0d Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 29 May 2024 17:25:21 +0100 Subject: [PATCH 124/202] another ditch attempt --- src/serialization/sb3.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 9437b7cd756..815c2837a4d 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -45,7 +45,6 @@ const INPUT_DIFF_BLOCK_SHADOW = 3; // obscured shadow // Constants used during deserialization of an SB3 file const CORE_EXTENSIONS = [ 'argument', - 'camera', 'colour', 'control', 'data', @@ -56,7 +55,6 @@ const CORE_EXTENSIONS = [ 'operator', 'procedures', 'sensing', - 'string', 'sound' ]; @@ -1240,7 +1238,8 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) { const newList = new Variable( listId, list[0], - Variable.LIST_TYPE + Variable.LIST_TYPE, + false ); newList.value = list[1]; target.variables[newList.id] = newList; @@ -1351,7 +1350,7 @@ const deserializeMonitor = function (monitorData, runtime, targets, extensions) } // Get information about this monitor, if it exists, given the monitor's opcode. - // This will be undefined for extension blocks. + // This will be undefined for extension blocks const monitorBlockInfo = runtime.monitorBlockInfo[monitorData.opcode]; // Due to a bug (see https://github.com/scratchfoundation/scratch-vm/pull/2322), renamed list monitors may have been serialized From 2d88620192ec5763cf43b18534fa1e25d5ca9440 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 29 May 2024 17:26:27 +0100 Subject: [PATCH 125/202] add back camera and string to core exts --- src/serialization/sb3.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 815c2837a4d..5b334ddee11 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -45,6 +45,7 @@ const INPUT_DIFF_BLOCK_SHADOW = 3; // obscured shadow // Constants used during deserialization of an SB3 file const CORE_EXTENSIONS = [ 'argument', + 'camera', 'colour', 'control', 'data', @@ -55,7 +56,8 @@ const CORE_EXTENSIONS = [ 'operator', 'procedures', 'sensing', - 'sound' + 'sound', + 'string' ]; // Constants referring to 'primitive' blocks that are usually shadows, From 72fe25ffd1556851af6cde542c4e13794d3f2e83 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 29 May 2024 17:28:52 +0100 Subject: [PATCH 126/202] remove locked state from xml --- src/engine/variable.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/engine/variable.js b/src/engine/variable.js index 50b2e347c28..a3d6478ddca 100644 --- a/src/engine/variable.js +++ b/src/engine/variable.js @@ -38,8 +38,7 @@ class Variable { toXML (isLocal) { isLocal = (isLocal === true); return `${xmlEscape(this.name)}`; + }" iscloud="${this.isCloud}">${xmlEscape(this.name)}`; } /** From 039c4e05f28bee4cbb155bc18a5aacb73b0275c2 Mon Sep 17 00:00:00 2001 From: LilyMakesThings <127533508+LilyMakesThings@users.noreply.github.com> Date: Wed, 5 Jun 2024 07:34:41 +0100 Subject: [PATCH 127/202] export value type in visual report (#49) --- 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 709b5e722c3..4cbc6c5a4c4 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -3237,7 +3237,7 @@ class Runtime extends EventEmitter { * @param {string} value Value to show associated with the block. */ visualReport (blockId, value) { - this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: String(value)}); + this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: String(value), type: typeof value}); } /** From 73bef55e0142a02d885e6ad4144c9718de266572 Mon Sep 17 00:00:00 2001 From: Ashime Sho <135030944+Ashimee@users.noreply.github.com> Date: Mon, 17 Jun 2024 09:43:06 -0400 Subject: [PATCH 128/202] Shut up eslint (#50) * Fix ESLINT +1 * Fix ESLINT +2 --------- Co-authored-by: Ashime --- src/blocks/scratch3_camera.js | 18 ++++++++---------- src/blocks/scratch3_event.js | 2 +- src/blocks/scratch3_string.js | 1 - src/compiler/irgen.js | 6 +++++- src/compiler/jsgen.js | 1 + src/engine/blocks.js | 18 +++++++++--------- src/engine/execute.js | 6 +++--- src/engine/runtime.js | 2 +- src/extension-support/extension-manager.js | 2 +- src/extensions/tw/index.js | 2 +- 10 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/blocks/scratch3_camera.js b/src/blocks/scratch3_camera.js index 29fe16def24..399ff987528 100644 --- a/src/blocks/scratch3_camera.js +++ b/src/blocks/scratch3_camera.js @@ -1,6 +1,4 @@ const Cast = require('../util/cast'); -const MathUtil = require('../util/math-util'); -const Timer = require('../util/timer'); class Scratch3CameraBlocks { constructor (runtime) { @@ -39,13 +37,13 @@ class Scratch3CameraBlocks { }; } - moveToXY (args, util) { + moveToXY (args) { const x = Cast.toNumber(args.X); const y = Cast.toNumber(args.Y); this.runtime.camera.setXY(x, y); } - changeByXY (args, util) { + changeByXY (args) { const x = Cast.toNumber(args.X); const y = Cast.toNumber(args.Y); const newX = x + this.runtime.camera.x; @@ -53,33 +51,33 @@ class Scratch3CameraBlocks { this.runtime.camera.setXY(newX, newY); } - setX (args, util) { + setX (args) { const x = Cast.toNumber(args.X); this.runtime.camera.setXY(x, this.runtime.camera.y); } - changeX (args, util) { + changeX (args) { const x = Cast.toNumber(args.X); const newX = x + this.runtime.camera.x; this.runtime.camera.setXY(newX, this.runtime.camera.y); } - setY (args, util) { + setY (args) { const y = Cast.toNumber(args.Y); this.runtime.camera.setXY(this.runtime.camera.x, y); } - changeY (args, util) { + changeY (args) { const y = Cast.toNumber(args.Y); const newY = y + this.runtime.camera.y; this.runtime.camera.setXY(this.runtime.camera.x, newY); } - getCameraX (args, util) { + getCameraX () { return this.runtime.camera.x; } - getCameraY (args, util) { + getCameraY () { return this.runtime.camera.y; } } diff --git a/src/blocks/scratch3_event.js b/src/blocks/scratch3_event.js index cf598829f43..5bfad8217f7 100644 --- a/src/blocks/scratch3_event.js +++ b/src/blocks/scratch3_event.js @@ -67,7 +67,7 @@ class Scratch3EventBlocks { }; } - when (args, util) { + when (args) { const condition = Cast.toBoolean(args.CONDITION); return condition; } diff --git a/src/blocks/scratch3_string.js b/src/blocks/scratch3_string.js index 783da2218f7..ed8a32ac67a 100644 --- a/src/blocks/scratch3_string.js +++ b/src/blocks/scratch3_string.js @@ -1,5 +1,4 @@ const Cast = require('../util/cast.js'); -const MathUtil = require('../util/math-util.js'); class Scratch3StringBlocks { constructor (runtime) { diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index bfe9bb0b006..2ea67ff216f 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -757,7 +757,11 @@ class ScriptTreeGenerator { const blockInfo = this.getBlockInfo(block.opcode); if (blockInfo) { const type = blockInfo.info.blockType; - if (type === BlockType.ARRAY || type === BlockType.OBJECT || type === BlockType.REPORTER || type === BlockType.BOOLEAN || type === BlockType.INLINE) { + if ( + type === BlockType.ARRAY || type === BlockType.OBJECT || + type === BlockType.REPORTER || type === BlockType.BOOLEAN || + type === BlockType.INLINE + ) { return this.descendCompatLayer(block); } } diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 9faaae31d48..5460a7f60eb 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -925,6 +925,7 @@ class JSGenerator { break; case 'control.allAtOnce': + // eslint-disable-next-line no-case-declarations const previousWarp = this.isWarp; this.isWarp = true; this.descendStack(node.code, new Frame(false, 'control.allAtOnce')); diff --git a/src/engine/blocks.js b/src/engine/blocks.js index e5b9410a113..bc58e876b8e 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -711,14 +711,7 @@ class Blocks { // Update block value if (!block.fields[args.name]) return; - if (typeof block.fields[args.name].variableType !== 'undefined') { - // Get variable name using the id in args.value. - const variable = this.runtime.getEditingTarget().lookupVariableById(args.value); - if (variable) { - block.fields[args.name].value = variable.name; - block.fields[args.name].id = args.value; - } - } else { + if (typeof block.fields[args.name].variableType === 'undefined') { // Changing the value in a dropdown block.fields[args.name].value = args.value; @@ -741,6 +734,13 @@ class Blocks { params: this._getBlockParams(flyoutBlock) })); } + } else { + // Get variable name using the id in args.value. + const variable = this.runtime.getEditingTarget().lookupVariableById(args.value); + if (variable) { + block.fields[args.name].value = variable.name; + block.fields[args.name].id = args.value; + } } break; case 'mutation': @@ -979,7 +979,7 @@ class Blocks { const variableBlocks = []; for (const blockId in this._blocks) { - if (!this._blocks.hasOwnProperty(blockId)) continue; + if (!Object.prototype.hasOwnProperty.call(this._blocks, blockId)) continue; const block = this._blocks[blockId]; // Check for blocks with fields referencing variable/list, otherwise variable/list reporters if (block.fields[fieldName] && diff --git a/src/engine/execute.js b/src/engine/execute.js index fc22789ae4e..1c9e7e72a00 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -313,13 +313,13 @@ class BlockCached { // Store the static fields onto _argValues. for (const fieldName in fields) { - if (typeof fields[fieldName].variableType !== 'undefined') { + if (typeof fields[fieldName].variableType === 'undefined') { + this._argValues[fieldName] = fields[fieldName].value; + } else { this._argValues[fieldName] = { id: fields[fieldName].id, name: fields[fieldName].value }; - } else { - this._argValues[fieldName] = fields[fieldName].value; } } diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 4cbc6c5a4c4..bdd21174011 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1566,7 +1566,7 @@ class Runtime extends EventEmitter { const mutation = blockInfo.isDynamic ? `` : ''; const inputs = context.inputList.join(''); const toolboxIdXml = blockInfo.isDynamic && blockInfo.paletteKey ? - ` id="${xmlEscape(blockInfo.paletteKey)}"` : ''; + ` id="${xmlEscape(blockInfo.paletteKey)}"` : ''; const blockXML = `${mutation}${inputs}`; if (blockInfo.extensions) { diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index cb1b9200f8b..236c41a3b32 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -494,7 +494,7 @@ class ExtensionManager { const menuState = { selectedValue: scratchBlocksMenuObject.getValue(), sourceBlock: scratchBlocksMenuObject.sourceBlock_, - state: scratchBlocksMenuObject, + state: scratchBlocksMenuObject }; // TODO: Fix this to use dispatch.call when extensions are running in workers. diff --git a/src/extensions/tw/index.js b/src/extensions/tw/index.js index 465c1b60347..97e3e11c8db 100644 --- a/src/extensions/tw/index.js +++ b/src/extensions/tw/index.js @@ -3,7 +3,7 @@ const BlockType = require('../../extension-support/block-type'); const ArgumentType = require('../../extension-support/argument-type'); const Cast = require('../../util/cast'); -// eslint-disable-next-line max-len +// eslint-disable-next-line const iconURI = `data:image/svg+xml;base64,${btoa('')}`; /** From d73611ce395b9e4e7efaed2cde81ded417e3a648 Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 19:35:26 -0400 Subject: [PATCH 129/202] Actually delete args.stringNumber --- src/compiler/jsgen.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 1ea900f81b5..40ab5631788 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -433,8 +433,6 @@ class JSGenerator { case 'addons.call': return new TypedInput(`(${this.descendAddonCall(node)})`, TYPE_UNKNOWN); - case 'args.stringNumber': - return new TypedInput(`p${node.index}`, TYPE_UNKNOWN); case 'args.parameter': return new TypedInput(`(thread.getParam("${node.name}") ?? 0)`, TYPE_UNKNOWN); From 75ced47ff982b201201c25509964d7498adf0d90 Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 20:10:42 -0400 Subject: [PATCH 130/202] iwnafhwtb for runtime --- src/engine/runtime.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index ef8e490e8f2..55b6b50a4ac 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -584,6 +584,19 @@ class Runtime extends EventEmitter { * Total number of finished or errored scratch-storage load() requests since the runtime was created or cleared. */ this.finishedAssetRequests = 0; + + /** + * Export some internal values for extensions. + */ + this.exports = { + i_will_not_ask_for_help_when_these_break: () => { + console.warn('You are using unsupported APIs. WHEN your code breaks, do not expect help.'); + return ({ + ArgumentTypeMap, + FieldTypeMap + }); + } + }; } /** From 97689b9e78b02bd419a24f96f66ab65c51b2d957 Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 20:16:53 -0400 Subject: [PATCH 131/202] Block shape support --- src/engine/runtime.js | 4 +++ src/extension-support/block-shape.js | 25 +++++++++++++++++++ .../tw-extension-api-common.js | 2 ++ 3 files changed, 31 insertions(+) create mode 100644 src/extension-support/block-shape.js diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 55b6b50a4ac..36da7c55145 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1520,6 +1520,10 @@ class Runtime extends EventEmitter { break; } + if (blockInfo.blockShape ?? blockInfo.outputShape) { + blockJSON.outputShape = blockInfo.blockShape ?? blockInfo.outputShape; + } + const blockText = Array.isArray(blockInfo.text) ? blockInfo.text : [blockInfo.text]; let inTextNum = 0; // text for the next block "arm" is blockText[inTextNum] let inBranchNum = 0; // how many branches have we placed into the JSON so far? diff --git a/src/extension-support/block-shape.js b/src/extension-support/block-shape.js new file mode 100644 index 00000000000..854975ada88 --- /dev/null +++ b/src/extension-support/block-shape.js @@ -0,0 +1,25 @@ +// Use the constants instead of manually redefining them again +const ScratchBlocksConstants = require('../engine/scratch-blocks-constants'); + +/** + * Types of block shapes + * @enum {number} + */ +const BlockShape = { + /** + * Output shape: hexagonal (booleans/predicates). + */ + HEXAGONAL: ScratchBlocksConstants.OUTPUT_SHAPE_HEXAGONAL, + + /** + * Output shape: rounded (numbers). + */ + ROUND: ScratchBlocksConstants.OUTPUT_SHAPE_ROUND, + + /** + * Output shape: squared (any/all values; strings). + */ + SQUARE: ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE +}; + +module.exports = BlockShape; diff --git a/src/extension-support/tw-extension-api-common.js b/src/extension-support/tw-extension-api-common.js index a0e49dc3f93..40d5669e99d 100644 --- a/src/extension-support/tw-extension-api-common.js +++ b/src/extension-support/tw-extension-api-common.js @@ -1,5 +1,6 @@ const ArgumentType = require('./argument-type'); const BlockType = require('./block-type'); +const BlockShape = require('./block-shape'); const TargetType = require('./target-type'); const ContextMenuContext = require('./context-menu-context'); const Cast = require('../util/cast'); @@ -8,6 +9,7 @@ const Util = require('../util/usb-util'); const Scratch = { ArgumentType, BlockType, + BlockShape, TargetType, ContextMenuContext, Cast, From abe5f464a37f397bc738fbcf46400262aa2172e1 Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 20:19:07 -0400 Subject: [PATCH 132/202] Export ScratchBlocksConstants --- src/engine/runtime.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 36da7c55145..6fe9986e997 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -592,6 +592,7 @@ class Runtime extends EventEmitter { i_will_not_ask_for_help_when_these_break: () => { console.warn('You are using unsupported APIs. WHEN your code breaks, do not expect help.'); return ({ + ScratchBlocksConstants, ArgumentTypeMap, FieldTypeMap }); From ba574fa031c48ab2bb2e10ae20b0c1907f768b91 Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 20:25:50 -0400 Subject: [PATCH 133/202] Mutator and output options --- src/engine/runtime.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 6fe9986e997..1ba86028510 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1525,6 +1525,15 @@ class Runtime extends EventEmitter { blockJSON.outputShape = blockInfo.blockShape ?? blockInfo.outputShape; } + if (Object.prototype.hasOwnProperty.call(blockInfo, 'output')) { + blockJSON.output = blockInfo.output; + } + + if (blockInfo.mutator) { + blockJSON.mutator = blockInfo.mutator; + } + + const blockText = Array.isArray(blockInfo.text) ? blockInfo.text : [blockInfo.text]; let inTextNum = 0; // text for the next block "arm" is blockText[inTextNum] let inBranchNum = 0; // how many branches have we placed into the JSON so far? From 5a9b186858727869e10bc8f09949227523a6cb0b Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 20:28:13 -0400 Subject: [PATCH 134/202] Inline support in the compiler --- src/compiler/jsgen.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 40ab5631788..f13be803c1b 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -437,6 +437,26 @@ class JSGenerator { return new TypedInput(`(thread.getParam("${node.name}") ?? 0)`, TYPE_UNKNOWN); case 'compat': + if (node.blockType === BlockType.INLINE) { + const branchVariable = this.localVariables.next(); + const returnVariable = this.localVariables.next(); + let source = '(yield* (function*() {\n'; + source += `let ${returnVariable} = undefined;\n`; + source += `const ${branchVariable} = createBranchInfo(false);\n`; + source += `${returnVariable} = (${this.generateCompatibilityLayerCall(node, false, branchVariable)});\n`; + source += `${branchVariable}.branch = globalState.blockUtility._startedBranch[0];\n`; + source += `switch (${branchVariable}.branch) {\n`; + for (const index in node.substacks) { + source += `case ${+index}: {\n`; + source += this.descendStackForSource(node.substacks[index], new Frame(false)); + source += `break;\n`; + source += `}\n`; // close case + } + source += '}\n'; // close switch + source += `return ${returnVariable};\n`; + source += '})())'; // close function and yield + return new TypedInput(source, TYPE_UNKNOWN); + } // Compatibility layer inputs never use flags. return new TypedInput(`(${this.generateCompatibilityLayerCall(node, false)})`, TYPE_UNKNOWN); @@ -1270,6 +1290,16 @@ class JSGenerator { this.popFrame(); } + descendStackForSource (nodes, frame) { + // Wrapper for descendStack to get the source + const oldSource = this.source; + this.source = ''; + this.descendStack(nodes, frame); + const stackSource = this.source; + this.source = oldSource; + return stackSource; + } + descendVariable (variable) { if (Object.prototype.hasOwnProperty.call(this.variableInputs, variable.id)) { return this.variableInputs[variable.id]; From a0d43b054f047e5d8a5a9e91c08ddd45c7f1272e Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 20:35:18 -0400 Subject: [PATCH 135/202] branch on end support (compiler part) --- src/compiler/compat-block-utility.js | 6 ++++-- src/compiler/jsexecute.js | 5 +++-- src/compiler/jsgen.js | 2 ++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/compiler/compat-block-utility.js b/src/compiler/compat-block-utility.js index 6fedba1d9d2..6b9033a2f46 100644 --- a/src/compiler/compat-block-utility.js +++ b/src/compiler/compat-block-utility.js @@ -10,7 +10,8 @@ class CompatibilityLayerBlockUtility extends BlockUtility { return this.thread.compatibilityStackFrame; } - startBranch (branchNumber, isLoop) { + startBranch (branchNumber, isLoop, onEnd) { + if (this._branchInfo && onEnd) this._branchInfo.onEnd.push(onEnd); this._startedBranch = [branchNumber, isLoop]; } @@ -29,10 +30,11 @@ class CompatibilityLayerBlockUtility extends BlockUtility { throw new Error('getParam is not supported by this BlockUtility'); } - init (thread, fakeBlockId, stackFrame) { + init (thread, fakeBlockId, stackFrame, branchInfo) { this.thread = thread; this.sequencer = thread.target.runtime.sequencer; this._startedBranch = null; + this._branchInfo = branchInfo; thread.stack[0] = fakeBlockId; thread.compatibilityStackFrame = stackFrame; } diff --git a/src/compiler/jsexecute.js b/src/compiler/jsexecute.js index d1122e458c2..2e38ed10c7d 100644 --- a/src/compiler/jsexecute.js +++ b/src/compiler/jsexecute.js @@ -152,7 +152,7 @@ const executeInCompatibilityLayer = function*(inputs, blockFunction, isWarp, use }; const executeBlock = () => { - blockUtility.init(thread, blockId, stackFrame); + blockUtility.init(thread, blockId, stackFrame, branchInfo); return blockFunction(inputs, blockUtility); }; @@ -207,7 +207,8 @@ runtimeFunctions.createBranchInfo = `const createBranchInfo = (isLoop) => ({ defaultIsLoop: isLoop, isLoop: false, branch: 0, - stackFrame: {} + stackFrame: {}, + onEnd: [] });`; /** diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index f13be803c1b..a741a89ddf8 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -453,6 +453,7 @@ class JSGenerator { source += `}\n`; // close case } source += '}\n'; // close switch + source += `if (${branchVariable}.onEnd[0]) yield ${branchVariable}.onEnd.shift()(${branchVariable});\n`; source += `return ${returnVariable};\n`; source += '})())'; // close function and yield return new TypedInput(source, TYPE_UNKNOWN); @@ -846,6 +847,7 @@ class JSGenerator { this.source += `}\n`; // close case } this.source += '}\n'; // close switch + this.source += `if (${branchVariable}.onEnd[0]) yield ${branchVariable}.onEnd.shift()(${branchVariable});\n`; this.source += `if (!${branchVariable}.isLoop) break;\n`; this.yieldLoop(); this.source += '}\n'; // close while From 56a43adf68241e39079ff0b7d58d09e04afc3031 Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 20:37:17 -0400 Subject: [PATCH 136/202] USB object and array shape support --- src/extension-support/block-shape.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/extension-support/block-shape.js b/src/extension-support/block-shape.js index 854975ada88..3cd0218521c 100644 --- a/src/extension-support/block-shape.js +++ b/src/extension-support/block-shape.js @@ -19,7 +19,17 @@ const BlockShape = { /** * Output shape: squared (any/all values; strings). */ - SQUARE: ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE + SQUARE: ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE, + + /** + * Output shape: array (this is just squared as of now) + */ + ARRAY: ScratchBlocksConstants.OUTPUT_SHAPE_ARRAY, + + /** + * Output shape: object (objects) + */ + OBJECT: ScratchBlockConstants.OUTPUT_SHAPE_OBJECT, }; module.exports = BlockShape; From c63ff1f9088681cd6ba455973dabaf303d9ef9b5 Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 20:46:45 -0400 Subject: [PATCH 137/202] ESLint --- src/engine/runtime.js | 6 +++--- src/extension-support/block-shape.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 1ba86028510..10b2b5c8e9c 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1522,15 +1522,15 @@ class Runtime extends EventEmitter { } if (blockInfo.blockShape ?? blockInfo.outputShape) { - blockJSON.outputShape = blockInfo.blockShape ?? blockInfo.outputShape; + blockJSON.outputShape = blockInfo.blockShape ?? blockInfo.outputShape; } if (Object.prototype.hasOwnProperty.call(blockInfo, 'output')) { - blockJSON.output = blockInfo.output; + blockJSON.output = blockInfo.output; } if (blockInfo.mutator) { - blockJSON.mutator = blockInfo.mutator; + blockJSON.mutator = blockInfo.mutator; } diff --git a/src/extension-support/block-shape.js b/src/extension-support/block-shape.js index 3cd0218521c..6a8eba3316d 100644 --- a/src/extension-support/block-shape.js +++ b/src/extension-support/block-shape.js @@ -29,7 +29,7 @@ const BlockShape = { /** * Output shape: object (objects) */ - OBJECT: ScratchBlockConstants.OUTPUT_SHAPE_OBJECT, + OBJECT: ScratchBlocksConstants.OUTPUT_SHAPE_OBJECT }; module.exports = BlockShape; From c05f1c79e0e8745371c658c9accd2c61fdf68941 Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Thu, 19 Sep 2024 20:54:22 -0400 Subject: [PATCH 138/202] Fix ESLint (final?) --- src/compiler/jsgen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 04058941787..ebe30076ff2 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -626,8 +626,8 @@ class JSGenerator { const right = this.descendInput(node.right); return new TypedInput(`(${left.asUnknown()} <= ${right.asUnknown()})`, TYPE_BOOLEAN); } - //case 'op.letterOf': - // return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); + // case 'op.letterOf': + // return new TypedInput(`((${this.descendInput(node.string).asString()})[(${this.descendInput(node.letter).asNumber()} | 0) - 1] || "")`, TYPE_STRING); case 'op.lettersOf': return new TypedInput(`((${this.descendInput(node.string).asString()}).substring(${this.descendInput(node.left).asNumber() - 1}, ${this.descendInput(node.right).asNumber()}) || "")`, TYPE_STRING); case 'op.ln': From 7038579ca076640155334f196f44264d793442eb Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Fri, 20 Sep 2024 13:22:15 -0400 Subject: [PATCH 139/202] Fix locked state not saving (2/2) --- package-lock.json | 5 ++--- package.json | 2 +- src/serialization/sb3.js | 7 +++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb08394a736..a48e799f0cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "format-message": "6.2.1", "htmlparser2": "3.10.0", "immutable": "3.8.2", - "scratch-parser": "github:TurboWarp/scratch-parser#master", + "scratch-parser": "github:Unsandboxed/scratch-parser", "scratch-sb1-converter": "0.2.7", "scratch-translate-extension-languages": "0.0.20191118205314", "text-encoding": "0.7.0", @@ -13379,8 +13379,7 @@ }, "node_modules/scratch-parser": { "version": "0.0.0-development", - "resolved": "git+ssh://git@github.com/TurboWarp/scratch-parser.git#9911fe8e6b5aed34ea19aff5490c3948d1523fc5", - "license": "MPL-2.0", + "resolved": "git+ssh://git@github.com/Unsandboxed/scratch-parser.git#b8228911219887a25f8154081f2dec24c3664ba4", "dependencies": { "@turbowarp/json": "^0.1.1", "@turbowarp/jszip": "^3.11.0", diff --git a/package.json b/package.json index b5286ec8a74..2db368f77f1 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "format-message": "6.2.1", "htmlparser2": "3.10.0", "immutable": "3.8.2", - "scratch-parser": "github:TurboWarp/scratch-parser#master", + "scratch-parser": "github:Unsandboxed/scratch-parser", "scratch-sb1-converter": "0.2.7", "scratch-translate-extension-languages": "0.0.20191118205314", "text-encoding": "0.7.0", diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 5b334ddee11..217822c6235 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -536,7 +536,7 @@ const serializeVariables = function (variables) { continue; } if (v.type === Variable.LIST_TYPE) { - obj.lists[varId] = [v.name, makeSafeForJSON(v.value)]; + obj.lists[varId] = [v.name, makeSafeForJSON(v.value), v.locked]; continue; } @@ -695,7 +695,9 @@ const serializeMonitors = function (monitors, runtime, extensions) { y: monitorData.y - yOffset, visible: monitorData.visible }; - if (monitorData.mode !== 'list') { + if (monitorData.mode === 'list') { + serializedMonitor.locked = monitorData.locked; + } else { serializedMonitor.sliderMin = monitorData.sliderMin; serializedMonitor.sliderMax = monitorData.sliderMax; serializedMonitor.isDiscrete = monitorData.isDiscrete; @@ -1244,6 +1246,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) { false ); newList.value = list[1]; + newList.locked = list[2] || false; target.variables[newList.id] = newList; } } From b94433c4d3de5c49f9789a39b2aae91fe402c0ee Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Fri, 20 Sep 2024 14:08:41 -0400 Subject: [PATCH 140/202] Tooltips and warnings (VM part) --- src/engine/runtime.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 10b2b5c8e9c..fa9ce2ed525 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1533,6 +1533,13 @@ class Runtime extends EventEmitter { blockJSON.mutator = blockInfo.mutator; } + if (Object.prototype.hasOwnProperty.call(blockInfo, 'tooltip')) { + blockJSON.tooltip = blockInfo.tooltip; + } + + if (Object.prototype.hasOwnProperty.call(blockInfo, 'warning')) { + blockJSON.warning = blockInfo.warning; + } const blockText = Array.isArray(blockInfo.text) ? blockInfo.text : [blockInfo.text]; let inTextNum = 0; // text for the next block "arm" is blockText[inTextNum] From baae53527d4bff9bd55ce1ef271197b29c42c109 Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Fri, 20 Sep 2024 14:13:53 -0400 Subject: [PATCH 141/202] Export extensions to VM automatically --- src/extension-support/extension-manager.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 236c41a3b32..b9a651615be 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -236,6 +236,7 @@ class ExtensionManager { dispatch.setServiceSync(serviceName, extensionObject); dispatch.callSync('extensions', 'registerExtensionServiceSync', serviceName); this._loadedExtensions.set(extensionInfo.id, serviceName); + this.runtime[`cext_${extensionInfo.id}`] = extensionObject; } this._finishedLoadingExtensionScript(); From c9e38ce09e6dc140d637a4f2473dc938fa4fbda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=82=88?= <135030944+yuri-kiss@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:55:32 -0400 Subject: [PATCH 142/202] Don't Cast objects to string (temporary fix) --- src/engine/runtime.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index fa9ce2ed525..c283ec32170 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -3272,7 +3272,9 @@ class Runtime extends EventEmitter { * @param {string} value Value to show associated with the block. */ visualReport (blockId, value) { - this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: String(value), type: typeof value}); + this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: (( + typeof value === 'object' + ) ? value : String(value)), type: typeof value}); } /** From 0e0f3442fc2b43647ec59af7a8266ea251f1303b Mon Sep 17 00:00:00 2001 From: Ashime Sho Date: Fri, 20 Sep 2024 19:49:11 -0400 Subject: [PATCH 143/202] bro actually commited wrong branch push --- src/engine/runtime.js | 44 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index c283ec32170..afa84130147 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -863,13 +863,21 @@ class Runtime extends EventEmitter { } /** - * Event name for reporting that an extension as asked for a custom field to be added + * Event name for reporting that an extension has asked for a custom field to be added * @const {string} */ static get EXTENSION_FIELD_ADDED () { return 'EXTENSION_FIELD_ADDED'; } + /** + * Event name for reporting that an extension has asked for a custom shape type to be added + * @const {string} + */ + static get EXTENSION_SHAPE_ADDED () { + return 'EXTENSION_SHAPE_TYPE_ADDED'; + } + /** * Event name for updating the available set of peripheral devices. * This causes the peripheral connection modal to update a list of @@ -1155,6 +1163,17 @@ class Runtime extends EventEmitter { } } + for (const blockShapeName in categoryInfo.customShapes) { + if (Object.prototype.hasOwnProperty.call(extensionInfo.customShapes, blockShapeName)) { + const blockShapeInfo = categoryInfo.customShapes[blockShapeName]; + + // Emit events for custom shape types from extension + this.emit(Runtime.EXTENSION_SHAPE_ADDED, { + implementation: blockShapeInfo, + }); + } + } + this.emit(Runtime.EXTENSION_ADDED, categoryInfo); } @@ -1183,6 +1202,7 @@ class Runtime extends EventEmitter { _fillExtensionCategory (categoryInfo, extensionInfo) { categoryInfo.blocks = []; categoryInfo.customFieldTypes = {}; + categoryInfo.customShapes = {}; categoryInfo.menus = []; categoryInfo.menuInfo = {}; categoryInfo.convertedMenuInfo = {}; @@ -1215,6 +1235,12 @@ class Runtime extends EventEmitter { categoryInfo.customFieldTypes[fieldTypeName] = fieldTypeInfo; } } + for (const blockShapeName in extensionInfo.customShapes) { + if (Object.prototype.hasOwnProperty.call(extensionInfo.customShapes, blockShapeName)) { + const shapeType = extensionInfo.customShapes[blockShapeName]; + categoryInfo.customShapes[blockShapeName] = shapeType; + } + } if (extensionInfo.docsURI) { const xml = '