@@ -112,6 +112,33 @@ const EXTENDABLE_OPERATORS = {
112112 operator_join : 'STRING'
113113} ;
114114
115+ // Blocks that vanilla Scratch has no opcode for, but which are constant enough to be saved as a
116+ // stage variable holding the value. Vanilla runs them as an ordinary variable reporter; MistWarp
117+ // turns them back into blocks on load. The id is the name so the same variable is reused forever.
118+ const VANILLA_CONSTANTS = {
119+ operator_pi : { name : 'mistwarp.pi' , value : Math . PI } ,
120+ operator_newline : { name : 'mistwarp.newline' , value : '\n' }
121+ } ;
122+
123+ const CONSTANT_OPCODE_BY_NAME = { } ;
124+ for ( const opcode in VANILLA_CONSTANTS ) {
125+ CONSTANT_OPCODE_BY_NAME [ VANILLA_CONSTANTS [ opcode ] . name ] = opcode ;
126+ }
127+
128+ const collapseConstants = function ( blocks ) {
129+ for ( const id in blocks ) {
130+ if ( ! hasOwnProperty . call ( blocks , id ) ) continue ;
131+ const block = blocks [ id ] ;
132+ if ( ! block || Array . isArray ( block ) || block . opcode !== 'data_variable' ) continue ;
133+ const variable = block . fields && block . fields . VARIABLE ;
134+ const opcode = variable && CONSTANT_OPCODE_BY_NAME [ variable . value ] ;
135+ if ( ! opcode ) continue ;
136+ block . opcode = opcode ;
137+ block . fields = { } ;
138+ }
139+ return blocks ;
140+ } ;
141+
115142const getOperatorItemCount = block => {
116143 const mutation = block . mutation ;
117144 if ( mutation && mutation . itemcount ) {
@@ -199,7 +226,7 @@ const collapseOperators = function (blocks) {
199226 return blocks ;
200227} ;
201228
202- const expandOperators = function ( blocks ) {
229+ const expandOperators = function ( blocks , usedConstants ) {
203230 const result = { } ;
204231 for ( const id in blocks ) {
205232 if ( hasOwnProperty . call ( blocks , id ) ) result [ id ] = blocks [ id ] ;
@@ -225,7 +252,23 @@ const expandOperators = function (blocks) {
225252 const originalIds = Object . keys ( result ) ;
226253 for ( const id of originalIds ) {
227254 const orig = result [ id ] ;
228- if ( ! orig || Array . isArray ( orig ) || ! orig . inputs ) continue ;
255+ if ( ! orig || Array . isArray ( orig ) ) continue ;
256+ const constant = VANILLA_CONSTANTS [ orig . opcode ] ;
257+ if ( constant ) {
258+ const block = cloneBlock ( id ) ;
259+ block . opcode = 'data_variable' ;
260+ block . fields = {
261+ VARIABLE : {
262+ name : 'VARIABLE' ,
263+ value : constant . name ,
264+ id : constant . name ,
265+ variableType : Variable . SCALAR_TYPE
266+ }
267+ } ;
268+ if ( usedConstants ) usedConstants . add ( constant ) ;
269+ continue ;
270+ }
271+ if ( ! orig . inputs ) continue ;
229272 const prefix = EXTENDABLE_OPERATORS [ orig . opcode ] ;
230273 if ( ! prefix ) continue ;
231274 const count = getOperatorItemCount ( orig ) ;
@@ -795,7 +838,7 @@ const serializeFrames = function (frames) {
795838 * @param {Set } extensions A set of extensions to add extension IDs to
796839 * @return {object } A serialized representation of the given target.
797840 */
798- const serializeTarget = function ( target , extensions ) {
841+ const serializeTarget = function ( target , extensions , usedConstants ) {
799842 const obj = Object . create ( null ) ;
800843 let targetExtensions = [ ] ;
801844 obj . isStage = target . isStage ;
@@ -804,7 +847,7 @@ const serializeTarget = function (target, extensions) {
804847 obj . variables = vars . variables ;
805848 obj . lists = vars . lists ;
806849 obj . broadcasts = vars . broadcasts ;
807- [ obj . blocks , targetExtensions ] = serializeBlocks ( expandOperators ( target . blocks ) ) ;
850+ [ obj . blocks , targetExtensions ] = serializeBlocks ( expandOperators ( target . blocks , usedConstants ) ) ;
808851 obj . comments = serializeComments ( target . comments ) ;
809852 if ( target . frames && Object . keys ( target . frames ) . length > 0 ) {
810853 obj . frames = serializeFrames ( target . frames ) ;
@@ -949,7 +992,9 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {})
949992 } ) ;
950993 }
951994
952- const serializedTargets = flattenedOriginalTargets . map ( t => serializeTarget ( t , extensions ) )
995+ const usedConstants = new Set ( ) ;
996+
997+ const serializedTargets = flattenedOriginalTargets . map ( t => serializeTarget ( t , extensions , usedConstants ) )
953998 . map ( ( serialized , index ) => {
954999 // can't serialize extensionStorage until the list of used extensions is fully known
9551000 const target = originalTargetsToSerialize [ index ] ;
@@ -960,6 +1005,18 @@ const serialize = function (runtime, targetId, {allowOptimization = true} = {})
9601005 return serialized ;
9611006 } ) ;
9621007
1008+ if ( usedConstants . size ) {
1009+ // Sprites are exported without a stage, so their copy of the variable has to be local.
1010+ const constantHost = targetId ?
1011+ serializedTargets [ 0 ] :
1012+ serializedTargets . find ( t => t . isStage ) ;
1013+ if ( constantHost ) {
1014+ for ( const constant of usedConstants ) {
1015+ constantHost . variables [ constant . name ] = [ constant . name , constant . value ] ;
1016+ }
1017+ }
1018+ }
1019+
9631020 const fonts = runtime . fontManager . serializeJSON ( ) ;
9641021 const customAssets = runtime . assetManager . serializeJSON ( ) ;
9651022
@@ -1398,6 +1455,7 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
13981455 }
13991456 if ( Object . prototype . hasOwnProperty . call ( object , 'blocks' ) ) {
14001457 deserializeBlocks ( object . blocks ) ;
1458+ collapseConstants ( object . blocks ) ;
14011459 if ( runtime . extendableOperators ) {
14021460 collapseOperators ( object . blocks ) ;
14031461 }
@@ -1439,6 +1497,8 @@ const parseScratchObject = function (object, runtime, extensions, zip, assets) {
14391497 if ( Object . prototype . hasOwnProperty . call ( object , 'variables' ) ) {
14401498 for ( const varId in object . variables ) {
14411499 const variable = object . variables [ varId ] ;
1500+ // Every reference to it just became a block again, so don't recreate the variable.
1501+ if ( CONSTANT_OPCODE_BY_NAME [ variable [ 0 ] ] ) continue ;
14421502 // A variable is a cloud variable if:
14431503 // - the project says it's a cloud variable, and
14441504 // - it's a stage variable, and
0 commit comments