Skip to content

Commit 764deef

Browse files
authored
Merge pull request #528 from scratchfoundation/fix/top-level-shadow-blocks
fix: prevent shadow blocks from being top-level in SB3 round-trip
2 parents cff4bb6 + 5fb564d commit 764deef

2 files changed

Lines changed: 110 additions & 1 deletion

File tree

packages/scratch-vm/src/serialization/sb3.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ const serializeBlock = function (block) {
201201
obj.inputs = serializeInputs(block.inputs);
202202
obj.fields = serializeFields(block.fields);
203203
obj.shadow = block.shadow;
204-
if (block.topLevel) {
204+
if (block.topLevel && !block.shadow) {
205205
obj.topLevel = true;
206206
obj.x = block.x ? Math.round(block.x) : 0;
207207
obj.y = block.y ? Math.round(block.y) : 0;
@@ -840,6 +840,12 @@ const deserializeBlocks = function (blocks) {
840840
block.id = blockId; // add id back to block since it wasn't serialized
841841
block.inputs = deserializeInputs(block.inputs, blockId, blocks);
842842
block.fields = deserializeFields(block.fields);
843+
// Obscured shadow blocks can be serialized as top-level by
844+
// production Scratch. Upstream Blockly throws if a shadow
845+
// appears at the top level of the workspace XML.
846+
if (block.shadow && block.topLevel) {
847+
block.topLevel = false;
848+
}
843849

844850
if (block.comment) {
845851
// Pre-Blockly v12 Scratch used arbitrary IDs for block comments.

packages/scratch-vm/test/unit/serialization_sb3.js

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,70 @@ test('serializeBlocks serializes x and y for topLevel blocks with x,y of 0,0', t
255255
});
256256
});
257257

258+
test('serializeBlock does not serialize shadow blocks as top-level', t => {
259+
const blocks = {
260+
hatBlock: {
261+
id: 'hatBlock',
262+
opcode: 'event_whenflagclicked',
263+
next: 'costumeBlock',
264+
parent: null,
265+
inputs: {},
266+
fields: {},
267+
shadow: false,
268+
topLevel: true,
269+
x: 0,
270+
y: 0
271+
},
272+
costumeBlock: {
273+
id: 'costumeBlock',
274+
opcode: 'looks_switchcostumeto',
275+
next: null,
276+
parent: 'hatBlock',
277+
inputs: {
278+
COSTUME: {
279+
name: 'COSTUME',
280+
block: 'varReporter',
281+
shadow: 'costumeShadow'
282+
}
283+
},
284+
fields: {},
285+
shadow: false,
286+
topLevel: false
287+
},
288+
varReporter: {
289+
id: 'varReporter',
290+
opcode: 'data_variable',
291+
next: null,
292+
parent: 'costumeBlock',
293+
inputs: {},
294+
fields: {VARIABLE: {name: 'VARIABLE', value: 'my variable', id: 'var1'}},
295+
shadow: false,
296+
topLevel: false
297+
},
298+
costumeShadow: {
299+
id: 'costumeShadow',
300+
opcode: 'looks_costume',
301+
next: null,
302+
parent: null,
303+
inputs: {},
304+
fields: {COSTUME: {name: 'COSTUME', value: 'costume1'}},
305+
shadow: true,
306+
topLevel: true // bug: should not be serialized as top-level
307+
}
308+
};
309+
310+
const result = sb3.serializeBlocks(blocks);
311+
const serialized = result[0];
312+
313+
t.equal(serialized.costumeShadow.topLevel, false,
314+
'shadow block should not be serialized as top-level');
315+
t.notOk(serialized.costumeShadow.x,
316+
'shadow block should not have x coordinate');
317+
t.notOk(serialized.costumeShadow.y,
318+
'shadow block should not have y coordinate');
319+
t.end();
320+
});
321+
258322
test('deserializeBlocks', t => {
259323
const vm = new VirtualMachine();
260324
vm.loadProject(readFileToBuffer(commentsSB3ProjectPath))
@@ -280,6 +344,45 @@ test('deserializeBlocks on already deserialized input', t => {
280344
});
281345
});
282346

347+
test('deserializeBlocks clears topLevel on shadow blocks', t => {
348+
// Production Scratch can serialize obscured shadow blocks as top-level
349+
// (e.g. when a variable reporter covers a dropdown's shadow). Upstream
350+
// Blockly throws "Shadow block cannot be a top-level block" if these
351+
// survive into the XML, so deserialization must clear the flag.
352+
const blocks = {
353+
parentBlock: {
354+
opcode: 'looks_switchcostumeto',
355+
next: null,
356+
parent: null,
357+
inputs: {
358+
COSTUME: [3, [12, 'my variable', 'var_id'], 'shadowBlock']
359+
},
360+
fields: {},
361+
shadow: false,
362+
topLevel: true,
363+
x: 0,
364+
y: 0
365+
},
366+
shadowBlock: {
367+
opcode: 'looks_costume',
368+
next: null,
369+
parent: null,
370+
inputs: {},
371+
fields: {COSTUME: ['costume1', null]},
372+
shadow: true,
373+
topLevel: true,
374+
x: 100,
375+
y: 100
376+
}
377+
};
378+
379+
sb3.deserializeBlocks(blocks);
380+
381+
t.equal(blocks.shadowBlock.topLevel, false,
382+
'shadow block should not be top-level after deserialization');
383+
t.end();
384+
});
385+
283386
test('getExtensionIdForOpcode', t => {
284387
t.equal(sb3.getExtensionIdForOpcode('wedo_loopy'), 'wedo');
285388

0 commit comments

Comments
 (0)