@scratch/scratch-vm 13.7.1 → 13.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/node/scratch-vm.js +1 -1
- package/dist/web/scratch-vm.js +1 -1
- package/package.json +4 -4
- package/src/engine/blocks.js +1 -1
- package/src/engine/target.js +147 -86
- package/src/virtual-machine.js +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scratch/scratch-vm",
|
|
3
|
-
"version": "13.7.
|
|
3
|
+
"version": "13.7.3",
|
|
4
4
|
"description": "Virtual Machine for Scratch 3.0",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/scratchfoundation/scratch-vm#readme",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"allow-incomplete-coverage": true
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@scratch/scratch-render": "13.7.
|
|
64
|
-
"@scratch/scratch-svg-renderer": "13.7.
|
|
63
|
+
"@scratch/scratch-render": "13.7.3",
|
|
64
|
+
"@scratch/scratch-svg-renderer": "13.7.3",
|
|
65
65
|
"@vernier/godirect": "1.8.3",
|
|
66
66
|
"arraybuffer-loader": "1.0.8",
|
|
67
67
|
"atob": "2.1.2",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"in-publish": "2.0.1",
|
|
100
100
|
"js-md5": "0.7.3",
|
|
101
101
|
"pngjs": "3.4.0",
|
|
102
|
-
"scratch-blocks": "2.1.
|
|
102
|
+
"scratch-blocks": "2.1.19",
|
|
103
103
|
"scratch-l10n": "6.1.72",
|
|
104
104
|
"scratch-render-fonts": "1.0.252",
|
|
105
105
|
"scratch-semantic-release-config": "4.0.1",
|
package/src/engine/blocks.js
CHANGED
|
@@ -598,7 +598,7 @@ class Blocks {
|
|
|
598
598
|
// Push block id to scripts array.
|
|
599
599
|
// Blocks are added as a top-level stack if they are marked as a top-block
|
|
600
600
|
// (if they were top-level XML in the event).
|
|
601
|
-
if (block.topLevel) {
|
|
601
|
+
if (block.topLevel && !block.shadow) {
|
|
602
602
|
this._addScript(block.id);
|
|
603
603
|
}
|
|
604
604
|
|
package/src/engine/target.js
CHANGED
|
@@ -654,52 +654,38 @@ class Target extends EventEmitter {
|
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
/**
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
* to variable names that already exist as global variables in the project
|
|
662
|
-
* (and thus are in scope for variable references in the given sprite).
|
|
657
|
+
* Reconciles variable, list, and broadcast references on this target against
|
|
658
|
+
* the variables actually defined in the project, creating definitions on the
|
|
659
|
+
* stage for any references whose ids are not defined anywhere. Does not
|
|
660
|
+
* rename any existing variables.
|
|
663
661
|
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
662
|
+
* For each variable, list, or broadcast referenced by a block on this target:
|
|
663
|
+
* - If the referenced id is found locally on this sprite or on the stage,
|
|
664
|
+
* the reference is already correct and nothing happens.
|
|
665
|
+
* - If the referenced id is not defined anywhere, the stage is checked for a
|
|
666
|
+
* variable with the same name and type. If one exists, the field id is
|
|
667
|
+
* remapped to that existing global. Otherwise, a new global is created on
|
|
668
|
+
* the stage with a non-conflicting name and the field name is updated.
|
|
668
669
|
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
* All blocks that reference the local variable will be updated to use the new name.
|
|
670
|
+
* Used during whole-project load to repair projects corrupted by historical
|
|
671
|
+
* bugs that left dangling references, and as the definition-creation phase
|
|
672
|
+
* of `fixUpVariableReferences` for sprite import and backpack paste.
|
|
673
673
|
*/
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (!this.runtime) return; // There's no runtime context to conflict with
|
|
677
|
-
if (this.isStage) return; // Stage can't have variable conflicts with itself (and also can't be uploaded)
|
|
674
|
+
reconcileVariableReferences () {
|
|
675
|
+
if (!this.runtime) return;
|
|
678
676
|
const stage = this.runtime.getTargetForStage();
|
|
679
677
|
if (!stage || !stage.variables) return;
|
|
680
678
|
|
|
681
|
-
const
|
|
682
|
-
const conflict = stage.lookupVariableByNameAndType(name, type);
|
|
683
|
-
if (conflict) {
|
|
684
|
-
const newName = StringUtil.unusedName(
|
|
685
|
-
`${this.getName()}: ${name}`,
|
|
686
|
-
this.getAllVariableNamesInScopeByType(type));
|
|
687
|
-
this.renameVariable(id, newName);
|
|
688
|
-
return newName;
|
|
689
|
-
}
|
|
690
|
-
return null;
|
|
691
|
-
};
|
|
692
|
-
|
|
693
|
-
const allReferences = this.blocks.getAllVariableAndListReferences();
|
|
694
|
-
const unreferencedLocalVarIds = [];
|
|
695
|
-
if (Object.keys(this.variables).length > 0) {
|
|
696
|
-
for (const localVarId in this.variables) {
|
|
697
|
-
if (!Object.prototype.hasOwnProperty.call(this.variables, localVarId)) continue;
|
|
698
|
-
if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
679
|
+
const allReferences = this.blocks.getAllVariableAndListReferences(null, true);
|
|
701
680
|
const conflictIdsToReplace = Object.create(null);
|
|
702
681
|
const conflictNamesToReplace = Object.create(null);
|
|
682
|
+
// When a dangling reference triggers creation of a new stage variable, remember
|
|
683
|
+
// the original (pre-bump) name so subsequent dangling references with the same
|
|
684
|
+
// original name and type coalesce to the same stage variable instead of creating
|
|
685
|
+
// a second one. Scratchers who pasted scripts referencing what they called
|
|
686
|
+
// "score" twice almost certainly meant one variable, not two.
|
|
687
|
+
const createdForOriginalName = Object.create(null);
|
|
688
|
+
const originalNameKey = (name, type) => `${type}\u0000${name}`;
|
|
703
689
|
|
|
704
690
|
// Cache the list of all variable names by type so that we don't need to
|
|
705
691
|
// re-calculate this in every iteration of the following loop.
|
|
@@ -712,77 +698,92 @@ class Target extends EventEmitter {
|
|
|
712
698
|
};
|
|
713
699
|
|
|
714
700
|
for (const varId in allReferences) {
|
|
715
|
-
|
|
701
|
+
const existing = this.lookupVariableById(varId);
|
|
702
|
+
if (existing) {
|
|
703
|
+
// The id resolves to a real variable. Normalize displayed names so
|
|
704
|
+
// every block field shows the variable's current name. A previous
|
|
705
|
+
// target's pass on this load may have created the stage variable with
|
|
706
|
+
// a bumped name; refs on this target that still show the original
|
|
707
|
+
// name need to be brought into agreement. Scan all refs (not just
|
|
708
|
+
// the first) so an inconsistent set of refs to the same id heals
|
|
709
|
+
// even when the first ref happens to already match.
|
|
710
|
+
if (!conflictNamesToReplace[varId]) {
|
|
711
|
+
const staleRef = allReferences[varId].find(
|
|
712
|
+
ref => ref.referencingField.value !== existing.name
|
|
713
|
+
);
|
|
714
|
+
if (staleRef) {
|
|
715
|
+
conflictNamesToReplace[varId] = existing.name;
|
|
716
|
+
log.warn(
|
|
717
|
+
`Reconciled stale displayed name on '${this.getName()}': updated to ` +
|
|
718
|
+
`'${existing.name}' for id '${varId}' ` +
|
|
719
|
+
`(was '${staleRef.referencingField.value}').`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
// The referenced id is not defined anywhere. Treat this as a reference
|
|
726
|
+
// to a global from a different project (or from a backpack paste / sprite
|
|
727
|
+
// import that lost its definition). Look for a same-name same-type global
|
|
728
|
+
// on the stage; if found, queue an id remap, otherwise create a fresh one.
|
|
716
729
|
const varRef = allReferences[varId][0];
|
|
717
730
|
const varName = varRef.referencingField.value;
|
|
718
731
|
const varType = varRef.type;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
if (newVarName) {
|
|
729
|
-
// We are not calling this.blocks.updateBlocksAfterVarRename
|
|
730
|
-
// here because it will search through all the blocks. We already
|
|
731
|
-
// have access to all the references for this var id.
|
|
732
|
-
allReferences[varId].map(ref => {
|
|
733
|
-
ref.referencingField.value = newVarName;
|
|
734
|
-
return ref;
|
|
735
|
-
});
|
|
736
|
-
}
|
|
732
|
+
const existingVar = stage.lookupVariableByNameAndType(varName, varType);
|
|
733
|
+
if (existingVar) {
|
|
734
|
+
if (!conflictIdsToReplace[varId]) {
|
|
735
|
+
conflictIdsToReplace[varId] = existingVar.id;
|
|
736
|
+
log.warn(
|
|
737
|
+
`Reconciled dangling reference on '${this.getName()}': remapped id '${varId}' ` +
|
|
738
|
+
`(name '${varName}', type '${varType}') to existing stage variable '${existingVar.id}'.`
|
|
739
|
+
);
|
|
737
740
|
}
|
|
738
741
|
} else {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
742
|
+
const coalesceKey = originalNameKey(varName, varType);
|
|
743
|
+
const earlierCreated = createdForOriginalName[coalesceKey];
|
|
744
|
+
if (earlierCreated) {
|
|
745
|
+
// An earlier dangling reference in this pass already triggered creation
|
|
746
|
+
// for this original name and type. Coalesce to that stage variable, and
|
|
747
|
+
// update this reference's displayed name to match the bumped name so the
|
|
748
|
+
// two blocks display consistently.
|
|
746
749
|
if (!conflictIdsToReplace[varId]) {
|
|
747
|
-
conflictIdsToReplace[varId] =
|
|
750
|
+
conflictIdsToReplace[varId] = earlierCreated.id;
|
|
751
|
+
conflictNamesToReplace[varId] = earlierCreated.freshName;
|
|
752
|
+
log.warn(
|
|
753
|
+
`Reconciled dangling reference on '${this.getName()}': coalesced id '${varId}' ` +
|
|
754
|
+
`(name '${varName}', type '${varType}') with earlier-created stage variable ` +
|
|
755
|
+
`'${earlierCreated.id}' (name '${earlierCreated.freshName}').`
|
|
756
|
+
);
|
|
748
757
|
}
|
|
749
758
|
} else {
|
|
750
|
-
// A global variable with the same name did not already exist,
|
|
751
|
-
// create a new one such that it does not conflict with any
|
|
752
|
-
// names of local variables of the same type.
|
|
753
759
|
const allNames = allVarNames(varType);
|
|
754
760
|
const freshName = StringUtil.unusedName(varName, allNames);
|
|
755
761
|
stage.createVariable(varId, freshName, varType);
|
|
762
|
+
// Track the new name so unusedName accounts for it on subsequent calls,
|
|
763
|
+
// and remember which stage variable served this original name so
|
|
764
|
+
// future same-name dangling references coalesce instead of duplicating.
|
|
765
|
+
allNames.push(freshName);
|
|
766
|
+
createdForOriginalName[coalesceKey] = {id: varId, freshName};
|
|
756
767
|
if (!conflictNamesToReplace[varId]) {
|
|
757
768
|
conflictNamesToReplace[varId] = freshName;
|
|
769
|
+
log.warn(
|
|
770
|
+
`Reconciled dangling reference on '${this.getName()}': created stage variable ` +
|
|
771
|
+
`'${varId}' (name '${freshName}', type '${varType}').`
|
|
772
|
+
);
|
|
758
773
|
}
|
|
759
774
|
}
|
|
760
775
|
}
|
|
761
776
|
}
|
|
762
|
-
|
|
763
|
-
//
|
|
764
|
-
for (const id in unreferencedLocalVarIds) {
|
|
765
|
-
const varId = unreferencedLocalVarIds[id];
|
|
766
|
-
const name = this.variables[varId].name;
|
|
767
|
-
const type = this.variables[varId].type;
|
|
768
|
-
renameConflictingLocalVar(varId, name, type);
|
|
769
|
-
}
|
|
770
|
-
// Handle global var conflicts with existing global vars (e.g. a sprite is uploaded, and has
|
|
771
|
-
// blocks referencing some variable that the sprite does not own, and this
|
|
772
|
-
// variable conflicts with a global var)
|
|
773
|
-
// In this case, we want to merge the new variable referenes with the
|
|
774
|
-
// existing global variable
|
|
777
|
+
|
|
778
|
+
// Apply queued id remaps (merge the dangling references with the existing global).
|
|
775
779
|
for (const conflictId in conflictIdsToReplace) {
|
|
776
780
|
const existingId = conflictIdsToReplace[conflictId];
|
|
777
781
|
const referencesToUpdate = allReferences[conflictId];
|
|
778
782
|
this.mergeVariables(conflictId, existingId, referencesToUpdate);
|
|
779
783
|
}
|
|
780
784
|
|
|
781
|
-
//
|
|
782
|
-
//
|
|
783
|
-
// variable conflcits with another sprite's local var).
|
|
784
|
-
// In this case, we want to go through the variable references and update
|
|
785
|
-
// the name of the variable in that reference.
|
|
785
|
+
// Apply queued field-name updates for newly-created globals whose name was
|
|
786
|
+
// bumped to avoid a collision.
|
|
786
787
|
for (const conflictId in conflictNamesToReplace) {
|
|
787
788
|
const newName = conflictNamesToReplace[conflictId];
|
|
788
789
|
const referencesToUpdate = allReferences[conflictId];
|
|
@@ -793,6 +794,66 @@ class Target extends EventEmitter {
|
|
|
793
794
|
}
|
|
794
795
|
}
|
|
795
796
|
|
|
797
|
+
/**
|
|
798
|
+
* Reconciles missing definitions (via `reconcileVariableReferences`) and then
|
|
799
|
+
* renames sprite-local variables that name-collide with stage globals so they
|
|
800
|
+
* remain distinguishable after import.
|
|
801
|
+
*
|
|
802
|
+
* Used when importing a sprite into an existing project and when pasting
|
|
803
|
+
* blocks from the backpack. Project load uses `reconcileVariableReferences`
|
|
804
|
+
* directly so that legitimately-existing local-vs-global name collisions in
|
|
805
|
+
* a saved project are not renamed.
|
|
806
|
+
*/
|
|
807
|
+
fixUpVariableReferences () {
|
|
808
|
+
if (!this.runtime) return;
|
|
809
|
+
const stage = this.runtime.getTargetForStage();
|
|
810
|
+
if (!stage || !stage.variables) return;
|
|
811
|
+
|
|
812
|
+
// First, ensure every referenced variable, list, or broadcast has a definition.
|
|
813
|
+
this.reconcileVariableReferences();
|
|
814
|
+
|
|
815
|
+
// The stage's variables are the global scope; there's no local-vs-global
|
|
816
|
+
// distinction to disambiguate.
|
|
817
|
+
if (this.isStage) return;
|
|
818
|
+
|
|
819
|
+
const renameConflictingLocalVar = (id, name, type) => {
|
|
820
|
+
const conflict = stage.lookupVariableByNameAndType(name, type);
|
|
821
|
+
if (conflict) {
|
|
822
|
+
const newName = StringUtil.unusedName(
|
|
823
|
+
`${this.getName()}: ${name}`,
|
|
824
|
+
this.getAllVariableNamesInScopeByType(type));
|
|
825
|
+
this.renameVariable(id, newName);
|
|
826
|
+
return newName;
|
|
827
|
+
}
|
|
828
|
+
return null;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
// Walk the references again and rename any sprite-local variables whose
|
|
832
|
+
// name and type collide with a stage global. References on this target
|
|
833
|
+
// that point at the local are updated to use the new name.
|
|
834
|
+
const allReferences = this.blocks.getAllVariableAndListReferences(null, true);
|
|
835
|
+
for (const varId in allReferences) {
|
|
836
|
+
if (!Object.prototype.hasOwnProperty.call(this.variables, varId)) continue;
|
|
837
|
+
const varRef = allReferences[varId][0];
|
|
838
|
+
const newVarName = renameConflictingLocalVar(varId, varRef.referencingField.value, varRef.type);
|
|
839
|
+
if (newVarName) {
|
|
840
|
+
allReferences[varId].map(ref => {
|
|
841
|
+
ref.referencingField.value = newVarName;
|
|
842
|
+
return ref;
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Rename any local variables that aren't referenced by any block but still
|
|
848
|
+
// collide with a stage global, so the sprite remains internally consistent.
|
|
849
|
+
for (const localVarId in this.variables) {
|
|
850
|
+
if (!Object.prototype.hasOwnProperty.call(this.variables, localVarId)) continue;
|
|
851
|
+
if (allReferences[localVarId]) continue;
|
|
852
|
+
const v = this.variables[localVarId];
|
|
853
|
+
renameConflictingLocalVar(localVarId, v.name, v.type);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
796
857
|
}
|
|
797
858
|
|
|
798
859
|
module.exports = Target;
|
package/src/virtual-machine.js
CHANGED
|
@@ -560,7 +560,13 @@ class VirtualMachine extends EventEmitter {
|
|
|
560
560
|
this.editingTarget = targets[0];
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
-
if (
|
|
563
|
+
if (wholeProject) {
|
|
564
|
+
// A loaded project may carry dangling variable, list, or broadcast
|
|
565
|
+
// references baked in by historical bugs. Reconcile each target so
|
|
566
|
+
// those references resolve cleanly without renaming any legitimate
|
|
567
|
+
// local-vs-global name collisions.
|
|
568
|
+
targets.forEach(target => target.reconcileVariableReferences());
|
|
569
|
+
} else {
|
|
564
570
|
this.editingTarget.fixUpVariableReferences();
|
|
565
571
|
}
|
|
566
572
|
|
|
@@ -1280,6 +1286,12 @@ class VirtualMachine extends EventEmitter {
|
|
|
1280
1286
|
target.blocks.createBlock(block);
|
|
1281
1287
|
});
|
|
1282
1288
|
target.blocks.updateTargetSpecificBlocks(target.isStage);
|
|
1289
|
+
if (!optFromTargetId) {
|
|
1290
|
+
// No source target means the blocks come from outside the project (e.g. the
|
|
1291
|
+
// backpack). Reconcile any variable, list, or broadcast references against
|
|
1292
|
+
// what's defined in the project, creating missing definitions on the stage.
|
|
1293
|
+
target.fixUpVariableReferences();
|
|
1294
|
+
}
|
|
1283
1295
|
});
|
|
1284
1296
|
}
|
|
1285
1297
|
|