mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 06:52:40 -05:00
Merge develop into feature/extension-serialization
This commit is contained in:
commit
0bed25e01b
19 changed files with 683 additions and 169 deletions
|
@ -28,8 +28,8 @@ jobs:
|
||||||
env: NPM_SCRIPT=build
|
env: NPM_SCRIPT=build
|
||||||
before_deploy:
|
before_deploy:
|
||||||
- npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
|
- npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
|
||||||
- git config --global user.email $(git log --pretty=format:"%ae" -n1)
|
- git config --global user.email "$(git log --pretty=format:"%ae" -n1)"
|
||||||
- git config --global user.name $(git log --pretty=format:"%an" -n1)
|
- git config --global user.name "$(git log --pretty=format:"%an" -n1)"
|
||||||
deploy:
|
deploy:
|
||||||
- provider: npm
|
- provider: npm
|
||||||
on:
|
on:
|
||||||
|
|
|
@ -88,6 +88,7 @@ class Scratch3SoundBlocks {
|
||||||
if (!soundState) {
|
if (!soundState) {
|
||||||
soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE);
|
soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE);
|
||||||
target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState);
|
target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState);
|
||||||
|
target.soundEffects = soundState.effects;
|
||||||
}
|
}
|
||||||
return soundState;
|
return soundState;
|
||||||
}
|
}
|
||||||
|
@ -139,20 +140,19 @@ class Scratch3SoundBlocks {
|
||||||
}
|
}
|
||||||
|
|
||||||
playSound (args, util) {
|
playSound (args, util) {
|
||||||
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
// Don't return the promise, it's the only difference for AndWait
|
||||||
if (index >= 0) {
|
this.playSoundAndWait(args, util);
|
||||||
const soundId = util.target.sprite.sounds[index].soundId;
|
|
||||||
if (util.target.audioPlayer === null) return;
|
|
||||||
util.target.audioPlayer.playSound(soundId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playSoundAndWait (args, util) {
|
playSoundAndWait (args, util) {
|
||||||
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
const index = this._getSoundIndex(args.SOUND_MENU, util);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
const soundId = util.target.sprite.sounds[index].soundId;
|
const {target} = util;
|
||||||
if (util.target.audioPlayer === null) return;
|
const {sprite} = target;
|
||||||
return util.target.audioPlayer.playSound(soundId);
|
const {soundId} = sprite.sounds[index];
|
||||||
|
if (sprite.soundBank) {
|
||||||
|
return sprite.soundBank.playSound(target, soundId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,8 +199,9 @@ class Scratch3SoundBlocks {
|
||||||
}
|
}
|
||||||
|
|
||||||
_stopAllSoundsForTarget (target) {
|
_stopAllSoundsForTarget (target) {
|
||||||
if (target.audioPlayer === null) return;
|
if (target.sprite.soundBank) {
|
||||||
target.audioPlayer.stopAllSounds();
|
target.sprite.soundBank.stopAllSounds(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setEffect (args, util) {
|
setEffect (args, util) {
|
||||||
|
@ -224,23 +225,19 @@ class Scratch3SoundBlocks {
|
||||||
soundState.effects[effect] = value;
|
soundState.effects[effect] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectRange = Scratch3SoundBlocks.EFFECT_RANGE[effect];
|
const {min, max} = Scratch3SoundBlocks.EFFECT_RANGE[effect];
|
||||||
soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], effectRange.min, effectRange.max);
|
soundState.effects[effect] = MathUtil.clamp(soundState.effects[effect], min, max);
|
||||||
|
|
||||||
if (util.target.audioPlayer === null) return;
|
|
||||||
util.target.audioPlayer.setEffect(effect, soundState.effects[effect]);
|
|
||||||
|
|
||||||
|
this._syncEffectsForTarget(util.target);
|
||||||
// Yield until the next tick.
|
// Yield until the next tick.
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
_syncEffectsForTarget (target) {
|
_syncEffectsForTarget (target) {
|
||||||
if (!target || !target.audioPlayer) return;
|
if (!target || !target.sprite.soundBank) return;
|
||||||
const soundState = this._getSoundState(target);
|
target.soundEffects = this._getSoundState(target).effects;
|
||||||
for (const effect in soundState.effects) {
|
|
||||||
if (!soundState.effects.hasOwnProperty(effect)) continue;
|
target.sprite.soundBank.setEffects(target);
|
||||||
target.audioPlayer.setEffect(effect, soundState.effects[effect]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearEffects (args, util) {
|
clearEffects (args, util) {
|
||||||
|
@ -253,8 +250,7 @@ class Scratch3SoundBlocks {
|
||||||
if (!soundState.effects.hasOwnProperty(effect)) continue;
|
if (!soundState.effects.hasOwnProperty(effect)) continue;
|
||||||
soundState.effects[effect] = 0;
|
soundState.effects[effect] = 0;
|
||||||
}
|
}
|
||||||
if (target.audioPlayer === null) return;
|
this._syncEffectsForTarget(target);
|
||||||
target.audioPlayer.clearEffects();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearEffectsForAllTargets () {
|
_clearEffectsForAllTargets () {
|
||||||
|
@ -278,8 +274,7 @@ class Scratch3SoundBlocks {
|
||||||
_updateVolume (volume, util) {
|
_updateVolume (volume, util) {
|
||||||
volume = MathUtil.clamp(volume, 0, 100);
|
volume = MathUtil.clamp(volume, 0, 100);
|
||||||
util.target.volume = volume;
|
util.target.volume = volume;
|
||||||
if (util.target.audioPlayer === null) return;
|
this._syncEffectsForTarget(util.target);
|
||||||
util.target.audioPlayer.setVolume(util.target.volume);
|
|
||||||
|
|
||||||
// Yield until the next tick.
|
// Yield until the next tick.
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|
|
@ -6,6 +6,7 @@ const Clone = require('../util/clone');
|
||||||
const {Map} = require('immutable');
|
const {Map} = require('immutable');
|
||||||
const BlocksExecuteCache = require('./blocks-execute-cache');
|
const BlocksExecuteCache = require('./blocks-execute-cache');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
|
const Variable = require('./variable');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fileoverview
|
* @fileoverview
|
||||||
|
@ -670,6 +671,44 @@ class Blocks {
|
||||||
this.resetCache();
|
this.resetCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a map of all references to variables or lists from blocks
|
||||||
|
* in this block container.
|
||||||
|
* @return {object} A map of variable ID to a list of all variable references
|
||||||
|
* for that ID. A variable reference contains the field referencing that variable
|
||||||
|
* and also the type of the variable being referenced.
|
||||||
|
*/
|
||||||
|
getAllVariableAndListReferences () {
|
||||||
|
const blocks = this._blocks;
|
||||||
|
const allReferences = Object.create(null);
|
||||||
|
for (const blockId in blocks) {
|
||||||
|
let varOrListField = null;
|
||||||
|
let varType = null;
|
||||||
|
if (blocks[blockId].fields.VARIABLE) {
|
||||||
|
varOrListField = blocks[blockId].fields.VARIABLE;
|
||||||
|
varType = Variable.SCALAR_TYPE;
|
||||||
|
} else if (blocks[blockId].fields.LIST) {
|
||||||
|
varOrListField = blocks[blockId].fields.LIST;
|
||||||
|
varType = Variable.LIST_TYPE;
|
||||||
|
}
|
||||||
|
if (varOrListField) {
|
||||||
|
const currVarId = varOrListField.id;
|
||||||
|
if (allReferences[currVarId]) {
|
||||||
|
allReferences[currVarId].push({
|
||||||
|
referencingField: varOrListField,
|
||||||
|
type: varType
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
allReferences[currVarId] = [{
|
||||||
|
referencingField: varOrListField,
|
||||||
|
type: varType
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allReferences;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keep blocks up to date after a variable gets renamed.
|
* Keep blocks up to date after a variable gets renamed.
|
||||||
* @param {string} varId The id of the variable that was renamed
|
* @param {string} varId The id of the variable that was renamed
|
||||||
|
|
|
@ -6,6 +6,7 @@ const Comment = require('../engine/comment');
|
||||||
const uid = require('../util/uid');
|
const uid = require('../util/uid');
|
||||||
const {Map} = require('immutable');
|
const {Map} = require('immutable');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
|
const StringUtil = require('../util/string-util');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fileoverview
|
* @fileoverview
|
||||||
|
@ -80,14 +81,40 @@ class Target extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look up a variable object, and create it if one doesn't exist.
|
* Get the names of all the variables of the given type that are in scope for this target.
|
||||||
|
* For targets that are not the stage, this includes any target-specific
|
||||||
|
* variables as well as any stage variables.
|
||||||
|
* For the stage, this is all stage variables.
|
||||||
|
* @param {string} type The variable type to search for; defaults to Variable.SCALAR_TYPE
|
||||||
|
* @return {Array<string>} A list of variable names
|
||||||
|
*/
|
||||||
|
getAllVariableNamesInScopeByType (type) {
|
||||||
|
if (typeof type !== 'string') type = Variable.SCALAR_TYPE;
|
||||||
|
const targetVariables = Object.values(this.variables)
|
||||||
|
.filter(v => v.type === type)
|
||||||
|
.map(variable => variable.name);
|
||||||
|
if (this.isStage || !this.runtime) {
|
||||||
|
return targetVariables;
|
||||||
|
}
|
||||||
|
const stage = this.runtime.getTargetForStage();
|
||||||
|
const stageVariables = stage.getAllVariableNamesInScopeByType(type);
|
||||||
|
return targetVariables.concat(stageVariables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a variable object, first by id, and then by name if the id is not found.
|
||||||
|
* Create a new variable if both lookups fail.
|
||||||
* @param {string} id Id of the variable.
|
* @param {string} id Id of the variable.
|
||||||
* @param {string} name Name of the variable.
|
* @param {string} name Name of the variable.
|
||||||
* @return {!Variable} Variable object.
|
* @return {!Variable} Variable object.
|
||||||
*/
|
*/
|
||||||
lookupOrCreateVariable (id, name) {
|
lookupOrCreateVariable (id, name) {
|
||||||
const variable = this.lookupVariableById(id);
|
let variable = this.lookupVariableById(id);
|
||||||
if (variable) return variable;
|
if (variable) return variable;
|
||||||
|
|
||||||
|
variable = this.lookupVariableByNameAndType(name, Variable.SCALAR_TYPE);
|
||||||
|
if (variable) return variable;
|
||||||
|
|
||||||
// No variable with this name exists - create it locally.
|
// No variable with this name exists - create it locally.
|
||||||
const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false);
|
const newVariable = new Variable(id, name, Variable.SCALAR_TYPE, false);
|
||||||
this.variables[id] = newVariable;
|
this.variables[id] = newVariable;
|
||||||
|
@ -161,6 +188,40 @@ class Target extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up a variable object by its name and variable type.
|
||||||
|
* Search begins with local variables; then global variables if a local one
|
||||||
|
* was not found.
|
||||||
|
* @param {string} name Name of the variable.
|
||||||
|
* @param {string} type Type of the variable. Defaults to Variable.SCALAR_TYPE.
|
||||||
|
* @return {?Variable} Variable object if found, or null if not.
|
||||||
|
*/
|
||||||
|
lookupVariableByNameAndType (name, type) {
|
||||||
|
if (typeof name !== 'string') return;
|
||||||
|
if (typeof type !== 'string') type = Variable.SCALAR_TYPE;
|
||||||
|
|
||||||
|
for (const varId in this.variables) {
|
||||||
|
const currVar = this.variables[varId];
|
||||||
|
if (currVar.name === name && currVar.type === type) {
|
||||||
|
return currVar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.runtime && !this.isStage) {
|
||||||
|
const stage = this.runtime.getTargetForStage();
|
||||||
|
if (stage) {
|
||||||
|
for (const varId in stage.variables) {
|
||||||
|
const currVar = stage.variables[varId];
|
||||||
|
if (currVar.name === name && currVar.type === type) {
|
||||||
|
return currVar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look up a list object for this target, and create it if one doesn't exist.
|
* Look up a list object for this target, and create it if one doesn't exist.
|
||||||
* Search begins for local lists; then look for globals.
|
* Search begins for local lists; then look for globals.
|
||||||
|
@ -169,8 +230,12 @@ class Target extends EventEmitter {
|
||||||
* @return {!Varible} Variable object representing the found/created list.
|
* @return {!Varible} Variable object representing the found/created list.
|
||||||
*/
|
*/
|
||||||
lookupOrCreateList (id, name) {
|
lookupOrCreateList (id, name) {
|
||||||
const list = this.lookupVariableById(id);
|
let list = this.lookupVariableById(id);
|
||||||
if (list) return list;
|
if (list) return list;
|
||||||
|
|
||||||
|
list = this.lookupVariableByNameAndType(name, Variable.LIST_TYPE);
|
||||||
|
if (list) return list;
|
||||||
|
|
||||||
// No variable with this name exists - create it locally.
|
// No variable with this name exists - create it locally.
|
||||||
const newList = new Variable(id, name, Variable.LIST_TYPE, false);
|
const newList = new Variable(id, name, Variable.LIST_TYPE, false);
|
||||||
this.variables[id] = newList;
|
this.variables[id] = newList;
|
||||||
|
@ -240,10 +305,13 @@ class Target extends EventEmitter {
|
||||||
name: 'VARIABLE',
|
name: 'VARIABLE',
|
||||||
value: id
|
value: id
|
||||||
}, this.runtime);
|
}, this.runtime);
|
||||||
this.runtime.requestUpdateMonitor(Map({
|
const monitorBlock = blocks.getBlock(variable.id);
|
||||||
id: id,
|
if (monitorBlock) {
|
||||||
params: blocks._getBlockParams(blocks.getBlock(variable.id))
|
this.runtime.requestUpdateMonitor(Map({
|
||||||
}));
|
id: id,
|
||||||
|
params: blocks._getBlockParams(monitorBlock)
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -264,6 +332,101 @@ class Target extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixes up variable references in this target avoiding conflicts with
|
||||||
|
* pre-existing variables in the same scope.
|
||||||
|
* This is used when uploading this target as a new sprite into an existing
|
||||||
|
* project, where the new sprite may contain references
|
||||||
|
* to variable names that already exist as global variables in the project
|
||||||
|
* (and thus are in scope for variable references in the given sprite).
|
||||||
|
*
|
||||||
|
* If the given target has a block that references an existing global variable and that
|
||||||
|
* variable *does not* exist in the target itself (e.g. it was a global variable in the
|
||||||
|
* project the sprite was originally exported from), fix the variable references in this sprite
|
||||||
|
* to reference the id of the pre-existing global variable.
|
||||||
|
* If the given target has a block that references an existing global variable and that
|
||||||
|
* variable does exist in the target itself (e.g. it's a local variable in the sprite being uploaded),
|
||||||
|
* then the variable is renamed to distinguish itself from the pre-existing variable.
|
||||||
|
* All blocks that reference the local variable will be updated to use the new name.
|
||||||
|
*/
|
||||||
|
fixUpVariableReferences () {
|
||||||
|
if (!this.runtime) return; // There's no runtime context to conflict with
|
||||||
|
if (this.isStage) return; // Stage can't have variable conflicts with itself (and also can't be uploaded)
|
||||||
|
const stage = this.runtime.getTargetForStage();
|
||||||
|
if (!stage || !stage.variables) return;
|
||||||
|
|
||||||
|
const renameConflictingLocalVar = (id, name, type) => {
|
||||||
|
const conflict = stage.lookupVariableByNameAndType(name, type);
|
||||||
|
if (conflict) {
|
||||||
|
const newName = StringUtil.unusedName(
|
||||||
|
`${this.getName()}: ${name}`,
|
||||||
|
this.getAllVariableNamesInScopeByType(type));
|
||||||
|
this.renameVariable(id, newName);
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const allReferences = this.blocks.getAllVariableAndListReferences();
|
||||||
|
const unreferencedLocalVarIds = [];
|
||||||
|
if (Object.keys(this.variables).length > 0) {
|
||||||
|
for (const localVarId in this.variables) {
|
||||||
|
if (!this.variables.hasOwnProperty(localVarId)) continue;
|
||||||
|
if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const conflictIdsToReplace = Object.create(null);
|
||||||
|
for (const varId in allReferences) {
|
||||||
|
// We don't care about which var ref we get, they should all have the same var info
|
||||||
|
const varRef = allReferences[varId][0];
|
||||||
|
const varName = varRef.referencingField.value;
|
||||||
|
const varType = varRef.type;
|
||||||
|
if (this.lookupVariableById(varId)) {
|
||||||
|
// Found a variable with the id in either the target or the stage,
|
||||||
|
// figure out which one.
|
||||||
|
if (this.variables.hasOwnProperty(varId)) {
|
||||||
|
// If the target has the variable, then check whether the stage
|
||||||
|
// has one with the same name and type. If it does, then rename
|
||||||
|
// this target specific variable so that there is a distinction.
|
||||||
|
const newVarName = renameConflictingLocalVar(varId, varName, varType);
|
||||||
|
|
||||||
|
if (newVarName) {
|
||||||
|
// We are not calling this.blocks.updateBlocksAfterVarRename
|
||||||
|
// here because it will search through all the blocks. We already
|
||||||
|
// have access to all the references for this var id.
|
||||||
|
allReferences[varId].map(ref => {
|
||||||
|
ref.referencingField.value = newVarName;
|
||||||
|
return ref;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const existingVar = this.lookupVariableByNameAndType(varName, varType);
|
||||||
|
if (existingVar && !conflictIdsToReplace[varId]) {
|
||||||
|
conflictIdsToReplace[varId] = existingVar.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Rename any local variables that were missed above because they aren't
|
||||||
|
// referenced by any blocks
|
||||||
|
for (const id in unreferencedLocalVarIds) {
|
||||||
|
const varId = unreferencedLocalVarIds[id];
|
||||||
|
const name = this.variables[varId].name;
|
||||||
|
const type = this.variables[varId].type;
|
||||||
|
renameConflictingLocalVar(varId, name, type);
|
||||||
|
}
|
||||||
|
// Finally, handle global var conflicts (e.g. a sprite is uploaded, and has
|
||||||
|
// blocks referencing some variable that the sprite does not own, and this
|
||||||
|
// variable conflicts with a global var)
|
||||||
|
for (const conflictId in conflictIdsToReplace) {
|
||||||
|
const existingId = conflictIdsToReplace[conflictId];
|
||||||
|
allReferences[conflictId].map(varRef => {
|
||||||
|
varRef.referencingField.id = existingId;
|
||||||
|
return varRef;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Post/edit sprite info.
|
* Post/edit sprite info.
|
||||||
* @param {object} data An object with sprite info data to set.
|
* @param {object} data An object with sprite info data to set.
|
||||||
|
|
|
@ -52,18 +52,25 @@ class Scratch3MusicBlocks {
|
||||||
this._concurrencyCounter = 0;
|
this._concurrencyCounter = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of audio buffers, one for each drum sound.
|
* An array of sound players, one for each drum sound.
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this._drumBuffers = [];
|
this._drumPlayers = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of arrays of audio buffers. Each instrument has one or more audio buffers.
|
* An array of arrays of sound players. Each instrument has one or more audio players.
|
||||||
* @type {Array[]}
|
* @type {Array[]}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
this._instrumentBufferArrays = [];
|
this._instrumentPlayerArrays = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of arrays of sound players. Each instrument mya have an audio player for each playable note.
|
||||||
|
* @type {Array[]}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._instrumentPlayerNoteArrays = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An array of audio bufferSourceNodes. Each time you play an instrument or drum sound,
|
* An array of audio bufferSourceNodes. Each time you play an instrument or drum sound,
|
||||||
|
@ -87,14 +94,15 @@ class Scratch3MusicBlocks {
|
||||||
const loadingPromises = [];
|
const loadingPromises = [];
|
||||||
this.DRUM_INFO.forEach((drumInfo, index) => {
|
this.DRUM_INFO.forEach((drumInfo, index) => {
|
||||||
const filePath = `drums/${drumInfo.fileName}`;
|
const filePath = `drums/${drumInfo.fileName}`;
|
||||||
const promise = this._storeSound(filePath, index, this._drumBuffers);
|
const promise = this._storeSound(filePath, index, this._drumPlayers);
|
||||||
loadingPromises.push(promise);
|
loadingPromises.push(promise);
|
||||||
});
|
});
|
||||||
this.INSTRUMENT_INFO.forEach((instrumentInfo, instrumentIndex) => {
|
this.INSTRUMENT_INFO.forEach((instrumentInfo, instrumentIndex) => {
|
||||||
this._instrumentBufferArrays[instrumentIndex] = [];
|
this._instrumentPlayerArrays[instrumentIndex] = [];
|
||||||
|
this._instrumentPlayerNoteArrays[instrumentIndex] = [];
|
||||||
instrumentInfo.samples.forEach((sample, noteIndex) => {
|
instrumentInfo.samples.forEach((sample, noteIndex) => {
|
||||||
const filePath = `instruments/${instrumentInfo.dirName}/${sample}`;
|
const filePath = `instruments/${instrumentInfo.dirName}/${sample}`;
|
||||||
const promise = this._storeSound(filePath, noteIndex, this._instrumentBufferArrays[instrumentIndex]);
|
const promise = this._storeSound(filePath, noteIndex, this._instrumentPlayerArrays[instrumentIndex]);
|
||||||
loadingPromises.push(promise);
|
loadingPromises.push(promise);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -104,22 +112,22 @@ class Scratch3MusicBlocks {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a sound and store the buffer in an array.
|
* Decode a sound and store the player in an array.
|
||||||
* @param {string} filePath - the audio file name.
|
* @param {string} filePath - the audio file name.
|
||||||
* @param {number} index - the index at which to store the audio buffer.
|
* @param {number} index - the index at which to store the audio player.
|
||||||
* @param {array} bufferArray - the array of buffers in which to store it.
|
* @param {array} playerArray - the array of players in which to store it.
|
||||||
* @return {Promise} - a promise which will resolve once the sound has been stored.
|
* @return {Promise} - a promise which will resolve once the sound has been stored.
|
||||||
*/
|
*/
|
||||||
_storeSound (filePath, index, bufferArray) {
|
_storeSound (filePath, index, playerArray) {
|
||||||
const fullPath = `${filePath}.mp3`;
|
const fullPath = `${filePath}.mp3`;
|
||||||
|
|
||||||
if (!assetData[fullPath]) return;
|
if (!assetData[fullPath]) return;
|
||||||
|
|
||||||
// The sound buffer has already been downloaded via the manifest file required above.
|
// The sound player has already been downloaded via the manifest file required above.
|
||||||
const soundBuffer = assetData[fullPath];
|
const soundBuffer = assetData[fullPath];
|
||||||
|
|
||||||
return this._decodeSound(soundBuffer).then(buffer => {
|
return this._decodeSound(soundBuffer).then(player => {
|
||||||
bufferArray[index] = buffer;
|
playerArray[index] = player;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,24 +137,14 @@ class Scratch3MusicBlocks {
|
||||||
* @return {Promise} - a promise which will resolve once the sound has decoded.
|
* @return {Promise} - a promise which will resolve once the sound has decoded.
|
||||||
*/
|
*/
|
||||||
_decodeSound (soundBuffer) {
|
_decodeSound (soundBuffer) {
|
||||||
const context = this.runtime.audioEngine && this.runtime.audioEngine.audioContext;
|
const engine = this.runtime.audioEngine;
|
||||||
|
|
||||||
if (!context) {
|
if (!engine) {
|
||||||
return Promise.reject(new Error('No Audio Context Detected'));
|
return Promise.reject(new Error('No Audio Context Detected'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for newer promise-based API
|
// Check for newer promise-based API
|
||||||
if (context.decodeAudioData.length === 1) {
|
return engine.decodeSoundPlayer({data: {buffer: soundBuffer}});
|
||||||
return context.decodeAudioData(soundBuffer);
|
|
||||||
} else { // eslint-disable-line no-else-return
|
|
||||||
// Fall back to callback API
|
|
||||||
return new Promise((resolve, reject) =>
|
|
||||||
context.decodeAudioData(soundBuffer,
|
|
||||||
buffer => resolve(buffer),
|
|
||||||
error => reject(error)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -623,7 +621,11 @@ class Scratch3MusicBlocks {
|
||||||
getInfo () {
|
getInfo () {
|
||||||
return {
|
return {
|
||||||
id: 'music',
|
id: 'music',
|
||||||
name: 'Music',
|
name: formatMessage({
|
||||||
|
id: 'music.categoryName',
|
||||||
|
default: 'Music',
|
||||||
|
description: 'Label for the Music extension category'
|
||||||
|
}),
|
||||||
menuIconURI: menuIconURI,
|
menuIconURI: menuIconURI,
|
||||||
blockIconURI: blockIconURI,
|
blockIconURI: blockIconURI,
|
||||||
blocks: [
|
blocks: [
|
||||||
|
@ -774,26 +776,34 @@ class Scratch3MusicBlocks {
|
||||||
*/
|
*/
|
||||||
_playDrumNum (util, drumNum) {
|
_playDrumNum (util, drumNum) {
|
||||||
if (util.runtime.audioEngine === null) return;
|
if (util.runtime.audioEngine === null) return;
|
||||||
if (util.target.audioPlayer === null) return;
|
if (util.target.sprite.soundBank === null) return;
|
||||||
// If we're playing too many sounds, do not play the drum sound.
|
// If we're playing too many sounds, do not play the drum sound.
|
||||||
if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
|
if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const outputNode = util.target.audioPlayer.getInputNode();
|
|
||||||
const context = util.runtime.audioEngine.audioContext;
|
|
||||||
const bufferSource = context.createBufferSource();
|
|
||||||
bufferSource.buffer = this._drumBuffers[drumNum];
|
|
||||||
bufferSource.connect(outputNode);
|
|
||||||
bufferSource.start();
|
|
||||||
|
|
||||||
const bufferSourceIndex = this._bufferSources.length;
|
const player = this._drumPlayers[drumNum];
|
||||||
this._bufferSources.push(bufferSource);
|
|
||||||
|
if (typeof player === 'undefined') return;
|
||||||
|
|
||||||
|
if (player.isPlaying && !player.isStarting) {
|
||||||
|
// Take the internal player state and create a new player with it.
|
||||||
|
// `.play` does this internally but then instructs the sound to
|
||||||
|
// stop.
|
||||||
|
player.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
const engine = util.runtime.audioEngine;
|
||||||
|
const chain = engine.createEffectChain();
|
||||||
|
chain.setEffectsFromTarget(util.target);
|
||||||
|
player.connect(chain);
|
||||||
|
|
||||||
this._concurrencyCounter++;
|
this._concurrencyCounter++;
|
||||||
bufferSource.onended = () => {
|
player.once('stop', () => {
|
||||||
this._concurrencyCounter--;
|
this._concurrencyCounter--;
|
||||||
delete this._bufferSources[bufferSourceIndex];
|
});
|
||||||
};
|
|
||||||
|
player.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -852,7 +862,7 @@ class Scratch3MusicBlocks {
|
||||||
*/
|
*/
|
||||||
_playNote (util, note, durationSec) {
|
_playNote (util, note, durationSec) {
|
||||||
if (util.runtime.audioEngine === null) return;
|
if (util.runtime.audioEngine === null) return;
|
||||||
if (util.target.audioPlayer === null) return;
|
if (util.target.sprite.soundBank === null) return;
|
||||||
|
|
||||||
// If we're playing too many sounds, do not play the note.
|
// If we're playing too many sounds, do not play the note.
|
||||||
if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
|
if (this._concurrencyCounter > Scratch3MusicBlocks.CONCURRENCY_LIMIT) {
|
||||||
|
@ -867,28 +877,37 @@ class Scratch3MusicBlocks {
|
||||||
const sampleIndex = this._selectSampleIndexForNote(note, sampleArray);
|
const sampleIndex = this._selectSampleIndexForNote(note, sampleArray);
|
||||||
|
|
||||||
// If the audio sample has not loaded yet, bail out
|
// If the audio sample has not loaded yet, bail out
|
||||||
if (typeof this._instrumentBufferArrays[inst] === 'undefined') return;
|
if (typeof this._instrumentPlayerArrays[inst] === 'undefined') return;
|
||||||
if (typeof this._instrumentBufferArrays[inst][sampleIndex] === 'undefined') return;
|
if (typeof this._instrumentPlayerArrays[inst][sampleIndex] === 'undefined') return;
|
||||||
|
|
||||||
// Create the audio buffer to play the note, and set its pitch
|
// Fetch the sound player to play the note.
|
||||||
const context = util.runtime.audioEngine.audioContext;
|
const engine = util.runtime.audioEngine;
|
||||||
const bufferSource = context.createBufferSource();
|
|
||||||
|
|
||||||
const bufferSourceIndex = this._bufferSources.length;
|
if (!this._instrumentPlayerNoteArrays[inst][note]) {
|
||||||
this._bufferSources.push(bufferSource);
|
this._instrumentPlayerNoteArrays[inst][note] = this._instrumentPlayerArrays[inst][sampleIndex].take();
|
||||||
|
}
|
||||||
|
|
||||||
bufferSource.buffer = this._instrumentBufferArrays[inst][sampleIndex];
|
const player = this._instrumentPlayerNoteArrays[inst][note];
|
||||||
|
|
||||||
|
if (player.isPlaying && !player.isStarting) {
|
||||||
|
// Take the internal player state and create a new player with it.
|
||||||
|
// `.play` does this internally but then instructs the sound to
|
||||||
|
// stop.
|
||||||
|
player.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
const chain = engine.createEffectChain();
|
||||||
|
chain.setEffectsFromTarget(util.target);
|
||||||
|
|
||||||
|
// Set its pitch.
|
||||||
const sampleNote = sampleArray[sampleIndex];
|
const sampleNote = sampleArray[sampleIndex];
|
||||||
bufferSource.playbackRate.value = this._ratioForPitchInterval(note - sampleNote);
|
const notePitchInterval = this._ratioForPitchInterval(note - sampleNote);
|
||||||
|
|
||||||
// Create a gain node for this note, and connect it to the sprite's audioPlayer.
|
// Create a gain node for this note, and connect it to the sprite's
|
||||||
const gainNode = context.createGain();
|
// simulated effectChain.
|
||||||
bufferSource.connect(gainNode);
|
const context = engine.audioContext;
|
||||||
const outputNode = util.target.audioPlayer.getInputNode();
|
const releaseGain = context.createGain();
|
||||||
gainNode.connect(outputNode);
|
releaseGain.connect(chain.getInputNode());
|
||||||
|
|
||||||
// Start playing the note
|
|
||||||
bufferSource.start();
|
|
||||||
|
|
||||||
// Schedule the release of the note, ramping its gain down to zero,
|
// Schedule the release of the note, ramping its gain down to zero,
|
||||||
// and then stopping the sound.
|
// and then stopping the sound.
|
||||||
|
@ -898,16 +917,24 @@ class Scratch3MusicBlocks {
|
||||||
}
|
}
|
||||||
const releaseStart = context.currentTime + durationSec;
|
const releaseStart = context.currentTime + durationSec;
|
||||||
const releaseEnd = releaseStart + releaseDuration;
|
const releaseEnd = releaseStart + releaseDuration;
|
||||||
gainNode.gain.setValueAtTime(1, releaseStart);
|
releaseGain.gain.setValueAtTime(1, releaseStart);
|
||||||
gainNode.gain.linearRampToValueAtTime(0.0001, releaseEnd);
|
releaseGain.gain.linearRampToValueAtTime(0.0001, releaseEnd);
|
||||||
bufferSource.stop(releaseEnd);
|
|
||||||
|
|
||||||
// Update the concurrency counter
|
|
||||||
this._concurrencyCounter++;
|
this._concurrencyCounter++;
|
||||||
bufferSource.onended = () => {
|
player.once('stop', () => {
|
||||||
this._concurrencyCounter--;
|
this._concurrencyCounter--;
|
||||||
delete this._bufferSources[bufferSourceIndex];
|
});
|
||||||
};
|
|
||||||
|
// Start playing the note
|
||||||
|
player.play();
|
||||||
|
// Connect the player to the gain node.
|
||||||
|
player.connect({getInputNode () {
|
||||||
|
return releaseGain;
|
||||||
|
}});
|
||||||
|
// Set playback now after play creates the outputNode.
|
||||||
|
player.outputNode.playbackRate.value = notePitchInterval;
|
||||||
|
// Schedule playback to stop.
|
||||||
|
player.outputNode.stop(releaseEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -282,7 +282,11 @@ class Scratch3PenBlocks {
|
||||||
getInfo () {
|
getInfo () {
|
||||||
return {
|
return {
|
||||||
id: 'pen',
|
id: 'pen',
|
||||||
name: 'Pen',
|
name: formatMessage({
|
||||||
|
id: 'pen.categoryName',
|
||||||
|
default: 'Pen',
|
||||||
|
description: 'Label for the pen extension category'
|
||||||
|
}),
|
||||||
blockIconURI: blockIconURI,
|
blockIconURI: blockIconURI,
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -86,7 +86,11 @@ class Scratch3TranslateBlocks {
|
||||||
getInfo () {
|
getInfo () {
|
||||||
return {
|
return {
|
||||||
id: 'translate',
|
id: 'translate',
|
||||||
name: 'Translate',
|
name: formatMessage({
|
||||||
|
id: 'translate.categoryName',
|
||||||
|
default: 'Translate',
|
||||||
|
description: 'Label for the translate extension category'
|
||||||
|
}),
|
||||||
menuIconURI: '', // TODO: Add the final icons.
|
menuIconURI: '', // TODO: Add the final icons.
|
||||||
blockIconURI: '',
|
blockIconURI: '',
|
||||||
blocks: [
|
blocks: [
|
||||||
|
|
|
@ -372,7 +372,11 @@ class Scratch3VideoSensingBlocks {
|
||||||
// Return extension definition
|
// Return extension definition
|
||||||
return {
|
return {
|
||||||
id: 'videoSensing',
|
id: 'videoSensing',
|
||||||
name: 'Video Motion',
|
name: formatMessage({
|
||||||
|
id: 'videoSensing.categoryName',
|
||||||
|
default: 'Video Motion',
|
||||||
|
description: 'Label for the video motion extension category'
|
||||||
|
}),
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
// @todo this hat needs to be set itself to restart existing
|
// @todo this hat needs to be set itself to restart existing
|
||||||
|
|
|
@ -8,27 +8,32 @@ const log = require('../util/log');
|
||||||
* @property {Buffer} data - sound data will be written here once loaded.
|
* @property {Buffer} data - sound data will be written here once loaded.
|
||||||
* @param {!Asset} soundAsset - the asset loaded from storage.
|
* @param {!Asset} soundAsset - the asset loaded from storage.
|
||||||
* @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
|
* @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
|
||||||
|
* @param {Sprite} sprite - Scratch sprite to add sounds to.
|
||||||
* @returns {!Promise} - a promise which will resolve to the sound when ready.
|
* @returns {!Promise} - a promise which will resolve to the sound when ready.
|
||||||
*/
|
*/
|
||||||
const loadSoundFromAsset = function (sound, soundAsset, runtime) {
|
const loadSoundFromAsset = function (sound, soundAsset, runtime, sprite) {
|
||||||
sound.assetId = soundAsset.assetId;
|
sound.assetId = soundAsset.assetId;
|
||||||
if (!runtime.audioEngine) {
|
if (!runtime.audioEngine) {
|
||||||
log.error('No audio engine present; cannot load sound asset: ', sound.md5);
|
log.error('No audio engine present; cannot load sound asset: ', sound.md5);
|
||||||
return Promise.resolve(sound);
|
return Promise.resolve(sound);
|
||||||
}
|
}
|
||||||
return runtime.audioEngine.decodeSound(Object.assign(
|
return runtime.audioEngine.decodeSoundPlayer(Object.assign(
|
||||||
{},
|
{},
|
||||||
sound,
|
sound,
|
||||||
{data: soundAsset.data}
|
{data: soundAsset.data}
|
||||||
)).then(soundId => {
|
)).then(soundPlayer => {
|
||||||
sound.soundId = soundId;
|
sound.soundId = soundPlayer.id;
|
||||||
// Set the sound sample rate and sample count based on the
|
// Set the sound sample rate and sample count based on the
|
||||||
// the audio buffer from the audio engine since the sound
|
// the audio buffer from the audio engine since the sound
|
||||||
// gets resampled by the audio engine
|
// gets resampled by the audio engine
|
||||||
const soundBuffer = runtime.audioEngine.getSoundBuffer(soundId);
|
const soundBuffer = soundPlayer.buffer;
|
||||||
sound.rate = soundBuffer.sampleRate;
|
sound.rate = soundBuffer.sampleRate;
|
||||||
sound.sampleCount = soundBuffer.length;
|
sound.sampleCount = soundBuffer.length;
|
||||||
|
|
||||||
|
if (sprite.soundBank !== null) {
|
||||||
|
sprite.soundBank.addSoundPlayer(soundPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
return sound;
|
return sound;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -39,9 +44,10 @@ const loadSoundFromAsset = function (sound, soundAsset, runtime) {
|
||||||
* @property {string} md5 - the MD5 and extension of the sound to be loaded.
|
* @property {string} md5 - the MD5 and extension of the sound to be loaded.
|
||||||
* @property {Buffer} data - sound data will be written here once loaded.
|
* @property {Buffer} data - sound data will be written here once loaded.
|
||||||
* @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
|
* @param {!Runtime} runtime - Scratch runtime, used to access the storage module.
|
||||||
|
* @param {Sprite} sprite - Scratch sprite to add sounds to.
|
||||||
* @returns {!Promise} - a promise which will resolve to the sound when ready.
|
* @returns {!Promise} - a promise which will resolve to the sound when ready.
|
||||||
*/
|
*/
|
||||||
const loadSound = function (sound, runtime) {
|
const loadSound = function (sound, runtime, sprite) {
|
||||||
if (!runtime.storage) {
|
if (!runtime.storage) {
|
||||||
log.error('No storage module present; cannot load sound asset: ', sound.md5);
|
log.error('No storage module present; cannot load sound asset: ', sound.md5);
|
||||||
return Promise.resolve(sound);
|
return Promise.resolve(sound);
|
||||||
|
@ -52,7 +58,7 @@ const loadSound = function (sound, runtime) {
|
||||||
return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
|
return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
|
||||||
.then(soundAsset => {
|
.then(soundAsset => {
|
||||||
sound.dataFormat = ext;
|
sound.dataFormat = ext;
|
||||||
return loadSoundFromAsset(sound, soundAsset, runtime);
|
return loadSoundFromAsset(sound, soundAsset, runtime, sprite);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -420,7 +420,7 @@ const parseScratchObject = function (object, runtime, extensions, topLevel, zip)
|
||||||
// followed by the file ext
|
// followed by the file ext
|
||||||
const assetFileName = `${soundSource.soundID}.${ext}`;
|
const assetFileName = `${soundSource.soundID}.${ext}`;
|
||||||
soundPromises.push(deserializeSound(sound, runtime, zip, assetFileName)
|
soundPromises.push(deserializeSound(sound, runtime, zip, assetFileName)
|
||||||
.then(() => loadSound(sound, runtime)));
|
.then(() => loadSound(sound, runtime, sprite)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -460,17 +460,27 @@ const serializeTarget = function (target, extensions) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the specified VM runtime.
|
* Serializes the specified VM runtime.
|
||||||
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
||||||
|
* @param {string=} targetId Optional target id if serializing only a single target
|
||||||
* @return {object} Serialized runtime instance.
|
* @return {object} Serialized runtime instance.
|
||||||
*/
|
*/
|
||||||
const serialize = function (runtime) {
|
const serialize = function (runtime, targetId) {
|
||||||
// Fetch targets
|
// Fetch targets
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
// Create extension set to hold extension ids found while serializing targets
|
// Create extension set to hold extension ids found while serializing targets
|
||||||
const extensions = new Set();
|
const extensions = new Set();
|
||||||
const flattenedOriginalTargets = JSON.parse(JSON.stringify(
|
const flattenedOriginalTargets = JSON.parse(JSON.stringify(targetId ?
|
||||||
|
[runtime.getTargetById(targetId)] :
|
||||||
runtime.targets.filter(target => target.isOriginal)));
|
runtime.targets.filter(target => target.isOriginal)));
|
||||||
obj.targets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions));
|
|
||||||
|
const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions));
|
||||||
|
|
||||||
|
if (targetId) {
|
||||||
|
return serializedTargets[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.targets = serializedTargets;
|
||||||
|
|
||||||
|
|
||||||
// TODO Serialize monitors
|
// TODO Serialize monitors
|
||||||
|
|
||||||
|
@ -819,7 +829,7 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
// any translation that needs to happen will happen in the process
|
// any translation that needs to happen will happen in the process
|
||||||
// of building up the costume object into an sb3 format
|
// of building up the costume object into an sb3 format
|
||||||
return deserializeSound(sound, runtime, zip)
|
return deserializeSound(sound, runtime, zip)
|
||||||
.then(() => loadSound(sound, runtime));
|
.then(() => loadSound(sound, runtime, sprite));
|
||||||
// Only attempt to load the sound after the deserialization
|
// Only attempt to load the sound after the deserialization
|
||||||
// process has been completed.
|
// process has been completed.
|
||||||
});
|
});
|
||||||
|
@ -945,10 +955,11 @@ const deserialize = function (json, runtime, zip, isSingleSprite) {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
((isSingleSprite ? [json] : json.targets) || []).map(target =>
|
((isSingleSprite ? [json] : json.targets) || []).map(target =>
|
||||||
parseScratchObject(target, runtime, extensions, zip))
|
parseScratchObject(target, runtime, extensions, zip))
|
||||||
).then(targets => ({
|
)
|
||||||
targets,
|
.then(targets => ({
|
||||||
extensions
|
targets,
|
||||||
}));
|
extensions
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -5,10 +5,11 @@
|
||||||
* to be written and the contents of the file, the serialized asset.
|
* to be written and the contents of the file, the serialized asset.
|
||||||
* @param {Runtime} runtime The runtime with the assets to be serialized
|
* @param {Runtime} runtime The runtime with the assets to be serialized
|
||||||
* @param {string} assetType The type of assets to be serialized: 'sounds' | 'costumes'
|
* @param {string} assetType The type of assets to be serialized: 'sounds' | 'costumes'
|
||||||
|
* @param {string=} optTargetId Optional target id to serialize assets for
|
||||||
* @returns {Array<object>} An array of file descriptors for each asset
|
* @returns {Array<object>} An array of file descriptors for each asset
|
||||||
*/
|
*/
|
||||||
const serializeAssets = function (runtime, assetType) {
|
const serializeAssets = function (runtime, assetType, optTargetId) {
|
||||||
const targets = runtime.targets;
|
const targets = optTargetId ? [runtime.getTargetById(optTargetId)] : runtime.targets;
|
||||||
const assetDescs = [];
|
const assetDescs = [];
|
||||||
for (let i = 0; i < targets.length; i++) {
|
for (let i = 0; i < targets.length; i++) {
|
||||||
const currTarget = targets[i];
|
const currTarget = targets[i];
|
||||||
|
@ -27,14 +28,16 @@ const serializeAssets = function (runtime, assetType) {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize all the sounds in the provided runtime into an array of file
|
* Serialize all the sounds in the provided runtime or, if a target id is provided,
|
||||||
* descriptors. A file descriptor is an object containing the name of the file
|
* in the specified target into an array of file descriptors.
|
||||||
|
* A file descriptor is an object containing the name of the file
|
||||||
* to be written and the contents of the file, the serialized sound.
|
* to be written and the contents of the file, the serialized sound.
|
||||||
* @param {Runtime} runtime The runtime with the sounds to be serialized
|
* @param {Runtime} runtime The runtime with the sounds to be serialized
|
||||||
|
* @param {string=} optTargetId Optional targetid for serializing sounds of a single target
|
||||||
* @returns {Array<object>} An array of file descriptors for each sound
|
* @returns {Array<object>} An array of file descriptors for each sound
|
||||||
*/
|
*/
|
||||||
const serializeSounds = function (runtime) {
|
const serializeSounds = function (runtime, optTargetId) {
|
||||||
return serializeAssets(runtime, 'sounds');
|
return serializeAssets(runtime, 'sounds', optTargetId);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,10 +45,11 @@ const serializeSounds = function (runtime) {
|
||||||
* descriptors. A file descriptor is an object containing the name of the file
|
* descriptors. A file descriptor is an object containing the name of the file
|
||||||
* to be written and the contents of the file, the serialized costume.
|
* to be written and the contents of the file, the serialized costume.
|
||||||
* @param {Runtime} runtime The runtime with the costumes to be serialized
|
* @param {Runtime} runtime The runtime with the costumes to be serialized
|
||||||
|
* @param {string} optTargetId Optional targetid for serializing costumes of a single target
|
||||||
* @returns {Array<object>} An array of file descriptors for each costume
|
* @returns {Array<object>} An array of file descriptors for each costume
|
||||||
*/
|
*/
|
||||||
const serializeCostumes = function (runtime) {
|
const serializeCostumes = function (runtime, optTargetId) {
|
||||||
return serializeAssets(runtime, 'costumes');
|
return serializeAssets(runtime, 'costumes', optTargetId);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
|
@ -170,21 +170,30 @@ class RenderedTarget extends Target {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get audioPlayer () {
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
console.warn('get audioPlayer deprecated, please update to use .sprite.soundBank methods');
|
||||||
|
console.warn(new Error('stack for debug').stack);
|
||||||
|
/* eslint-enable no-console */
|
||||||
|
const bank = this.sprite.soundBank;
|
||||||
|
const audioPlayerProxy = {
|
||||||
|
playSound: soundId => bank.play(this, soundId)
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(this, 'audioPlayer', {
|
||||||
|
configurable: false,
|
||||||
|
enumerable: true,
|
||||||
|
writable: false,
|
||||||
|
value: audioPlayerProxy
|
||||||
|
});
|
||||||
|
|
||||||
|
return audioPlayerProxy;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the audio player for this sprite or clone.
|
* Initialize the audio player for this sprite or clone.
|
||||||
*/
|
*/
|
||||||
initAudio () {
|
initAudio () {
|
||||||
this.audioPlayer = null;
|
|
||||||
if (this.runtime && this.runtime.audioEngine) {
|
|
||||||
this.audioPlayer = this.runtime.audioEngine.createPlayer();
|
|
||||||
// If this is a clone, it gets a reference to its parent's activeSoundPlayers object.
|
|
||||||
if (!this.isOriginal) {
|
|
||||||
const parent = this.sprite.clones[0];
|
|
||||||
if (parent && parent.audioPlayer) {
|
|
||||||
this.audioPlayer.activeSoundPlayers = parent.audioPlayer.activeSoundPlayers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1004,8 +1013,8 @@ class RenderedTarget extends Target {
|
||||||
const newTarget = newSprite.createClone();
|
const newTarget = newSprite.createClone();
|
||||||
// Copy all properties.
|
// Copy all properties.
|
||||||
// @todo refactor with clone methods
|
// @todo refactor with clone methods
|
||||||
newTarget.x = Math.random() * 400 / 2;
|
newTarget.x = (Math.random() - 0.5) * 400 / 2;
|
||||||
newTarget.y = Math.random() * 300 / 2;
|
newTarget.y = (Math.random() - 0.5) * 300 / 2;
|
||||||
newTarget.direction = this.direction;
|
newTarget.direction = this.direction;
|
||||||
newTarget.draggable = this.draggable;
|
newTarget.draggable = this.draggable;
|
||||||
newTarget.visible = this.visible;
|
newTarget.visible = this.visible;
|
||||||
|
@ -1034,9 +1043,8 @@ class RenderedTarget extends Target {
|
||||||
*/
|
*/
|
||||||
onStopAll () {
|
onStopAll () {
|
||||||
this.clearEffects();
|
this.clearEffects();
|
||||||
if (this.audioPlayer) {
|
if (this.sprite.soundBank) {
|
||||||
this.audioPlayer.stopAllSounds();
|
this.sprite.soundBank.stopAllSounds();
|
||||||
this.audioPlayer.clearEffects();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1132,10 +1140,6 @@ class RenderedTarget extends Target {
|
||||||
this.runtime.requestRedraw();
|
this.runtime.requestRedraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.audioPlayer) {
|
|
||||||
this.audioPlayer.stopAllSounds();
|
|
||||||
this.audioPlayer.dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ const StageLayering = require('../engine/stage-layering');
|
||||||
class Sprite {
|
class Sprite {
|
||||||
/**
|
/**
|
||||||
* Sprite to be used on the Scratch stage.
|
* Sprite to be used on the Scratch stage.
|
||||||
* All clones of a sprite have shared blocks, shared costumes, shared variables.
|
* All clones of a sprite have shared blocks, shared costumes, shared variables,
|
||||||
|
* shared sounds, etc.
|
||||||
* @param {?Blocks} blocks Shared blocks object for all clones of sprite.
|
* @param {?Blocks} blocks Shared blocks object for all clones of sprite.
|
||||||
* @param {Runtime} runtime Reference to the runtime.
|
* @param {Runtime} runtime Reference to the runtime.
|
||||||
* @constructor
|
* @constructor
|
||||||
|
@ -47,6 +48,11 @@ class Sprite {
|
||||||
* @type {Array.<!RenderedTarget>}
|
* @type {Array.<!RenderedTarget>}
|
||||||
*/
|
*/
|
||||||
this.clones = [];
|
this.clones = [];
|
||||||
|
|
||||||
|
this.soundBank = null;
|
||||||
|
if (this.runtime && this.runtime.audioEngine) {
|
||||||
|
this.soundBank = this.runtime.audioEngine.createBank();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -149,12 +155,18 @@ class Sprite {
|
||||||
newSprite.sounds = this.sounds.map(sound => {
|
newSprite.sounds = this.sounds.map(sound => {
|
||||||
const newSound = Object.assign({}, sound);
|
const newSound = Object.assign({}, sound);
|
||||||
const soundAsset = this.runtime.storage.get(sound.assetId);
|
const soundAsset = this.runtime.storage.get(sound.assetId);
|
||||||
assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime));
|
assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime, this));
|
||||||
return newSound;
|
return newSound;
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(assetPromises).then(() => newSprite);
|
return Promise.all(assetPromises).then(() => newSprite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose () {
|
||||||
|
if (this.soundBank) {
|
||||||
|
this.soundBank.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Sprite;
|
module.exports = Sprite;
|
||||||
|
|
|
@ -263,14 +263,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
|
|
||||||
// Put everything in a zip file
|
// Put everything in a zip file
|
||||||
zip.file('project.json', projectJson);
|
zip.file('project.json', projectJson);
|
||||||
for (let i = 0; i < soundDescs.length; i++) {
|
this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip);
|
||||||
const currSound = soundDescs[i];
|
|
||||||
zip.file(currSound.fileName, currSound.fileContent);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < costumeDescs.length; i++) {
|
|
||||||
const currCostume = costumeDescs[i];
|
|
||||||
zip.file(currCostume.fileName, currCostume.fileContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return zip.generateAsync({
|
return zip.generateAsync({
|
||||||
type: 'blob',
|
type: 'blob',
|
||||||
|
@ -281,6 +274,43 @@ class VirtualMachine extends EventEmitter {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_addFileDescsToZip (fileDescs, zip) {
|
||||||
|
for (let i = 0; i < fileDescs.length; i++) {
|
||||||
|
const currFileDesc = fileDescs[i];
|
||||||
|
zip.file(currFileDesc.fileName, currFileDesc.fileContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports a sprite in the sprite3 format.
|
||||||
|
* @param {string} targetId ID of the target to export
|
||||||
|
* @param {string=} optZipType Optional type that the resulting
|
||||||
|
* zip should be outputted in. Options are: base64, binarystring,
|
||||||
|
* array, uint8array, arraybuffer, blob, or nodebuffer. Defaults to
|
||||||
|
* blob if argument not provided.
|
||||||
|
* See https://stuk.github.io/jszip/documentation/api_jszip/generate_async.html#type-option
|
||||||
|
* for more information about these options.
|
||||||
|
* @return {object} A generated zip of the sprite and its assets in the format
|
||||||
|
* specified by optZipType or blob by default.
|
||||||
|
*/
|
||||||
|
exportSprite (targetId, optZipType) {
|
||||||
|
const soundDescs = serializeSounds(this.runtime, targetId);
|
||||||
|
const costumeDescs = serializeCostumes(this.runtime, targetId);
|
||||||
|
const spriteJson = JSON.stringify(sb3.serialize(this.runtime, targetId));
|
||||||
|
|
||||||
|
const zip = new JSZip();
|
||||||
|
zip.file('sprite.json', spriteJson);
|
||||||
|
this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip);
|
||||||
|
|
||||||
|
return zip.generateAsync({
|
||||||
|
type: typeof optZipType === 'string' ? optZipType : 'blob',
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
compressionOptions: {
|
||||||
|
level: 6
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export project as a Scratch 3.0 JSON representation.
|
* Export project as a Scratch 3.0 JSON representation.
|
||||||
* @return {string} Serialized state of the runtime.
|
* @return {string} Serialized state of the runtime.
|
||||||
|
@ -368,6 +398,10 @@ class VirtualMachine extends EventEmitter {
|
||||||
this.editingTarget = targets[0];
|
this.editingTarget = targets[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!wholeProject) {
|
||||||
|
this.editingTarget.fixUpVariableReferences();
|
||||||
|
}
|
||||||
|
|
||||||
// Update the VM user's knowledge of targets and blocks on the workspace.
|
// Update the VM user's knowledge of targets and blocks on the workspace.
|
||||||
this.emitTargetsUpdate();
|
this.emitTargetsUpdate();
|
||||||
this.emitWorkspaceUpdate();
|
this.emitWorkspaceUpdate();
|
||||||
|
@ -495,7 +529,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
duplicateSound (soundIndex) {
|
duplicateSound (soundIndex) {
|
||||||
const originalSound = this.editingTarget.getSounds()[soundIndex];
|
const originalSound = this.editingTarget.getSounds()[soundIndex];
|
||||||
const clone = Object.assign({}, originalSound);
|
const clone = Object.assign({}, originalSound);
|
||||||
return loadSound(clone, this.runtime).then(() => {
|
return loadSound(clone, this.runtime, this.editingTarget.sprite).then(() => {
|
||||||
this.editingTarget.addSound(clone, soundIndex + 1);
|
this.editingTarget.addSound(clone, soundIndex + 1);
|
||||||
this.emitTargetsUpdate();
|
this.emitTargetsUpdate();
|
||||||
});
|
});
|
||||||
|
@ -525,7 +559,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
* @returns {?Promise} - a promise that resolves when the sound has been decoded and added
|
* @returns {?Promise} - a promise that resolves when the sound has been decoded and added
|
||||||
*/
|
*/
|
||||||
addSound (soundObject) {
|
addSound (soundObject) {
|
||||||
return loadSound(soundObject, this.runtime).then(() => {
|
return loadSound(soundObject, this.runtime, this.editingTarget.sprite).then(() => {
|
||||||
this.editingTarget.addSound(soundObject);
|
this.editingTarget.addSound(soundObject);
|
||||||
this.emitTargetsUpdate();
|
this.emitTargetsUpdate();
|
||||||
});
|
});
|
||||||
|
@ -549,7 +583,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
getSoundBuffer (soundIndex) {
|
getSoundBuffer (soundIndex) {
|
||||||
const id = this.editingTarget.sprite.sounds[soundIndex].soundId;
|
const id = this.editingTarget.sprite.sounds[soundIndex].soundId;
|
||||||
if (id && this.runtime && this.runtime.audioEngine) {
|
if (id && this.runtime && this.runtime.audioEngine) {
|
||||||
return this.runtime.audioEngine.getSoundBuffer(id);
|
return this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -564,7 +598,7 @@ class VirtualMachine extends EventEmitter {
|
||||||
const sound = this.editingTarget.sprite.sounds[soundIndex];
|
const sound = this.editingTarget.sprite.sounds[soundIndex];
|
||||||
const id = sound ? sound.soundId : null;
|
const id = sound ? sound.soundId : null;
|
||||||
if (id && this.runtime && this.runtime.audioEngine) {
|
if (id && this.runtime && this.runtime.audioEngine) {
|
||||||
this.runtime.audioEngine.updateSoundBuffer(id, newBuffer);
|
this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer = newBuffer;
|
||||||
}
|
}
|
||||||
// Update sound in runtime
|
// Update sound in runtime
|
||||||
if (soundEncoding) {
|
if (soundEncoding) {
|
||||||
|
@ -966,8 +1000,8 @@ class VirtualMachine extends EventEmitter {
|
||||||
shareSoundToTarget (soundIndex, targetId) {
|
shareSoundToTarget (soundIndex, targetId) {
|
||||||
const originalSound = this.editingTarget.getSounds()[soundIndex];
|
const originalSound = this.editingTarget.getSounds()[soundIndex];
|
||||||
const clone = Object.assign({}, originalSound);
|
const clone = Object.assign({}, originalSound);
|
||||||
return loadSound(clone, this.runtime).then(() => {
|
const target = this.runtime.getTargetById(targetId);
|
||||||
const target = this.runtime.getTargetById(targetId);
|
return loadSound(clone, this.runtime, target.sprite).then(() => {
|
||||||
if (target) {
|
if (target) {
|
||||||
target.addSound(clone);
|
target.addSound(clone);
|
||||||
this.emitTargetsUpdate();
|
this.emitTargetsUpdate();
|
||||||
|
|
12
test/fixtures/events.json
vendored
12
test/fixtures/events.json
vendored
|
@ -90,5 +90,17 @@
|
||||||
"type": "comment_create",
|
"type": "comment_create",
|
||||||
"commentId": "a comment",
|
"commentId": "a comment",
|
||||||
"xy": {"x": 10, "y": 20}
|
"xy": {"x": 10, "y": 20}
|
||||||
|
},
|
||||||
|
"mockVariableBlock": {
|
||||||
|
"name": "block",
|
||||||
|
"xml": {
|
||||||
|
"outerHTML": "<block type='data_variable' id='a block' x='0' y='0'><field name='VARIABLE' id='mock var id' variabletype=''>a mock variable</field></block>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mockListBlock": {
|
||||||
|
"name": "block",
|
||||||
|
"xml": {
|
||||||
|
"outerHTML": "<block type='data_listcontents' id='another block' x='0' y='0'><field name='LIST' id='mock list id' variabletype=''>a mock list</field></block>"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,10 @@ const util = {
|
||||||
{name: 'second name', soundId: 'second soundId'},
|
{name: 'second name', soundId: 'second soundId'},
|
||||||
{name: 'third name', soundId: 'third soundId'},
|
{name: 'third name', soundId: 'third soundId'},
|
||||||
{name: '6', soundId: 'fourth soundId'}
|
{name: '6', soundId: 'fourth soundId'}
|
||||||
]
|
],
|
||||||
},
|
soundBank: {
|
||||||
audioPlayer: {
|
playSound: (target, soundId) => (playedSound = soundId)
|
||||||
playSound: soundId => (playedSound = soundId)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
const test = require('tap').test;
|
const test = require('tap').test;
|
||||||
const Blocks = require('../../src/engine/blocks');
|
const Blocks = require('../../src/engine/blocks');
|
||||||
|
const Variable = require('../../src/engine/variable');
|
||||||
|
const adapter = require('../../src/engine/adapter');
|
||||||
|
const events = require('../fixtures/events.json');
|
||||||
|
|
||||||
test('spec', t => {
|
test('spec', t => {
|
||||||
const b = new Blocks();
|
const b = new Blocks();
|
||||||
|
@ -776,3 +779,32 @@ test('updateTargetSpecificBlocks changes sprite clicked hat to stage clicked for
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('getAllVariableAndListReferences returns an empty map references when variable blocks do not exist', t => {
|
||||||
|
const b = new Blocks();
|
||||||
|
t.equal(Object.keys(b.getAllVariableAndListReferences()).length, 0);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getAllVariableAndListReferences returns references when variable blocks exist', t => {
|
||||||
|
const b = new Blocks();
|
||||||
|
|
||||||
|
let varListRefs = b.getAllVariableAndListReferences();
|
||||||
|
t.equal(Object.keys(varListRefs).length, 0);
|
||||||
|
|
||||||
|
b.createBlock(adapter(events.mockVariableBlock)[0]);
|
||||||
|
b.createBlock(adapter(events.mockListBlock)[0]);
|
||||||
|
|
||||||
|
varListRefs = b.getAllVariableAndListReferences();
|
||||||
|
t.equal(Object.keys(varListRefs).length, 2);
|
||||||
|
t.equal(Array.isArray(varListRefs['mock var id']), true);
|
||||||
|
t.equal(varListRefs['mock var id'].length, 1);
|
||||||
|
t.equal(varListRefs['mock var id'][0].type, Variable.SCALAR_TYPE);
|
||||||
|
t.equal(varListRefs['mock var id'][0].referencingField.value, 'a mock variable');
|
||||||
|
t.equal(Array.isArray(varListRefs['mock list id']), true);
|
||||||
|
t.equal(varListRefs['mock list id'].length, 1);
|
||||||
|
t.equal(varListRefs['mock list id'][0].type, Variable.LIST_TYPE);
|
||||||
|
t.equal(varListRefs['mock list id'][0].referencingField.value, 'a mock list');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
const test = require('tap').test;
|
const test = require('tap').test;
|
||||||
const Target = require('../../src/engine/target');
|
const Target = require('../../src/engine/target');
|
||||||
const Variable = require('../../src/engine/variable');
|
const Variable = require('../../src/engine/variable');
|
||||||
|
const adapter = require('../../src/engine/adapter');
|
||||||
|
const Runtime = require('../../src/engine/runtime');
|
||||||
|
const events = require('../fixtures/events.json');
|
||||||
|
|
||||||
test('spec', t => {
|
test('spec', t => {
|
||||||
const target = new Target();
|
const target = new Target();
|
||||||
|
@ -145,7 +148,7 @@ test('deleteVariable2', t => {
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('lookupOrCreateList creates a list if var with given id does not exist', t => {
|
test('lookupOrCreateList creates a list if var with given id or var with given name does not exist', t => {
|
||||||
const target = new Target();
|
const target = new Target();
|
||||||
const variables = target.variables;
|
const variables = target.variables;
|
||||||
|
|
||||||
|
@ -174,6 +177,22 @@ test('lookupOrCreateList returns list if one with given id exists', t => {
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('lookupOrCreateList succeeds in finding list if id is incorrect but name matches', t => {
|
||||||
|
const target = new Target();
|
||||||
|
const variables = target.variables;
|
||||||
|
|
||||||
|
t.equal(Object.keys(variables).length, 0);
|
||||||
|
target.createVariable('foo', 'bar', Variable.LIST_TYPE);
|
||||||
|
t.equal(Object.keys(variables).length, 1);
|
||||||
|
|
||||||
|
const listVar = target.lookupOrCreateList('not foo', 'bar');
|
||||||
|
t.equal(Object.keys(variables).length, 1);
|
||||||
|
t.equal(listVar.id, 'foo');
|
||||||
|
t.equal(listVar.name, 'bar');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
test('lookupBroadcastMsg returns the var with given id if exists', t => {
|
test('lookupBroadcastMsg returns the var with given id if exists', t => {
|
||||||
const target = new Target();
|
const target = new Target();
|
||||||
const variables = target.variables;
|
const variables = target.variables;
|
||||||
|
@ -263,3 +282,147 @@ test('creating a comment with a blockId also updates the comment property on the
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('fixUpVariableReferences fixes sprite global var conflicting with project global var', t => {
|
||||||
|
const runtime = new Runtime();
|
||||||
|
|
||||||
|
const stage = new Target(runtime);
|
||||||
|
stage.isStage = true;
|
||||||
|
|
||||||
|
const target = new Target(runtime);
|
||||||
|
target.isStage = false;
|
||||||
|
|
||||||
|
runtime.targets = [stage, target];
|
||||||
|
|
||||||
|
// Create a global variable
|
||||||
|
stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||||
|
|
||||||
|
target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
|
||||||
|
|
||||||
|
t.equal(Object.keys(target.variables).length, 0);
|
||||||
|
t.equal(Object.keys(stage.variables).length, 1);
|
||||||
|
t.type(target.blocks.getBlock('a block'), 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||||
|
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||||
|
|
||||||
|
target.fixUpVariableReferences();
|
||||||
|
|
||||||
|
t.equal(Object.keys(target.variables).length, 0);
|
||||||
|
t.equal(Object.keys(stage.variables).length, 1);
|
||||||
|
t.type(target.blocks.getBlock('a block'), 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||||
|
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'pre-existing global var id');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fixUpVariableReferences fixes sprite local var conflicting with project global var', t => {
|
||||||
|
const runtime = new Runtime();
|
||||||
|
|
||||||
|
const stage = new Target(runtime);
|
||||||
|
stage.isStage = true;
|
||||||
|
|
||||||
|
const target = new Target(runtime);
|
||||||
|
target.isStage = false;
|
||||||
|
target.getName = () => 'Target';
|
||||||
|
|
||||||
|
runtime.targets = [stage, target];
|
||||||
|
|
||||||
|
// Create a global variable
|
||||||
|
stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||||
|
target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||||
|
|
||||||
|
target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
|
||||||
|
|
||||||
|
t.equal(Object.keys(target.variables).length, 1);
|
||||||
|
t.equal(Object.keys(stage.variables).length, 1);
|
||||||
|
t.type(target.blocks.getBlock('a block'), 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||||
|
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||||
|
t.equal(target.variables['mock var id'].name, 'a mock variable');
|
||||||
|
|
||||||
|
target.fixUpVariableReferences();
|
||||||
|
|
||||||
|
t.equal(Object.keys(target.variables).length, 1);
|
||||||
|
t.equal(Object.keys(stage.variables).length, 1);
|
||||||
|
t.type(target.blocks.getBlock('a block'), 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||||
|
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||||
|
t.equal(target.variables['mock var id'].name, 'Target: a mock variable');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fixUpVariableReferences fixes conflicting sprite local var without blocks referencing var', t => {
|
||||||
|
const runtime = new Runtime();
|
||||||
|
|
||||||
|
const stage = new Target(runtime);
|
||||||
|
stage.isStage = true;
|
||||||
|
|
||||||
|
const target = new Target(runtime);
|
||||||
|
target.isStage = false;
|
||||||
|
target.getName = () => 'Target';
|
||||||
|
|
||||||
|
runtime.targets = [stage, target];
|
||||||
|
|
||||||
|
// Create a global variable
|
||||||
|
stage.createVariable('pre-existing global var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||||
|
target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||||
|
|
||||||
|
|
||||||
|
t.equal(Object.keys(target.variables).length, 1);
|
||||||
|
t.equal(Object.keys(stage.variables).length, 1);
|
||||||
|
t.equal(target.variables['mock var id'].name, 'a mock variable');
|
||||||
|
|
||||||
|
target.fixUpVariableReferences();
|
||||||
|
|
||||||
|
t.equal(Object.keys(target.variables).length, 1);
|
||||||
|
t.equal(Object.keys(stage.variables).length, 1);
|
||||||
|
t.equal(target.variables['mock var id'].name, 'Target: a mock variable');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fixUpVariableReferences does not change variable name if there is no variable conflict', t => {
|
||||||
|
const runtime = new Runtime();
|
||||||
|
|
||||||
|
const stage = new Target(runtime);
|
||||||
|
stage.isStage = true;
|
||||||
|
|
||||||
|
const target = new Target(runtime);
|
||||||
|
target.isStage = false;
|
||||||
|
target.getName = () => 'Target';
|
||||||
|
|
||||||
|
runtime.targets = [stage, target];
|
||||||
|
|
||||||
|
// Create a global variable
|
||||||
|
stage.createVariable('pre-existing global var id', 'a variable', Variable.SCALAR_TYPE);
|
||||||
|
stage.createVariable('pre-existing global list id', 'a mock variable', Variable.LIST_TYPE);
|
||||||
|
target.createVariable('mock var id', 'a mock variable', Variable.SCALAR_TYPE);
|
||||||
|
|
||||||
|
target.blocks.createBlock(adapter(events.mockVariableBlock)[0]);
|
||||||
|
|
||||||
|
t.equal(Object.keys(target.variables).length, 1);
|
||||||
|
t.equal(Object.keys(stage.variables).length, 2);
|
||||||
|
t.type(target.blocks.getBlock('a block'), 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||||
|
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||||
|
t.equal(target.variables['mock var id'].name, 'a mock variable');
|
||||||
|
|
||||||
|
target.fixUpVariableReferences();
|
||||||
|
|
||||||
|
t.equal(Object.keys(target.variables).length, 1);
|
||||||
|
t.equal(Object.keys(stage.variables).length, 2);
|
||||||
|
t.type(target.blocks.getBlock('a block'), 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields, 'object');
|
||||||
|
t.type(target.blocks.getBlock('a block').fields.VARIABLE, 'object');
|
||||||
|
t.equal(target.blocks.getBlock('a block').fields.VARIABLE.id, 'mock var id');
|
||||||
|
t.equal(target.variables['mock var id'].name, 'a mock variable');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue