Skip to content

Commit

Permalink
Add mismatched platform event (vm part)
Browse files Browse the repository at this point in the history
  • Loading branch information
GarboMuffin committed May 9, 2024
1 parent 7efdf51 commit b05cbde
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 1 deletion.
7 changes: 7 additions & 0 deletions src/engine/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
37 changes: 36 additions & 1 deletion src/serialization/sb3.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -1471,6 +1472,36 @@ const replaceUnsafeCharsInVariableIds = function (targets) {
return targets;
};

/**
* @param {object} json
* @param {Runtime} runtime
* @returns {void|Promise<void>} 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.
Expand All @@ -1479,16 +1510,20 @@ const replaceUnsafeCharsInVariableIds = function (targets) {
* @param {boolean} isSingleSprite - If true treat as single sprite, else treat as whole project
* @returns {Promise.<ImportedProject>} 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()
};

// 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;
}

Expand Down
135 changes: 135 additions & 0 deletions test/integration/tw_platform.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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);
});

0 comments on commit b05cbde

Please sign in to comment.