Skip to content

Commit

Permalink
lazy: export assets from unloaded lazy sprites
Browse files Browse the repository at this point in the history
  • Loading branch information
GarboMuffin committed Jan 2, 2025
1 parent 2822b69 commit 420aa4f
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 18 deletions.
35 changes: 35 additions & 0 deletions src/sprites/tw-lazy-sprite.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,41 @@ class LazySprite extends Sprite {
this.runtime.disposeTarget(target);
}
}

/**
* Fetch all assets used in this sprite for serialization.
* @returns {Promise<Array<{fileName: string; fileContents: Uint8Array}>>}
*/
async serializeAssets () {
// Loaded lazily to avoid circular dependencies
const deserializeAssets = require('../serialization/deserialize-assets');

const promises = [];
for (const costume of this.object.costumes) {
if (!costume.asset) {
promises.push(deserializeAssets.deserializeCostume(costume, this.runtime, assetCacheSingleton));
}
}
for (const sound of this.object.sounds) {
if (!sound.asset) {
promises.push(deserializeAssets.deserializeSound(sound, this.runtime, assetCacheSingleton));
}
}
await Promise.all(promises);

const allResources = [
...this.object.costumes,
...this.object.sounds
];

return allResources
.map(o => (o.broken ? o.broken.asset : o.asset))
.filter(asset => asset)
.map(asset => ({
fileName: `${asset.assetId}.${asset.dataFormat}`,
fileContent: asset.data
}));
}
}

// Export enums
Expand Down
51 changes: 34 additions & 17 deletions src/virtual-machine.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,9 @@ class VirtualMachine extends EventEmitter {
}

/**
* @returns {JSZip} JSZip zip object representing the sb3.
* @returns {Promise<JSZip>} JSZip zip object representing the sb3.
*/
_saveProjectZip () {
async _saveProjectZip () {
const projectJson = this.toJSON();

// TODO want to eventually move zip creation out of here, and perhaps
Expand All @@ -525,7 +525,7 @@ class VirtualMachine extends EventEmitter {

// Put everything in a zip file
zip.file('project.json', projectJson);
this._addFileDescsToZip(this.serializeAssets(), zip);
this._addFileDescsToZip(await this.serializeAssets(), zip);

// Use a fixed modification date for the files in the zip instead of letting JSZip use the
// current time to avoid a very small metadata leak and make zipping deterministic. The magic
Expand All @@ -543,8 +543,9 @@ class VirtualMachine extends EventEmitter {
* @param {JSZip.OutputType} [type] JSZip output type. Defaults to 'blob' for Scratch compatibility.
* @returns {Promise<unknown>} Compressed sb3 file in a type determined by the type argument.
*/
saveProjectSb3 (type) {
return this._saveProjectZip().generateAsync({
async saveProjectSb3 (type) {
const zip = await this._saveProjectZip();
return zip.generateAsync({
type: type || 'blob',
mimeType: 'application/x.scratch.sb3',
compression: 'DEFLATE'
Expand All @@ -553,11 +554,12 @@ class VirtualMachine extends EventEmitter {

/**
* @param {JSZip.OutputType} [type] JSZip output type. Defaults to 'arraybuffer'.
* @returns {StreamHelper} JSZip StreamHelper object generating the compressed sb3.
* @returns {Promise<StreamHelper>} JSZip StreamHelper object generating the compressed sb3.
* See: https://stuk.github.io/jszip/documentation/api_streamhelper.html
*/
saveProjectSb3Stream (type) {
return this._saveProjectZip().generateInternalStream({
async saveProjectSb3Stream (type) {
const zip = await this._saveProjectZip();
return zip.generateInternalStream({
type: type || 'arraybuffer',
mimeType: 'application/x.scratch.sb3',
compression: 'DEFLATE'
Expand Down Expand Up @@ -601,19 +603,34 @@ class VirtualMachine extends EventEmitter {

/**
* @param {string} targetId Optional ID of target to export
* @returns {Array<{fileName: string; fileContent: Uint8Array;}} list of file descs
* @returns {Promise<Array<{fileName: string; fileContent: Uint8Array;}>} list of file descs
*/
serializeAssets (targetId) {
const costumeDescs = serializeCostumes(this.runtime, targetId);
const soundDescs = serializeSounds(this.runtime, targetId);
async serializeAssets (targetId) {
// This will include non-lazy sprites and loaded lazy sprites.
const loadedCostumeDescs = serializeCostumes(this.runtime, targetId);
const loadedSoundDescs = serializeSounds(this.runtime, targetId);

// Assume every target needs all fonts.
const fontDescs = this.runtime.fontManager.serializeAssets().map(asset => ({
fileName: `${asset.assetId}.${asset.dataFormat}`,
fileContent: asset.data
}));

// Fetch assets used by lazy sprites.
const unloadedSprites = this.runtime.lazySprites.filter(i => i.clones.length === 0);
const unloadedSpriteDescs = await Promise.all(unloadedSprites.map(s => s.serializeAssets()));
const flattenedUnloadedSpriteDescs = [];
for (const descs of unloadedSpriteDescs) {
for (const desc of descs) {
flattenedUnloadedSpriteDescs.push(desc);
}
}

return [
...costumeDescs,
...soundDescs,
...fontDescs
...loadedCostumeDescs,
...loadedSoundDescs,
...fontDescs,
...flattenedUnloadedSpriteDescs
];
}

Expand All @@ -637,12 +654,12 @@ class VirtualMachine extends EventEmitter {
* @return {object} A generated zip of the sprite and its assets in the format
* specified by optZipType or blob by default.
*/
exportSprite (targetId, optZipType) {
async exportSprite (targetId, optZipType) {
const spriteJson = this.toJSON(targetId);

const zip = new JSZip();
zip.file('sprite.json', spriteJson);
this._addFileDescsToZip(this.serializeAssets(targetId), zip);
this._addFileDescsToZip(await this.serializeAssets(targetId), zip);

return zip.generateAsync({
type: typeof optZipType === 'string' ? optZipType : 'blob',
Expand Down
6 changes: 5 additions & 1 deletion test/integration/tw_lazy.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ test('sb2 has no lazy sprites', t => {
for (const load of [true, false]) {
test(`export lazy sprites ${load ? 'after' : 'before'} loading`, t => {
const vm = new VM();
vm.attachStorage(makeTestStorage());
const fixture = fs.readFileSync(path.join(__dirname, '../fixtures/tw-lazy-simple.sb3'));

vm.loadProject(fixture).then(async () => {
Expand Down Expand Up @@ -216,7 +217,10 @@ for (const load of [true, false]) {
delete fixtureJSON.targets[1].layerOrder;

t.same(json.targets[1], fixtureJSON.targets[1]);


// Check for lazy loaded sprite's costume existing
t.not(zip.file('927d672925e7b99f7813735c484c6922.svg'), null);

t.end();
});
});
Expand Down

0 comments on commit 420aa4f

Please sign in to comment.