mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-07-02 17:20:32 -04:00
Update storage and update 'loadSound' to handle null asset from storage. Track additional metadata for broken costumes: 'bitmapResolution' and 'dataFormat'.
This commit is contained in:
parent
11f938f8a9
commit
4679d06ac0
8 changed files with 168 additions and 43 deletions
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -15811,9 +15811,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scratch-storage": {
|
"scratch-storage": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/scratch-storage/-/scratch-storage-2.0.1.tgz",
|
||||||
"integrity": "sha512-eLqI5bBWTS1d43BY3zSzJYerBfdwa2l5myLD+IASkGN8eBJtW+/CDsKQC0FtI6xV9Afb7req9eeikHlPYczIuw==",
|
"integrity": "sha512-1Z4sR6jwhpcaFeOY9W5l/u3KKzGKDQcV0WH77OVQ6FFpxXZuKcE/PcXukKnu/PozB/l6QvX2fSADjoXNJ6hbOQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"arraybuffer-loader": "^1.0.3",
|
"arraybuffer-loader": "^1.0.3",
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
"scratch-l10n": "3.14.20220510031559",
|
"scratch-l10n": "3.14.20220510031559",
|
||||||
"scratch-render": "0.1.0-prerelease.20211028200436",
|
"scratch-render": "0.1.0-prerelease.20211028200436",
|
||||||
"scratch-render-fonts": "1.0.0-prerelease.20210401210003",
|
"scratch-render-fonts": "1.0.0-prerelease.20210401210003",
|
||||||
"scratch-storage": "2.0.0",
|
"scratch-storage": "2.0.1",
|
||||||
"scratch-svg-renderer": "0.2.0-prerelease.20210727023023",
|
"scratch-svg-renderer": "0.2.0-prerelease.20210727023023",
|
||||||
"script-loader": "0.7.2",
|
"script-loader": "0.7.2",
|
||||||
"stats.js": "0.17.0",
|
"stats.js": "0.17.0",
|
||||||
|
|
|
@ -256,6 +256,8 @@ const handleCostumeLoadError = function (costume, runtime) {
|
||||||
const oldAssetId = costume.assetId;
|
const oldAssetId = costume.assetId;
|
||||||
const oldRotationX = costume.rotationCenterX;
|
const oldRotationX = costume.rotationCenterX;
|
||||||
const oldRotationY = costume.rotationCenterY;
|
const oldRotationY = costume.rotationCenterY;
|
||||||
|
const oldBitmapResolution = costume.bitmapResolution;
|
||||||
|
const oldDataFormat = costume.dataFormat;
|
||||||
|
|
||||||
const AssetType = runtime.storage.AssetType;
|
const AssetType = runtime.storage.AssetType;
|
||||||
const isVector = costume.dataFormat === AssetType.ImageVector.runtimeFormat;
|
const isVector = costume.dataFormat === AssetType.ImageVector.runtimeFormat;
|
||||||
|
@ -265,7 +267,7 @@ const handleCostumeLoadError = function (costume, runtime) {
|
||||||
runtime.storage.defaultAssetId.ImageVector :
|
runtime.storage.defaultAssetId.ImageVector :
|
||||||
runtime.storage.defaultAssetId.ImageBitmap;
|
runtime.storage.defaultAssetId.ImageBitmap;
|
||||||
costume.asset = runtime.storage.get(costume.assetId);
|
costume.asset = runtime.storage.get(costume.assetId);
|
||||||
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
|
costume.md5 = `${costume.assetId}.${costume.asset.dataFormat}`;
|
||||||
|
|
||||||
const defaultCostumePromise = (isVector) ?
|
const defaultCostumePromise = (isVector) ?
|
||||||
loadVector_(costume, runtime) : loadBitmap_(costume, runtime);
|
loadVector_(costume, runtime) : loadBitmap_(costume, runtime);
|
||||||
|
@ -273,13 +275,15 @@ const handleCostumeLoadError = function (costume, runtime) {
|
||||||
return defaultCostumePromise.then(loadedCostume => {
|
return defaultCostumePromise.then(loadedCostume => {
|
||||||
loadedCostume.broken = {};
|
loadedCostume.broken = {};
|
||||||
loadedCostume.broken.assetId = oldAssetId;
|
loadedCostume.broken.assetId = oldAssetId;
|
||||||
loadedCostume.broken.md5 = `${oldAssetId}.${costume.dataFormat}`;
|
loadedCostume.broken.md5 = `${oldAssetId}.${oldDataFormat}`;
|
||||||
|
|
||||||
// Should be null if we got here because the costume was missing
|
// Should be null if we got here because the costume was missing
|
||||||
loadedCostume.broken.asset = oldAsset;
|
loadedCostume.broken.asset = oldAsset;
|
||||||
|
loadedCostume.broken.dataFormat = oldDataFormat;
|
||||||
|
|
||||||
loadedCostume.broken.rotationCenterX = oldRotationX;
|
loadedCostume.broken.rotationCenterX = oldRotationX;
|
||||||
loadedCostume.broken.rotationCenterY = oldRotationY;
|
loadedCostume.broken.rotationCenterY = oldRotationY;
|
||||||
|
loadedCostume.broken.bitmapResolution = oldBitmapResolution;
|
||||||
return loadedCostume;
|
return loadedCostume;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -38,6 +38,40 @@ const loadSoundFromAsset = function (sound, soundAsset, runtime, soundBank) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle sound loading errors by replacing the runtime sound with the
|
||||||
|
// default sound from storage, but keeping track of the original sound metadata
|
||||||
|
// in a `broken` field
|
||||||
|
const handleSoundLoadError = function (sound, runtime, soundBank) {
|
||||||
|
// Keep track of the old asset information until we're done loading the default sound
|
||||||
|
const oldAsset = sound.asset; // could be null
|
||||||
|
const oldAssetId = sound.assetId;
|
||||||
|
const oldSample = sound.sampleCount;
|
||||||
|
const oldRate = sound.rate;
|
||||||
|
const oldFormat = sound.format;
|
||||||
|
const oldDataFormat = sound.dataFormat;
|
||||||
|
|
||||||
|
// Use default asset if original fails to load
|
||||||
|
sound.assetId = runtime.storage.defaultAssetId.Sound;
|
||||||
|
sound.asset = runtime.storage.get(sound.assetId);
|
||||||
|
sound.md5 = `${sound.assetId}.${sound.asset.dataFormat}`;
|
||||||
|
|
||||||
|
return loadSoundFromAsset(sound, sound.asset, runtime, soundBank).then(loadedSound => {
|
||||||
|
loadedSound.broken = {};
|
||||||
|
loadedSound.broken.assetId = oldAssetId;
|
||||||
|
loadedSound.broken.md5 = `${oldAssetId}.${oldDataFormat}`;
|
||||||
|
|
||||||
|
// Should be null if we got here because the sound was missing
|
||||||
|
loadedSound.broken.asset = oldAsset;
|
||||||
|
|
||||||
|
loadedSound.broken.sampleCount = oldSample;
|
||||||
|
loadedSound.broken.rate = oldRate;
|
||||||
|
loadedSound.broken.format = oldFormat;
|
||||||
|
loadedSound.broken.dataFormat = oldDataFormat;
|
||||||
|
|
||||||
|
return loadedSound;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a sound's asset into memory asynchronously.
|
* Load a sound's asset into memory asynchronously.
|
||||||
* @param {!object} sound - the Scratch sound object.
|
* @param {!object} sound - the Scratch sound object.
|
||||||
|
@ -60,13 +94,13 @@ const loadSound = function (sound, runtime, soundBank) {
|
||||||
(sound.asset && Promise.resolve(sound.asset)) ||
|
(sound.asset && Promise.resolve(sound.asset)) ||
|
||||||
runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
|
runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
|
||||||
).then(soundAsset => {
|
).then(soundAsset => {
|
||||||
if (!soundAsset) {
|
|
||||||
log.warn('Failed to find sound data: ', sound);
|
|
||||||
// TODO add missing sound error handling that adds the "gray question sound"
|
|
||||||
return sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
sound.asset = soundAsset;
|
sound.asset = soundAsset;
|
||||||
|
|
||||||
|
if (!soundAsset) {
|
||||||
|
log.warn('Failed to find sound data: ', sound.md5);
|
||||||
|
return handleSoundLoadError(sound, runtime, soundBank);
|
||||||
|
}
|
||||||
|
|
||||||
return loadSoundFromAsset(sound, soundAsset, runtime, soundBank);
|
return loadSoundFromAsset(sound, soundAsset, runtime, soundBank);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -346,29 +346,24 @@ const serializeBlocks = function (blocks) {
|
||||||
const serializeCostume = function (costume) {
|
const serializeCostume = function (costume) {
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
obj.name = costume.name;
|
obj.name = costume.name;
|
||||||
obj.bitmapResolution = costume.bitmapResolution;
|
|
||||||
obj.dataFormat = costume.dataFormat.toLowerCase();
|
const costumeToSerialize = costume.broken || costume;
|
||||||
if (costume.broken) {
|
|
||||||
obj.assetId = costume.broken.assetId;
|
obj.bitmapResolution = costumeToSerialize.bitmapResolution;
|
||||||
|
obj.dataFormat = costumeToSerialize.dataFormat.toLowerCase();
|
||||||
// serialize this property with the name 'md5ext' because that's
|
|
||||||
// what it's actually referring to. TODO runtime objects need to be
|
obj.assetId = costumeToSerialize.assetId;
|
||||||
// updated to actually refer to this as 'md5ext' instead of 'md5'
|
|
||||||
// but that change should be made carefully since it is very
|
// serialize this property with the name 'md5ext' because that's
|
||||||
// pervasive
|
// what it's actually referring to. TODO runtime objects need to be
|
||||||
obj.md5ext = (costume.broken.md5);
|
// updated to actually refer to this as 'md5ext' instead of 'md5'
|
||||||
|
// but that change should be made carefully since it is very
|
||||||
obj.rotationCenterX = costume.broken.rotationCenterX;
|
// pervasive
|
||||||
obj.rotationCenterY = costume.broken.rotationCenterY;
|
obj.md5ext = costumeToSerialize.md5;
|
||||||
} else {
|
|
||||||
obj.assetId = costume.assetId;
|
obj.rotationCenterX = costumeToSerialize.rotationCenterX;
|
||||||
|
obj.rotationCenterY = costumeToSerialize.rotationCenterY;
|
||||||
// See related comment above
|
|
||||||
obj.md5ext = costume.md5;
|
|
||||||
|
|
||||||
obj.rotationCenterX = costume.rotationCenterX;
|
|
||||||
obj.rotationCenterY = costume.rotationCenterY;
|
|
||||||
}
|
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -379,18 +374,21 @@ const serializeCostume = function (costume) {
|
||||||
*/
|
*/
|
||||||
const serializeSound = function (sound) {
|
const serializeSound = function (sound) {
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
obj.assetId = sound.assetId;
|
|
||||||
obj.name = sound.name;
|
obj.name = sound.name;
|
||||||
obj.dataFormat = sound.dataFormat.toLowerCase();
|
|
||||||
obj.format = sound.format;
|
const soundToSerialize = sound.broken || sound;
|
||||||
obj.rate = sound.rate;
|
|
||||||
obj.sampleCount = sound.sampleCount;
|
obj.assetId = soundToSerialize.assetId;
|
||||||
|
obj.dataFormat = soundToSerialize.dataFormat.toLowerCase();
|
||||||
|
obj.format = soundToSerialize.format;
|
||||||
|
obj.rate = soundToSerialize.rate;
|
||||||
|
obj.sampleCount = soundToSerialize.sampleCount;
|
||||||
// serialize this property with the name 'md5ext' because that's
|
// serialize this property with the name 'md5ext' because that's
|
||||||
// what it's actually referring to. TODO runtime objects need to be
|
// what it's actually referring to. TODO runtime objects need to be
|
||||||
// updated to actually refer to this as 'md5ext' instead of 'md5'
|
// updated to actually refer to this as 'md5ext' instead of 'md5'
|
||||||
// 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 = sound.md5;
|
obj.md5ext = soundToSerialize.md5;
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -366,9 +366,11 @@ class VirtualMachine extends EventEmitter {
|
||||||
const vm = this;
|
const vm = this;
|
||||||
const promise = storage.load(storage.AssetType.Project, id);
|
const promise = storage.load(storage.AssetType.Project, id);
|
||||||
promise.then(projectAsset => {
|
promise.then(projectAsset => {
|
||||||
if (projectAsset) {
|
if (!projectAsset) {
|
||||||
return vm.loadProject(projectAsset.data);
|
log.error("Failed to fetch project with id: ", id);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
return vm.loadProject(projectAsset.data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
test/fixtures/missing_sound.sb3
vendored
Normal file
BIN
test/fixtures/missing_sound.sb3
vendored
Normal file
Binary file not shown.
87
test/integration/sb3_missing_sound.js
Normal file
87
test/integration/sb3_missing_sound.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/**
|
||||||
|
* This test ensures that the VM gracefully handles an sb3 project with
|
||||||
|
* a missing sound. The project should load without error.
|
||||||
|
* TODO: handle missing or corrupted sounds by replacing the missing sound data
|
||||||
|
* with the empty sound file but keeping the info about the original missing / corrupted sound
|
||||||
|
* so that user data does not get overwritten / lost.
|
||||||
|
*/
|
||||||
|
const path = require('path');
|
||||||
|
const tap = require('tap');
|
||||||
|
const makeTestStorage = require('../fixtures/make-test-storage');
|
||||||
|
const readFileToBuffer = require('../fixtures/readProjectFile').readFileToBuffer;
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
const {serializeSounds} = require('../../src/serialization/serialize-assets');
|
||||||
|
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/missing_sound.sb3');
|
||||||
|
const project = readFileToBuffer(projectUri);
|
||||||
|
|
||||||
|
const missingSoundAssetId = '78618aadd225b1db7bf837fa17dc0568';
|
||||||
|
|
||||||
|
let vm;
|
||||||
|
|
||||||
|
tap.beforeEach(() => {
|
||||||
|
const storage = makeTestStorage();
|
||||||
|
|
||||||
|
vm = new VirtualMachine();
|
||||||
|
vm.attachStorage(storage);
|
||||||
|
|
||||||
|
return vm.loadProject(project);
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = tap.test;
|
||||||
|
|
||||||
|
test('loading sb3 project with missing sound file', t => {
|
||||||
|
t.equal(vm.runtime.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = vm.runtime.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const catSprite = vm.runtime.targets[1];
|
||||||
|
t.equal(catSprite.getSounds().length, 1);
|
||||||
|
|
||||||
|
const missingSound = catSprite.getSounds()[0];
|
||||||
|
t.equal(missingSound.name, 'Boop Sound Recording');
|
||||||
|
// Sound should have original data but no asset
|
||||||
|
const defaultSoundAssetId = vm.runtime.storage.defaultAssetId.Sound;
|
||||||
|
t.equal(missingSound.assetId, defaultSoundAssetId);
|
||||||
|
t.equal(missingSound.dataFormat, 'wav');
|
||||||
|
|
||||||
|
// Runtime should have info about broken asset
|
||||||
|
t.ok(missingSound.broken);
|
||||||
|
t.equal(missingSound.broken.assetId, missingSoundAssetId);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('load and then save sb3 project with missing sound file', t => {
|
||||||
|
const resavedProject = JSON.parse(vm.toJSON());
|
||||||
|
|
||||||
|
t.equal(resavedProject.targets.length, 2);
|
||||||
|
|
||||||
|
const stage = resavedProject.targets[0];
|
||||||
|
t.ok(stage.isStage);
|
||||||
|
|
||||||
|
const catSprite = resavedProject.targets[1];
|
||||||
|
t.equal(catSprite.name, 'Sprite1');
|
||||||
|
t.equal(catSprite.sounds.length, 1);
|
||||||
|
|
||||||
|
const missingSound = catSprite.sounds[0];
|
||||||
|
t.equal(missingSound.name, 'Boop Sound Recording');
|
||||||
|
// Costume should have both default sound data (e.g. "Gray Question Sound" ^_^) and original data
|
||||||
|
t.equal(missingSound.assetId, missingSoundAssetId);
|
||||||
|
t.equal(missingSound.dataFormat, 'wav');
|
||||||
|
// Test that we didn't save any data about the costume being broken
|
||||||
|
t.notOk(missingSound.broken);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('serializeCostume does not save data for missing costume', t => {
|
||||||
|
const soundDescs = serializeSounds(vm.runtime);
|
||||||
|
|
||||||
|
t.equal(soundDescs.length, 1); // Should only have one sound, the pop sound for the stage
|
||||||
|
t.not(soundDescs[0].fileName, `${missingSoundAssetId}.wav`);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue