2016-12-30 10:19:58 -05:00
|
|
|
/**
|
|
|
|
* @fileoverview
|
|
|
|
* Partial implementation of a SB3 serializer and deserializer. Parses provided
|
|
|
|
* JSON and then generates all needed scratch-vm runtime structures.
|
|
|
|
*/
|
|
|
|
|
2017-04-26 16:50:53 -04:00
|
|
|
const vmPackage = require('../../package.json');
|
|
|
|
const Blocks = require('../engine/blocks');
|
|
|
|
const Sprite = require('../sprites/sprite');
|
|
|
|
const Variable = require('../engine/variable');
|
2016-12-30 10:19:58 -05:00
|
|
|
|
2017-09-11 09:42:16 -04:00
|
|
|
const {loadCostume} = require('../import/load-costume.js');
|
|
|
|
const {loadSound} = require('../import/load-sound.js');
|
2018-02-16 00:44:23 -05:00
|
|
|
const {deserializeCostume, deserializeSound} = require('./deserialize-assets.js');
|
2017-04-27 17:08:06 -04:00
|
|
|
|
2017-11-03 14:17:16 -04:00
|
|
|
/**
|
|
|
|
* @typedef {object} ImportedProject
|
|
|
|
* @property {Array.<Target>} targets - the imported Scratch 3.0 target objects.
|
|
|
|
* @property {ImportedExtensionsInfo} extensionsInfo - the ID of each extension actually used by this project.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef {object} ImportedExtensionsInfo
|
|
|
|
* @property {Set.<string>} extensionIDs - the ID of each extension actually in use by blocks in this project.
|
|
|
|
* @property {Map.<string, string>} extensionURLs - map of ID => URL from project metadata. May not match extensionIDs.
|
|
|
|
*/
|
|
|
|
|
2016-12-30 10:19:58 -05:00
|
|
|
/**
|
|
|
|
* Serializes the specified VM runtime.
|
|
|
|
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
2017-04-27 17:49:57 -04:00
|
|
|
* @return {object} Serialized runtime instance.
|
2016-12-30 10:19:58 -05:00
|
|
|
*/
|
2017-04-26 16:50:53 -04:00
|
|
|
const serialize = function (runtime) {
|
2016-12-30 10:19:58 -05:00
|
|
|
// Fetch targets
|
2017-04-26 16:50:53 -04:00
|
|
|
const obj = Object.create(null);
|
2017-05-17 13:07:35 -04:00
|
|
|
obj.targets = runtime.targets.filter(target => target.isOriginal);
|
2016-12-30 10:19:58 -05:00
|
|
|
|
|
|
|
// Assemble metadata
|
2017-04-26 16:50:53 -04:00
|
|
|
const meta = Object.create(null);
|
2016-12-30 10:19:58 -05:00
|
|
|
meta.semver = '3.0.0';
|
2017-04-26 16:50:53 -04:00
|
|
|
meta.vm = vmPackage.version;
|
2016-12-30 10:19:58 -05:00
|
|
|
|
|
|
|
// Attach full user agent string to metadata if available
|
|
|
|
meta.agent = null;
|
|
|
|
if (typeof navigator !== 'undefined') meta.agent = navigator.userAgent;
|
|
|
|
|
|
|
|
// Assemble payload and return
|
|
|
|
obj.meta = meta;
|
|
|
|
return obj;
|
|
|
|
};
|
|
|
|
|
2017-01-27 20:05:54 -05:00
|
|
|
/**
|
|
|
|
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
2017-04-26 16:50:53 -04:00
|
|
|
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
2017-01-27 20:05:54 -05:00
|
|
|
* @param {!Runtime} runtime Runtime object to load all structures into.
|
2017-11-03 14:17:16 -04:00
|
|
|
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
2018-02-16 00:44:23 -05:00
|
|
|
* @param {JSZip} zip Sb3 file describing this project (to load assets from)
|
2017-11-03 14:17:16 -04:00
|
|
|
* @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
|
2017-01-27 20:05:54 -05:00
|
|
|
*/
|
2018-02-16 00:44:23 -05:00
|
|
|
const parseScratchObject = function (object, runtime, extensions, zip) {
|
2017-01-27 20:05:54 -05:00
|
|
|
if (!object.hasOwnProperty('name')) {
|
|
|
|
// Watcher/monitor - skip this object until those are implemented in VM.
|
|
|
|
// @todo
|
2017-11-03 14:17:16 -04:00
|
|
|
return Promise.resolve(null);
|
2017-01-27 20:05:54 -05:00
|
|
|
}
|
|
|
|
// Blocks container for this object.
|
2017-04-26 16:50:53 -04:00
|
|
|
const blocks = new Blocks();
|
2017-01-27 20:05:54 -05:00
|
|
|
|
|
|
|
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
|
2017-04-26 16:50:53 -04:00
|
|
|
const sprite = new Sprite(blocks, runtime);
|
2017-01-27 20:05:54 -05:00
|
|
|
|
|
|
|
// Sprite/stage name from JSON.
|
|
|
|
if (object.hasOwnProperty('name')) {
|
|
|
|
sprite.name = object.name;
|
|
|
|
}
|
|
|
|
if (object.hasOwnProperty('blocks')) {
|
2017-08-26 13:07:47 -04:00
|
|
|
for (const blockId in object.blocks) {
|
2017-11-03 14:42:20 -04:00
|
|
|
const blockJSON = object.blocks[blockId];
|
2017-11-03 14:17:16 -04:00
|
|
|
blocks.createBlock(blockJSON);
|
|
|
|
|
|
|
|
const dotIndex = blockJSON.opcode.indexOf('.');
|
|
|
|
if (dotIndex >= 0) {
|
|
|
|
const extensionId = blockJSON.opcode.substring(0, dotIndex);
|
|
|
|
extensions.extensionIDs.add(extensionId);
|
|
|
|
}
|
2017-01-27 20:05:54 -05:00
|
|
|
}
|
2017-04-26 16:50:53 -04:00
|
|
|
// console.log(blocks);
|
2017-01-27 20:05:54 -05:00
|
|
|
}
|
|
|
|
// Costumes from JSON.
|
2017-04-27 17:08:06 -04:00
|
|
|
const costumePromises = (object.costumes || []).map(costumeSource => {
|
|
|
|
// @todo: Make sure all the relevant metadata is being pulled out.
|
|
|
|
const costume = {
|
|
|
|
skinId: null,
|
|
|
|
name: costumeSource.name,
|
|
|
|
bitmapResolution: costumeSource.bitmapResolution,
|
|
|
|
rotationCenterX: costumeSource.rotationCenterX,
|
|
|
|
rotationCenterY: costumeSource.rotationCenterY
|
|
|
|
};
|
2017-06-13 17:56:06 -04:00
|
|
|
const dataFormat =
|
|
|
|
costumeSource.dataFormat ||
|
|
|
|
(costumeSource.assetType && costumeSource.assetType.runtimeFormat) || // older format
|
|
|
|
'png'; // if all else fails, guess that it might be a PNG
|
|
|
|
const costumeMd5 = `${costumeSource.assetId}.${dataFormat}`;
|
2018-02-16 00:44:23 -05:00
|
|
|
costume.md5 = costumeMd5;
|
|
|
|
return deserializeCostume(costumeSource, runtime, zip)
|
|
|
|
.then(() => loadCostume(costumeMd5, costume, runtime));
|
|
|
|
// Only attempt to load the costume after the deserialization
|
|
|
|
// process has been completed
|
2017-04-27 17:08:06 -04:00
|
|
|
});
|
2017-01-27 20:05:54 -05:00
|
|
|
// Sounds from JSON
|
2017-04-27 17:08:06 -04:00
|
|
|
const soundPromises = (object.sounds || []).map(soundSource => {
|
|
|
|
const sound = {
|
|
|
|
format: soundSource.format,
|
2018-02-16 00:44:23 -05:00
|
|
|
// fileUrl: soundSource.fileUrl,
|
2017-04-27 17:08:06 -04:00
|
|
|
rate: soundSource.rate,
|
|
|
|
sampleCount: soundSource.sampleCount,
|
|
|
|
soundID: soundSource.soundID,
|
|
|
|
name: soundSource.name,
|
|
|
|
md5: soundSource.md5,
|
|
|
|
data: null
|
|
|
|
};
|
2018-02-16 00:44:23 -05:00
|
|
|
return deserializeSound(soundSource, runtime, zip)
|
|
|
|
.then(() => loadSound(sound, runtime));
|
|
|
|
// Only attempt to load the sound after the deserialization
|
|
|
|
// process has been completed.
|
2017-04-27 17:08:06 -04:00
|
|
|
});
|
2017-01-27 20:05:54 -05:00
|
|
|
// Create the first clone, and load its run-state from JSON.
|
2017-04-26 16:50:53 -04:00
|
|
|
const target = sprite.createClone();
|
2017-01-27 20:05:54 -05:00
|
|
|
// Load target properties from JSON.
|
|
|
|
if (object.hasOwnProperty('variables')) {
|
2017-08-26 13:07:47 -04:00
|
|
|
for (const j in object.variables) {
|
2017-04-26 16:50:53 -04:00
|
|
|
const variable = object.variables[j];
|
2017-06-15 17:29:15 -04:00
|
|
|
const newVariable = new Variable(
|
|
|
|
variable.id,
|
2017-01-27 20:05:54 -05:00
|
|
|
variable.name,
|
2017-11-09 17:19:34 -05:00
|
|
|
variable.type,
|
2017-01-27 20:05:54 -05:00
|
|
|
variable.isPersistent
|
|
|
|
);
|
2017-11-09 17:19:34 -05:00
|
|
|
newVariable.value = variable.value;
|
2017-06-15 17:29:15 -04:00
|
|
|
target.variables[newVariable.id] = newVariable;
|
2017-01-27 20:05:54 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (object.hasOwnProperty('x')) {
|
|
|
|
target.x = object.x;
|
|
|
|
}
|
|
|
|
if (object.hasOwnProperty('y')) {
|
|
|
|
target.y = object.y;
|
|
|
|
}
|
|
|
|
if (object.hasOwnProperty('direction')) {
|
|
|
|
target.direction = object.direction;
|
|
|
|
}
|
|
|
|
if (object.hasOwnProperty('size')) {
|
|
|
|
target.size = object.size;
|
|
|
|
}
|
|
|
|
if (object.hasOwnProperty('visible')) {
|
|
|
|
target.visible = object.visible;
|
|
|
|
}
|
|
|
|
if (object.hasOwnProperty('currentCostume')) {
|
|
|
|
target.currentCostume = object.currentCostume;
|
|
|
|
}
|
|
|
|
if (object.hasOwnProperty('rotationStyle')) {
|
|
|
|
target.rotationStyle = object.rotationStyle;
|
|
|
|
}
|
2017-02-07 18:38:44 -05:00
|
|
|
if (object.hasOwnProperty('isStage')) {
|
|
|
|
target.isStage = object.isStage;
|
2017-01-27 20:05:54 -05:00
|
|
|
}
|
2017-04-27 17:08:06 -04:00
|
|
|
Promise.all(costumePromises).then(costumes => {
|
|
|
|
sprite.costumes = costumes;
|
|
|
|
});
|
|
|
|
Promise.all(soundPromises).then(sounds => {
|
|
|
|
sprite.sounds = sounds;
|
|
|
|
});
|
|
|
|
return Promise.all(costumePromises.concat(soundPromises)).then(() => target);
|
2017-01-27 20:05:54 -05:00
|
|
|
};
|
|
|
|
|
2016-12-30 10:19:58 -05:00
|
|
|
/**
|
2017-11-03 14:17:16 -04:00
|
|
|
* Deserialize the specified representation of a VM runtime and loads it into the provided runtime instance.
|
|
|
|
* TODO: parse extension info (also, design extension info storage...)
|
|
|
|
* @param {object} json - JSON representation of a VM runtime.
|
|
|
|
* @param {Runtime} runtime - Runtime instance
|
2018-02-16 00:44:23 -05:00
|
|
|
* @param {JSZip} zip - Sb3 file describing this project (to load assets from)
|
2017-11-03 14:17:16 -04:00
|
|
|
* @returns {Promise.<ImportedProject>} Promise that resolves to the list of targets after the project is deserialized
|
2016-12-30 10:19:58 -05:00
|
|
|
*/
|
2018-02-16 00:44:23 -05:00
|
|
|
const deserialize = function (json, runtime, zip) {
|
2017-11-03 14:17:16 -04:00
|
|
|
const extensions = {
|
|
|
|
extensionIDs: new Set(),
|
|
|
|
extensionURLs: new Map()
|
|
|
|
};
|
|
|
|
return Promise.all(
|
2018-02-16 00:44:23 -05:00
|
|
|
(json.targets || []).map(target => parseScratchObject(target, runtime, extensions, zip))
|
2017-11-03 14:17:16 -04:00
|
|
|
).then(targets => ({
|
|
|
|
targets,
|
|
|
|
extensions
|
|
|
|
}));
|
2016-12-30 10:19:58 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
serialize: serialize,
|
|
|
|
deserialize: deserialize
|
|
|
|
};
|