mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-07-09 20:43:59 -04:00
Merge pull request #1031 from kchadha/block_serialization_compression
Block serialization compression
This commit is contained in:
commit
8739a524eb
9 changed files with 634 additions and 52 deletions
|
@ -57,7 +57,7 @@
|
||||||
"promise": "8.0.1",
|
"promise": "8.0.1",
|
||||||
"scratch-audio": "latest",
|
"scratch-audio": "latest",
|
||||||
"scratch-blocks": "latest",
|
"scratch-blocks": "latest",
|
||||||
"scratch-parser": "latest",
|
"scratch-parser": "^3.0.0",
|
||||||
"scratch-render": "latest",
|
"scratch-render": "latest",
|
||||||
"scratch-storage": "^0.4.0",
|
"scratch-storage": "^0.4.0",
|
||||||
"script-loader": "0.7.2",
|
"script-loader": "0.7.2",
|
||||||
|
|
|
@ -74,7 +74,7 @@ class Scratch3DataBlocks {
|
||||||
addToList (args, util) {
|
addToList (args, util) {
|
||||||
const list = util.target.lookupOrCreateList(
|
const list = util.target.lookupOrCreateList(
|
||||||
args.LIST.id, args.LIST.name);
|
args.LIST.id, args.LIST.name);
|
||||||
list.value.push(args.ITEM);
|
if (list.value.length < Scratch3DataBlocks.LIST_ITEM_LIMIT) list.value.push(args.ITEM);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteOfList (args, util) {
|
deleteOfList (args, util) {
|
||||||
|
@ -98,7 +98,14 @@ class Scratch3DataBlocks {
|
||||||
if (index === Cast.LIST_INVALID) {
|
if (index === Cast.LIST_INVALID) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const listLimit = Scratch3DataBlocks.LIST_ITEM_LIMIT;
|
||||||
|
if (index > listLimit) return;
|
||||||
list.value.splice(index - 1, 0, item);
|
list.value.splice(index - 1, 0, item);
|
||||||
|
if (list.value.length > listLimit) {
|
||||||
|
// If inserting caused the list to grow larger than the limit,
|
||||||
|
// remove the last element in the list
|
||||||
|
list.value.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceItemOfList (args, util) {
|
replaceItemOfList (args, util) {
|
||||||
|
@ -144,6 +151,14 @@ class Scratch3DataBlocks {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type representation for list variables.
|
||||||
|
* @const {string}
|
||||||
|
*/
|
||||||
|
static get LIST_ITEM_LIMIT () {
|
||||||
|
return 200000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Scratch3DataBlocks;
|
module.exports = Scratch3DataBlocks;
|
||||||
|
|
|
@ -12,6 +12,10 @@ const log = require('../util/log');
|
||||||
*/
|
*/
|
||||||
const loadSoundFromAsset = function (sound, soundAsset, runtime) {
|
const loadSoundFromAsset = function (sound, soundAsset, runtime) {
|
||||||
sound.assetId = soundAsset.assetId;
|
sound.assetId = soundAsset.assetId;
|
||||||
|
if (!runtime.audioEngine) {
|
||||||
|
log.error('No audio engine present; cannot load sound asset: ', sound.md5);
|
||||||
|
return Promise.resolve(sound);
|
||||||
|
}
|
||||||
return runtime.audioEngine.decodeSound(Object.assign(
|
return runtime.audioEngine.decodeSound(Object.assign(
|
||||||
{},
|
{},
|
||||||
sound,
|
sound,
|
||||||
|
@ -35,10 +39,6 @@ const loadSound = function (sound, runtime) {
|
||||||
log.error('No storage module present; cannot load sound asset: ', sound.md5);
|
log.error('No storage module present; cannot load sound asset: ', sound.md5);
|
||||||
return Promise.resolve(sound);
|
return Promise.resolve(sound);
|
||||||
}
|
}
|
||||||
if (!runtime.audioEngine) {
|
|
||||||
log.error('No audio engine present; cannot load sound asset: ', sound.md5);
|
|
||||||
return Promise.resolve(sound);
|
|
||||||
}
|
|
||||||
const idParts = StringUtil.splitFirst(sound.md5, '.');
|
const idParts = StringUtil.splitFirst(sound.md5, '.');
|
||||||
const md5 = idParts[0];
|
const md5 = idParts[0];
|
||||||
const ext = idParts[1].toLowerCase();
|
const ext = idParts[1].toLowerCase();
|
||||||
|
|
|
@ -30,11 +30,7 @@ const deserializeSound = function (sound, runtime, zip, assetFileName) {
|
||||||
// This sound has already been cached.
|
// This sound has already been cached.
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
if (!zip) {
|
if (!zip) { // Zip will not be provided if loading project json from server
|
||||||
// TODO adding this case to make integration tests pass, need to rethink
|
|
||||||
// the entire structure of saving/loading here (w.r.t. differences between
|
|
||||||
// loading from local zip file or from server)
|
|
||||||
log.error('Zipped assets were not provided.');
|
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
const soundFile = zip.file(fileName);
|
const soundFile = zip.file(fileName);
|
||||||
|
@ -93,11 +89,7 @@ const deserializeCostume = function (costume, runtime, zip, assetFileName) {
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!zip) {
|
if (!zip) { // Zip will not be provided if loading project json from server
|
||||||
// TODO adding this case to make integration tests pass, need to rethink
|
|
||||||
// the entire structure of saving/loading here (w.r.t. differences between
|
|
||||||
// loading from local zip file or from server)
|
|
||||||
log.error('Zipped assets were not provided.');
|
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,14 +98,11 @@ const deserializeCostume = function (costume, runtime, zip, assetFileName) {
|
||||||
log.error(`Could not find costume file associated with the ${costume.name} costume.`);
|
log.error(`Could not find costume file associated with the ${costume.name} costume.`);
|
||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
let dataFormat = null;
|
|
||||||
let assetType = null;
|
let assetType = null;
|
||||||
const costumeFormat = costume.dataFormat.toLowerCase();
|
const costumeFormat = costume.dataFormat.toLowerCase();
|
||||||
if (costumeFormat === 'svg') {
|
if (costumeFormat === 'svg') {
|
||||||
dataFormat = storage.DataFormat.SVG;
|
|
||||||
assetType = storage.AssetType.ImageVector;
|
assetType = storage.AssetType.ImageVector;
|
||||||
} else if (costumeFormat === 'png') {
|
} else if (['png', 'bmp', 'jpeg', 'jpg', 'gif'].indexOf(costumeFormat) >= 0) {
|
||||||
dataFormat = storage.DataFormat.PNG;
|
|
||||||
assetType = storage.AssetType.ImageBitmap;
|
assetType = storage.AssetType.ImageBitmap;
|
||||||
} else {
|
} else {
|
||||||
log.error(`Unexpected file format for costume: ${costumeFormat}`);
|
log.error(`Unexpected file format for costume: ${costumeFormat}`);
|
||||||
|
@ -126,7 +115,8 @@ const deserializeCostume = function (costume, runtime, zip, assetFileName) {
|
||||||
return costumeFile.async('uint8array').then(data => {
|
return costumeFile.async('uint8array').then(data => {
|
||||||
storage.builtinHelper.cache(
|
storage.builtinHelper.cache(
|
||||||
assetType,
|
assetType,
|
||||||
dataFormat,
|
// TODO eventually we want to map non-png's to their actual file types?
|
||||||
|
costumeFormat,
|
||||||
data,
|
data,
|
||||||
assetId
|
assetId
|
||||||
);
|
);
|
||||||
|
|
|
@ -215,7 +215,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
|
||||||
const sprite = new Sprite(blocks, runtime);
|
const sprite = new Sprite(blocks, runtime);
|
||||||
// Sprite/stage name from JSON.
|
// Sprite/stage name from JSON.
|
||||||
if (object.hasOwnProperty('objName')) {
|
if (object.hasOwnProperty('objName')) {
|
||||||
sprite.name = object.objName;
|
sprite.name = topLevel ? 'Stage' : object.objName;
|
||||||
}
|
}
|
||||||
// Costumes from JSON.
|
// Costumes from JSON.
|
||||||
const costumePromises = [];
|
const costumePromises = [];
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* @fileoverview
|
* @fileoverview
|
||||||
* Partial implementation of a SB3 serializer and deserializer. Parses provided
|
* An SB3 serializer and deserializer. Parses provided
|
||||||
* JSON and then generates all needed scratch-vm runtime structures.
|
* JSON and then generates all needed scratch-vm runtime structures.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -8,6 +8,8 @@ const vmPackage = require('../../package.json');
|
||||||
const Blocks = require('../engine/blocks');
|
const Blocks = require('../engine/blocks');
|
||||||
const Sprite = require('../sprites/sprite');
|
const Sprite = require('../sprites/sprite');
|
||||||
const Variable = require('../engine/variable');
|
const Variable = require('../engine/variable');
|
||||||
|
const log = require('../util/log');
|
||||||
|
const uid = require('../util/uid');
|
||||||
|
|
||||||
const {loadCostume} = require('../import/load-costume.js');
|
const {loadCostume} = require('../import/load-costume.js');
|
||||||
const {loadSound} = require('../import/load-sound.js');
|
const {loadSound} = require('../import/load-sound.js');
|
||||||
|
@ -25,14 +27,156 @@ const {deserializeCostume, deserializeSound} = require('./deserialize-assets.js'
|
||||||
* @property {Map.<string, string>} extensionURLs - map of ID => URL from project metadata. May not match extensionIDs.
|
* @property {Map.<string, string>} extensionURLs - map of ID => URL from project metadata. May not match extensionIDs.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const serializeBlock = function (block) {
|
// Constants used during serialization and deserialization
|
||||||
|
const INPUT_SAME_BLOCK_SHADOW = 1; // unobscured shadow
|
||||||
|
const INPUT_BLOCK_NO_SHADOW = 2; // no shadow
|
||||||
|
const INPUT_DIFF_BLOCK_SHADOW = 3; // obscured shadow
|
||||||
|
// There shouldn't be a case where block is null, but shadow is present...
|
||||||
|
|
||||||
|
// Constants referring to 'primitive' blocks that are usually shadows,
|
||||||
|
// or in the case of variables and lists, appear quite often in projects
|
||||||
|
// math_number
|
||||||
|
const MATH_NUM_PRIMITIVE = 4; // there's no reason these constants can't collide
|
||||||
|
// math_positive_number
|
||||||
|
const POSITIVE_NUM_PRIMITIVE = 5; // with the above, but removing duplication for clarity
|
||||||
|
// math_whole_number
|
||||||
|
const WHOLE_NUM_PRIMITIVE = 6;
|
||||||
|
// math_integer
|
||||||
|
const INTEGER_NUM_PRIMITIVE = 7;
|
||||||
|
// math_angle
|
||||||
|
const ANGLE_NUM_PRIMITIVE = 8;
|
||||||
|
// colour_picker
|
||||||
|
const COLOR_PICKER_PRIMITIVE = 9;
|
||||||
|
// text
|
||||||
|
const TEXT_PRIMITIVE = 10;
|
||||||
|
// event_broadcast_menu
|
||||||
|
const BROADCAST_PRIMITIVE = 11;
|
||||||
|
// data_variable
|
||||||
|
const VAR_PRIMITIVE = 12;
|
||||||
|
// data_listcontents
|
||||||
|
const LIST_PRIMITIVE = 13;
|
||||||
|
|
||||||
|
// Map block opcodes to the above primitives and the name of the field we can use
|
||||||
|
// to find the value of the field
|
||||||
|
const primitiveOpcodeInfoMap = {
|
||||||
|
math_number: [MATH_NUM_PRIMITIVE, 'NUM'],
|
||||||
|
math_positive_number: [POSITIVE_NUM_PRIMITIVE, 'NUM'],
|
||||||
|
math_whole_number: [WHOLE_NUM_PRIMITIVE, 'NUM'],
|
||||||
|
math_integer: [INTEGER_NUM_PRIMITIVE, 'NUM'],
|
||||||
|
math_angle: [ANGLE_NUM_PRIMITIVE, 'NUM'],
|
||||||
|
colour_picker: [COLOR_PICKER_PRIMITIVE, 'COLOUR'],
|
||||||
|
text: [TEXT_PRIMITIVE, 'TEXT'],
|
||||||
|
event_broadcast_menu: [BROADCAST_PRIMITIVE, 'BROADCAST_OPTION'],
|
||||||
|
data_variable: [VAR_PRIMITIVE, 'VARIABLE'],
|
||||||
|
data_listcontents: [LIST_PRIMITIVE, 'LIST']
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes primitives described above into a more compact format
|
||||||
|
* @param {object} block the block to serialize
|
||||||
|
* @return {array} An array representing the information in the block,
|
||||||
|
* or null if the given block is not one of the primitives described above.
|
||||||
|
*/
|
||||||
|
const serializePrimitiveBlock = function (block) {
|
||||||
|
// Returns an array represeting a primitive block or null if not one of
|
||||||
|
// the primitive types above
|
||||||
|
if (primitiveOpcodeInfoMap.hasOwnProperty(block.opcode)) {
|
||||||
|
const primitiveInfo = primitiveOpcodeInfoMap[block.opcode];
|
||||||
|
const primitiveConstant = primitiveInfo[0];
|
||||||
|
const fieldName = primitiveInfo[1];
|
||||||
|
const field = block.fields[fieldName];
|
||||||
|
const primitiveDesc = [primitiveConstant, field.value];
|
||||||
|
if (block.opcode === 'event_broadcast_menu') {
|
||||||
|
primitiveDesc.push(field.id);
|
||||||
|
} else if (block.opcode === 'data_variable' || block.opcode === 'data_listcontents') {
|
||||||
|
primitiveDesc.push(field.id);
|
||||||
|
if (block.topLevel) {
|
||||||
|
primitiveDesc.push(block.x ? Math.round(block.x) : 0);
|
||||||
|
primitiveDesc.push(block.y ? Math.round(block.y) : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return primitiveDesc;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the inputs field of a block in a compact form using
|
||||||
|
* constants described above to represent the relationship between the
|
||||||
|
* inputs of this block (e.g. if there is an unobscured shadow, an obscured shadow
|
||||||
|
* -- a block plugged into a droppable input -- or, if there is just a block).
|
||||||
|
* Based on this relationship, serializes the ids of the block and shadow (if present)
|
||||||
|
*
|
||||||
|
* @param {object} inputs The inputs to serialize
|
||||||
|
* @return {object} An object representing the serialized inputs
|
||||||
|
*/
|
||||||
|
const serializeInputs = function (inputs) {
|
||||||
|
const obj = Object.create(null);
|
||||||
|
for (const inputName in inputs) {
|
||||||
|
if (!inputs.hasOwnProperty(inputName)) continue;
|
||||||
|
// if block and shadow refer to the same block, only serialize one
|
||||||
|
if (inputs[inputName].block === inputs[inputName].shadow) {
|
||||||
|
// has block and shadow, and they are the same
|
||||||
|
obj[inputName] = [
|
||||||
|
INPUT_SAME_BLOCK_SHADOW,
|
||||||
|
inputs[inputName].block
|
||||||
|
];
|
||||||
|
} else if (inputs[inputName].shadow === null) {
|
||||||
|
// does not have shadow
|
||||||
|
obj[inputName] = [
|
||||||
|
INPUT_BLOCK_NO_SHADOW,
|
||||||
|
inputs[inputName].block
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// block and shadow are both present and are different
|
||||||
|
obj[inputName] = [
|
||||||
|
INPUT_DIFF_BLOCK_SHADOW,
|
||||||
|
inputs[inputName].block,
|
||||||
|
inputs[inputName].shadow
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the fields of a block in a more compact form.
|
||||||
|
* @param {object} fields The fields object to serialize
|
||||||
|
* @return {object} An object representing the serialized fields
|
||||||
|
*/
|
||||||
|
const serializeFields = function (fields) {
|
||||||
|
const obj = Object.create(null);
|
||||||
|
for (const fieldName in fields) {
|
||||||
|
if (!fields.hasOwnProperty(fieldName)) continue;
|
||||||
|
obj[fieldName] = [fields[fieldName].value];
|
||||||
|
if (fields[fieldName].hasOwnProperty('id')) {
|
||||||
|
obj[fieldName].push(fields[fieldName].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the given block in the SB3 format with some compression of inputs,
|
||||||
|
* fields, and primitives.
|
||||||
|
* @param {object} block The block to serialize
|
||||||
|
* @return {object | array} A serialized representation of the block. This is an
|
||||||
|
* array if the block is one of the primitive types described above or an object,
|
||||||
|
* if not.
|
||||||
|
*/
|
||||||
|
const serializeBlock = function (block) {
|
||||||
|
const serializedPrimitive = serializePrimitiveBlock(block);
|
||||||
|
if (serializedPrimitive) return serializedPrimitive;
|
||||||
|
// If serializedPrimitive is null, proceed with serializing a non-primitive block
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
obj.id = block.id;
|
|
||||||
obj.opcode = block.opcode;
|
obj.opcode = block.opcode;
|
||||||
|
// NOTE: this is extremely important to serialize even if null;
|
||||||
|
// not serializing `next: null` results in strange behavior with block
|
||||||
|
// execution
|
||||||
obj.next = block.next;
|
obj.next = block.next;
|
||||||
obj.parent = block.parent;
|
obj.parent = block.parent;
|
||||||
obj.inputs = block.inputs;
|
obj.inputs = serializeInputs(block.inputs);
|
||||||
obj.fields = block.fields;
|
obj.fields = serializeFields(block.fields);
|
||||||
obj.topLevel = block.topLevel ? block.topLevel : false;
|
obj.topLevel = block.topLevel ? block.topLevel : false;
|
||||||
obj.shadow = block.shadow;
|
obj.shadow = block.shadow;
|
||||||
if (block.topLevel) {
|
if (block.topLevel) {
|
||||||
|
@ -49,15 +193,108 @@ const serializeBlock = function (block) {
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compresses the serialized inputs replacing block/shadow ids that refer to
|
||||||
|
* one of the primitives with the primitive itself. E.g.
|
||||||
|
*
|
||||||
|
* blocks: {
|
||||||
|
* aUidForMyBlock: {
|
||||||
|
* inputs: {
|
||||||
|
* MYINPUT: [1, 'aUidForAnUnobscuredShadowPrimitive']
|
||||||
|
* }
|
||||||
|
* },
|
||||||
|
* aUidForAnUnobscuredShadowPrimitive: [4, 10]
|
||||||
|
* // the above is a primitive representing a 'math_number' with value 10
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* becomes:
|
||||||
|
*
|
||||||
|
* blocks: {
|
||||||
|
* aUidForMyBlock: {
|
||||||
|
* inputs: {
|
||||||
|
* MYINPUT: [1, [4, 10]]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* Note: this function modifies the given blocks object in place
|
||||||
|
* @param {object} block The block with inputs to compress
|
||||||
|
* @param {objec} blocks The object containing all the blocks currently getting serialized
|
||||||
|
* @return {object} The serialized block with compressed inputs
|
||||||
|
*/
|
||||||
|
const compressInputTree = function (block, blocks) {
|
||||||
|
// This is the second pass on the block
|
||||||
|
// so the inputs field should be an object of key - array pairs
|
||||||
|
const serializedInputs = block.inputs;
|
||||||
|
for (const inputName in serializedInputs) {
|
||||||
|
// don't need to check for hasOwnProperty because of how we constructed
|
||||||
|
// inputs
|
||||||
|
const currInput = serializedInputs[inputName];
|
||||||
|
// traverse currInput skipping the first element, which describes whether the block
|
||||||
|
// and shadow are the same
|
||||||
|
for (let i = 1; i < currInput.length; i++) {
|
||||||
|
if (!currInput[i]) continue; // need this check b/c block/shadow can be null
|
||||||
|
const blockOrShadowID = currInput[i];
|
||||||
|
// replace element of currInput directly
|
||||||
|
// (modifying input block directly)
|
||||||
|
const blockOrShadow = blocks[blockOrShadowID];
|
||||||
|
if (Array.isArray(blockOrShadow)) {
|
||||||
|
currInput[i] = blockOrShadow;
|
||||||
|
// Modifying blocks in place!
|
||||||
|
delete blocks[blockOrShadowID];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the given blocks object (representing all the blocks for the target
|
||||||
|
* currently being serialized.)
|
||||||
|
* @param {object} blocks The blocks to be serialized
|
||||||
|
* @return {object} The serialized blocks with compressed inputs and compressed
|
||||||
|
* primitives.
|
||||||
|
*/
|
||||||
const serializeBlocks = function (blocks) {
|
const serializeBlocks = function (blocks) {
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
for (const blockID in blocks) {
|
for (const blockID in blocks) {
|
||||||
if (!blocks.hasOwnProperty(blockID)) continue;
|
if (!blocks.hasOwnProperty(blockID)) continue;
|
||||||
obj[blockID] = serializeBlock(blocks[blockID]);
|
obj[blockID] = serializeBlock(blocks[blockID], blocks);
|
||||||
|
}
|
||||||
|
// once we have completed a first pass, do a second pass on block inputs
|
||||||
|
for (const blockID in obj) {
|
||||||
|
// don't need to do the hasOwnProperty check here since we
|
||||||
|
// created an object that doesn't get extra properties/functions
|
||||||
|
const serializedBlock = obj[blockID];
|
||||||
|
// caution, this function deletes parts of this object in place as
|
||||||
|
// it's traversing it
|
||||||
|
obj[blockID] = compressInputTree(serializedBlock, obj);
|
||||||
|
// second pass on connecting primitives to serialized inputs directly
|
||||||
|
}
|
||||||
|
// Do one last pass and remove any top level shadows (these are caused by
|
||||||
|
// a bug: LLK/scratch-vm#1011, and this pass should be removed once that is
|
||||||
|
// completely fixed)
|
||||||
|
for (const blockID in obj) {
|
||||||
|
const serializedBlock = obj[blockID];
|
||||||
|
// If the current block is serialized as a primitive (e.g. it's an array
|
||||||
|
// instead of an object), AND it is not one of the top level primitives
|
||||||
|
// e.g. variable getter or list getter, then it should be deleted as it's
|
||||||
|
// a shadow block, and there are no blocks that reference it, otherwise
|
||||||
|
// they would have been compressed in the last pass)
|
||||||
|
if (Array.isArray(serializedBlock) &&
|
||||||
|
[VAR_PRIMITIVE, LIST_PRIMITIVE].indexOf(serializedBlock) < 0) {
|
||||||
|
log.warn(`Found an unexpected top level primitive with block ID: ${
|
||||||
|
blockID}; deleting it from serialized blocks.`);
|
||||||
|
delete obj[blockID];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the given costume.
|
||||||
|
* @param {object} costume The costume to be serialized.
|
||||||
|
* @return {object} A serialized representation of the costume.
|
||||||
|
*/
|
||||||
const serializeCostume = function (costume) {
|
const serializeCostume = function (costume) {
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
obj.assetId = costume.assetId;
|
obj.assetId = costume.assetId;
|
||||||
|
@ -69,17 +306,22 @@ const serializeCostume = function (costume) {
|
||||||
// but that change should be made carefully since it is very
|
// but that change should be made carefully since it is very
|
||||||
// pervasive
|
// pervasive
|
||||||
obj.md5ext = costume.md5;
|
obj.md5ext = costume.md5;
|
||||||
obj.dataFormat = costume.dataFormat;
|
obj.dataFormat = costume.dataFormat.toLowerCase();
|
||||||
obj.rotationCenterX = costume.rotationCenterX;
|
obj.rotationCenterX = costume.rotationCenterX;
|
||||||
obj.rotationCenterY = costume.rotationCenterY;
|
obj.rotationCenterY = costume.rotationCenterY;
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the given sound.
|
||||||
|
* @param {object} sound The sound to be serialized.
|
||||||
|
* @return {object} A serialized representation of the sound.
|
||||||
|
*/
|
||||||
const serializeSound = function (sound) {
|
const serializeSound = function (sound) {
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
obj.assetId = sound.assetId;
|
obj.assetId = sound.assetId;
|
||||||
obj.name = sound.name;
|
obj.name = sound.name;
|
||||||
obj.dataFormat = sound.dataFormat;
|
obj.dataFormat = sound.dataFormat.toLowerCase();
|
||||||
obj.format = sound.format;
|
obj.format = sound.format;
|
||||||
obj.rate = sound.rate;
|
obj.rate = sound.rate;
|
||||||
obj.sampleCount = sound.sampleCount;
|
obj.sampleCount = sound.sampleCount;
|
||||||
|
@ -92,17 +334,63 @@ const serializeSound = function (sound) {
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the given variables object.
|
||||||
|
* @param {object} variables The variables to be serialized.
|
||||||
|
* @return {object} A serialized representation of the variables. They get
|
||||||
|
* separated by type to compress the representation of each given variable and
|
||||||
|
* reduce duplicate information.
|
||||||
|
*/
|
||||||
|
const serializeVariables = function (variables) {
|
||||||
|
const obj = Object.create(null);
|
||||||
|
// separate out variables into types at the top level so we don't have
|
||||||
|
// keep track of a type for each
|
||||||
|
obj.variables = Object.create(null);
|
||||||
|
obj.lists = Object.create(null);
|
||||||
|
obj.broadcasts = Object.create(null);
|
||||||
|
for (const varId in variables) {
|
||||||
|
const v = variables[varId];
|
||||||
|
if (v.type === Variable.BROADCAST_MESSAGE_TYPE) {
|
||||||
|
obj.broadcasts[varId] = v.value; // name and value is the same for broadcast msgs
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (v.type === Variable.LIST_TYPE) {
|
||||||
|
obj.lists[varId] = [v.name, v.value];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize the given target. Only serialize properties that are necessary
|
||||||
|
* for saving and loading this target.
|
||||||
|
* @param {object} target The target to be serialized.
|
||||||
|
* @return {object} A serialized representation of the given target.
|
||||||
|
*/
|
||||||
const serializeTarget = function (target) {
|
const serializeTarget = function (target) {
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
obj.isStage = target.isStage;
|
obj.isStage = target.isStage;
|
||||||
obj.name = target.name;
|
obj.name = obj.isStage ? 'Stage' : target.name;
|
||||||
obj.variables = target.variables; // This means that uids for variables will persist across saves/loads
|
const vars = serializeVariables(target.variables);
|
||||||
|
obj.variables = vars.variables;
|
||||||
|
obj.lists = vars.lists;
|
||||||
|
obj.broadcasts = vars.broadcasts;
|
||||||
obj.blocks = serializeBlocks(target.blocks);
|
obj.blocks = serializeBlocks(target.blocks);
|
||||||
obj.currentCostume = target.currentCostume;
|
obj.currentCostume = target.currentCostume;
|
||||||
obj.costumes = target.costumes.map(serializeCostume);
|
obj.costumes = target.costumes.map(serializeCostume);
|
||||||
obj.sounds = target.sounds.map(serializeSound);
|
obj.sounds = target.sounds.map(serializeSound);
|
||||||
if (!obj.isStage) {
|
if (target.hasOwnProperty('volume')) obj.volume = target.volume;
|
||||||
// Stage does not need the following properties
|
if (obj.isStage) { // Only the stage should have these properties
|
||||||
|
if (target.hasOwnProperty('tempo')) obj.tempo = target.tempo;
|
||||||
|
if (target.hasOwnProperty('videoTransparency')) obj.videoTransparency = target.videoTransparency;
|
||||||
|
if (target.hasOwnProperty('videoState')) obj.videoState = target.videoState;
|
||||||
|
} else { // The stage does not need the following properties, but sprites should
|
||||||
obj.visible = target.visible;
|
obj.visible = target.visible;
|
||||||
obj.x = target.x;
|
obj.x = target.x;
|
||||||
obj.y = target.y;
|
obj.y = target.y;
|
||||||
|
@ -117,7 +405,7 @@ const serializeTarget = function (target) {
|
||||||
/**
|
/**
|
||||||
* Serializes the specified VM runtime.
|
* Serializes the specified VM runtime.
|
||||||
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
||||||
* @return {object} Serialized runtime instance.
|
* @return {object} Serialized runtime instance.
|
||||||
*/
|
*/
|
||||||
const serialize = function (runtime) {
|
const serialize = function (runtime) {
|
||||||
// Fetch targets
|
// Fetch targets
|
||||||
|
@ -142,6 +430,226 @@ const serialize = function (runtime) {
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize a block input descriptors. This is either a
|
||||||
|
* block id or a serialized primitive, e.g. an array
|
||||||
|
* (see serializePrimitiveBlock function).
|
||||||
|
* @param {string | array} inputDescOrId The block input descriptor to be serialized.
|
||||||
|
* @param {string} parentId The id of the parent block for this input block.
|
||||||
|
* @param {boolean} isShadow Whether or not this input block is a shadow.
|
||||||
|
* @param {object} blocks The entire blocks object currently in the process of getting serialized.
|
||||||
|
* @return {object} The deserialized input descriptor.
|
||||||
|
*/
|
||||||
|
const deserializeInputDesc = function (inputDescOrId, parentId, isShadow, blocks) {
|
||||||
|
if (!Array.isArray(inputDescOrId)) return inputDescOrId;
|
||||||
|
const primitiveObj = Object.create(null);
|
||||||
|
const newId = uid();
|
||||||
|
primitiveObj.id = newId;
|
||||||
|
primitiveObj.next = null;
|
||||||
|
primitiveObj.parent = parentId;
|
||||||
|
primitiveObj.shadow = isShadow;
|
||||||
|
primitiveObj.inputs = Object.create(null);
|
||||||
|
// need a reference to parent id
|
||||||
|
switch (inputDescOrId[0]) {
|
||||||
|
case MATH_NUM_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'math_number';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
NUM: {
|
||||||
|
name: 'NUM',
|
||||||
|
value: inputDescOrId[1]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
primitiveObj.topLevel = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case POSITIVE_NUM_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'math_positive_number';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
NUM: {
|
||||||
|
name: 'NUM',
|
||||||
|
value: inputDescOrId[1]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
primitiveObj.topLevel = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WHOLE_NUM_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'math_whole_number';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
NUM: {
|
||||||
|
name: 'NUM',
|
||||||
|
value: inputDescOrId[1]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
primitiveObj.topLevel = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case INTEGER_NUM_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'math_integer';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
NUM: {
|
||||||
|
name: 'NUM',
|
||||||
|
value: inputDescOrId[1]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
primitiveObj.topLevel = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ANGLE_NUM_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'math_angle';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
NUM: {
|
||||||
|
name: 'NUM',
|
||||||
|
value: inputDescOrId[1]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
primitiveObj.topLevel = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case COLOR_PICKER_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'colour_picker';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
COLOUR: {
|
||||||
|
name: 'COLOUR',
|
||||||
|
value: inputDescOrId[1]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
primitiveObj.topLevel = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TEXT_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'text';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
TEXT: {
|
||||||
|
name: 'TEXT',
|
||||||
|
value: inputDescOrId[1]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
primitiveObj.topLevel = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BROADCAST_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'event_broadcast_menu';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
BROADCAST_OPTION: {
|
||||||
|
name: 'BROADCAST_OPTION',
|
||||||
|
value: inputDescOrId[1],
|
||||||
|
id: inputDescOrId[2],
|
||||||
|
variableType: Variable.BROADCAST_MESSAGE_TYPE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
primitiveObj.topLevel = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case VAR_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'data_variable';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
VARIABLE: {
|
||||||
|
name: 'VARIABLE',
|
||||||
|
value: inputDescOrId[1],
|
||||||
|
id: inputDescOrId[2],
|
||||||
|
variableType: Variable.SCALAR_TYPE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (inputDescOrId.length > 3) {
|
||||||
|
primitiveObj.topLevel = true;
|
||||||
|
primitiveObj.x = inputDescOrId[3];
|
||||||
|
primitiveObj.y = inputDescOrId[4];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case LIST_PRIMITIVE: {
|
||||||
|
primitiveObj.opcode = 'data_listcontents';
|
||||||
|
primitiveObj.fields = {
|
||||||
|
LIST: {
|
||||||
|
name: 'LIST',
|
||||||
|
value: inputDescOrId[1],
|
||||||
|
id: inputDescOrId[2],
|
||||||
|
variableType: Variable.LIST_TYPE
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (inputDescOrId.length > 3) {
|
||||||
|
primitiveObj.topLevel = true;
|
||||||
|
primitiveObj.x = inputDescOrId[3];
|
||||||
|
primitiveObj.y = inputDescOrId[4];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
log.error(`Found unknown primitive type during deserialization: ${JSON.stringify(inputDescOrId)}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blocks[newId] = primitiveObj;
|
||||||
|
return newId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize the given block inputs.
|
||||||
|
* @param {object} inputs The inputs to deserialize.
|
||||||
|
* @param {string} parentId The block id of the parent block
|
||||||
|
* @param {object} blocks The object representing the entire set of blocks currently
|
||||||
|
* in the process of getting deserialized.
|
||||||
|
* @return {object} The deserialized and uncompressed inputs.
|
||||||
|
*/
|
||||||
|
const deserializeInputs = function (inputs, parentId, blocks) {
|
||||||
|
// Explicitly not using Object.create(null) here
|
||||||
|
// because we call prototype functions later in the vm
|
||||||
|
const obj = {};
|
||||||
|
for (const inputName in inputs) {
|
||||||
|
if (!inputs.hasOwnProperty(inputName)) continue;
|
||||||
|
const inputDescArr = inputs[inputName];
|
||||||
|
let block = null;
|
||||||
|
let shadow = null;
|
||||||
|
const blockShadowInfo = inputDescArr[0];
|
||||||
|
if (blockShadowInfo === INPUT_SAME_BLOCK_SHADOW) {
|
||||||
|
// block and shadow are the same id, and only one is provided
|
||||||
|
block = shadow = deserializeInputDesc(inputDescArr[1], parentId, true, blocks);
|
||||||
|
} else if (blockShadowInfo === INPUT_BLOCK_NO_SHADOW) {
|
||||||
|
block = deserializeInputDesc(inputDescArr[1], parentId, false, blocks);
|
||||||
|
} else { // assume INPUT_DIFF_BLOCK_SHADOW
|
||||||
|
block = deserializeInputDesc(inputDescArr[1], parentId, false, blocks);
|
||||||
|
shadow = deserializeInputDesc(inputDescArr[2], parentId, true, blocks);
|
||||||
|
}
|
||||||
|
obj[inputName] = {
|
||||||
|
name: inputName,
|
||||||
|
block: block,
|
||||||
|
shadow: shadow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize the given block fields.
|
||||||
|
* @param {object} fields The fields to be deserialized
|
||||||
|
* @return {object} The deserialized and uncompressed block fields.
|
||||||
|
*/
|
||||||
|
const deserializeFields = function (fields) {
|
||||||
|
// Explicitly not using Object.create(null) here
|
||||||
|
// because we call prototype functions later in the vm
|
||||||
|
const obj = {};
|
||||||
|
for (const fieldName in fields) {
|
||||||
|
if (!fields.hasOwnProperty(fieldName)) continue;
|
||||||
|
const fieldDescArr = fields[fieldName];
|
||||||
|
obj[fieldName] = {
|
||||||
|
name: fieldName,
|
||||||
|
value: fieldDescArr[0]
|
||||||
|
};
|
||||||
|
if (fieldDescArr.length > 1) {
|
||||||
|
obj[fieldName].id = fieldDescArr[1];
|
||||||
|
}
|
||||||
|
if (fieldName === 'BROADCAST_OPTION') {
|
||||||
|
obj[fieldName].variableType = Variable.BROADCAST_MESSAGE_TYPE;
|
||||||
|
} else if (fieldName === 'VARIABLE') {
|
||||||
|
obj[fieldName].variableType = Variable.SCALAR_TYPE;
|
||||||
|
} else if (fieldName === 'LIST') {
|
||||||
|
obj[fieldName].variableType = Variable.LIST_TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
* Parse a single "Scratch object" and create all its in-memory VM objects.
|
||||||
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
||||||
|
@ -167,6 +675,26 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
sprite.name = object.name;
|
sprite.name = object.name;
|
||||||
}
|
}
|
||||||
if (object.hasOwnProperty('blocks')) {
|
if (object.hasOwnProperty('blocks')) {
|
||||||
|
for (const blockId in object.blocks) {
|
||||||
|
if (!object.blocks.hasOwnProperty(blockId)) continue;
|
||||||
|
const blockJSON = object.blocks[blockId];
|
||||||
|
if (Array.isArray(blockJSON)) {
|
||||||
|
// this is one of the primitives
|
||||||
|
// delete the old entry in object.blocks and replace it w/the
|
||||||
|
// deserialized object
|
||||||
|
delete object.blocks[blockId];
|
||||||
|
deserializeInputDesc(blockJSON, null, false, object.blocks);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
blockJSON.id = blockId; // add id back to block since it wasn't serialized
|
||||||
|
const serializedInputs = blockJSON.inputs;
|
||||||
|
const deserializedInputs = deserializeInputs(serializedInputs, blockId, object.blocks);
|
||||||
|
blockJSON.inputs = deserializedInputs;
|
||||||
|
const serializedFields = blockJSON.fields;
|
||||||
|
const deserializedFields = deserializeFields(serializedFields);
|
||||||
|
blockJSON.fields = deserializedFields;
|
||||||
|
}
|
||||||
|
// Take a second pass to create objects and add extensions
|
||||||
for (const blockId in object.blocks) {
|
for (const blockId in object.blocks) {
|
||||||
if (!object.blocks.hasOwnProperty(blockId)) continue;
|
if (!object.blocks.hasOwnProperty(blockId)) continue;
|
||||||
const blockJSON = object.blocks[blockId];
|
const blockJSON = object.blocks[blockId];
|
||||||
|
@ -178,7 +706,6 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
extensions.extensionIDs.add(extensionId);
|
extensions.extensionIDs.add(extensionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// console.log(blocks);
|
|
||||||
}
|
}
|
||||||
// Costumes from JSON.
|
// Costumes from JSON.
|
||||||
const costumePromises = (object.costumes || []).map(costumeSource => {
|
const costumePromises = (object.costumes || []).map(costumeSource => {
|
||||||
|
@ -237,19 +764,58 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
// Create the first clone, and load its run-state from JSON.
|
// Create the first clone, and load its run-state from JSON.
|
||||||
const target = sprite.createClone();
|
const target = sprite.createClone();
|
||||||
// Load target properties from JSON.
|
// Load target properties from JSON.
|
||||||
|
if (object.hasOwnProperty('tempo')) {
|
||||||
|
target.tempo = object.tempo;
|
||||||
|
}
|
||||||
|
if (object.hasOwnProperty('volume')) {
|
||||||
|
target.volume = object.volume;
|
||||||
|
}
|
||||||
|
if (object.hasOwnProperty('videoTransparency')) {
|
||||||
|
target.videoTransparency = object.videoTransparency;
|
||||||
|
}
|
||||||
|
if (object.hasOwnProperty('videoState')) {
|
||||||
|
target.videoState = object.videoState;
|
||||||
|
}
|
||||||
if (object.hasOwnProperty('variables')) {
|
if (object.hasOwnProperty('variables')) {
|
||||||
for (const j in object.variables) {
|
for (const varId in object.variables) {
|
||||||
const variable = object.variables[j];
|
const variable = object.variables[varId];
|
||||||
const newVariable = new Variable(
|
const newVariable = new Variable(
|
||||||
variable.id,
|
varId, // var id is the index of the variable desc array in the variables obj
|
||||||
variable.name,
|
variable[0], // name of the variable
|
||||||
variable.type,
|
Variable.SCALAR_TYPE, // type of the variable
|
||||||
variable.isPersistent
|
(variable.length === 3) ? variable[2] : false // isPersistent/isCloud
|
||||||
);
|
);
|
||||||
newVariable.value = variable.value;
|
newVariable.value = variable[1];
|
||||||
target.variables[newVariable.id] = newVariable;
|
target.variables[newVariable.id] = newVariable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (object.hasOwnProperty('lists')) {
|
||||||
|
for (const listId in object.lists) {
|
||||||
|
const list = object.lists[listId];
|
||||||
|
const newList = new Variable(
|
||||||
|
listId,
|
||||||
|
list[0],
|
||||||
|
Variable.LIST_TYPE,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
newList.value = list[1];
|
||||||
|
target.variables[newList.id] = newList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (object.hasOwnProperty('broadcasts')) {
|
||||||
|
for (const broadcastId in object.broadcasts) {
|
||||||
|
const broadcast = object.broadcasts[broadcastId];
|
||||||
|
const newBroadcast = new Variable(
|
||||||
|
broadcastId,
|
||||||
|
broadcast,
|
||||||
|
Variable.BROADCAST_MESSAGE_TYPE,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
// no need to explicitly set the value, variable constructor
|
||||||
|
// sets the value to the same as the name for broadcast msgs
|
||||||
|
target.variables[newBroadcast.id] = newBroadcast;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (object.hasOwnProperty('x')) {
|
if (object.hasOwnProperty('x')) {
|
||||||
target.x = object.x;
|
target.x = object.x;
|
||||||
}
|
}
|
||||||
|
@ -285,7 +851,6 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deserialize the specified representation of a VM runtime and loads it into the provided runtime instance.
|
* 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 {object} json - JSON representation of a VM runtime.
|
||||||
* @param {Runtime} runtime - Runtime instance
|
* @param {Runtime} runtime - Runtime instance
|
||||||
* @param {JSZip} zip - Sb3 file describing this project (to load assets from)
|
* @param {JSZip} zip - Sb3 file describing this project (to load assets from)
|
||||||
|
|
|
@ -215,9 +215,9 @@ class RenderedTarget extends Target {
|
||||||
*/
|
*/
|
||||||
static get VIDEO_STATE () {
|
static get VIDEO_STATE () {
|
||||||
return {
|
return {
|
||||||
'OFF': 'off',
|
OFF: 'off',
|
||||||
'ON': 'on',
|
ON: 'on',
|
||||||
'ON-FLIPPED': 'on-flipped'
|
ON_FLIPPED: 'on-flipped'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1007,7 +1007,12 @@ class RenderedTarget extends Target {
|
||||||
variables: this.variables,
|
variables: this.variables,
|
||||||
lists: this.lists,
|
lists: this.lists,
|
||||||
costumes: costumes,
|
costumes: costumes,
|
||||||
sounds: this.getSounds()
|
sounds: this.getSounds(),
|
||||||
|
tempo: this.tempo,
|
||||||
|
volume: this.volume,
|
||||||
|
videoTransparency: this.videoTransparency,
|
||||||
|
videoState: this.videoState
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -257,7 +257,6 @@ class VirtualMachine extends EventEmitter {
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
// Put everything in a zip file
|
// Put everything in a zip file
|
||||||
// TODO compression?
|
|
||||||
zip.file('project.json', projectJson);
|
zip.file('project.json', projectJson);
|
||||||
for (let i = 0; i < soundDescs.length; i++) {
|
for (let i = 0; i < soundDescs.length; i++) {
|
||||||
const currSound = soundDescs[i];
|
const currSound = soundDescs[i];
|
||||||
|
@ -268,7 +267,13 @@ class VirtualMachine extends EventEmitter {
|
||||||
zip.file(currCostume.fileName, currCostume.fileContent);
|
zip.file(currCostume.fileName, currCostume.fileContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return zip.generateAsync({type: 'blob'});
|
return zip.generateAsync({
|
||||||
|
type: 'blob',
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
compressionOptions: {
|
||||||
|
level: 6 // Tradeoff between best speed (1) and best compression (9)
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
const test = require('tap').test;
|
const test = require('tap').test;
|
||||||
|
const path = require('path');
|
||||||
const VirtualMachine = require('../../src/index');
|
const VirtualMachine = require('../../src/index');
|
||||||
const sb3 = require('../../src/serialization/sb3');
|
const sb3 = require('../../src/serialization/sb3');
|
||||||
const demoSb3 = require('../fixtures/demo.json');
|
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||||
|
const projectPath = path.resolve(__dirname, '../fixtures/clone-cleanup.sb2');
|
||||||
|
|
||||||
test('serialize', t => {
|
test('serialize', t => {
|
||||||
const vm = new VirtualMachine();
|
const vm = new VirtualMachine();
|
||||||
vm.loadProject(JSON.stringify(demoSb3))
|
vm.loadProject(readFileToBuffer(projectPath))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const result = sb3.serialize(vm.runtime);
|
const result = sb3.serialize(vm.runtime);
|
||||||
// @todo Analyze
|
// @todo Analyze
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue