diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 56e1a572a..43dc2873e 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -71,6 +71,47 @@ const ArgumentTypeMap = (() => { 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. + * @typedef {object} CloudDataManager + * @property {function} canAddNewCloudVariable A function to call to check that + * a cloud variable can be added. + * @property {function} removeExistingCloudVariable A function to call when + * removing an existing cloud variable. + */ + +/** + * Creates and manages cloud variable limit in a project, + * and returns two functions to be used to add a new + * cloud variable (while checking that it can be added) + * and remove an existing cloud variable. + * These are to be called whenever attempting to create or delete + * a cloud variable. + * @return {CloudDataManager} The functions to be used when adding or removing a + * cloud variable. + */ +const cloudDataManager = () => { + let cloudVariableLimit = 8; + + const canAddNewCloudVariable = () => { + if (cloudVariableLimit > 0) { + cloudVariableLimit--; + return true; + } + return false; + }; + + const removeExistingCloudVariable = () => { + cloudVariableLimit++; + }; + + return { + canAddNewCloudVariable, + removeExistingCloudVariable + }; +}; + /** * Predefined "Converted block info" for a separator between blocks in a block category * @type {ConvertedBlockInfo} @@ -283,6 +324,30 @@ class Runtime extends EventEmitter { * @type {Profiler} */ this.profiler = null; + + /** + * Whether this runtime uses/interacts with cloud data. + * @type {boolean} + */ + this.hasCloudData = false; + + const newCloudDataManager = cloudDataManager(); + + /** + * A function which checks whether a new cloud variable can be added + * to the runtime. + * @type {function} + * @return {boolean} Whether or not a new cloud variable can be added + * to the runtime. + */ + this.canAddNewCloudVariable = newCloudDataManager.canAddNewCloudVariable; + + /** + * A function which updates the runtime's cloud variable limit + * when removing a cloud variable. + * @type {function} + */ + this.removeExistingCloudVariable = newCloudDataManager.removeExistingCloudVariable; } /** @@ -1394,6 +1459,13 @@ class Runtime extends EventEmitter { this._monitorState = OrderedMap({}); // @todo clear out extensions? turboMode? etc. this.ioDevices.cloud.clear(); + + // Reset runtime cloud data info + this.hasCloudData = false; + const newCloudDataManager = cloudDataManager(); + this.canAddNewCloudVariable = newCloudDataManager.canAddNewCloudVariable; + this.removeExistingCloudVariable = newCloudDataManager.removeExistingCloudVariable; + } /** diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 5021c14df..0562f3405 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -470,12 +470,21 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) if (object.hasOwnProperty('variables')) { for (let j = 0; j < object.variables.length; j++) { const variable = object.variables[j]; + // A variable is a cloud variable if: + // - the project says it's a cloud variable, and + // - it's a stage variable, and + // - the runtime can support another cloud variable + const isCloud = variable.isPersistent && topLevel && + // It's important that this part of the check goes last + // because it will update the cloud variable limit counter. + runtime.canAddNewCloudVariable(); const newVariable = new Variable( getVariableId(variable.name, Variable.SCALAR_TYPE), variable.name, Variable.SCALAR_TYPE, - variable.isPersistent + isCloud ); + if (isCloud && !runtime.hasCloudData) runtime.hasCloudData = true; newVariable.value = variable.value; target.variables[newVariable.id] = newVariable; } diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 7012aa949..8d11d565a 100644 --- a/src/serialization/sb3.js +++ b/src/serialization/sb3.js @@ -393,7 +393,7 @@ const serializeVariables = function (variables) { // otherwise should be a scalar type obj.variables[varId] = [v.name, v.value]; // only scalar vars have the potential to be cloud vars - if (v.isPersistent) obj.variables[varId].push(true); + if (v.isCloud) obj.variables[varId].push(true); } return obj; }; @@ -900,12 +900,21 @@ const parseScratchObject = function (object, runtime, extensions, zip) { if (object.hasOwnProperty('variables')) { for (const varId in object.variables) { const variable = object.variables[varId]; + // A variable is a cloud variable if: + // - the project says it's a cloud variable, and + // - it's a stage variable, and + // - the runtime can support another cloud variable + const isCloud = (variable.length === 3) && variable[2] && object.isStage && + // It's important that this part of the check goes last + // because it will update the cloud variable limit counter. + runtime.canAddNewCloudVariable(); const newVariable = new Variable( varId, // var id is the index of the variable desc array in the variables obj variable[0], // name of the variable Variable.SCALAR_TYPE, // type of the variable - (variable.length === 3) ? variable[2] : false // isPersistent/isCloud + isCloud ); + if (isCloud && !runtime.hasCloudData) runtime.hasCloudData = true; newVariable.value = variable[1]; target.variables[newVariable.id] = newVariable; }