mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-01-14 11:39:59 -05:00
Can load saved sb3 files (including sounds and costumes that were modified in the 3.0 editors and saved in the sb3 zip when the project was saved). Tests still need to be fixed.
This commit is contained in:
parent
089df0a50f
commit
e0b23cde31
4 changed files with 158 additions and 6 deletions
|
@ -49,6 +49,7 @@
|
|||
"immutable": "3.8.1",
|
||||
"in-publish": "^2.0.0",
|
||||
"json": "^9.0.4",
|
||||
"jszip": "^3.1.5",
|
||||
"lodash.defaultsdeep": "4.6.0",
|
||||
"minilog": "3.1.0",
|
||||
"nets": "3.2.0",
|
||||
|
|
120
src/serialization/deserialize-assets.js
Normal file
120
src/serialization/deserialize-assets.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
const JSZip = require('jszip');
|
||||
const log = require('../util/log');
|
||||
|
||||
/**
|
||||
* Deserializes sound from file into storage cache so that it can
|
||||
* be loaded into the runtime.
|
||||
* @param {object} sound Descriptor for sound from sb3 file
|
||||
* @param {Runtime} runtime The runtime containing the storage to cache the sounds in
|
||||
* @param {JSZip} zip The zip containing the sound file being described by `sound`
|
||||
* @return {Promise} Promise that resolves after the described sound has been stored
|
||||
* into the runtime storage cache, the sound was already stored, or an error has
|
||||
* occurred.
|
||||
*/
|
||||
const deserializeSound = function (sound, runtime, zip) {
|
||||
const fileName = sound.md5; // The md5 property has the full file name
|
||||
const storage = runtime.storage;
|
||||
if (!storage) {
|
||||
log.error('No storage module present; cannot load sound asset: ', fileName);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const assetId = sound.assetId;
|
||||
|
||||
// TODO Is there a faster way to check that this asset
|
||||
// has already been initialized?
|
||||
if (storage.get(assetId)) {
|
||||
// This sound has already been cached.
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const soundFile = zip.file(fileName);
|
||||
if (!soundFile) {
|
||||
log.error(`Could not find sound file associated with the ${sound.name} sound.`);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
let dataFormat = null;
|
||||
if (sound.dataFormat.toLowerCase() === 'wav') {
|
||||
dataFormat = storage.DataFormat.WAV;
|
||||
}
|
||||
if (!JSZip.support.uint8array) {
|
||||
log.error('JSZip uint8array is not supported in this browser.');
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return soundFile.async('uint8array').then(data => {
|
||||
storage.builtinHelper.cache(
|
||||
storage.AssetType.Sound,
|
||||
dataFormat,
|
||||
data,
|
||||
assetId
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Deserializes costume from file into storage cache so that it can
|
||||
* be loaded into the runtime.
|
||||
* @param {object} costume Descriptor for costume from sb3 file
|
||||
* @param {Runtime} runtime The runtime containing the storage to cache the costumes in
|
||||
* @param {JSZip} zip The zip containing the costume file being described by `costume`
|
||||
* @return {Promise} Promise that resolves after the described costume has been stored
|
||||
* into the runtime storage cache, the costume was already stored, or an error has
|
||||
* occurred.
|
||||
*/
|
||||
const deserializeCostume = function (costume, runtime, zip) {
|
||||
const storage = runtime.storage;
|
||||
const assetId = costume.assetId;
|
||||
const fileName = costume.md5 ?
|
||||
costume.md5 :
|
||||
`${assetId}.${costume.dataFormat}`; // The md5 property has the full file name
|
||||
|
||||
if (!storage) {
|
||||
log.error('No storage module present; cannot load costume asset: ', fileName);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
|
||||
// TODO Is there a faster way to check that this asset
|
||||
// has already been initialized?
|
||||
if (storage.get(assetId)) {
|
||||
// This costume has already been cached.
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const costumeFile = zip.file(fileName);
|
||||
if (!costumeFile) {
|
||||
log.error(`Could not find costume file associated with the ${costume.name} costume.`);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
let dataFormat = null;
|
||||
let assetType = null;
|
||||
const costumeFormat = costume.dataFormat.toLowerCase();
|
||||
if (costumeFormat === 'svg') {
|
||||
dataFormat = storage.DataFormat.SVG;
|
||||
assetType = storage.AssetType.ImageVector;
|
||||
} else if (costumeFormat === 'png') {
|
||||
dataFormat = storage.DataFormat.PNG;
|
||||
assetType = storage.AssetType.ImageBitmap;
|
||||
} else {
|
||||
log.error(`Unexpected file format for costume: ${costumeFormat}`);
|
||||
}
|
||||
if (!JSZip.support.uint8array) {
|
||||
log.error('JSZip uint8array is not supported in this browser.');
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return costumeFile.async('uint8array').then(data => {
|
||||
storage.builtinHelper.cache(
|
||||
assetType,
|
||||
dataFormat,
|
||||
data,
|
||||
assetId
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
deserializeSound,
|
||||
deserializeCostume
|
||||
};
|
|
@ -11,6 +11,7 @@ const Variable = require('../engine/variable');
|
|||
|
||||
const {loadCostume} = require('../import/load-costume.js');
|
||||
const {loadSound} = require('../import/load-sound.js');
|
||||
const {deserializeCostume, deserializeSound} = require('./deserialize-assets.js');
|
||||
|
||||
/**
|
||||
* @typedef {object} ImportedProject
|
||||
|
@ -53,9 +54,10 @@ const serialize = function (runtime) {
|
|||
* @param {!object} object From-JSON "Scratch object:" sprite, stage, watcher.
|
||||
* @param {!Runtime} runtime Runtime object to load all structures into.
|
||||
* @param {ImportedExtensionsInfo} extensions - (in/out) parsed extension information will be stored here.
|
||||
* @param {JSZip} zip Sb3 file describing this project (to load assets from)
|
||||
* @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
|
||||
*/
|
||||
const parseScratchObject = function (object, runtime, extensions) {
|
||||
const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||
if (!object.hasOwnProperty('name')) {
|
||||
// Watcher/monitor - skip this object until those are implemented in VM.
|
||||
// @todo
|
||||
|
@ -99,13 +101,17 @@ const parseScratchObject = function (object, runtime, extensions) {
|
|||
(costumeSource.assetType && costumeSource.assetType.runtimeFormat) || // older format
|
||||
'png'; // if all else fails, guess that it might be a PNG
|
||||
const costumeMd5 = `${costumeSource.assetId}.${dataFormat}`;
|
||||
return loadCostume(costumeMd5, costume, runtime);
|
||||
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
|
||||
});
|
||||
// Sounds from JSON
|
||||
const soundPromises = (object.sounds || []).map(soundSource => {
|
||||
const sound = {
|
||||
format: soundSource.format,
|
||||
fileUrl: soundSource.fileUrl,
|
||||
// fileUrl: soundSource.fileUrl,
|
||||
rate: soundSource.rate,
|
||||
sampleCount: soundSource.sampleCount,
|
||||
soundID: soundSource.soundID,
|
||||
|
@ -113,7 +119,10 @@ const parseScratchObject = function (object, runtime, extensions) {
|
|||
md5: soundSource.md5,
|
||||
data: null
|
||||
};
|
||||
return loadSound(sound, runtime);
|
||||
return deserializeSound(soundSource, runtime, zip)
|
||||
.then(() => loadSound(sound, runtime));
|
||||
// Only attempt to load the sound after the deserialization
|
||||
// process has been completed.
|
||||
});
|
||||
// Create the first clone, and load its run-state from JSON.
|
||||
const target = sprite.createClone();
|
||||
|
@ -169,15 +178,16 @@ const parseScratchObject = function (object, runtime, extensions) {
|
|||
* TODO: parse extension info (also, design extension info storage...)
|
||||
* @param {object} json - JSON representation of a VM runtime.
|
||||
* @param {Runtime} runtime - Runtime instance
|
||||
* @param {JSZip} zip - Sb3 file describing this project (to load assets from)
|
||||
* @returns {Promise.<ImportedProject>} Promise that resolves to the list of targets after the project is deserialized
|
||||
*/
|
||||
const deserialize = function (json, runtime) {
|
||||
const deserialize = function (json, runtime, zip) {
|
||||
const extensions = {
|
||||
extensionIDs: new Set(),
|
||||
extensionURLs: new Map()
|
||||
};
|
||||
return Promise.all(
|
||||
(json.targets || []).map(target => parseScratchObject(target, runtime, extensions))
|
||||
(json.targets || []).map(target => parseScratchObject(target, runtime, extensions, zip))
|
||||
).then(targets => ({
|
||||
targets,
|
||||
extensions
|
||||
|
|
|
@ -185,6 +185,27 @@ class VirtualMachine extends EventEmitter {
|
|||
return this.fromJSON(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a project from a Scratch 3.0 sb3 file containing a project json
|
||||
* and all of the sound and costume files.
|
||||
* @param {JSZip} sb3File The sb3 file representing the project to load.
|
||||
* @return {!Promise} Promise that resolves after targets are installed.
|
||||
*/
|
||||
loadProjectLocal (sb3File) {
|
||||
// TODO need to handle sb2 files as well, and will possibly merge w/
|
||||
// above function
|
||||
return sb3File.file('project.json').async('string')
|
||||
.then(json => {
|
||||
// TODO look at promise documentation to do this on success,
|
||||
// but something else on error
|
||||
|
||||
json = JSON.parse(json); // TODO catch errors here (validation)
|
||||
return sb3.deserialize(json, this.runtime, sb3File)
|
||||
.then(({targets, extensions}) =>
|
||||
this.installTargets(targets, extensions, true));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a project from the Scratch web site, by ID.
|
||||
* @param {string} id - the ID of the project to download, as a string.
|
||||
|
|
Loading…
Reference in a new issue