Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block versioning extension #5

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
11 changes: 11 additions & 0 deletions src/engine/blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const BlocksRuntimeCache = require('./blocks-runtime-cache');
const log = require('../util/log');
const Variable = require('./variable');
const getMonitorIdForBlockWithArgs = require('../util/get-monitor-id');
const ScratchBlocks = require('scratch-blocks');

/**
* @fileoverview
Expand Down Expand Up @@ -554,6 +555,16 @@ class Blocks {
// A new block was actually added to the block container,
// emit a project changed event
this.emitProjectChanged();

if (ScratchBlocks.getMainWorkspace()) {
const scratchBlock = ScratchBlocks.getMainWorkspace().getBlockById(block.id);
if (scratchBlock) {
const blockSvg = scratchBlock.getSvgRoot()
blockSvg.setAttribute('data-opcode', block.opcode);
}
}


}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/extension-support/extension-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,11 @@ class ExtensionManager {

// avoid promise latency if we can call direct
const serviceObject = dispatch.services[serviceName];
// var newFunc = funcName
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code

// if (funcName.includes("_v")) {
// newFunc = `internal_${funcName}`;
// }

if (!serviceObject[funcName]) {
// The function might show up later as a dynamic property of the service object
log.warn(`Could not find extension block function called ${funcName}`);
Expand Down
50 changes: 33 additions & 17 deletions src/serialization/sb3.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,16 +297,16 @@ const getExtensionIdForOpcode = function (opcode) {
* compressed primitives and the list of all extension IDs present
* in the serialized blocks.
*/
const serializeBlocks = function (blocks) {
const serializeBlocks = function (blocks, extensionManager) {
const obj = Object.create(null);
const extensionIDs = new Set();
for (const blockID in blocks) {
if (!Object.prototype.hasOwnProperty.call(blocks, blockID)) continue;
obj[blockID] = serializeBlock(blocks[blockID], blocks);
const extensionID = getExtensionIdForOpcode(blocks[blockID].opcode);
if (extensionID) {
extensionIDs.add(extensionID);
extensionIDs.add(extensionID);
}
obj[blockID] = serializeBlock(blocks[blockID], blocks);
}
// once we have completed a first pass, do a second pass on block inputs
for (const blockID in obj) {
Expand Down Expand Up @@ -451,7 +451,7 @@ const serializeComments = function (comments) {
* @param {Set} extensions A set of extensions to add extension IDs to
* @return {object} A serialized representation of the given target.
*/
const serializeTarget = function (target, extensions) {
const serializeTarget = function (target, extensions, extensionManager) {
const obj = Object.create(null);
let targetExtensions = [];
obj.isStage = target.isStage;
Expand All @@ -460,7 +460,7 @@ const serializeTarget = function (target, extensions) {
obj.variables = vars.variables;
obj.lists = vars.lists;
obj.broadcasts = vars.broadcasts;
[obj.blocks, targetExtensions] = serializeBlocks(target.blocks);
[obj.blocks, targetExtensions] = serializeBlocks(target.blocks, extensionManager);
obj.comments = serializeComments(target.comments);

// TODO remove this check/patch when (#1901) is fixed
Expand Down Expand Up @@ -564,7 +564,7 @@ const serialize = function (runtime, targetId, /* PRG ADDITION BEGIN */ extensio
});
}

const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions));
const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions, extensionManager));

if (targetId) {
return serializedTargets[0];
Expand All @@ -577,6 +577,7 @@ const serialize = function (runtime, targetId, /* PRG ADDITION BEGIN */ extensio
/* PRG ADDITION BEGIN */
extensionManager.getLoadedExtensionIDs().forEach(id => {
const instance = extensionManager.getExtensionInstance(id);
obj.targets = instance["alterOpcodes"]?.(obj.targets);
instance["save"]?.(obj, extensions);
});
/* PRG ADDITION END */
Expand Down Expand Up @@ -839,12 +840,14 @@ const deserializeFields = function (fields) {
* @param {object} blocks Serialized SB3 "blocks" property of a target. Will be mutated.
* @return {object} input is modified and returned
*/
const deserializeBlocks = function (blocks) {
const deserializeBlocks = function (blocks, extensionManager) {
for (const blockId in blocks) {
if (!Object.prototype.hasOwnProperty.call(blocks, blockId)) {
continue;
}
const block = blocks[blockId];


if (Array.isArray(block)) {
// this is one of the primitives
// delete the old entry in object.blocks and replace it w/the
Expand All @@ -854,8 +857,10 @@ const deserializeBlocks = function (blocks) {
continue;
}
block.id = blockId; // add id back to block since it wasn't serialized

block.inputs = deserializeInputs(block.inputs, blockId, blocks);
block.fields = deserializeFields(block.fields);

}
return blocks;
};
Expand Down Expand Up @@ -957,7 +962,7 @@ const parseScratchAssets = function (object, runtime, zip) {
* into costumes and sounds
* @return {!Promise.<Target>} Promise for the target created (stage or sprite), or null for unsupported objects.
*/
const parseScratchObject = function (object, runtime, extensions, zip, assets) {
const parseScratchObject = function (object, runtime, extensions, zip, assets, extensionManager) {
if (!Object.prototype.hasOwnProperty.call(object, 'name')) {
// Watcher/monitor - skip this object until those are implemented in VM.
// @todo
Expand All @@ -974,18 +979,18 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
sprite.name = object.name;
}
if (Object.prototype.hasOwnProperty.call(object, 'blocks')) {
deserializeBlocks(object.blocks);
deserializeBlocks(object.blocks, extensionManager);
// Take a second pass to create objects and add extensions
for (const blockId in object.blocks) {
if (!Object.prototype.hasOwnProperty.call(object.blocks, blockId)) continue;
const blockJSON = object.blocks[blockId];
blocks.createBlock(blockJSON);

// If the block is from an extension, record it.
let blockJSON = object.blocks[blockId];
const extensionID = getExtensionIdForOpcode(blockJSON.opcode);
blocks.createBlock(blockJSON);
if (extensionID) {
extensions.extensionIDs.add(extensionID);
}


}
}
// Costumes from JSON.
Expand Down Expand Up @@ -1125,6 +1130,7 @@ const deserializeMonitor = function (monitorData, runtime, targets, extensions)
// If the serialized monitor has spriteName defined, look up the sprite
// by name in the given list of targets and update the monitor's targetId
// to match the sprite's id.

if (monitorData.spriteName) {
const filteredTargets = targets.filter(t => t.sprite.name === monitorData.spriteName);
if (filteredTargets && filteredTargets.length > 0) {
Expand Down Expand Up @@ -1263,16 +1269,23 @@ const replaceUnsafeCharsInVariableIds = function (targets) {
* @param {Runtime} runtime - Runtime instance
* @param {JSZip} zip - Sb3 file describing this project (to load assets from)
* @param {boolean} isSingleSprite - If true treat as single sprite, else treat as whole project
* @param {import("../extension-support/extension-manager")} extensionManager Reference to VM's extension manager.
* @returns {Promise.<ImportedProject>} Promise that resolves to the list of targets after the project is deserialized
*/
const deserialize = function (json, runtime, zip, isSingleSprite) {
const deserialize = function (json, runtime, zip, isSingleSpriteOrExtensionManager) {
const extensions = {
extensionIDs: new Set(),
extensionURLs: new Map()
};

/* PRG ADDITION BEGIN */
json["extensions"]?.forEach(id => extensions.extensionIDs.add(id));

var extensionManager = null;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mayarajan3 Are these changes still necessary?

var isSingleSprite = false;
if (isSingleSpriteOrExtensionManager && isSingleSpriteOrExtensionManager.runtime == runtime) {
extensionManager = isSingleSpriteOrExtensionManager;
} else if (isSingleSpriteOrExtensionManager == true && isSingleSpriteOrExtensionManager == false) {
isSingleSprite = isSingleSpriteOrExtensionManager;
}

// Unpack the data for the text model
runtime.modelData = { "textData": {}, "classifierData": {}, "nextLabelNumber": 1 };
Expand All @@ -1297,6 +1310,9 @@ const deserialize = function (json, runtime, zip, isSingleSprite) {
runtime.origin = null;
}

console.log("extension manager");
console.log(extensionManager);

// First keep track of the current target order in the json,
// then sort by the layer order property before parsing the targets
// so that their corresponding render drawables can be created in
Expand All @@ -1316,7 +1332,7 @@ const deserialize = function (json, runtime, zip, isSingleSprite) {
.then(assets => Promise.resolve(assets))
.then(assets => Promise.all(targetObjects
.map((target, index) =>
parseScratchObject(target, runtime, extensions, zip, assets[index]))))
parseScratchObject(target, runtime, extensions, zip, assets[index], extensionManager))))
.then(targets => targets // Re-sort targets back into original sprite-pane ordering
.map((t, i) => {
// Add layer order property to deserialized targets.
Expand Down
19 changes: 19 additions & 0 deletions src/virtual-machine.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ class VirtualMachine extends EventEmitter {
performance.mark('scratch-vm-deserialize-start');
}
const runtime = this.runtime;
const { extensionManager } = this;
const deserializePromise = function () {
const projectVersion = projectJSON.projectVersion;
if (projectVersion === 2) {
Expand All @@ -560,7 +561,25 @@ class VirtualMachine extends EventEmitter {
}
if (projectVersion === 3) {
const sb3 = require('./serialization/sb3');
var extensionPromises = [];
if (projectJSON["extensions"]) {
extensionPromises = projectJSON["extensions"].map(async extensionID => {
if (!extensionManager.isExtensionLoaded(extensionID)) {
await extensionManager.loadExtensionURL(extensionID);
console.log("EXTENSION ID LOADED");
}
const instance = extensionManager.getExtensionInstance(extensionID);
projectJSON = instance.alterJSON(projectJSON);
return instance;
});
}

return Promise.all(extensionPromises)
.then(() => {
return sb3.deserialize(projectJSON, runtime, zip, extensionManager);
})
return sb3.deserialize(projectJSON, runtime, zip);

}
// TODO: reject with an Error (possible breaking API change!)
// eslint-disable-next-line prefer-promise-reject-errors
Expand Down