diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 17cd755dcf9..92e368923cd 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -918,6 +918,13 @@ class Runtime extends EventEmitter { return 'BLOCKS_NEED_UPDATE'; } + /** + * Event name when platform name inside a project does not match the runtime. + */ + static get PLATFORM_MISMATCH () { + return 'PLATFORM_MISMATCH'; + } + /** * How rapidly we try to step threads by default, in ms. */ diff --git a/src/serialization/sb3.js b/src/serialization/sb3.js index 99397b27593..bad68d84114 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'); @@ -1471,6 +1472,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. @@ -1479,7 +1510,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() @@ -1487,8 +1520,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; } diff --git a/test/integration/tw_platform.js b/test/integration/tw_platform.js index 84b3aaf3529..d487cbe0b54 100644 --- a/test/integration/tw_platform.js +++ b/test/integration/tw_platform.js @@ -1,6 +1,7 @@ const {test} = require('tap'); const VM = require('../../src/virtual-machine'); const platform = require('../../src/engine/tw-platform'); +const Clone = require('../../src/util/clone'); test('the internal object', t => { // the idea with this test is to make it harder for forks to screw up modifying the file @@ -24,3 +25,137 @@ test('sanitize', t => { t.not(json.meta.platform, vm.runtime.platform, 'not the same object as runtime.platform'); t.end(); }); + +const vanillaProject = { + targets: [ + { + isStage: true, + name: 'Stage', + variables: {}, + lists: {}, + broadcasts: {}, + blocks: {}, + comments: {}, + currentCostume: 0, + costumes: [ + { + name: 'backdrop1', + dataFormat: 'svg', + assetId: 'cd21514d0531fdffb22204e0ec5ed84a', + md5ext: 'cd21514d0531fdffb22204e0ec5ed84a.svg', + rotationCenterX: 240, + rotationCenterY: 180 + } + ], + sounds: [], + volume: 100, + layerOrder: 0, + tempo: 60, + videoTransparency: 50, + videoState: 'on', + textToSpeechLanguage: null + } + ], + monitors: [], + extensions: [], + meta: { + semver: '3.0.0', + vm: '0.2.0', + agent: '' + } +}; + +test('deserialize no platform', t => { + const vm = new VM(); + vm.runtime.on('PLATFORM_MISMATCH', () => { + t.fail('Called PLATFORM_MISMATCH'); + }); + vm.loadProject(vanillaProject).then(() => { + t.end(); + }); +}); + +test('deserialize matching platform', t => { + const vm = new VM(); + vm.runtime.on('PLATFORM_MISMATCH', () => { + t.fail('Called PLATFORM_MISMATCH'); + }); + const project = Clone.simple(vanillaProject); + project.meta.platform = Object.assign({}, platform); + vm.loadProject(project).then(() => { + t.end(); + }); +}); + +test('deserialize mismatching platform with no listener', t => { + const vm = new VM(); + const project = Clone.simple(vanillaProject); + project.meta.platform = { + name: '3tw4ergo980uitegr5hoijuk;' + }; + vm.loadProject(project).then(() => { + t.end(); + }); +}); + +test('deserialize mismatching platform with 1 listener', t => { + t.plan(2); + const vm = new VM(); + vm.runtime.on('PLATFORM_MISMATCH', (pl, callback) => { + t.same(pl, { + name: 'aa', + url: '...' + }); + t.ok('called PLATFORM_MISMATCH'); + callback(); + }); + const project = Clone.simple(vanillaProject); + project.meta.platform = { + name: 'aa', + url: '...' + }; + vm.loadProject(project).then(() => { + t.end(); + }); +}); + +test('deserialize mismatching platform with 3 listeners', t => { + t.plan(2); + + const calls = []; + let expectedToLoad = false; + const vm = new VM(); + vm.runtime.on('PLATFORM_MISMATCH', (_, callback) => { + calls.push([1, callback]); + }); + vm.runtime.on('PLATFORM_MISMATCH', (_, callback) => { + calls.push([2, callback]); + }); + vm.runtime.on('PLATFORM_MISMATCH', (_, callback) => { + calls.push([3, callback]); + }); + + const project = Clone.simple(vanillaProject); + project.meta.platform = { + name: '' + }; + vm.loadProject(project).then(() => { + t.ok(expectedToLoad); + t.end(); + }); + + // loadProject is async, may need to wait a bit + setTimeout(async () => { + t.same(calls.map(i => i[0]), [1, 2, 3], 'listeners called in correct order'); + + // loadProject should not finish until we call all of the listeners' callbacks + calls[0][1](); + await new Promise(resolve => setTimeout(resolve, 100)); + + calls[1][1](); + await new Promise(resolve => setTimeout(resolve, 100)); + + expectedToLoad = true; + calls[2][1](); + }, 0); +});