Skip to content

Commit

Permalink
Add platform info (#205)
Browse files Browse the repository at this point in the history
- Platform info is now stored in project.json.
- Available to extensions on vm.runtime.platform.
- When loading a project with a different platform, emit an event and wait for callbacks before continuing.

Co-authored-by: Muffin <[email protected]>
  • Loading branch information
JeremyGamer13 and GarboMuffin authored May 9, 2024
1 parent 11eec66 commit 31c9eba
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 1 deletion.
13 changes: 13 additions & 0 deletions src/engine/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const xmlEscape = require('../util/xml-escape');
const ScratchLinkWebSocket = require('../util/scratch-link-websocket');
const FontManager = require('./tw-font-manager');
const fetchWithTimeout = require('../util/fetch-with-timeout');
const platform = require('./tw-platform.js');

// Virtual I/O devices.
const Clock = require('../io/clock');
Expand Down Expand Up @@ -439,6 +440,11 @@ class Runtime extends EventEmitter {
*/
this.origin = null;

/**
* Metadata about the platform this VM is part of.
*/
this.platform = Object.assign({}, platform);

this._initScratchLink();

this.resetRunId();
Expand Down Expand Up @@ -912,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
7 changes: 7 additions & 0 deletions src/engine/tw-platform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Forks should change this.
// This can be accessed externally on `vm.runtime.platform`

module.exports = {
name: 'TurboWarp',
url: 'https://turbowarp.org/'
};
40 changes: 39 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 @@ -794,6 +795,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;

Expand Down Expand Up @@ -1468,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 @@ -1476,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
161 changes: 161 additions & 0 deletions test/integration/tw_platform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
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
t.type(platform.name, 'string');
t.type(platform.url, 'string');
t.end();
});

test('vm property', t => {
const vm = new VM();
t.same(vm.runtime.platform, platform, 'copy of tw-platform.js');
t.not(vm.runtime.platform, platform, 'not the same object as tw-platform.js');
t.end();
});

test('sanitize', t => {
const vm = new VM();
vm.runtime.platform.name += ' - test';
const json = JSON.parse(vm.toJSON());
t.same(json.meta.platform, vm.runtime.platform, 'copy of runtime.platform');
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 31c9eba

Please sign in to comment.