diff --git a/src/serialization/sb2.js b/src/serialization/sb2.js index 2e5a556af..42a612b4c 100644 --- a/src/serialization/sb2.js +++ b/src/serialization/sb2.js @@ -391,47 +391,28 @@ const parseMonitorObject = (object, runtime, targets, extensions) => { }; /** - * Parse a single "Scratch object" and create all its in-memory VM objects. - * TODO: parse the "info" section, especially "savedExtensions" + * Parse the assets of a single "Scratch object" and load them. This + * preprocesses objects to support loading the data for those assets over a + * network while the objects are further processed into Blocks, Sprites, and a + * list of needed Extensions. * @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 {boolean} topLevel - Whether this is the top-level object (stage). * @param {?object} zip - Optional zipped assets for local file import - * @return {!Promise.>} Promise for the loaded targets when ready, or null for unsupported objects. + * @return {?{costumePromises:Array.,soundPromises:Array.,children:object}} + * Object of arrays of promises and child objects for asset objects used in + * Sprites. null for unsupported objects. */ -const parseScratchObject = function (object, runtime, extensions, topLevel, zip) { +const parseScratchAssets = function (object, runtime, topLevel, zip) { if (!object.hasOwnProperty('objName')) { - if (object.hasOwnProperty('listName')) { - // Shim these objects so they can be processed as monitors - object.cmd = 'contentsOfList:'; - object.param = object.listName; - object.mode = 'list'; - } - // Defer parsing monitors until targets are all parsed - object.deferredMonitor = true; - return Promise.resolve(object); + // Skip parsing monitors. Or any other objects missing objName. + return null; } - // Blocks container for this object. - const blocks = new Blocks(runtime); - // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite. - const sprite = new Sprite(blocks, runtime); - // Sprite/stage name from JSON. - if (object.hasOwnProperty('objName')) { - if (topLevel && object.objName !== 'Stage') { - for (const child of object.children) { - if (!child.hasOwnProperty('objName') && child.target === object.objName) { - child.target = 'Stage'; - } - } - object.objName = 'Stage'; - } + const assets = {costumePromises: [], soundPromises: [], children: []}; - sprite.name = object.objName; - } // Costumes from JSON. - const costumePromises = []; + const costumePromises = assets.costumePromises; if (object.hasOwnProperty('costumes')) { for (let i = 0; i < object.costumes.length; i++) { const costumeSource = object.costumes[i]; @@ -476,7 +457,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) } } // Sounds from JSON - const soundPromises = []; + const soundPromises = assets.soundPromises; if (object.hasOwnProperty('sounds')) { for (let s = 0; s < object.sounds.length; s++) { const soundSource = object.sounds[s]; @@ -505,12 +486,72 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) // the file name of the sound should be the soundID (provided from the project.json) // followed by the file ext const assetFileName = `${soundSource.soundID}.${ext}`; - soundPromises.push(deserializeSound(sound, runtime, zip, assetFileName) - .then(() => loadSound(sound, runtime, sprite)) - ); + soundPromises.push(deserializeSound(sound, runtime, zip, assetFileName).then(() => sound)); } } + // The stage will have child objects; recursively process them. + const childrenAssets = assets.children; + if (object.children) { + for (let m = 0; m < object.children.length; m++) { + childrenAssets.push(parseScratchAssets(object.children[m], runtime, false, zip)); + } + } + + return assets; +}; + +/** + * Parse a single "Scratch object" and create all its in-memory VM objects. + * TODO: parse the "info" section, especially "savedExtensions" + * @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 {boolean} topLevel - Whether this is the top-level object (stage). + * @param {?object} zip - Optional zipped assets for local file import + * @param {object} assets - Promises for assets of this scratch object grouped + * into costumes and sounds + * @return {!Promise.>} Promise for the loaded targets when ready, or null for unsupported objects. + */ +const parseScratchObject = function (object, runtime, extensions, topLevel, zip, assets) { + if (!object.hasOwnProperty('objName')) { + if (object.hasOwnProperty('listName')) { + // Shim these objects so they can be processed as monitors + object.cmd = 'contentsOfList:'; + object.param = object.listName; + object.mode = 'list'; + } + // Defer parsing monitors until targets are all parsed + object.deferredMonitor = true; + return Promise.resolve(object); + } + + // Blocks container for this object. + const blocks = new Blocks(runtime); + // @todo: For now, load all Scratch objects (stage/sprites) as a Sprite. + const sprite = new Sprite(blocks, runtime); + // Sprite/stage name from JSON. + if (object.hasOwnProperty('objName')) { + if (topLevel && object.objName !== 'Stage') { + for (const child of object.children) { + if (!child.hasOwnProperty('objName') && child.target === object.objName) { + child.target = 'Stage'; + } + } + object.objName = 'Stage'; + } + + sprite.name = object.objName; + } + // Costumes from JSON. + const costumePromises = assets.costumePromises; + // Sounds from JSON + const soundPromises = assets.soundPromises; + for (let s = 0; s < soundPromises.length; s++) { + soundPromises[s] = soundPromises[s] + .then(sound => loadSound(sound, runtime, sprite)); + } + // Create the first clone, and load its run-state from JSON. const target = sprite.createClone(topLevel ? StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER); @@ -703,7 +744,9 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip) const childrenPromises = []; if (object.children) { for (let m = 0; m < object.children.length; m++) { - childrenPromises.push(parseScratchObject(object.children[m], runtime, extensions, false, zip)); + childrenPromises.push( + parseScratchObject(object.children[m], runtime, extensions, false, zip, assets.children[m]) + ); } } @@ -810,7 +853,13 @@ const sb2import = function (json, runtime, optForceSprite, zip) { extensionIDs: new Set(), extensionURLs: new Map() }; - return parseScratchObject(json, runtime, extensions, !optForceSprite, zip) + return Promise.resolve(parseScratchAssets(json, runtime, !optForceSprite, zip)) + // Force this promise to wait for the next loop in the js tick. Let + // storage have some time to send off asset requests. + .then(assets => Promise.resolve(assets)) + .then(assets => ( + parseScratchObject(json, runtime, extensions, !optForceSprite, zip, assets) + )) .then(reorderParsedTargets) .then(targets => ({ targets,