From 7589acf2f7b452b7ba3e0b5e3e7ad29a9000586d Mon Sep 17 00:00:00 2001 From: FurryR Date: Wed, 24 Sep 2025 13:44:39 +0800 Subject: [PATCH 1/2] Frameloop nextgen update (with lint) Signed-off-by: FurryR --- src/blocks/scratch3_json.js | 16 +- src/blocks/scratch3_operators.js | 2 +- src/blocks/scratch3_sensing.js | 1 + src/compiler/irgen.js | 20 +- src/compiler/jsexecute.js | 2 +- src/compiler/jsgen.js | 2 + src/engine/blocks.js | 6 +- src/engine/runtime.js | 810 ++++++++++++++------- src/engine/sequencer.js | 85 ++- src/engine/tw-frame-loop.js | 193 ++++- src/extension-support/extension-manager.js | 4 +- 11 files changed, 793 insertions(+), 348 deletions(-) diff --git a/src/blocks/scratch3_json.js b/src/blocks/scratch3_json.js index 786f78a1708..901dc62bfc4 100644 --- a/src/blocks/scratch3_json.js +++ b/src/blocks/scratch3_json.js @@ -65,7 +65,7 @@ class Scratch3JSONBlocks { valueOfKey (args) { args.OBJ = Cast.toObject(args.OBJ); args.KEY = Cast.toString(args.KEY); - return args.OBJ[args.KEY] ?? ""; + return args.OBJ[args.KEY] ?? ''; } setKey (args) { @@ -106,12 +106,12 @@ class Scratch3JSONBlocks { valueOfIndex (args) { args.ARR = Cast.toArray(args.ARR); args.INDEX = Cast.toNumber(args.INDEX); - return args.ARR[args.INDEX] ?? ""; + return args.ARR[args.INDEX] ?? ''; } indexOfValue (args) { args.ARR = Cast.toArray(args.ARR); - return args.ARR.indexOf(args.VALUE) !== -1 ? args.ARR.indexOf(args.VALUE) : ""; + return args.ARR.indexOf(args.VALUE) !== -1 ? args.ARR.indexOf(args.VALUE) : ''; } addItem (args) { @@ -126,9 +126,9 @@ class Scratch3JSONBlocks { if (args.INDEX >= 0 && args.INDEX < args.ARR.length) { args.ARR[args.INDEX] = args.ITEM; return args.ARR; - } else { - return new Array(); } + return new Array(); + } deleteIndex (args) { @@ -137,15 +137,15 @@ class Scratch3JSONBlocks { if (args.INDEX >= 0 && args.INDEX < args.ARR.length) { args.ARR.splice(args.INDEX, 1); return args.ARR; - } else { - return new Array(); } + return new Array(); + } deleteAllOccurrences (args) { args.ARR = Cast.toArray(args.ARR); args.ITEM = Cast.toString(args.ITEM); - return args.ARR.filter((item) => item !== args.ITEM); + return args.ARR.filter(item => item !== args.ITEM); } mergeArray (args) { diff --git a/src/blocks/scratch3_operators.js b/src/blocks/scratch3_operators.js index 4a86e4738b9..addd9226e50 100644 --- a/src/blocks/scratch3_operators.js +++ b/src/blocks/scratch3_operators.js @@ -39,7 +39,7 @@ class Scratch3OperatorsBlocks { } checkbox () { - return true; + return true; } add (args) { diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index 15c071cb7f5..936c9d3f95c 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 'refreshTime': return this.runtime.screenRefreshTime / 1000; } return 0; } diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index c2724f4685d..1e1387160a4 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -728,6 +728,10 @@ class ScriptTreeGenerator { return { kind: 'sensing.second' }; + case 'refreshTime': + return { + kind: 'sensing.refreshTime' + }; } return { kind: 'constant', @@ -796,25 +800,25 @@ class ScriptTreeGenerator { return { kind: 'comments.reporter', value: this.descendInputOfBlock(block, 'VALUE'), - comment: this.descendInputOfBlock(block, 'COMMENT'), + comment: this.descendInputOfBlock(block, 'COMMENT') }; case 'comments_boolean': return { kind: 'comments.boolean', value: this.descendInputOfBlock(block, 'VALUE'), - comment: this.descendInputOfBlock(block, 'COMMENT'), + comment: this.descendInputOfBlock(block, 'COMMENT') }; case 'comments_object': return { kind: 'comments.object', value: this.descendInputOfBlock(block, 'VALUE'), - comment: this.descendInputOfBlock(block, 'COMMENT'), + comment: this.descendInputOfBlock(block, 'COMMENT') }; case 'comments_array': return { kind: 'comments.array', value: this.descendInputOfBlock(block, 'VALUE'), - comment: this.descendInputOfBlock(block, 'COMMENT'), + comment: this.descendInputOfBlock(block, 'COMMENT') }; case 'tw_getLastKeyPressed': @@ -834,10 +838,10 @@ class ScriptTreeGenerator { if (blockInfo) { const type = blockInfo.info.blockType; if ( - type === BlockType.REPORTER - || type === BlockType.BOOLEAN - || type === BlockType.OBJECT - || type === BlockType.ARRAY + type === BlockType.REPORTER || + type === BlockType.BOOLEAN || + type === BlockType.OBJECT || + type === BlockType.ARRAY ) { return this.descendCompatLayer(block); } diff --git a/src/compiler/jsexecute.js b/src/compiler/jsexecute.js index 9a7aa1b81e7..5be58b5952c 100644 --- a/src/compiler/jsexecute.js +++ b/src/compiler/jsexecute.js @@ -2,7 +2,7 @@ * @fileoverview Runtime for scripts generated by jsgen */ -const { supportsNullishCoalescing: canNullCoalsh } = require('./environment'); +const {supportsNullishCoalescing: canNullCoalsh} = require('./environment'); /* eslint-disable no-unused-vars */ /* eslint-disable prefer-template */ diff --git a/src/compiler/jsgen.js b/src/compiler/jsgen.js index 86e37cab0b6..735be962ee6 100644 --- a/src/compiler/jsgen.js +++ b/src/compiler/jsgen.js @@ -778,6 +778,8 @@ class JSGenerator { return new TypedInput(`(new Date().getMinutes())`, TYPE_NUMBER); case 'sensing.month': return new TypedInput(`(new Date().getMonth() + 1)`, TYPE_NUMBER); + case 'sensing.refreshTime': + return new TypedInput(`(runtime.screenRefreshTime / 1000)`, TYPE_NUMBER_NAN); case 'sensing.of': { const object = this.descendInput(node.object).asString(); const property = node.property; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 6a4eb41e574..ee358763802 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -919,9 +919,9 @@ class Blocks { if (preserve) { const parent = this._blocks[block.parent]; const next = this._blocks[block.next]; - const input = parent?.inputs - ? Object.values(parent.inputs).find(input => input.block === blockId) - : null; + const input = parent?.inputs ? + Object.values(parent.inputs).find(input => input.block === blockId) : + null; if (parent && !input) { parent.next = block.next; } diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 0525e44ef3b..6c9e48fcb02 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -202,12 +202,6 @@ let stepProfilerId = -1; */ let stepThreadsProfilerId = -1; -/** - * Numeric ID for RenderWebGL.draw in Profiler instances. - * @type {number} - */ -let rendererDrawProfilerId = -1; - /** * Manages targets, scripts, and the sequencer. * @constructor @@ -305,7 +299,7 @@ class Runtime extends EventEmitter { * by behaviour in Sequencer.stepThreads. * @type {Array} */ - this._lastStepDoneThreads = null; + this._lastStepDoneThreads = []; /** * Currently known number of clones, used to enforce clone limit. @@ -423,7 +417,8 @@ class Runtime extends EventEmitter { * A function which returns the number of cloud variables in the runtime. * @returns {number} */ - this.getNumberOfCloudVariables = newCloudDataManager.getNumberOfCloudVariables; + this.getNumberOfCloudVariables = + newCloudDataManager.getNumberOfCloudVariables; /** * A function that tracks a new cloud variable in the runtime, @@ -432,7 +427,8 @@ class Runtime extends EventEmitter { * being added. * @type {function} */ - this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager); + this.addCloudVariable = + this._initializeAddCloudVariable(newCloudDataManager); /** * A function which updates the runtime's cloud variable limit @@ -440,7 +436,8 @@ class Runtime extends EventEmitter { * if the last of the cloud variables is being removed. * @type {function} */ - this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager); + this.removeCloudVariable = + this._initializeRemoveCloudVariable(newCloudDataManager); /** * A string representing the origin of the current project from outside of the @@ -478,7 +475,12 @@ class Runtime extends EventEmitter { this.debug = false; - this._lastStepTime = Date.now(); + this.screenRefreshTime = 0; + + Object.defineProperty(this, '_lastStepTime', { + get: () => this.frameLoop._lastStepTime + }); + this.interpolationEnabled = false; this._defaultStoredSettings = this._generateAllProjectOptions(); @@ -965,24 +967,24 @@ class Runtime extends EventEmitter { // Helper function for initializing the addCloudVariable function _initializeAddCloudVariable (newCloudDataManager) { // The addCloudVariable function - return (() => { + return () => { const hadCloudVarsBefore = this.hasCloudData(); newCloudDataManager.addCloudVariable(); if (!hadCloudVarsBefore && this.hasCloudData()) { this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, true); } - }); + }; } // Helper function for initializing the removeCloudVariable function _initializeRemoveCloudVariable (newCloudDataManager) { - return (() => { + return () => { const hadCloudVarsBefore = this.hasCloudData(); newCloudDataManager.removeCloudVariable(); if (hadCloudVarsBefore && !this.hasCloudData()) { this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, false); } - }); + }; } /** @@ -992,14 +994,26 @@ class Runtime extends EventEmitter { */ _registerBlockPackages () { for (const packageName in defaultBlockPackages) { - if (Object.prototype.hasOwnProperty.call(defaultBlockPackages, packageName)) { + if ( + Object.prototype.hasOwnProperty.call( + defaultBlockPackages, + packageName + ) + ) { // @todo pass a different runtime depending on package privilege? - const packageObject = new (defaultBlockPackages[packageName])(this); + const packageObject = new defaultBlockPackages[packageName]( + this + ); // Collect primitives from package. if (packageObject.getPrimitives) { const packagePrimitives = packageObject.getPrimitives(); for (const op in packagePrimitives) { - if (Object.prototype.hasOwnProperty.call(packagePrimitives, op)) { + if ( + Object.prototype.hasOwnProperty.call( + packagePrimitives, + op + ) + ) { this._primitives[op] = packagePrimitives[op].bind(packageObject); } @@ -1009,14 +1023,23 @@ class Runtime extends EventEmitter { if (packageObject.getHats) { const packageHats = packageObject.getHats(); for (const hatName in packageHats) { - if (Object.prototype.hasOwnProperty.call(packageHats, hatName)) { + if ( + Object.prototype.hasOwnProperty.call( + packageHats, + hatName + ) + ) { this._hats[hatName] = packageHats[hatName]; } } } // Collect monitored from package. if (packageObject.getMonitored) { - this.monitorBlockInfo = Object.assign({}, this.monitorBlockInfo, packageObject.getMonitored()); + this.monitorBlockInfo = Object.assign( + {}, + this.monitorBlockInfo, + packageObject.getMonitored() + ); } this.compilerRegisterExtension(packageName, packageObject); @@ -1052,7 +1075,9 @@ class Runtime extends EventEmitter { const context = {}; target = target || this.getEditingTarget() || this.getTargetForStage(); if (target) { - context.targetType = (target.isStage ? TargetType.STAGE : TargetType.SPRITE); + context.targetType = target.isStage ? + TargetType.STAGE : + TargetType.SPRITE; } } @@ -1085,8 +1110,14 @@ class Runtime extends EventEmitter { this._fillExtensionCategory(categoryInfo, extensionInfo); for (const fieldTypeName in categoryInfo.customFieldTypes) { - if (Object.prototype.hasOwnProperty.call(extensionInfo.customFieldTypes, fieldTypeName)) { - const fieldTypeInfo = categoryInfo.customFieldTypes[fieldTypeName]; + if ( + Object.prototype.hasOwnProperty.call( + extensionInfo.customFieldTypes, + fieldTypeName + ) + ) { + const fieldTypeInfo = + categoryInfo.customFieldTypes[fieldTypeName]; // Emit events for custom field types from extension this.emit(Runtime.EXTENSION_FIELD_ADDED, { @@ -1112,7 +1143,11 @@ class Runtime extends EventEmitter { this._blockInfo.push(undefined); } } - this._blockInfo.splice(reorderIndex, 0, this._blockInfo.splice(extensionIndex, 1)[0]); + this._blockInfo.splice( + reorderIndex, + 0, + this._blockInfo.splice(extensionIndex, 1)[0] + ); this.emit(Runtime.EXTENSION_REORDERED); } @@ -1122,7 +1157,9 @@ class Runtime extends EventEmitter { * @private */ _removeExtensionPrimitive (extensionId) { - const extensionIndex = this._blockInfo.findIndex(extension => extension.id === extensionId); + const extensionIndex = this._blockInfo.findIndex( + extension => extension.id === extensionId + ); const info = this._blockInfo[extensionIndex]; this._blockInfo.splice(extensionIndex, 1); this.emit(Runtime.EXTENSION_REMOVED); @@ -1144,7 +1181,9 @@ class Runtime extends EventEmitter { * @private */ _refreshExtensionPrimitives (extensionInfo) { - const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id); + const categoryInfo = this._blockInfo.find( + info => info.id === extensionInfo.id + ); if (categoryInfo) { categoryInfo.name = maybeFormatMessage(extensionInfo.name); this._fillExtensionCategory(categoryInfo, extensionInfo); @@ -1167,15 +1206,29 @@ class Runtime extends EventEmitter { categoryInfo.menuInfo = {}; for (const menuName in extensionInfo.menus) { - if (Object.prototype.hasOwnProperty.call(extensionInfo.menus, menuName)) { + if ( + Object.prototype.hasOwnProperty.call( + extensionInfo.menus, + menuName + ) + ) { const menuInfo = extensionInfo.menus[menuName]; - const convertedMenu = this._buildMenuForScratchBlocks(menuName, menuInfo, categoryInfo); + const convertedMenu = this._buildMenuForScratchBlocks( + menuName, + menuInfo, + categoryInfo + ); categoryInfo.menus.push(convertedMenu); categoryInfo.menuInfo[menuName] = menuInfo; } } for (const fieldTypeName in extensionInfo.customFieldTypes) { - if (Object.prototype.hasOwnProperty.call(extensionInfo.customFieldTypes, fieldTypeName)) { + if ( + Object.prototype.hasOwnProperty.call( + extensionInfo.customFieldTypes, + fieldTypeName + ) + ) { const fieldType = extensionInfo.customFieldTypes[fieldTypeName]; const fieldTypeInfo = this._buildCustomFieldInfo( fieldTypeName, @@ -1189,12 +1242,16 @@ class Runtime extends EventEmitter { } if (extensionInfo.docsURI) { - const xml = '`; const block = { @@ -1206,17 +1263,24 @@ class Runtime extends EventEmitter { for (const blockInfo of extensionInfo.blocks) { try { - const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo); + const convertedBlock = this._convertForScratchBlocks( + blockInfo, + categoryInfo + ); categoryInfo.blocks.push(convertedBlock); if (convertedBlock.json) { const opcode = convertedBlock.json.type; if (blockInfo.blockType !== BlockType.EVENT) { this._primitives[opcode] = convertedBlock.info.func; } - if (blockInfo.blockType === BlockType.EVENT || blockInfo.blockType === BlockType.HAT) { + if ( + blockInfo.blockType === BlockType.EVENT || + blockInfo.blockType === BlockType.HAT + ) { this._hats[opcode] = { edgeActivated: blockInfo.isEdgeActivated, - restartExistingThreads: blockInfo.shouldRestartExistingThreads + restartExistingThreads: + blockInfo.shouldRestartExistingThreads }; } else if (blockInfo.blockType === BlockType.CONDITIONAL) { this._flowing[opcode] = { @@ -1231,7 +1295,10 @@ class Runtime extends EventEmitter { } } } catch (e) { - log.error('Error parsing block: ', {block: blockInfo, error: e}); + log.error('Error parsing block: ', { + block: blockInfo, + error: e + }); } } } @@ -1247,14 +1314,25 @@ class Runtime extends EventEmitter { if (typeof menuItems !== 'function') { const extensionMessageContext = this.makeMessageContextForTarget(); return menuItems.map(item => { - const formattedItem = maybeFormatMessage(item, extensionMessageContext); + const formattedItem = maybeFormatMessage( + item, + extensionMessageContext + ); switch (typeof formattedItem) { case 'string': return [formattedItem, formattedItem]; case 'object': - return [maybeFormatMessage(item.text, extensionMessageContext), item.value]; + return [ + maybeFormatMessage( + item.text, + extensionMessageContext + ), + item.value + ]; default: - throw new Error(`Can't interpret menu item: ${JSON.stringify(item)}`); + throw new Error( + `Can't interpret menu item: ${JSON.stringify(item)}` + ); } }); } @@ -1284,7 +1362,8 @@ class Runtime extends EventEmitter { colourSecondary: categoryInfo.color2, colourTertiary: categoryInfo.color3, outputShape: menuInfo.acceptReporters ? - ScratchBlocksConstants.OUTPUT_SHAPE_ROUND : ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE, + ScratchBlocksConstants.OUTPUT_SHAPE_ROUND : + ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE, args0: [ { type: 'field_dropdown', @@ -1326,7 +1405,12 @@ class Runtime extends EventEmitter { * @param {object} categoryInfo - The category the field belongs to (Used to set its colors) * @returns {object} - Object to be inserted into scratch-blocks */ - _buildCustomFieldTypeForScratchBlocks (fieldName, output, outputShape, categoryInfo) { + _buildCustomFieldTypeForScratchBlocks ( + fieldName, + output, + outputShape, + categoryInfo + ) { return { json: { type: fieldName, @@ -1435,41 +1519,50 @@ class Runtime extends EventEmitter { const separatorJSON = { type: 'field_vertical_separator' }; - blockJSON.args0 = [ - iconJSON, - separatorJSON - ]; + blockJSON.args0 = [iconJSON, separatorJSON]; } switch (blockInfo.blockType) { case BlockType.COMMAND: - blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; + blockJSON.outputShape = + ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; blockJSON.previousStatement = null; // null = available connection; undefined = hat if (!blockInfo.isTerminal) { blockJSON.nextStatement = null; // null = available connection; undefined = terminal } break; case BlockType.REPORTER: - blockJSON.output = blockInfo.allowDropAnywhere ? null : 'String'; // TODO: distinguish number & string here? - blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_ROUND; + blockJSON.output = blockInfo.allowDropAnywhere ? + null : + 'String'; // TODO: distinguish number & string here? + blockJSON.outputShape = + ScratchBlocksConstants.OUTPUT_SHAPE_ROUND; break; case BlockType.BOOLEAN: blockJSON.output = 'Boolean'; - blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_HEXAGONAL; + blockJSON.outputShape = + ScratchBlocksConstants.OUTPUT_SHAPE_HEXAGONAL; break; case BlockType.HAT: case BlockType.EVENT: - if (!Object.prototype.hasOwnProperty.call(blockInfo, 'isEdgeActivated')) { + if ( + !Object.prototype.hasOwnProperty.call( + blockInfo, + 'isEdgeActivated' + ) + ) { // if absent, this property defaults to true blockInfo.isEdgeActivated = true; } - blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; + blockJSON.outputShape = + ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; blockJSON.nextStatement = null; // null = available connection; undefined = terminal break; case BlockType.CONDITIONAL: case BlockType.LOOP: blockInfo.branchCount = blockInfo.branchCount || 1; - blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; + blockJSON.outputShape = + ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; blockJSON.previousStatement = null; // null = available connection; undefined = hat if (!blockInfo.isTerminal) { blockJSON.nextStatement = null; // null = available connection; undefined = terminal @@ -1477,11 +1570,13 @@ class Runtime extends EventEmitter { break; case BlockType.OBJECT: blockJSON.output = 'Object'; - blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_OBJECT; + blockJSON.outputShape = + ScratchBlocksConstants.OUTPUT_SHAPE_OBJECT; break; case BlockType.ARRAY: blockJSON.output = 'Array'; - blockJSON.outputShape = ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; + blockJSON.outputShape = + ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE; break; } @@ -1490,19 +1585,33 @@ class Runtime extends EventEmitter { blockJSON.outputShape = blockInfo.blockShape; } - const blockText = Array.isArray(blockInfo.text) ? blockInfo.text : [blockInfo.text]; + 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? let outLineNum = 0; // used for scratch-blocks `message${outLineNum}` and `args${outLineNum}` - const convertPlaceholders = this._convertPlaceholders.bind(this, context); + const convertPlaceholders = this._convertPlaceholders.bind( + this, + context + ); const extensionMessageContext = this.makeMessageContextForTarget(); // alternate between a block "arm" with text on it and an open slot for a substack - while (inTextNum < blockText.length || inBranchNum < blockInfo.branchCount) { + while ( + inTextNum < blockText.length || + inBranchNum < blockInfo.branchCount + ) { if (inTextNum < blockText.length) { context.outLineNum = outLineNum; - const lineText = maybeFormatMessage(blockText[inTextNum], extensionMessageContext); - const convertedText = lineText.replace(/\[(.+?)]/g, convertPlaceholders); + const lineText = maybeFormatMessage( + blockText[inTextNum], + extensionMessageContext + ); + const convertedText = lineText.replace( + /\[(.+?)]/g, + convertPlaceholders + ); if (blockJSON[`message${outLineNum}`]) { blockJSON[`message${outLineNum}`] += convertedText; } else { @@ -1513,42 +1622,57 @@ class Runtime extends EventEmitter { } if (inBranchNum < blockInfo.branchCount) { blockJSON[`message${outLineNum}`] = '%1'; - blockJSON[`args${outLineNum}`] = [{ - type: 'input_statement', - name: `SUBSTACK${inBranchNum > 0 ? inBranchNum + 1 : ''}` - }]; + blockJSON[`args${outLineNum}`] = [ + { + type: 'input_statement', + name: `SUBSTACK${ + inBranchNum > 0 ? inBranchNum + 1 : '' + }` + } + ]; ++inBranchNum; ++outLineNum; } } - if (blockInfo.blockType === BlockType.REPORTER || blockInfo.blockType === BlockType.BOOLEAN) { + if ( + blockInfo.blockType === BlockType.REPORTER || + blockInfo.blockType === BlockType.BOOLEAN + ) { if (!blockInfo.disableMonitor && context.inputList.length === 0) { blockJSON.checkboxInFlyout = true; } } else if ( - blockInfo.branchIconURI || ( - blockInfo.blockType === BlockType.LOOP && - !Object.prototype.hasOwnProperty.call(blockInfo, 'branchIconURI') - ) + blockInfo.branchIconURI || + (blockInfo.blockType === BlockType.LOOP && + !Object.prototype.hasOwnProperty.call( + blockInfo, + 'branchIconURI' + )) ) { // Add icon to the bottom right of a loop block blockJSON[`lastDummyAlign${outLineNum}`] = 'RIGHT'; blockJSON[`message${outLineNum}`] = '%1'; - blockJSON[`args${outLineNum}`] = [{ - type: 'field_image', - src: blockInfo.branchIconURI ?? 'media://repeat.svg', - width: 24, - height: 24, - alt: '*', // TODO remove this since we don't use collapsed blocks in scratch - flip_rtl: true - }]; + blockJSON[`args${outLineNum}`] = [ + { + type: 'field_image', + src: blockInfo.branchIconURI ?? 'media://repeat.svg', + width: 24, + height: 24, + alt: '*', // TODO remove this since we don't use collapsed blocks in scratch + flip_rtl: true + } + ]; ++outLineNum; } - const mutation = blockInfo.isDynamic ? `` : ''; + const mutation = blockInfo.isDynamic ? + `` : + ''; const inputs = context.inputList.join(''); - const blockXML = `${mutation}${inputs}`; + const blockXML = `${mutation}${inputs}`; if (blockInfo.extensions) { for (const extension of blockInfo.extensions) { @@ -1591,7 +1715,7 @@ class Runtime extends EventEmitter { xml: `` }; } - + /** * Convert a button for scratch-blocks. A button has no opcode but specifies a callback name in the `func` field. * @param {ExtensionBlockMetadata} buttonInfo - the button to convert @@ -1602,12 +1726,21 @@ class Runtime extends EventEmitter { */ _convertButtonForScratchBlocks (buttonInfo, categoryInfo) { const extensionMessageContext = this.makeMessageContextForTarget(); - const buttonText = maybeFormatMessage(buttonInfo.text, extensionMessageContext); - const nativeCallbackKeys = ['MAKE_A_LIST', 'MAKE_A_PROCEDURE', 'MAKE_A_VARIABLE']; + const buttonText = maybeFormatMessage( + buttonInfo.text, + extensionMessageContext + ); + const nativeCallbackKeys = [ + 'MAKE_A_LIST', + 'MAKE_A_PROCEDURE', + 'MAKE_A_VARIABLE' + ]; if (nativeCallbackKeys.includes(buttonInfo.func)) { return { info: buttonInfo, - xml: `` + xml: `` }; } // Callbacks with data will be forwarded from GUI @@ -1616,7 +1749,8 @@ class Runtime extends EventEmitter { this.extensionButtons.set(id, buttonInfo.callFunc); return { info: buttonInfo, - xml: `` }; @@ -1642,7 +1776,9 @@ class Runtime extends EventEmitter { */ _constructInlineImageJson (argInfo) { if (!argInfo.dataURI) { - log.warn('Missing data URI in extension block with argument type IMAGE'); + log.warn( + 'Missing data URI in extension block with argument type IMAGE' + ); } return { type: 'field_image', @@ -1672,8 +1808,13 @@ class Runtime extends EventEmitter { let argTypeInfo = ArgumentTypeMap[argInfo.type] || {}; // Field type not a standard field type, see if extension has registered custom field type - if (!ArgumentTypeMap[argInfo.type] && context.categoryInfo.customFieldTypes[argInfo.type]) { - argTypeInfo = context.categoryInfo.customFieldTypes[argInfo.type].argumentTypeInfo; + if ( + !ArgumentTypeMap[argInfo.type] && + context.categoryInfo.customFieldTypes[argInfo.type] + ) { + argTypeInfo = + context.categoryInfo.customFieldTypes[argInfo.type] + .argumentTypeInfo; } // Start to construct the scratch-blocks style JSON defining how the block should be @@ -1694,8 +1835,12 @@ class Runtime extends EventEmitter { }; const defaultValue = - typeof argInfo.defaultValue === 'undefined' ? null : - maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString(); + typeof argInfo.defaultValue === 'undefined' ? + null : + maybeFormatMessage( + argInfo.defaultValue, + this.makeMessageContextForTarget() + ).toString(); if (argTypeInfo.check) { // Right now the only type of 'check' we have specifies that the @@ -1711,7 +1856,10 @@ class Runtime extends EventEmitter { const menuInfo = context.categoryInfo.menuInfo[argInfo.menu]; if (menuInfo.acceptReporters) { valueName = placeholder; - shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id); + shadowType = this._makeExtensionMenuId( + argInfo.menu, + context.categoryInfo.id + ); fieldName = argInfo.menu; } else { argJSON.type = 'field_dropdown'; @@ -1722,25 +1870,36 @@ class Runtime extends EventEmitter { } } else { valueName = placeholder; - shadowType = (argTypeInfo.shadow && argTypeInfo.shadow.type) || null; - fieldName = (argTypeInfo.shadow && argTypeInfo.shadow.fieldName) || null; + shadowType = + (argTypeInfo.shadow && argTypeInfo.shadow.type) || null; + fieldName = + (argTypeInfo.shadow && argTypeInfo.shadow.fieldName) || + null; } // is the ScratchBlocks name for a block input. if (valueName) { - context.inputList.push(``); + context.inputList.push( + `` + ); } // The is a placeholder for a reporter and is visible when there's no reporter in this input. // Boolean inputs don't need to specify a shadow in the XML. if (shadowType) { - context.inputList.push(``); + context.inputList.push( + `` + ); } // A displays a dynamic value: a user-editable text field, a drop-down menu, etc. // Leave out the field if defaultValue or fieldName are not specified if (defaultValue !== null && fieldName) { - context.inputList.push(`${xmlEscape(defaultValue)}`); + context.inputList.push( + `${xmlEscape( + defaultValue + )}` + ); } if (shadowType) { @@ -1753,7 +1912,8 @@ class Runtime extends EventEmitter { } const argsName = `args${context.outLineNum}`; - const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []); + const blockArgs = (context.blockJSON[argsName] = + context.blockJSON[argsName] || []); if (argJSON) blockArgs.push(argJSON); const argNum = blockArgs.length; context.argsMap[placeholder] = argNum; @@ -1785,7 +1945,9 @@ class Runtime extends EventEmitter { return blockFilterIncludesTarget && !block.info.hideFromPalette; }); - const colorXML = `colour="${xmlEscape(color1)}" secondaryColour="${xmlEscape(color2)}"`; + const colorXML = `colour="${xmlEscape( + color1 + )}" secondaryColour="${xmlEscape(color2)}"`; // Use a menu icon if there is one. Otherwise, use the block icon. If there's no icon, // the category menu will show its default colored circle. @@ -1796,7 +1958,8 @@ class Runtime extends EventEmitter { menuIconURI = categoryInfo.blockIconURI; } const menuIconXML = menuIconURI ? - `iconURI="${xmlEscape(menuIconURI)}"` : ''; + `iconURI="${xmlEscape(menuIconURI)}"` : + ''; let statusButtonXML = ''; if (categoryInfo.showStatusButton) { @@ -1823,7 +1986,12 @@ class Runtime extends EventEmitter { */ getBlocksJSON () { return this._blockInfo.reduce( - (result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []); + (result, categoryInfo) => + result.concat( + categoryInfo.blocks.map(blockInfo => blockInfo.json) + ), + [] + ); } /** @@ -1832,7 +2000,8 @@ class Runtime extends EventEmitter { _initScratchLink () { // Check that we're actually in a real browser, not Node.js or JSDOM, and we have a valid-looking origin. // note that `if (self?....)` will throw if `self` is undefined, so check for that first! - if (typeof self !== 'undefined' && + if ( + typeof self !== 'undefined' && typeof document !== 'undefined' && document.getElementById && self.origin && @@ -1845,7 +2014,9 @@ class Runtime extends EventEmitter { ) ) { // Create a script tag for the Scratch Link browser extension, unless one already exists - const scriptElement = document.getElementById('scratch-link-extension-script'); + const scriptElement = document.getElementById( + 'scratch-link-extension-script' + ); if (!scriptElement) { const script = document.createElement('script'); script.id = 'scratch-link-extension-script'; @@ -1864,7 +2035,8 @@ class Runtime extends EventEmitter { * @returns {ScratchLinkSocket} The scratch link socket. */ getScratchLinkSocket (type) { - const factory = this._linkSocketFactory || this._defaultScratchLinkSocketFactory; + const factory = + this._linkSocketFactory || this._defaultScratchLinkSocketFactory; return factory(type); } @@ -1884,10 +2056,15 @@ class Runtime extends EventEmitter { */ _defaultScratchLinkSocketFactory (type) { const Scratch = self.Scratch; - const ScratchLinkSafariSocket = Scratch && Scratch.ScratchLinkSafariSocket; + const ScratchLinkSafariSocket = + Scratch && Scratch.ScratchLinkSafariSocket; // detect this every time in case the user turns on the extension after loading the page - const useSafariSocket = ScratchLinkSafariSocket && ScratchLinkSafariSocket.isSafariHelperCompatible(); - return useSafariSocket ? new ScratchLinkSafariSocket(type) : new ScratchLinkWebSocket(type); + const useSafariSocket = + ScratchLinkSafariSocket && + ScratchLinkSafariSocket.isSafariHelperCompatible(); + return useSafariSocket ? + new ScratchLinkSafariSocket(type) : + new ScratchLinkWebSocket(type); } /** @@ -1976,11 +2153,12 @@ class Runtime extends EventEmitter { * @return {boolean} True if the op is known to be a edge-activated hat. */ getIsEdgeActivatedHat (opcode) { - return Object.prototype.hasOwnProperty.call(this._hats, opcode) && - this._hats[opcode].edgeActivated; + return ( + Object.prototype.hasOwnProperty.call(this._hats, opcode) && + this._hats[opcode].edgeActivated + ); } - /** * Attach the audio engine * @param {!AudioEngine} audioEngine The audio engine to attach @@ -2022,7 +2200,13 @@ class Runtime extends EventEmitter { const originalCreateAsset = storage.createAsset; let assetIdCounter = 0; // eslint-disable-next-line no-unused-vars - storage.createAsset = function packagedCreateAsset (assetType, dataFormat, data, assetId, generateId) { + storage.createAsset = function packagedCreateAsset ( + assetType, + dataFormat, + data, + assetId, + generateId + ) { if (!assetId) { assetId = (++assetIdCounter).toString(); } @@ -2129,10 +2313,10 @@ class Runtime extends EventEmitter { */ isActiveThread (thread) { return ( - ( - thread.stack.length > 0 && - thread.status !== Thread.STATUS_DONE) && - this.threads.indexOf(thread) > -1); + thread.stack.length > 0 && + thread.status !== Thread.STATUS_DONE && + this.threads.indexOf(thread) > -1 + ); } /** @@ -2157,18 +2341,32 @@ class Runtime extends EventEmitter { * determines whether we show a visual report when turning on the script. */ toggleScript (topBlockId, opts) { - opts = Object.assign({ - target: this._editingTarget, - stackClick: false - }, opts); + if (opts?.stackClick && !this.frameLoop.running) { + this.frameLoop.start(); + } + opts = Object.assign( + { + target: this._editingTarget, + stackClick: false + }, + opts + ); // Remove any existing thread. for (let i = 0; i < this.threads.length; i++) { // Toggling a script that's already running turns it off - if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE) { + if ( + this.threads[i].topBlock === topBlockId && + this.threads[i].status !== Thread.STATUS_DONE + ) { const blockContainer = opts.target.blocks; - const opcode = blockContainer.getOpcode(blockContainer.getBlock(topBlockId)); + const opcode = blockContainer.getOpcode( + blockContainer.getBlock(topBlockId) + ); - if (this.getIsEdgeActivatedHat(opcode) && this.threads[i].stackClick !== opts.stackClick) { + if ( + this.getIsEdgeActivatedHat(opcode) && + this.threads[i].stackClick !== opts.stackClick + ) { // Allow edge activated hat thread stack click to coexist with // edge activated hat thread that runs every frame continue; @@ -2190,8 +2388,11 @@ class Runtime extends EventEmitter { if (!optTarget) optTarget = this._editingTarget; for (let i = 0; i < this.threads.length; i++) { // Don't re-add the script if it's already running - if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE && - this.threads[i].updateMonitor) { + if ( + this.threads[i].topBlock === topBlockId && + this.threads[i].status !== Thread.STATUS_DONE && + this.threads[i].updateMonitor + ) { return; } } @@ -2229,7 +2430,10 @@ class Runtime extends EventEmitter { } for (let t = targets.length - 1; t >= 0; t--) { const target = targets[t]; - const scripts = BlocksRuntimeCache.getScripts(target.blocks, opcode); + const scripts = BlocksRuntimeCache.getScripts( + target.blocks, + opcode + ); for (let j = 0; j < scripts.length; j++) { f(scripts[j], target); } @@ -2243,9 +2447,13 @@ class Runtime extends EventEmitter { * @param {Target=} optTarget Optionally, a target to restrict to. * @return {Array.} List of threads started by this function. */ - startHats (requestedHatOpcode, - optMatchFields, optTarget) { - if (!Object.prototype.hasOwnProperty.call(this._hats, requestedHatOpcode)) { + startHats (requestedHatOpcode, optMatchFields, optTarget) { + if ( + !Object.prototype.hasOwnProperty.call( + this._hats, + requestedHatOpcode + ) + ) { // No known hat with this opcode. return; } @@ -2255,7 +2463,9 @@ class Runtime extends EventEmitter { const hatMeta = instance._hats[requestedHatOpcode]; for (const opts in optMatchFields) { - if (!Object.prototype.hasOwnProperty.call(optMatchFields, opts)) continue; + if (!Object.prototype.hasOwnProperty.call(optMatchFields, opts)) { + continue; + } optMatchFields[opts] = optMatchFields[opts].toUpperCase(); } @@ -2264,49 +2474,58 @@ class Runtime extends EventEmitter { const startingThreadListLength = this.threads.length; // Consider all scripts, looking for hats with opcode `requestedHatOpcode`. - this.allScriptsByOpcodeDo(requestedHatOpcode, (script, target) => { - const { - blockId: topBlockId, - fieldsOfInputs: hatFields - } = script; - - // Match any requested fields. - // For example: ensures that broadcasts match. - // This needs to happen before the block is evaluated - // (i.e., before the predicate can be run) because "broadcast and wait" - // needs to have a precise collection of started threads. - for (const matchField in optMatchFields) { - if (hatFields[matchField].value !== optMatchFields[matchField]) { - // Field mismatch. - return; + this.allScriptsByOpcodeDo( + requestedHatOpcode, + (script, target) => { + const {blockId: topBlockId, fieldsOfInputs: hatFields} = + script; + + // Match any requested fields. + // For example: ensures that broadcasts match. + // This needs to happen before the block is evaluated + // (i.e., before the predicate can be run) because "broadcast and wait" + // needs to have a precise collection of started threads. + for (const matchField in optMatchFields) { + if ( + hatFields[matchField].value !== + optMatchFields[matchField] + ) { + // Field mismatch. + return; + } } - } - if (hatMeta.restartExistingThreads) { - // If `restartExistingThreads` is true, we should stop - // any existing threads starting with the top block. - const existingThread = this.threadMap.get(Thread.getIdFromTargetAndBlock(target, topBlockId)); - if (existingThread) { - newThreads.push(this._restartThread(existingThread)); - return; - } - } else { - // If `restartExistingThreads` is false, we should - // give up if any threads with the top block are running. - for (let j = 0; j < startingThreadListLength; j++) { - if (this.threads[j].target === target && - this.threads[j].topBlock === topBlockId && - // stack click threads and hat threads can coexist - !this.threads[j].stackClick && - this.threads[j].status !== Thread.STATUS_DONE) { - // Some thread is already running. + if (hatMeta.restartExistingThreads) { + // If `restartExistingThreads` is true, we should stop + // any existing threads starting with the top block. + const existingThread = this.threadMap.get( + Thread.getIdFromTargetAndBlock(target, topBlockId) + ); + if (existingThread) { + newThreads.push(this._restartThread(existingThread)); return; } + } else { + // If `restartExistingThreads` is false, we should + // give up if any threads with the top block are running. + for (let j = 0; j < startingThreadListLength; j++) { + if ( + this.threads[j].target === target && + this.threads[j].topBlock === topBlockId && + // stack click threads and hat threads can coexist + !this.threads[j].stackClick && + this.threads[j].status !== Thread.STATUS_DONE + ) { + // Some thread is already running. + return; + } + } } - } - // Start the thread with this top block. - newThreads.push(this._pushThread(topBlockId, target)); - }, optTarget); + // Start the thread with this top block. + newThreads.push(this._pushThread(topBlockId, target)); + }, + optTarget + ); // 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 => { @@ -2326,7 +2545,6 @@ class Runtime extends EventEmitter { return newThreads; } - /** * Dispose all targets. Return to clean state. */ @@ -2365,9 +2583,12 @@ class Runtime extends EventEmitter { const newCloudDataManager = cloudDataManager(this.cloudOptions); this.hasCloudData = newCloudDataManager.hasCloudVariables; this.canAddCloudVariable = newCloudDataManager.canAddCloudVariable; - this.getNumberOfCloudVariables = newCloudDataManager.getNumberOfCloudVariables; - this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager); - this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager); + this.getNumberOfCloudVariables = + newCloudDataManager.getNumberOfCloudVariables; + this.addCloudVariable = + this._initializeAddCloudVariable(newCloudDataManager); + this.removeCloudVariable = + this._initializeRemoveCloudVariable(newCloudDataManager); this.resetProgress(); } @@ -2404,7 +2625,10 @@ class Runtime extends EventEmitter { newIndex = this.executableTargets.length; } if (newIndex <= 0) { - if (this.executableTargets.length > 0 && this.executableTargets[0].isStage) { + if ( + this.executableTargets.length > 0 && + this.executableTargets[0].isStage + ) { newIndex = 1; } else { newIndex = 0; @@ -2487,13 +2711,19 @@ class Runtime extends EventEmitter { } const newRunId = uuid.v1(); - this.storage.scratchFetch.setMetadata(this.storage.scratchFetch.RequestMetadata.RunId, newRunId); + this.storage.scratchFetch.setMetadata( + this.storage.scratchFetch.RequestMetadata.RunId, + newRunId + ); } /** * Start all threads that start with the green flag. */ greenFlag () { + if (!this.frameLoop.running) { + this.frameLoop.start(); + } this.stopAll(); this.emit(Runtime.PROJECT_START); this.updateCurrentMSecs(); @@ -2517,8 +2747,13 @@ class Runtime extends EventEmitter { const newTargets = []; for (let i = 0; i < this.targets.length; i++) { this.targets[i].onStopAll(); - if (Object.prototype.hasOwnProperty.call(this.targets[i], 'isOriginal') && - !this.targets[i].isOriginal) { + if ( + Object.prototype.hasOwnProperty.call( + this.targets[i], + 'isOriginal' + ) && + !this.targets[i].isOriginal + ) { this.targets[i].dispose(); } else { newTargets.push(this.targets[i]); @@ -2537,10 +2772,13 @@ class Runtime extends EventEmitter { } _renderInterpolatedPositions () { - const frameStarted = this._lastStepTime; + const frameStarted = this.frameLoop._lastStepTime; const now = Date.now(); const timeSinceStart = now - frameStarted; - const progressInFrame = Math.min(1, Math.max(0, timeSinceStart / this.currentStepTime)); + const progressInFrame = Math.min( + 1, + Math.max(0, timeSinceStart / this.currentStepTime) + ); interpolate.interpolate(this, progressInFrame); @@ -2563,10 +2801,6 @@ class Runtime extends EventEmitter { * inactive threads after each iteration. */ _step () { - if (this.interpolationEnabled) { - interpolate.setupInitialState(this); - } - if (this.profiler !== null) { if (stepProfilerId === -1) { stepProfilerId = this.profiler.idByName('Runtime._step'); @@ -2580,7 +2814,9 @@ class Runtime extends EventEmitter { // Find all edge-activated hats, and add them to threads to be evaluated. for (const hatType in this._hats) { - if (!Object.prototype.hasOwnProperty.call(this._hats, hatType)) continue; + if (!Object.prototype.hasOwnProperty.call(this._hats, hatType)) { + continue; + } const hat = this._hats[hatType]; if (hat.edgeActivated) { this.startHats(hatType); @@ -2590,7 +2826,9 @@ class Runtime extends EventEmitter { this._pushMonitors(); if (this.profiler !== null) { if (stepThreadsProfilerId === -1) { - stepThreadsProfilerId = this.profiler.idByName('Sequencer.stepThreads'); + stepThreadsProfilerId = this.profiler.idByName( + 'Sequencer.stepThreads' + ); } this.profiler.start(stepThreadsProfilerId); } @@ -2601,51 +2839,27 @@ class Runtime extends EventEmitter { } this.emit(Runtime.AFTER_EXECUTE); this._updateGlows(doneThreads); - // Add done threads so that even if a thread finishes within 1 frame, the green - // flag will still indicate that a script ran. - this._emitProjectRunStatus( - this.threads.length + doneThreads.length - - this._getMonitorThreadCount([...this.threads, ...doneThreads])); + // // Add done threads so that even if a thread finishes within 1 frame, the green + // // flag will still indicate that a script ran. + // this._emitProjectRunStatus( + // this.threads.length + + // doneThreads.length - + // this._getMonitorThreadCount([...this.threads, ...doneThreads]) + // ) // Store threads that completed this iteration for testing and other // internal purposes. this._lastStepDoneThreads = doneThreads; - if (this.renderer) { - // @todo: Only render when this.redrawRequested or clones rendered. - if (this.profiler !== null) { - if (rendererDrawProfilerId === -1) { - rendererDrawProfilerId = this.profiler.idByName('RenderWebGL.draw'); - } - this.profiler.start(rendererDrawProfilerId); - } - // tw: do not draw if document is hidden or a rAF loop is running - // Checking for the animation frame loop is more reliable than using - // interpolationEnabled in some edge cases - if (!document.hidden && !this.frameLoop._interpolationAnimation) { - this.renderer.draw(); - } - if (this.profiler !== null) { - this.profiler.stop(); - } - } - - if (this._refreshTargets) { - this.emit(Runtime.TARGETS_UPDATE, false /* Don't emit project changed */); - this._refreshTargets = false; - } - if (!this._prevMonitorState.equals(this._monitorState)) { - this.emit(Runtime.MONITORS_UPDATE, this._monitorState); - this._prevMonitorState = this._monitorState; + if ( + !this._updateMonitorState && + !this._prevMonitorState.equals(this._monitorState) + ) { + this._updateMonitorState = true; } - if (this.profiler !== null) { this.profiler.stop(); this.profiler.reportFrames(); } - - if (this.interpolationEnabled) { - this._lastStepTime = Date.now(); - } } /** @@ -2704,9 +2918,6 @@ class Runtime extends EventEmitter { * @param {number} framerate Target frames per second */ setFramerate (framerate) { - // Setting framerate to anything greater than this is unnecessary and can break the sequencer - // Additionally, the JS spec says intervals can't run more than once every 4ms (250/s) anyways - if (framerate > 250) framerate = 250; // Convert negative framerates to 1FPS // Note that 0 is a special value which means "matching device screen refresh rate" if (framerate < 0) framerate = 1; @@ -2729,7 +2940,11 @@ class Runtime extends EventEmitter { * @param {*} runtimeOptions New options */ setRuntimeOptions (runtimeOptions) { - this.runtimeOptions = Object.assign({}, this.runtimeOptions, runtimeOptions); + this.runtimeOptions = Object.assign( + {}, + this.runtimeOptions, + runtimeOptions + ); this.emit(Runtime.RUNTIME_OPTIONS_CHANGED, this.runtimeOptions); if (this.renderer) { this.renderer.offscreenTouching = !this.runtimeOptions.fencing; @@ -2741,7 +2956,11 @@ class Runtime extends EventEmitter { * @param {*} compilerOptions New options */ setCompilerOptions (compilerOptions) { - this.compilerOptions = Object.assign({}, this.compilerOptions, compilerOptions); + this.compilerOptions = Object.assign( + {}, + this.compilerOptions, + compilerOptions + ); this.resetAllCaches(); this.emit(Runtime.COMPILER_OPTIONS_CHANGED, this.compilerOptions); } @@ -2795,7 +3014,9 @@ class Runtime extends EventEmitter { */ convertToPackagedRuntime () { if (this.storage) { - throw new Error('convertToPackagedRuntime must be called before attachStorage'); + throw new Error( + 'convertToPackagedRuntime must be called before attachStorage' + ); } this.isPackaged = true; @@ -2840,18 +3061,22 @@ class Runtime extends EventEmitter { let blockInfo = this._blockInfo.find(i => i.id === ID); if (!blockInfo) { // eslint-disable-next-line max-len - const ICON = ''; + const ICON = + ''; blockInfo = { id: ID, name: maybeFormatMessage({ id: 'tw.blocks.addons', default: 'Addons', - description: 'Name of the addon block category in the extension list' + description: + 'Name of the addon block category in the extension list' }), color1: '#29beb8', color2: '#3aa8a4', color3: '#3aa8a4', - menuIconURI: `data:image/svg+xml;,${encodeURIComponent(ICON)}`, + menuIconURI: `data:image/svg+xml;,${encodeURIComponent( + ICON + )}`, blocks: [], customFieldTypes: {}, menus: [] @@ -2861,12 +3086,20 @@ class Runtime extends EventEmitter { blockInfo.blocks.push({ info: {}, xml: - '' }); } @@ -2875,7 +3108,12 @@ class Runtime extends EventEmitter { } getAddonBlock (procedureCode) { - if (Object.prototype.hasOwnProperty.call(this.addonBlocks, procedureCode)) { + if ( + Object.prototype.hasOwnProperty.call( + this.addonBlocks, + procedureCode + ) + ) { return this.addonBlocks[procedureCode]; } return null; @@ -2895,13 +3133,18 @@ class Runtime extends EventEmitter { parseProjectOptions () { const comment = this.findProjectOptionsComment(); if (!comment) return; - const lineWithMagic = comment.text.split('\n').find(i => i.endsWith(COMMENT_CONFIG_MAGIC)); + const lineWithMagic = comment.text + .split('\n') + .find(i => i.endsWith(COMMENT_CONFIG_MAGIC)); if (!lineWithMagic) { log.warn('Config comment does not contain valid line'); return; } - const jsonText = lineWithMagic.substr(0, lineWithMagic.length - COMMENT_CONFIG_MAGIC.length); + const jsonText = lineWithMagic.substr( + 0, + lineWithMagic.length - COMMENT_CONFIG_MAGIC.length + ); let parsed; try { parsed = ExtendedJSON.parse(jsonText); @@ -2931,7 +3174,10 @@ class Runtime extends EventEmitter { } const storedWidth = +parsed.width || this.stageWidth; const storedHeight = +parsed.height || this.stageHeight; - if (storedWidth !== this.stageWidth || storedHeight !== this.stageHeight) { + if ( + storedWidth !== this.stageWidth || + storedHeight !== this.stageHeight + ) { this.setStageSize(storedWidth, storedHeight); } } @@ -2965,13 +3211,18 @@ class Runtime extends EventEmitter { } return result; }; - return difference(this._defaultStoredSettings, this._generateAllProjectOptions()); + return difference( + this._defaultStoredSettings, + this._generateAllProjectOptions() + ); } storeProjectOptions () { const options = this.generateDifferingProjectOptions(); // TODO: translate - const text = `Configuration for https://github.com/Nitro-Bolt/\nYou can move, resize, and minimize this comment, but don't edit it by hand. This comment can be deleted to remove the stored settings.\n${ExtendedJSON.stringify(options)}${COMMENT_CONFIG_MAGIC}`; + const text = `Configuration for https://github.com/Nitro-Bolt/\nYou can move, resize, and minimize this comment, but don't edit it by hand. This comment can be deleted to remove the stored settings.\n${ExtendedJSON.stringify( + options + )}${COMMENT_CONFIG_MAGIC}`; const existingComment = this.findProjectOptionsComment(); if (existingComment) { existingComment.text = text; @@ -3025,12 +3276,12 @@ class Runtime extends EventEmitter { if (target === this._editingTarget) { const blockForThread = thread.blockGlowInFrame; if (thread.requestScriptGlowInFrame || thread.stackClick) { - let script = target.blocks.getTopLevelScript(blockForThread); + let script = + target.blocks.getTopLevelScript(blockForThread); if (!script) { // Attempt to find in flyout blocks. - script = this.flyoutBlocks.getTopLevelScript( - blockForThread - ); + script = + this.flyoutBlocks.getTopLevelScript(blockForThread); } if (script) { requestedGlowsThisFrame.push(script); @@ -3140,7 +3391,10 @@ class Runtime extends EventEmitter { */ visualReport (target, blockId, value) { if (target === this.getEditingTarget()) { - this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: Cast.toString(value)}); + this.emit(Runtime.VISUAL_REPORT, { + id: blockId, + value: Cast.toString(value) + }); } } @@ -3151,7 +3405,8 @@ class Runtime extends EventEmitter { */ requestAddMonitor (monitor) { const id = monitor.get('id'); - if (!this.requestUpdateMonitor(monitor)) { // update monitor if it exists in the state + if (!this.requestUpdateMonitor(monitor)) { + // update monitor if it exists in the state // if the monitor did not exist in the state, add it this._monitorState = this._monitorState.set(id, monitor); } @@ -3166,15 +3421,22 @@ class Runtime extends EventEmitter { */ requestUpdateMonitor (monitor) { const id = monitor.get('id'); + const visible = monitor.get('visible'); + if (visible && !this.frameLoop.running) { + this.frameLoop.start(); + } if (this._monitorState.has(id)) { this._monitorState = // Use mergeWith here to prevent undefined values from overwriting existing ones - this._monitorState.set(id, this._monitorState.get(id).mergeWith((prev, next) => { - if (typeof next === 'undefined' || next === null) { - return prev; - } - return next; - }, monitor)); + this._monitorState.set( + id, + this._monitorState.get(id).mergeWith((prev, next) => { + if (typeof next === 'undefined' || next === null) { + return prev; + } + return next; + }, monitor) + ); return true; } return false; @@ -3195,10 +3457,12 @@ class Runtime extends EventEmitter { * @return {boolean} true if monitor exists and was updated, false otherwise */ requestHideMonitor (monitorId) { - return this.requestUpdateMonitor(new Map([ - ['id', monitorId], - ['visible', false] - ])); + return this.requestUpdateMonitor( + new Map([ + ['id', monitorId], + ['visible', false] + ]) + ); } /** @@ -3208,10 +3472,12 @@ class Runtime extends EventEmitter { * @return {boolean} true if monitor exists and was updated, false otherwise */ requestShowMonitor (monitorId) { - return this.requestUpdateMonitor(new Map([ - ['id', monitorId], - ['visible', true] - ])); + return this.requestUpdateMonitor( + new Map([ + ['id', monitorId], + ['visible', true] + ]) + ); } /** @@ -3220,7 +3486,9 @@ class Runtime extends EventEmitter { * @param {!string} targetId Remove all monitors with given target ID. */ requestRemoveMonitorByTargetId (targetId) { - this._monitorState = this._monitorState.filterNot(value => value.targetId === targetId); + this._monitorState = this._monitorState.filterNot( + value => value.targetId === targetId + ); } /** @@ -3344,7 +3612,10 @@ class Runtime extends EventEmitter { getAllVarNamesOfType (varType) { let varNames = []; for (const target of this.targets) { - const targetVarNames = target.getAllVariableNamesInScopeByType(varType, true); + const targetVarNames = target.getAllVariableNamesInScopeByType( + varType, + true + ); varNames = varNames.concat(targetVarNames); } return varNames; @@ -3385,7 +3656,8 @@ class Runtime extends EventEmitter { * @return {Variable} The new variable that was created. */ createNewGlobalVariable (variableName, optVarId, optVarType) { - const varType = (typeof optVarType === 'string') ? optVarType : Variable.SCALAR_TYPE; + const varType = + typeof optVarType === 'string' ? optVarType : Variable.SCALAR_TYPE; const allVariableNames = this.getAllVarNamesOfType(varType); const newName = StringUtil.unusedName(variableName, allVariableNames); const variable = new Variable(optVarId || uid(), newName, varType); @@ -3483,10 +3755,9 @@ class Runtime extends EventEmitter { } updatePrivacy () { - const enforceRestrictions = ( + const enforceRestrictions = this.enforcePrivacy && - Object.values(this.externalCommunicationMethods).some(i => i) - ); + Object.values(this.externalCommunicationMethods).some(i => i); if (this.renderer && this.renderer.setPrivateSkinAccess) { this.renderer.setPrivateSkinAccess(!enforceRestrictions); } @@ -3505,7 +3776,12 @@ class Runtime extends EventEmitter { * @param {boolean} enabled True if the feature is enabled. */ setExternalCommunicationMethod (method, enabled) { - if (!Object.prototype.hasOwnProperty.call(this.externalCommunicationMethods, method)) { + if ( + !Object.prototype.hasOwnProperty.call( + this.externalCommunicationMethods, + method + ) + ) { throw new Error(`Unknown method: ${method}`); } this.externalCommunicationMethods[method] = enabled; @@ -3513,7 +3789,11 @@ class Runtime extends EventEmitter { } emitAssetProgress () { - this.emit(Runtime.ASSET_PROGRESS, this.finishedAssetRequests, this.totalAssetRequests); + this.emit( + Runtime.ASSET_PROGRESS, + this.finishedAssetRequests, + this.totalAssetRequests + ); } resetProgress () { diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index c374fb694b9..93871e776ca 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -82,17 +82,23 @@ class Sequencer { // Whether `stepThreads` has run through a full single tick. let ranFirstTick = false; const doneThreads = []; + + // tw: If this happens, the runtime is in initialization, do not execute any thread. + if (this.runtime.currentStepTime === 0) return []; // Conditions for continuing to stepping threads: // 1. We must have threads in the list, and some must be active. // 2. Time elapsed must be less than WORK_TIME. // 3. Either turbo mode, or no redraw has been requested by a primitive. - while (this.runtime.threads.length > 0 && - numActiveThreads > 0 && - this.timer.timeElapsed() < WORK_TIME && - (this.runtime.turboMode || !this.runtime.redrawRequested)) { + while ( + this.runtime.threads.length > 0 && + numActiveThreads > 0 && + (this.runtime.turboMode || !this.runtime.redrawRequested) + ) { if (this.runtime.profiler !== null) { if (stepThreadsInnerProfilerId === -1) { - stepThreadsInnerProfilerId = this.runtime.profiler.idByName(stepThreadsInnerProfilerFrame); + stepThreadsInnerProfilerId = this.runtime.profiler.idByName( + stepThreadsInnerProfilerFrame + ); } this.runtime.profiler.start(stepThreadsInnerProfilerId); } @@ -102,25 +108,34 @@ class Sequencer { // Attempt to run each thread one time. const threads = this.runtime.threads; for (let i = 0; i < threads.length; i++) { - const activeThread = this.activeThread = threads[i]; + const activeThread = (this.activeThread = threads[i]); // Check if the thread is done so it is not executed. - if (activeThread.stack.length === 0 || - activeThread.status === Thread.STATUS_DONE) { + if ( + activeThread.stack.length === 0 || + activeThread.status === Thread.STATUS_DONE + ) { // Finished with this thread. stoppedThread = true; continue; } - if (activeThread.status === Thread.STATUS_YIELD_TICK && - !ranFirstTick) { + if ( + activeThread.status === Thread.STATUS_YIELD_TICK && + !ranFirstTick + ) { // Clear single-tick yield from the last call of `stepThreads`. activeThread.status = Thread.STATUS_RUNNING; } - if (activeThread.status === Thread.STATUS_RUNNING || - activeThread.status === Thread.STATUS_YIELD) { + if ( + activeThread.status === Thread.STATUS_RUNNING || + activeThread.status === Thread.STATUS_YIELD + ) { // Normal-mode thread: step. if (this.runtime.profiler !== null) { if (stepThreadProfilerId === -1) { - stepThreadProfilerId = this.runtime.profiler.idByName(stepThreadProfilerFrame); + stepThreadProfilerId = + this.runtime.profiler.idByName( + stepThreadProfilerFrame + ); } // Increment the number of times stepThread is called. @@ -134,8 +149,10 @@ class Sequencer { } // Check if the thread completed while it just stepped to make // sure we remove it before the next iteration of all threads. - if (activeThread.stack.length === 0 || - activeThread.status === Thread.STATUS_DONE) { + if ( + activeThread.stack.length === 0 || + activeThread.status === Thread.STATUS_DONE + ) { // Finished with this thread. stoppedThread = true; } @@ -153,8 +170,10 @@ class Sequencer { let nextActiveThread = 0; for (let i = 0; i < this.runtime.threads.length; i++) { const thread = this.runtime.threads[i]; - if (thread.stack.length !== 0 && - thread.status !== Thread.STATUS_DONE) { + if ( + thread.stack.length !== 0 && + thread.status !== Thread.STATUS_DONE + ) { this.runtime.threads[nextActiveThread] = thread; nextActiveThread++; } else { @@ -164,6 +183,10 @@ class Sequencer { } this.runtime.threads.length = nextActiveThread; } + + // tw: Detect timer here so the sequencer won't break when FPS is greater than 1000 + // and performance.now() is not available. + if (this.timer.timeElapsed() >= WORK_TIME) break; } this.activeThread = null; @@ -205,7 +228,8 @@ class Sequencer { // Execute the current block. if (this.runtime.profiler !== null) { if (executeProfilerId === -1) { - executeProfilerId = this.runtime.profiler.idByName(executeProfilerFrame); + executeProfilerId = + this.runtime.profiler.idByName(executeProfilerFrame); } // Increment the number of times execute is called. @@ -222,8 +246,10 @@ class Sequencer { // Mark as running for next iteration. thread.status = Thread.STATUS_RUNNING; // In warp mode, yielded blocks are re-executed immediately. - if (isWarpMode && - thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) { + if ( + isWarpMode && + thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME + ) { continue; } return; @@ -265,8 +291,10 @@ class Sequencer { // Return to yield for the frame/tick in general. // Unless we're in warp mode - then only return if the // warp timer is up. - if (!isWarpMode || - thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) { + if ( + !isWarpMode || + thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME + ) { // Don't do anything to the stack, since loops need // to be re-executed. return; @@ -274,7 +302,6 @@ class Sequencer { // Don't go to the next block for this level of the stack, // since loops need to be re-executed. continue; - } else if (stackFrame.waitingReporter) { // This level of the stack was waiting for a value. // This means a reporter has just returned - so don't go @@ -317,7 +344,8 @@ class Sequencer { * @param {!string} procedureCode Procedure code of procedure to step to. */ stepToProcedure (thread, procedureCode) { - const definition = thread.target.blocks.getProcedureDefinition(procedureCode); + const definition = + thread.target.blocks.getProcedureDefinition(procedureCode); if (!definition) { return; } @@ -331,15 +359,18 @@ class Sequencer { // from the stack by the sequencer, returning control to the caller. thread.pushStack(definition); // In known warp-mode threads, only yield when time is up. - if (thread.peekStackFrame().warpMode && - thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) { + if ( + thread.peekStackFrame().warpMode && + thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME + ) { thread.status = Thread.STATUS_YIELD; } else { // Look for warp-mode flag on definition, and set the thread // to warp-mode if needed. const definitionBlock = thread.target.blocks.getBlock(definition); const innerBlock = thread.target.blocks.getBlock( - definitionBlock.inputs.custom_block.block); + definitionBlock.inputs.custom_block.block + ); let doWarp = false; if (innerBlock && innerBlock.mutation) { const warp = innerBlock.mutation.warp; diff --git a/src/engine/tw-frame-loop.js b/src/engine/tw-frame-loop.js index 45423739623..9db40242f3e 100644 --- a/src/engine/tw-frame-loop.js +++ b/src/engine/tw-frame-loop.js @@ -1,40 +1,58 @@ // Due to the existence of features such as interpolation and "0 FPS" being treated as "screen refresh rate", // The VM loop logic has become much more complex +const interpolate = require('./tw-interpolate'); +/** + * Numeric ID for RenderWebGL.draw in Profiler instances. + * @type {number} + */ +let rendererDrawProfilerId = -1; + // Use setTimeout to polyfill requestAnimationFrame in Node.js environments -const _requestAnimationFrame = typeof requestAnimationFrame === 'function' ? - requestAnimationFrame : - (f => setTimeout(f, 1000 / 60)); -const _cancelAnimationFrame = typeof requestAnimationFrame === 'function' ? - cancelAnimationFrame : - clearTimeout; - -const animationFrameWrapper = callback => { +const _requestAnimationFrame = + typeof requestAnimationFrame === 'function' ? + requestAnimationFrame : + f => setTimeout(f, 1000 / 60); +const _cancelAnimationFrame = + typeof requestAnimationFrame === 'function' ? + cancelAnimationFrame : + clearTimeout; + +const taskWrapper = (callback, requestFn, cancelFn, manualInterval) => { let id; + let cancelled = false; const handle = () => { - id = _requestAnimationFrame(handle); callback(); + if (manualInterval) id = requestFn(handle); + }; + const cancel = () => { + if (!cancelled) cancelFn(id); + cancelled = true; }; - const cancel = () => _cancelAnimationFrame(id); - id = _requestAnimationFrame(handle); + id = requestFn(handle); return { cancel }; }; class FrameLoop { + /** + * @param {import('./runtime')} runtime + */ constructor (runtime) { this.runtime = runtime; this.running = false; - this.setFramerate(30); + this.setFramerate(this.runtime.frameLoop?.framerate ?? 30); this.setInterpolation(false); - - this.stepCallback = this.stepCallback.bind(this); - this.interpolationCallback = this.interpolationCallback.bind(this); + this._lastRenderTime = 0; + this._lastStepTime = 0; this._stepInterval = null; - this._interpolationAnimation = null; - this._stepAnimation = null; + this._renderInterval = null; + } + + now () { + return (performance || Date).now(); } setFramerate (fps) { @@ -49,10 +67,95 @@ class FrameLoop { stepCallback () { this.runtime._step(); + this._lastStepTime = this.now(); + } + + stepImmediateCallback () { + if (this.now() - this._lastStepTime >= this.runtime.currentStepTime) { + this.runtime._step(); + this._lastStepTime = this.now(); + } } - interpolationCallback () { - this.runtime._renderInterpolatedPositions(); + renderCallback () { + if (this.runtime.renderer) { + const renderTime = this.now(); + if (this.interpolation && this.framerate !== 0) { + if (!document.hidden) { + this.runtime._renderInterpolatedPositions(); + } + this.runtime.screenRefreshTime = + renderTime - this._lastRenderTime; // Screen refresh time (from rate) + this._lastRenderTime = renderTime; + } else if ( + this.framerate === 0 || + renderTime - this._lastRenderTime >= + this.runtime.currentStepTime + ) { + if (this.runtime.interpolationEnabled) { + interpolate.setupInitialState(this); + } + // @todo: Only render when this.redrawRequested or clones rendered. + if (this.runtime.profiler !== null) { + if (rendererDrawProfilerId === -1) { + rendererDrawProfilerId = + this.runtime.profiler.idByName('RenderWebGL.draw'); + } + this.runtime.profiler.start(rendererDrawProfilerId); + } + // tw: do not draw if document is hidden or a rAF loop is running + // Checking for the animation frame loop is more reliable than using + // interpolationEnabled in some edge cases + if (!document.hidden) { + this.runtime.renderer.draw(); + } + if (this.runtime.profiler !== null) { + this.runtime.profiler.stop(); + } + this.runtime.screenRefreshTime = + renderTime - this._lastRenderTime; // Screen refresh time (from rate) + this._lastRenderTime = renderTime; + if (this.framerate === 0) { + this.runtime.currentStepTime = + this.runtime.screenRefreshTime; + } + + const aliveThreads = + this.runtime.threads.length + + this.runtime._lastStepDoneThreads.length; + + // Add done threads so that even if a thread finishes within 1 frame, the green + // flag will still indicate that a script ran. + this.runtime._emitProjectRunStatus( + aliveThreads - + this.runtime._getMonitorThreadCount([ + ...this.runtime.threads, + ...this.runtime._lastStepDoneThreads + ]) + ); + + if (aliveThreads === 0) { + this.stop(); + } + + if (this.runtime._refreshTargets) { + this.runtime.emit( + this.runtime.constructor.TARGETS_UPDATE, + false /* Don't emit project changed */ + ); + this.runtime._refreshTargets = false; + } + + if (this.runtime._updateMonitorState) { + this.runtime.emit( + this.runtime.constructor.MONITORS_UPDATE, + this.runtime._monitorState + ); + this.runtime._prevMonitorState = this.runtime._monitorState; + this.runtime._updateMonitorState = false; + } + } + } } _restart () { @@ -65,29 +168,53 @@ class FrameLoop { start () { this.running = true; if (this.framerate === 0) { - this._stepAnimation = animationFrameWrapper(this.stepCallback); - this.runtime.currentStepTime = 1000 / 60; + this._stepInterval = this._renderInterval = taskWrapper( + () => { + this.stepCallback(); + this.renderCallback(); + }, + _requestAnimationFrame, + _cancelAnimationFrame, + true + ); + this.runtime.currentStepTime = 0; } else { // Interpolation should never be enabled when framerate === 0 as that's just redundant - if (this.interpolation) { - this._interpolationAnimation = animationFrameWrapper(this.interpolationCallback); + this._renderInterval = taskWrapper( + this.renderCallback.bind(this), + _requestAnimationFrame, + _cancelAnimationFrame, + true + ); + if ( + this.framerate > 250 && + global.setImmediate && + global.clearImmediate + ) { + // High precision implementation via setImmediate (polyfilled) + // bug: very unfriendly to DevTools + this._stepInterval = taskWrapper( + this.stepImmediateCallback.bind(this), + global.setImmediate, + global.clearImmediate, + true + ); + } else { + this._stepInterval = taskWrapper( + this.stepCallback.bind(this), + fn => setInterval(fn, 1000 / this.framerate), + clearInterval, + false + ); } - this._stepInterval = setInterval(this.stepCallback, 1000 / this.framerate); this.runtime.currentStepTime = 1000 / this.framerate; } } stop () { this.running = false; - clearInterval(this._stepInterval); - if (this._interpolationAnimation) { - this._interpolationAnimation.cancel(); - } - if (this._stepAnimation) { - this._stepAnimation.cancel(); - } - this._interpolationAnimation = null; - this._stepAnimation = null; + this._renderInterval.cancel(); + this._stepInterval.cancel(); } } diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index de00ebb84ad..f8ed6163f4c 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -266,7 +266,7 @@ class ExtensionManager { * @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure */ reorderExtension (extensionIndex, reorderIndex) { - let extensions = Array.from(this._loadedExtensions); + const extensions = Array.from(this._loadedExtensions); if (reorderIndex >= extensions.length) { const padding = reorderIndex - extensions + 1; while (padding--) { @@ -274,7 +274,7 @@ class ExtensionManager { } } extensions.splice(reorderIndex, 0, extensions.splice(extensionIndex, 1)[0]); - this._loadedExtensions = new Map(extensions.map((extension) => [extension[0], extension[1]])); + this._loadedExtensions = new Map(extensions.map(extension => [extension[0], extension[1]])); dispatch.call('runtime', '_reorderExtensionPrimitive', extensionIndex, reorderIndex); this.refreshBlocks(); } From 3ccb6be3e5b95e32360f7efcfe7436446933bbc6 Mon Sep 17 00:00:00 2001 From: FurryR Date: Wed, 24 Sep 2025 14:02:49 +0800 Subject: [PATCH 2/2] Fix menu for sensing_current Signed-off-by: FurryR --- src/blocks/scratch3_sensing.js | 2 +- src/compiler/irgen.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index 936c9d3f95c..f99c7813118 100644 --- a/src/blocks/scratch3_sensing.js +++ b/src/blocks/scratch3_sensing.js @@ -253,7 +253,7 @@ class Scratch3SensingBlocks { case 'hour': return date.getHours(); case 'minute': return date.getMinutes(); case 'second': return date.getSeconds(); - case 'refreshTime': return this.runtime.screenRefreshTime / 1000; + case 'refreshtime': return this.runtime.screenRefreshTime / 1000; } return 0; } diff --git a/src/compiler/irgen.js b/src/compiler/irgen.js index 1e1387160a4..f82be1450e4 100644 --- a/src/compiler/irgen.js +++ b/src/compiler/irgen.js @@ -728,7 +728,7 @@ class ScriptTreeGenerator { return { kind: 'sensing.second' }; - case 'refreshTime': + case 'refreshtime': return { kind: 'sensing.refreshTime' };