mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-25 15:32:40 -05:00
Block and variable compression. No need to serialize uid of block and shadow if they are the same, reduce duplication of information.
This commit is contained in:
parent
4b62a938c4
commit
334058b081
2 changed files with 204 additions and 16 deletions
|
@ -8,6 +8,7 @@ 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 {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,16 +26,63 @@ 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 INPUT_SAME_BLOCK_SHADOW = 1;
|
||||||
|
const INPUT_BLOCK_NO_SHADOW = 2;
|
||||||
|
const INPUT_DIFF_BLOCK_SHADOW = 3;
|
||||||
|
// haven't found a case where block = null, but shadow is present...
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
const serializeBlock = function (block) {
|
const serializeBlock = function (block) {
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
obj.id = block.id;
|
// obj.id = block.id; // don't need this, it's the index of this block in its containing object
|
||||||
obj.opcode = block.opcode;
|
obj.opcode = block.opcode;
|
||||||
obj.next = block.next;
|
if (block.next) obj.next = block.next; // don't serialize next if null
|
||||||
|
// obj.next = if (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; // I think we don't need this either..
|
||||||
if (block.topLevel) {
|
if (block.topLevel) {
|
||||||
if (block.x) {
|
if (block.x) {
|
||||||
obj.x = Math.round(block.x);
|
obj.x = Math.round(block.x);
|
||||||
|
@ -92,11 +140,48 @@ const serializeSound = function (sound) {
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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.name, v.value];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (v.type === Variable.LIST_TYPE) {
|
||||||
|
obj.lists[varId] = [v.name, v.value];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should be a scalar type
|
||||||
|
obj.variables[varId] = [v.name]
|
||||||
|
let val = v.value;
|
||||||
|
if ((typeof val !== 'string') && (typeof val !== 'number')) {
|
||||||
|
log.info(`Variable: ${v.name} had value ${val} of type: ${typeof val} converting to string`);
|
||||||
|
val = JSON.stringify(val);
|
||||||
|
}
|
||||||
|
obj.variables[varId].push(val);
|
||||||
|
// Some hacked blocks have booleans as variable values
|
||||||
|
// (typeof v.value === 'string') || (typeof v.value === 'number') ?
|
||||||
|
// v.value : JSON.stringify(v.value)];
|
||||||
|
if (v.isPersistent) obj.variables[varId].push(true);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
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 = 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);
|
||||||
|
@ -142,6 +227,59 @@ const serialize = function (runtime) {
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deserializeInputs = function (inputs) {
|
||||||
|
// 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 = inputDescArr[1];
|
||||||
|
} else if (blockShadowInfo === INPUT_BLOCK_NO_SHADOW) {
|
||||||
|
block = inputDescArr[1];
|
||||||
|
} else { // assume INPUT_DIFF_BLOCK_SHADOW
|
||||||
|
block = inputDescArr[1];
|
||||||
|
shadow = inputDescArr[2];
|
||||||
|
}
|
||||||
|
obj[inputName] = {
|
||||||
|
name: inputName,
|
||||||
|
block: block,
|
||||||
|
shadow: shadow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
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.
|
||||||
|
@ -170,6 +308,13 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
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];
|
||||||
|
blockJSON.id = blockId; // add id back to block since it wasn't serialized
|
||||||
|
const serializedInputs = blockJSON.inputs;
|
||||||
|
const deserializedInputs = deserializeInputs(serializedInputs);
|
||||||
|
blockJSON.inputs = deserializedInputs;
|
||||||
|
const serializedFields = blockJSON.fields;
|
||||||
|
const deserializedFields = deserializeFields(serializedFields);
|
||||||
|
blockJSON.fields = deserializedFields;
|
||||||
blocks.createBlock(blockJSON);
|
blocks.createBlock(blockJSON);
|
||||||
|
|
||||||
const dotIndex = blockJSON.opcode.indexOf('.');
|
const dotIndex = blockJSON.opcode.indexOf('.');
|
||||||
|
@ -238,18 +383,55 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
const target = sprite.createClone();
|
const target = sprite.createClone();
|
||||||
// Load target properties from JSON.
|
// Load target properties from JSON.
|
||||||
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(
|
let newVariable;
|
||||||
|
if (Array.isArray(variable)) {
|
||||||
|
newVariable = new Variable(
|
||||||
|
varId, // var id is the index of the variable desc array in the variables obj
|
||||||
|
variable[0], // name of the variable
|
||||||
|
Variable.SCALAR_TYPE, // type of the variable
|
||||||
|
(variable.length === 3) ? variable[2] : false // isPersistent/isCloud
|
||||||
|
);
|
||||||
|
newVariable.value = variable[1];
|
||||||
|
} else {
|
||||||
|
newVariable = new Variable(
|
||||||
variable.id,
|
variable.id,
|
||||||
variable.name,
|
variable.name,
|
||||||
variable.type,
|
variable.type,
|
||||||
variable.isPersistent
|
variable.isPersistent
|
||||||
);
|
);
|
||||||
newVariable.value = variable.value;
|
newVariable.value = variable.value;
|
||||||
|
}
|
||||||
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[0],
|
||||||
|
Variable.BROADCAST_MESSAGE_TYPE,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
newBroadcast.value = broadcast[1];
|
||||||
|
target.variables[newBroadcast.id] = newBroadcast;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (object.hasOwnProperty('x')) {
|
if (object.hasOwnProperty('x')) {
|
||||||
target.x = object.x;
|
target.x = object.x;
|
||||||
}
|
}
|
||||||
|
|
|
@ -256,7 +256,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: 9 // best compression (level 1 would be best speed)
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue