Merge branch 'develop' of https://github.com/LLK/scratch-vm into develop

This commit is contained in:
Eric Rosenbaum 2016-09-27 18:12:20 -04:00
commit 7c91565408
29 changed files with 1444 additions and 17552 deletions

7
.gitignore vendored
View file

@ -8,3 +8,10 @@ npm-*
# Testing
/.nyc_output
/coverage
/dist.js
/vm.js
/vm.min.js
/playground/media
/playground/vendor.js
/playground/vm.js
/playground/zenburn.css

2
.npmignore Normal file
View file

@ -0,0 +1,2 @@
/.nyc_output
/coverage

View file

@ -6,3 +6,27 @@ sudo: false
cache:
directories:
- node_modules
after_script:
- |
# RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel
declare exitCode
$(npm bin)/travis-after-all
exitCode=$?
if [[
# Execute after all jobs finish successfully
$exitCode = 0 &&
# Only release on release branches
$RELEASE_BRANCHES =~ $TRAVIS_BRANCH &&
# Don't release on PR builds
$TRAVIS_PULL_REQUEST = "false"
]]; then
# Authenticate NPM
echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc
# Set version to timestamp
npm --no-git-tag-version version $($(npm bin)/json -f package.json version)-prerelease.$(date +%s)
npm publish
# Publish to gh-pages as most recent committer
git config --global user.email $(git log --pretty=format:"%ce" -n1)
git config --global user.name $(git log --pretty=format:"%cn" -n1)
./node_modules/.bin/gh-pages -x -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git -d playground -m "Build for $(git log --pretty=format:%H)"
fi

View file

@ -13,7 +13,7 @@ watch:
$(WEBPACK) --watch
serve:
$(WEBPACK_DEV_SERVER) --host 0.0.0.0 --content-base ./
$(WEBPACK_DEV_SERVER)
# ------------------------------------------------------------------------------

View file

@ -14,7 +14,7 @@ npm install https://github.com/LLK/scratch-vm.git
```
If you want to edit/play yourself:
```bash
git clone git@github.com:LLK/scratch-vm.git
git clone https://github.com/LLK/scratch-vm.git
cd scratch-vm
npm install
```
@ -35,7 +35,7 @@ StartServerWindows.bat
```
## Playground
To run the Playground, make sure the dev server's running and go to [http://localhost:8080/](http://localhost:8080/) - you will be redirected to the playground, which demonstrates various tools and internal state.
To run the Playground, make sure the dev server's running and go to [http://localhost:8080/](http://localhost:8080/) - you will be directed to the playground, which demonstrates various tools and internal state.
![VM Playground Screenshot](https://i.imgur.com/nOCNqEc.gif)

View file

@ -1,2 +1,2 @@
@echo off
node_modules\.bin\webpack-dev-server --host 0.0.0.0 --content-base .
node_modules\.bin\webpack-dev-server --host 0.0.0.0 --content-base .\playground

View file

@ -1,7 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="refresh" content="0; URL='/playground'" />
<title>Redirect to playground</title>
</head>
</html>

View file

@ -9,23 +9,30 @@
"type": "git",
"url": "git+ssh://git@github.com/LLK/scratch-vm.git"
},
"main": "./src/index.js",
"main": "./dist.js",
"scripts": {
"prepublish": "./node_modules/.bin/webpack --bail",
"start": "make serve",
"test": "make test",
"start": "webpack-dev-server --host 0.0.0.0 --content-base ."
},
"dependencies": {
"htmlparser2": "3.9.0",
"promise": "7.1.1"
"version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\""
},
"devDependencies": {
"copy-webpack-plugin": "3.0.1",
"eslint": "2.7.0",
"expose-loader": "0.7.1",
"gh-pages": "0.11.0",
"highlightjs": "8.7.0",
"htmlparser2": "3.9.0",
"json": "9.0.4",
"json-loader": "0.5.4",
"scratch-blocks": "git+https://git@github.com/LLK/scratch-blocks.git#develop",
"scratch-render": "git+https://git@github.com/LLK/scratch-render.git#develop",
"lodash.defaultsdeep": "4.6.0",
"promise": "7.1.1",
"scratch-blocks": "^0.1.0-prepublish",
"scratch-render": "^0.1.0-prepublish",
"script-loader": "0.7.0",
"stats.js": "0.16.0",
"tap": "5.7.1",
"travis-after-all": "1.4.4",
"webpack": "1.13.0",
"webpack-dev-server": "1.14.1"
}

View file

@ -5,7 +5,7 @@
<meta charset="utf-8">
<title>Scratch VM Playground</title>
<link rel="stylesheet" href="playground.css">
<link rel="stylesheet" href="../node_modules/highlightjs/styles/zenburn.css">
<link rel="stylesheet" href="zenburn.css">
</head>
<body>
<div id="vm-devtools">
@ -35,10 +35,10 @@
<pre id="blockexplorer"></pre>
</div>
<div id="tab-importexport">
<button id="createEmptyProject">New Project</button><br />
Import/Export<br />
Project ID: <input id="projectId" value="119615668" />
<button id="projectLoadButton">Load</button><br />
<button id="projectLoadButton">Load</button>
<button id="createEmptyProject">New Project</button><br />
<p>
<input type="button" value="Export to XML" onclick="toXml()">
&nbsp;
@ -266,6 +266,7 @@
<block type="looks_backdropname"></block>
<block type="looks_size"></block>
</category>
<category name="Sound" colour="#D65CD6">
<block type="sound_playsound">
@ -378,6 +379,7 @@
</block>
<block type="sound_tempo"></block>
</category>
<category name="Pen" colour="#00B295">
<block type="pen_clear"></block>
<block type="pen_stamp"></block>
@ -432,24 +434,74 @@
</value>
</block>
</category>
<category name="Data" colour="#FF8C1A" custom="VARIABLE"></category>
<category name="Data" colour="#FF8C1A" custom="VARIABLE">
</category>
<category name="Lists" colour="#FF8C1A">
<block type="data_list"></block>
<block type="data_addtolist">
<value name="ITEM">
<shadow type="text">
<field name="TEXT">thing</field>
</shadow>
</value>
</block>
<block type="data_deleteoflist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="data_insertatlist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
<value name="ITEM">
<shadow type="text">
<field name="TEXT">thing</field>
</shadow>
</value>
</block>
<block type="data_replaceitemoflist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
<value name="ITEM">
<shadow type="text">
<field name="TEXT">thing</field>
</shadow>
</value>
</block>
<block type="data_itemoflist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="data_lengthoflist"></block>
<block type="data_listcontainsitem">
<value name="ITEM">
<shadow type="text">
<field name="TEXT">thing</field>
</shadow>
</value>
</block>
<block type="data_showlist"></block>
<block type="data_hidelist"></block>
</category>
<category name="Events" colour="#FFD500">
<block type="event_whenflagclicked"></block>
<block type="event_whenkeypressed">
<value name="KEY_OPTION">
<shadow type="event_keyoptions"></shadow>
</value>
</block>
<block type="event_whenthisspriteclicked"></block>
<block type="event_whenbackdropswitchesto">
<value name="BACKDROP">
<shadow type="event_backdrops"></shadow>
</value>
</block>
<block type="event_whengreaterthan">
<value name="WHENGREATERTHANMENU">
<shadow type="event_whengreaterthanmenu"></shadow>
</value>
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">10</field>
@ -457,9 +509,6 @@
</value>
</block>
<block type="event_whenbroadcastreceived">
<value name="BROADCAST_OPTION">
<shadow type="event_broadcast_menu"></shadow>
</value>
</block>
<block type="event_broadcast">
<value name="BROADCAST_OPTION">
@ -568,6 +617,14 @@
</block>
<block type="sensing_timer"></block>
<block type="sensing_resettimer"></block>
<block type="sensing_of">
<value name="PROPERTY">
<shadow type="sensing_of_property_menu"></shadow>
</value>
<value name="OBJECT">
<shadow type="sensing_of_object_menu"></shadow>
</value>
</block>
<block type="sensing_current">
<value name="CURRENTMENU">
<shadow type="sensing_currentmenu"></shadow>
@ -737,27 +794,17 @@
</value>
</block>
</category>
<category name="More Blocks" colour="#FF6680"></category>
<category name="More Blocks" colour="#FF6680" custom="PROCEDURE"></category>
</xml>
<!-- FPS counter -->
<script src="../node_modules/stats.js/build/stats.min.js"></script>
<!-- Syntax highlighter -->
<script src="../node_modules/highlightjs/highlight.pack.min.js"></script>
<!-- Scratch Blocks -->
<!-- For easier development between the two, use `npm link` -->
<script src="../node_modules/scratch-blocks/blockly_compressed_vertical.js"></script>
<script src="../node_modules/scratch-blocks/blocks_compressed.js"></script>
<script src="../node_modules/scratch-blocks/blocks_compressed_vertical.js"></script>
<script src="../node_modules/scratch-blocks/msg/messages.js"></script>
<!-- Renderer -->
<script src="../node_modules/scratch-render/render.js"></script>
<!-- FPS counter, Syntax highlighter, Blocks, Renderer -->
<script src="./vendor.js"></script>
<!-- VM Worker -->
<script src="./vm.js"></script>
<!-- Playground -->
<script src="./playground.js"></script>
<!-- Audio -->
<script src="../AudioEngine.js"></script>
<script src="../Tone.min.js"></script>
<!-- VM Worker -->
<script src="../vm.js"></script>
<!-- Playground -->
<script src="./playground.js"></script>
<script>
function toXml() {
var output = document.getElementById('importExport');

View file

@ -21,7 +21,7 @@ var loadProject = function () {
window.onload = function() {
// Lots of global variables to make debugging easier
// Instantiate the VM worker.
// Instantiate the VM.
var vm = new window.VirtualMachine();
window.vm = vm;
@ -30,7 +30,8 @@ window.onload = function() {
document.location = '#' + document.getElementById('projectId').value;
location.reload();
};
document.getElementById('createEmptyProject').addEventListener('click', function() {
document.getElementById('createEmptyProject').addEventListener('click',
function() {
document.location = '#' + 'createEmptyProject';
location.reload();
});
@ -38,7 +39,9 @@ window.onload = function() {
// Instantiate the renderer and connect it to the VM.
var canvas = document.getElementById('scratch-stage');
window.renderer = new window.RenderWebGLLocal(canvas);
var renderer = new window.RenderWebGL(canvas);
window.renderer = renderer;
vm.attachRenderer(renderer);
// Instantiate audio engine
window.audioEngine = new window.AudioEngine();
@ -47,7 +50,7 @@ window.onload = function() {
var toolbox = document.getElementById('toolbox');
var workspace = window.Blockly.inject('blocks', {
toolbox: toolbox,
media: '../node_modules/scratch-blocks/media/',
media: './media/',
zoom: {
controls: true,
wheel: true,
@ -87,8 +90,7 @@ window.onload = function() {
// Thread representation tab.
var threadexplorer = document.getElementById('threadexplorer');
var cachedThreadJSON = '';
var updateThreadExplorer = function (threads) {
var newJSON = JSON.stringify(threads, null, 2);
var updateThreadExplorer = function (newJSON) {
if (newJSON != cachedThreadJSON) {
cachedThreadJSON = newJSON;
threadexplorer.innerHTML = cachedThreadJSON;

View file

@ -19,9 +19,21 @@ Scratch3ControlBlocks.prototype.getPrimitives = function() {
'control_repeat_until': this.repeatUntil,
'control_forever': this.forever,
'control_wait': this.wait,
'control_wait_until': this.waitUntil,
'control_if': this.if,
'control_if_else': this.ifElse,
'control_stop': this.stop
'control_stop': this.stop,
'control_create_clone_of_menu': this.createCloneMenu,
'control_create_clone_of': this.createClone,
'control_delete_this_clone': this.deleteClone
};
};
Scratch3ControlBlocks.prototype.getHats = function () {
return {
'control_start_as_clone': {
restartExistingThreads: false
}
};
};
@ -65,6 +77,14 @@ Scratch3ControlBlocks.prototype.repeatUntil = function(args, util) {
}
};
Scratch3ControlBlocks.prototype.waitUntil = function(args, util) {
var condition = Cast.toBoolean(args.CONDITION);
// Only execute once per frame.
if (!condition) {
util.yieldFrame();
}
};
Scratch3ControlBlocks.prototype.forever = function(args, util) {
// Only execute once per frame.
// When the branch finishes, `forever` will be executed again and
@ -118,4 +138,30 @@ Scratch3ControlBlocks.prototype.stop = function() {
this.runtime.stopAll();
};
// @todo (GH-146): remove.
Scratch3ControlBlocks.prototype.createCloneMenu = function (args) {
return args.CLONE_OPTION;
};
Scratch3ControlBlocks.prototype.createClone = function (args, util) {
var cloneTarget;
if (args.CLONE_OPTION == '_myself_') {
cloneTarget = util.target;
} else {
cloneTarget = this.runtime.getSpriteTargetByName(args.CLONE_OPTION);
}
if (!cloneTarget) {
return;
}
var newClone = cloneTarget.makeClone();
if (newClone) {
this.runtime.targets.push(newClone);
}
};
Scratch3ControlBlocks.prototype.deleteClone = function (args, util) {
this.runtime.disposeTarget(util.target);
this.runtime.stopForTarget(util.target);
};
module.exports = Scratch3ControlBlocks;

136
src/blocks/scratch3_data.js Normal file
View file

@ -0,0 +1,136 @@
var Cast = require('../util/cast');
function Scratch3DataBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3DataBlocks.prototype.getPrimitives = function () {
return {
'data_variable': this.getVariable,
'data_setvariableto': this.setVariableTo,
'data_changevariableby': this.changeVariableBy,
'data_list': this.getListContents,
'data_addtolist': this.addToList,
'data_deleteoflist': this.deleteOfList,
'data_insertatlist': this.insertAtList,
'data_replaceitemoflist': this.replaceItemOfList,
'data_itemoflist': this.getItemOfList,
'data_lengthoflist': this.lengthOfList,
'data_listcontainsitem': this.listContainsItem
};
};
Scratch3DataBlocks.prototype.getVariable = function (args, util) {
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
return variable.value;
};
Scratch3DataBlocks.prototype.setVariableTo = function (args, util) {
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
variable.value = args.VALUE;
};
Scratch3DataBlocks.prototype.changeVariableBy = function (args, util) {
var variable = util.target.lookupOrCreateVariable(args.VARIABLE);
var castedValue = Cast.toNumber(variable.value);
var dValue = Cast.toNumber(args.VALUE);
variable.value = castedValue + dValue;
};
Scratch3DataBlocks.prototype.getListContents = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
// Determine if the list is all single letters.
// If it is, report contents joined together with no separator.
// If it's not, report contents joined together with a space.
var allSingleLetters = true;
for (var i = 0; i < list.contents.length; i++) {
var listItem = list.contents[i];
if (!((typeof listItem === 'string') &&
(listItem.length == 1))) {
allSingleLetters = false;
break;
}
}
if (allSingleLetters) {
return list.contents.join('');
} else {
return list.contents.join(' ');
}
};
Scratch3DataBlocks.prototype.addToList = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
list.contents.push(args.ITEM);
};
Scratch3DataBlocks.prototype.deleteOfList = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
var index = Cast.toListIndex(args.INDEX, list.contents.length);
if (index === Cast.LIST_INVALID) {
return;
} else if (index === Cast.LIST_ALL) {
list.contents = [];
return;
}
list.contents.splice(index - 1, 1);
};
Scratch3DataBlocks.prototype.insertAtList = function (args, util) {
var item = args.ITEM;
var list = util.target.lookupOrCreateList(args.LIST);
var index = Cast.toListIndex(args.INDEX, list.contents.length + 1);
if (index === Cast.LIST_INVALID) {
return;
}
list.contents.splice(index - 1, 0, item);
};
Scratch3DataBlocks.prototype.replaceItemOfList = function (args, util) {
var item = args.ITEM;
var list = util.target.lookupOrCreateList(args.LIST);
var index = Cast.toListIndex(args.INDEX, list.contents.length);
if (index === Cast.LIST_INVALID) {
return;
}
list.contents.splice(index - 1, 1, item);
};
Scratch3DataBlocks.prototype.getItemOfList = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
var index = Cast.toListIndex(args.INDEX, list.contents.length);
if (index === Cast.LIST_INVALID) {
return '';
}
return list.contents[index - 1];
};
Scratch3DataBlocks.prototype.lengthOfList = function (args, util) {
var list = util.target.lookupOrCreateList(args.LIST);
return list.contents.length;
};
Scratch3DataBlocks.prototype.listContainsItem = function (args, util) {
var item = args.ITEM;
var list = util.target.lookupOrCreateList(args.LIST);
if (list.contents.indexOf(item) >= 0) {
return true;
}
// Try using Scratch comparison operator on each item.
// (Scratch considers the string '123' equal to the number 123).
for (var i = 0; i < list.contents.length; i++) {
if (Cast.compare(list.contents[i], item) == 0) {
return true;
}
}
return false;
};
module.exports = Scratch3DataBlocks;

View file

@ -16,7 +16,7 @@ var isPromise = function (value) {
*/
var execute = function (sequencer, thread) {
var runtime = sequencer.runtime;
var target = runtime.targetForThread(thread);
var target = thread.target;
// Current block to execute is the one on the top of the stack.
var currentBlockId = thread.peekStack();

16
src/engine/list.js Normal file
View file

@ -0,0 +1,16 @@
/**
* @fileoverview
* Object representing a Scratch list.
*/
/**
* @param {!string} name Name of the list.
* @param {Array} contents Contents of the list, as an array.
* @constructor
*/
function List (name, contents) {
this.name = name;
this.contents = contents;
}
module.exports = List;

View file

@ -15,7 +15,8 @@ var defaultBlockPackages = {
'scratch3_motion': require('../blocks/scratch3_motion'),
'scratch3_operators': require('../blocks/scratch3_operators'),
'scratch3_sound': require('../blocks/scratch3_sound'),
'scratch3_sensing': require('../blocks/scratch3_sensing')
'scratch3_sensing': require('../blocks/scratch3_sensing'),
'scratch3_data': require('../blocks/scratch3_data')
};
/**
@ -61,6 +62,11 @@ function Runtime () {
this._scriptGlowsPreviousFrame = [];
this._editingTarget = null;
/**
* Currently known number of clones.
* @type {number}
*/
this._cloneCounter = 0;
}
/**
@ -103,6 +109,11 @@ util.inherits(Runtime, EventEmitter);
*/
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
/**
* How many clones can be created at a time.
* @const {number}
*/
Runtime.MAX_CLONES = 300;
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
@ -190,16 +201,26 @@ Runtime.prototype.clearEdgeActivatedValues = function () {
this._edgeActivatedHatValues = {};
};
/**
* Attach the renderer
* @param {!RenderWebGL} renderer The renderer to attach
*/
Runtime.prototype.attachRenderer = function (renderer) {
this.renderer = renderer;
};
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
/**
* Create a thread and push it to the list of threads.
* @param {!string} id ID of block that starts the stack
* @param {!string} id ID of block that starts the stack.
* @param {!Target} target Target to run thread on.
* @return {!Thread} The newly created thread.
*/
Runtime.prototype._pushThread = function (id) {
Runtime.prototype._pushThread = function (id, target) {
var thread = new Thread(id);
thread.setTarget(target);
thread.pushStack(id);
this.threads.push(thread);
return thread;
@ -238,7 +259,7 @@ Runtime.prototype.toggleScript = function (topBlockId) {
}
}
// Otherwise add it.
this._pushThread(topBlockId);
this._pushThread(topBlockId, this._editingTarget);
};
/**
@ -307,7 +328,8 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
// If `restartExistingThreads` is true, we should stop
// any existing threads starting with the top block.
for (var i = 0; i < instance.threads.length; i++) {
if (instance.threads[i].topBlock === topBlockId) {
if (instance.threads[i].topBlock === topBlockId &&
(!opt_target || instance.threads[i].target == opt_target)) {
instance._removeThread(instance.threads[i]);
}
}
@ -315,31 +337,76 @@ Runtime.prototype.startHats = function (requestedHatOpcode,
// If `restartExistingThreads` is false, we should
// give up if any threads with the top block are running.
for (var j = 0; j < instance.threads.length; j++) {
if (instance.threads[j].topBlock === topBlockId) {
if (instance.threads[j].topBlock === topBlockId &&
(!opt_target || instance.threads[j].target == opt_target)) {
// Some thread is already running.
return;
}
}
}
// Start the thread with this top block.
newThreads.push(instance._pushThread(topBlockId));
newThreads.push(instance._pushThread(topBlockId, target));
}, opt_target);
return newThreads;
};
/**
* Dispose of a target.
* @param {!Target} target Target to dispose of.
*/
Runtime.prototype.disposeTarget = function (target) {
// Allow target to do dispose actions.
target.dispose();
// Remove from list of targets.
var index = this.targets.indexOf(target);
if (index > -1) {
this.targets.splice(index, 1);
}
};
/**
* Stop any threads acting on the target.
* @param {!Target} target Target to stop threads for.
*/
Runtime.prototype.stopForTarget = function (target) {
// Stop any threads on the target.
for (var i = 0; i < this.threads.length; i++) {
if (this.threads[i].target == target) {
this._removeThread(this.threads[i]);
}
}
};
/**
* Start all threads that start with the green flag.
*/
Runtime.prototype.greenFlag = function () {
this.stopAll();
this.ioDevices.clock.resetProjectTimer();
this.clearEdgeActivatedValues();
// Inform all targets of the green flag.
for (var i = 0; i < this.targets.length; i++) {
this.targets[i].onGreenFlag();
}
this.startHats('event_whenflagclicked');
};
/**
* Stop "everything"
* Stop "everything."
*/
Runtime.prototype.stopAll = function () {
// Dispose all clones.
var newTargets = [];
for (var i = 0; i < this.targets.length; i++) {
if (this.targets[i].hasOwnProperty('isOriginal') &&
!this.targets[i].isOriginal) {
this.targets[i].dispose();
} else {
newTargets.push(this.targets[i]);
}
}
this.targets = newTargets;
// Dispose all threads.
var threadsCopy = this.threads.slice();
while (threadsCopy.length > 0) {
var poppedThread = threadsCopy.pop();
@ -380,7 +447,7 @@ Runtime.prototype._updateScriptGlows = function () {
// Find all scripts that should be glowing.
for (var i = 0; i < this.threads.length; i++) {
var thread = this.threads[i];
var target = this.targetForThread(thread);
var target = thread.target;
if (thread.requestScriptGlowInFrame && target == this._editingTarget) {
var blockForThread = thread.peekStack() || thread.topBlock;
var script = target.blocks.getTopLevelScript(blockForThread);
@ -459,23 +526,6 @@ Runtime.prototype.visualReport = function (blockId, value) {
this.emit(Runtime.VISUAL_REPORT, blockId, String(value));
};
/**
* Return the Target for a particular thread.
* @param {!Thread} thread Thread to determine target for.
* @return {?Target} Target object, if one exists.
*/
Runtime.prototype.targetForThread = function (thread) {
// @todo This is a messy solution,
// but prevents having circular data references.
// Have a map or some other way to associate target with threads.
for (var t = 0; t < this.targets.length; t++) {
var target = this.targets[t];
if (target.blocks.getBlock(thread.topBlock)) {
return target;
}
}
};
/**
* Get a target by its id.
* @param {string} targetId Id of target to find.
@ -490,6 +540,36 @@ Runtime.prototype.getTargetById = function (targetId) {
}
};
/**
* Get the first original (non-clone-block-created) sprite given a name.
* @param {string} spriteName Name of sprite to look for.
* @return {?Target} Target representing a sprite of the given name.
*/
Runtime.prototype.getSpriteTargetByName = function (spriteName) {
for (var i = 0; i < this.targets.length; i++) {
var target = this.targets[i];
if (target.sprite && target.sprite.name == spriteName) {
return target;
}
}
};
/**
* Update the clone counter to track how many clones are created.
* @param {number} changeAmount How many clones have been created/destroyed.
*/
Runtime.prototype.changeCloneCounter = function (changeAmount) {
this._cloneCounter += changeAmount;
};
/**
* Return whether there are clones available.
* @return {boolean} True until the number of clones hits Runtime.MAX_CLONES.
*/
Runtime.prototype.clonesAvailable = function () {
return this._cloneCounter < Runtime.MAX_CLONES;
};
/**
* Get a target representing the Scratch stage, if one exists.
* @return {?Target} The target, if found.
@ -507,8 +587,8 @@ Runtime.prototype.getTargetForStage = function () {
* Handle an animation frame from the main thread.
*/
Runtime.prototype.animationFrame = function () {
if (self.renderer) {
self.renderer.draw();
if (this.renderer) {
this.renderer.draw();
}
};

View file

@ -111,7 +111,7 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
branchNum = 1;
}
var currentBlockId = thread.peekStack();
var branchId = this.runtime.targetForThread(thread).blocks.getBranch(
var branchId = thread.target.blocks.getBranch(
currentBlockId,
branchNum
);
@ -155,8 +155,7 @@ Sequencer.prototype.proceedThread = function (thread) {
// Pop from the stack - finished this level of execution.
thread.popStack();
// Push next connected block, if there is one.
var nextBlockId = (this.runtime.targetForThread(thread).
blocks.getNextBlock(currentBlockId));
var nextBlockId = thread.target.blocks.getNextBlock(currentBlockId);
if (nextBlockId) {
thread.pushStack(nextBlockId);
}

View file

@ -1,4 +1,6 @@
var Blocks = require('./blocks');
var Variable = require('../engine/variable');
var List = require('../engine/list');
var uid = require('../util/uid');
/**
@ -25,8 +27,26 @@ function Target (blocks) {
* @type {!Blocks}
*/
this.blocks = blocks;
/**
* Dictionary of variables and their values for this target.
* Key is the variable name.
* @type {Object.<string,*>}
*/
this.variables = {};
/**
* Dictionary of lists and their contents for this target.
* Key is the list name.
* @type {Object.<string,*>}
*/
this.lists = {};
}
/**
* Called when the project receives a "green flag."
* @abstract
*/
Target.prototype.onGreenFlag = function () {};
/**
* Return a human-readable name for this target.
* Target implementations should override this.
@ -37,4 +57,60 @@ Target.prototype.getName = function () {
return this.id;
};
/**
* Look up a variable object, and create it if one doesn't exist.
* Search begins for local variables; then look for globals.
* @param {!string} name Name of the variable.
* @return {!Variable} Variable object.
*/
Target.prototype.lookupOrCreateVariable = function (name) {
// If we have a local copy, return it.
if (this.variables.hasOwnProperty(name)) {
return this.variables[name];
}
// If the stage has a global copy, return it.
if (this.runtime && !this.isStage) {
var stage = this.runtime.getTargetForStage();
if (stage.variables.hasOwnProperty(name)) {
return stage.variables[name];
}
}
// No variable with this name exists - create it locally.
var newVariable = new Variable(name, 0, false);
this.variables[name] = newVariable;
return newVariable;
};
/**
* 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.
* @param {!string} name Name of the list.
* @return {!List} List object.
*/
Target.prototype.lookupOrCreateList = function (name) {
// If we have a local copy, return it.
if (this.lists.hasOwnProperty(name)) {
return this.lists[name];
}
// If the stage has a global copy, return it.
if (this.runtime && !this.isStage) {
var stage = this.runtime.getTargetForStage();
if (stage.lists.hasOwnProperty(name)) {
return stage.lists[name];
}
}
// No list with this name exists - create it locally.
var newList = new List(name, []);
this.lists[name] = newList;
return newList;
};
/**
* Call to destroy a target.
* @abstract
*/
Target.prototype.dispose = function () {
};
module.exports = Target;

View file

@ -29,6 +29,12 @@ function Thread (firstBlock) {
*/
this.status = 0; /* Thread.STATUS_RUNNING */
/**
* Target of this thread.
* @type {?Target}
*/
this.target = null;
/**
* Whether the thread requests its script to glow during this frame.
* @type {boolean}
@ -145,4 +151,20 @@ Thread.prototype.setStatus = function (status) {
this.status = status;
};
/**
* Set thread target.
* @param {?Target} target Target for this thread.
*/
Thread.prototype.setTarget = function (target) {
this.target = target;
};
/**
* Get thread target.
* @return {?Target} Target for this thread, if available.
*/
Thread.prototype.getTarget = function () {
return this.target;
};
module.exports = Thread;

18
src/engine/variable.js Normal file
View file

@ -0,0 +1,18 @@
/**
* @fileoverview
* Object representing a Scratch variable.
*/
/**
* @param {!string} name Name of the variable.
* @param {(string|Number)} value Value of the variable.
* @param {boolean} isCloud Whether the variable is stored in the cloud.
* @constructor
*/
function Variable (name, value, isCloud) {
this.name = name;
this.value = value;
this.isCloud = isCloud;
}
module.exports = Variable;

View file

@ -10,6 +10,8 @@ var Sprite = require('../sprites/sprite');
var Color = require('../util/color.js');
var uid = require('../util/uid');
var specMap = require('./sb2specmap');
var Variable = require('../engine/variable');
var List = require('../engine/list');
/**
* Top-level handler. Parse provided JSON,
@ -40,7 +42,7 @@ function parseScratchObject (object, runtime, topLevel) {
// Blocks container for this object.
var blocks = new Blocks();
// @todo: For now, load all Scratch objects (stage/sprites) as a Sprite.
var sprite = new Sprite(blocks);
var sprite = new Sprite(blocks, runtime);
// Sprite/stage name from JSON.
if (object.hasOwnProperty('objName')) {
sprite.name = object.objName;
@ -68,30 +70,52 @@ function parseScratchObject (object, runtime, topLevel) {
var target = sprite.createClone();
// Add it to the runtime's list of targets.
runtime.targets.push(target);
if (object.scratchX) {
// Load target properties from JSON.
if (object.hasOwnProperty('variables')) {
for (var j = 0; j < object.variables.length; j++) {
var variable = object.variables[j];
target.variables[variable.name] = new Variable(
variable.name,
variable.value,
variable.isPersistent
);
}
}
if (object.hasOwnProperty('lists')) {
for (var k = 0; k < object.lists.length; k++) {
var list = object.lists[k];
// @todo: monitor properties.
target.lists[list.listName] = new List(
list.listName,
list.contents
);
}
}
if (object.hasOwnProperty('scratchX')) {
target.x = object.scratchX;
}
if (object.scratchY) {
if (object.hasOwnProperty('scratchY')) {
target.y = object.scratchY;
}
if (object.direction) {
if (object.hasOwnProperty('direction')) {
target.direction = object.direction;
}
if (object.scale) {
if (object.hasOwnProperty('scale')) {
// SB2 stores as 1.0 = 100%; we use % in the VM.
target.size = object.scale * 100;
}
if (object.visible) {
if (object.hasOwnProperty('visible')) {
target.visible = object.visible;
}
if (object.currentCostumeIndex) {
if (object.hasOwnProperty('currentCostumeIndex')) {
target.currentCostume = object.currentCostumeIndex;
}
target.isStage = topLevel;
target.updateAllDrawableProperties();
// The stage will have child objects; recursively process them.
if (object.children) {
for (var j = 0; j < object.children.length; j++) {
parseScratchObject(object.children[j], runtime, false);
for (var m = 0; m < object.children.length; m++) {
parseScratchObject(object.children[m], runtime, false);
}
}
}

View file

@ -4,9 +4,22 @@
* the SB2 JSON format and the data we need to run a project
* in the Scratch 3.0 VM.
* Notably:
* - Map 2.0-format opcodes (forward:) into 3.0-format (motion_movesteps).
* - Map 2.0 and 1.4 opcodes (forward:) into 3.0-format (motion_movesteps).
* - Map ordered, unnamed args to unordered, named inputs and fields.
* Keep this up-to-date as 3.0 blocks are renamed, changed, etc.
* Originally this was generated largely by a hand-guided scripting process.
* The relevant data lives here:
* https://github.com/LLK/scratch-flash/blob/master/src/Specs.as
* (for the old opcode and argument order).
* and here:
* https://github.com/LLK/scratch-blocks/tree/develop/blocks_vertical
* (for the new opcodes and argument names).
* and here:
* https://github.com/LLK/scratch-blocks/blob/develop/tests/
* (for the shadow blocks created for each block).
* I started with the `commands` array in Specs.as, and discarded irrelevant
* properties. By hand, I matched the opcode name to the 3.0 opcode.
* Finally, I filled in the expected arguments as below.
*/
var specMap = {
'forward:':{
@ -905,12 +918,12 @@ var specMap = {
'argMap':[
{
'type':'input',
'inputOp':'sensing_ofattributemenu',
'inputName':'ATTRIBUTE'
'inputOp':'sensing_of_property_menu',
'inputName':'PROPERTY'
},
{
'type':'input',
'inputOp':'sensing_ofobjectmenu',
'inputOp':'sensing_of_object_menu',
'inputName':'OBJECT'
}
]
@ -1230,13 +1243,22 @@ var specMap = {
}
]
},
'contentsOfList:':{
'opcode':'data_list',
'argMap':[
{
'type':'field',
'fieldName':'LIST'
}
]
},
'append:toList:':{
'opcode':'data_listadd',
'opcode':'data_addtolist',
'argMap':[
{
'type':'input',
'inputOp':'text',
'inputName':'VALUE'
'inputName':'ITEM'
},
{
'type':'field',
@ -1245,12 +1267,12 @@ var specMap = {
]
},
'deleteLine:ofList:':{
'opcode':'data_listdelete',
'opcode':'data_deleteoflist',
'argMap':[
{
'type':'input',
'inputOp':'text',
'inputName':'LINE'
'inputOp':'math_integer',
'inputName':'INDEX'
},
{
'type':'field',
@ -1259,17 +1281,17 @@ var specMap = {
]
},
'insert:at:ofList:':{
'opcode':'data_listinsert',
'opcode':'data_insertatlist',
'argMap':[
{
'type':'input',
'inputOp':'text',
'inputName':'VALUE'
'inputName':'ITEM'
},
{
'type':'input',
'inputOp':'text',
'inputName':'LINE'
'inputOp':'math_integer',
'inputName':'INDEX'
},
{
'type':'field',
@ -1278,12 +1300,12 @@ var specMap = {
]
},
'setLine:ofList:to:':{
'opcode':'data_listreplace',
'opcode':'data_replaceitemoflist',
'argMap':[
{
'type':'input',
'inputOp':'text',
'inputName':'LINE'
'inputOp':'math_integer',
'inputName':'INDEX'
},
{
'type':'field',
@ -1292,17 +1314,17 @@ var specMap = {
{
'type':'input',
'inputOp':'text',
'inputName':'VALUE'
'inputName':'ITEM'
}
]
},
'getLine:ofList:':{
'opcode':'data_listitem',
'opcode':'data_itemoflist',
'argMap':[
{
'type':'input',
'inputOp':'text',
'inputName':'LINE'
'inputOp':'math_integer',
'inputName':'INDEX'
},
{
'type':'field',
@ -1311,7 +1333,7 @@ var specMap = {
]
},
'lineCountOfList:':{
'opcode':'data_listlength',
'opcode':'data_lengthoflist',
'argMap':[
{
'type':'field',
@ -1320,7 +1342,7 @@ var specMap = {
]
},
'list:contains:':{
'opcode':'data_listcontains',
'opcode':'data_listcontainsitem',
'argMap':[
{
'type':'field',
@ -1329,7 +1351,7 @@ var specMap = {
{
'type':'input',
'inputOp':'text',
'inputName':'VALUE'
'inputName':'ITEM'
}
]
},

View file

@ -76,9 +76,19 @@ VirtualMachine.prototype.stopAll = function () {
* Get data for playground. Data comes back in an emitted event.
*/
VirtualMachine.prototype.getPlaygroundData = function () {
var instance = this;
// Only send back thread data for the current editingTarget.
var threadData = this.runtime.threads.filter(function(thread) {
return thread.target == instance.editingTarget;
});
// Remove the target key, since it's a circular reference.
var filteredThreadData = JSON.stringify(threadData, function(key, value) {
if (key == 'target') return undefined;
return value;
}, 2);
this.emit('playgroundData', {
blocks: this.editingTarget.blocks,
threads: this.runtime.threads
threads: filteredThreadData
});
};
@ -162,6 +172,14 @@ VirtualMachine.prototype.createEmptyProject = function () {
this.emitWorkspaceUpdate();
};
/**
* Set the renderer for the VM/runtime
* @param {!RenderWebGL} renderer The renderer to attach
*/
VirtualMachine.prototype.attachRenderer = function (renderer) {
this.runtime.attachRenderer(renderer);
};
/**
* Handle a Blockly event for the current editing target.
* @param {!Blockly.Event} e Any Blockly event.
@ -207,7 +225,10 @@ VirtualMachine.prototype.setEditingTarget = function (targetId) {
VirtualMachine.prototype.emitTargetsUpdate = function () {
this.emit('targetsUpdate', {
// [[target id, human readable target name], ...].
targetList: this.runtime.targets.map(function(target) {
targetList: this.runtime.targets.filter(function (target) {
// Don't report clones.
return !target.hasOwnProperty('isOriginal') || target.isOriginal;
}).map(function(target) {
return [target.id, target.getName()];
}),
// Currently editing target id.
@ -224,8 +245,5 @@ VirtualMachine.prototype.emitWorkspaceUpdate = function () {
'xml': this.editingTarget.blocks.toXML()
});
};
/**
* Export and bind to `window`
*/
module.exports = VirtualMachine;
if (typeof window !== 'undefined') window.VirtualMachine = module.exports;

View file

@ -28,20 +28,17 @@ Mouse.prototype.postData = function(data) {
};
Mouse.prototype._activateClickHats = function (x, y) {
if (self.renderer) {
var pickPromise = self.renderer.pick(x, y);
var instance = this;
pickPromise.then(function(drawableID) {
for (var i = 0; i < instance.runtime.targets.length; i++) {
var target = instance.runtime.targets[i];
if (this.runtime.renderer) {
var drawableID = this.runtime.renderer.pick(x, y);
for (var i = 0; i < this.runtime.targets.length; i++) {
var target = this.runtime.targets[i];
if (target.hasOwnProperty('drawableID') &&
target.drawableID == drawableID) {
instance.runtime.startHats('event_whenthisspriteclicked',
this.runtime.startHats('event_whenthisspriteclicked',
null, target);
return;
}
}
});
}
};

View file

@ -5,10 +5,12 @@ var Target = require('../engine/target');
/**
* Clone (instance) of a sprite.
* @param {!Sprite} sprite Reference to the sprite.
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
function Clone(sprite) {
function Clone(sprite, runtime) {
Target.call(this, sprite.blocks);
this.runtime = runtime;
/**
* Reference to the sprite that this is a clone of.
* @type {!Sprite}
@ -19,19 +21,14 @@ function Clone(sprite) {
* @type {?RenderWebGLWorker}
*/
this.renderer = null;
// If this is not true, there is no renderer (e.g., running in a test env).
if (typeof self !== 'undefined' && self.renderer) {
// Pull from `self.renderer`.
this.renderer = self.renderer;
if (this.runtime) {
this.renderer = this.runtime.renderer;
}
/**
* ID of the drawable for this clone returned by the renderer, if rendered.
* @type {?Number}
*/
this.drawableID = null;
this.initDrawable();
}
util.inherits(Clone, Target);
@ -40,17 +37,25 @@ util.inherits(Clone, Target);
*/
Clone.prototype.initDrawable = function () {
if (this.renderer) {
var createPromise = this.renderer.createDrawable();
var instance = this;
createPromise.then(function (id) {
instance.drawableID = id;
// Once the drawable is created, send our current set of properties.
instance.updateAllDrawableProperties();
});
this.drawableID = this.renderer.createDrawable();
this.updateAllDrawableProperties();
}
// If we're a clone, start the hats.
if (!this.isOriginal) {
this.runtime.startHats(
'control_start_as_clone', null, this
);
}
};
// Clone-level properties.
/**
* Whether this represents an "original" clone, i.e., created by the editor
* and not clone blocks. In interface terms, this true for a "sprite."
* @type {boolean}
*/
Clone.prototype.isOriginal = true;
/**
* Whether this clone represents the Scratch stage.
* @type {boolean}
@ -304,4 +309,50 @@ Clone.prototype.colorIsTouchingColor = function (targetRgb, maskRgb) {
return false;
};
/**
* Make a clone of this clone, copying any run-time properties.
* If we've hit the global clone limit, returns null.
* @return {!Clone} New clone object.
*/
Clone.prototype.makeClone = function () {
if (!this.runtime.clonesAvailable()) {
return; // Hit max clone limit.
}
this.runtime.changeCloneCounter(1);
var newClone = this.sprite.createClone();
newClone.x = this.x;
newClone.y = this.y;
newClone.direction = this.direction;
newClone.visible = this.visible;
newClone.size = this.size;
newClone.currentCostume = this.currentCostume;
newClone.effects = JSON.parse(JSON.stringify(this.effects));
newClone.variables = JSON.parse(JSON.stringify(this.variables));
newClone.lists = JSON.parse(JSON.stringify(this.lists));
newClone.initDrawable();
newClone.updateAllDrawableProperties();
return newClone;
};
/**
* Called when the project receives a "green flag."
* For a clone, this clears graphic effects.
*/
Clone.prototype.onGreenFlag = function () {
this.clearEffects();
};
/**
* Dispose of this clone, destroying any run-time properties.
*/
Clone.prototype.dispose = function () {
if (this.isOriginal) { // Don't allow a non-clone to delete itself.
return;
}
this.runtime.changeCloneCounter(-1);
if (this.renderer && this.drawableID !== null) {
this.renderer.destroyDrawable(this.drawableID);
}
};
module.exports = Clone;

View file

@ -5,9 +5,11 @@ var Blocks = require('../engine/blocks');
* Sprite to be used on the Scratch stage.
* All clones of a sprite have shared blocks, shared costumes, shared variables.
* @param {?Blocks} blocks Shared blocks object for all clones of sprite.
* @param {Runtime} runtime Reference to the runtime.
* @constructor
*/
function Sprite (blocks) {
function Sprite (blocks, runtime) {
this.runtime = runtime;
if (!blocks) {
// Shared set of blocks for all clones.
blocks = new Blocks();
@ -43,8 +45,13 @@ function Sprite (blocks) {
* @returns {!Clone} Newly created clone.
*/
Sprite.prototype.createClone = function () {
var newClone = new Clone(this);
var newClone = new Clone(this, this.runtime);
newClone.isOriginal = this.clones.length == 0;
this.clones.push(newClone);
if (newClone.isOriginal) {
newClone.initDrawable();
newClone.updateAllDrawableProperties();
}
return newClone;
};

View file

@ -125,4 +125,39 @@ Cast.isInt = function (val) {
return false;
};
Cast.LIST_INVALID = 'INVALID';
Cast.LIST_ALL = 'ALL';
/**
* Compute a 1-based index into a list, based on a Scratch argument.
* Two special cases may be returned:
* LIST_ALL: if the block is referring to all of the items in the list.
* LIST_INVALID: if the index was invalid in any way.
* @param {*} index Scratch arg, including 1-based numbers or special cases.
* @param {number} length Length of the list.
* @return {(number|string)} 1-based index for list, LIST_ALL, or LIST_INVALID.
*/
Cast.toListIndex = function (index, length) {
if (typeof index !== 'number') {
if (index == 'all') {
return Cast.LIST_ALL;
}
if (index == 'last') {
if (length > 0) {
return length;
}
return Cast.LIST_INVALID;
} else if (index == 'random' || index == 'any') {
if (length > 0) {
return 1 + Math.floor(Math.random() * length);
}
return Cast.LIST_INVALID;
}
}
index = Math.floor(Cast.toNumber(index));
if (index < 1 || index > length) {
return Cast.LIST_INVALID;
}
return index;
};
module.exports = Cast;

16811
vm.js

File diff suppressed because it is too large Load diff

11
vm.min.js vendored

File diff suppressed because one or more lines are too long

View file

@ -1,13 +1,12 @@
var CopyWebpackPlugin = require('copy-webpack-plugin');
var defaultsDeep = require('lodash.defaultsdeep');
var path = require('path');
var webpack = require('webpack');
module.exports = {
entry: {
'vm': './src/index.js',
'vm.min': './src/index.js'
},
output: {
path: __dirname,
filename: '[name].js'
var base = {
devServer: {
contentBase: path.resolve(__dirname, 'playground'),
host: '0.0.0.0'
},
module: {
loaders: [
@ -27,3 +26,89 @@ module.exports = {
})
]
};
module.exports = [
// Web-compatible, playground
defaultsDeep({}, base, {
entry: {
'vm': './src/index.js',
'vm.min': './src/index.js'
},
output: {
path: __dirname,
filename: '[name].js'
},
module: {
loaders: base.module.loaders.concat([
{
test: require.resolve('./src/index.js'),
loader: 'expose?VirtualMachine'
}
])
}
}),
// Webpack-compatible
defaultsDeep({}, base, {
entry: {
'dist': './src/index.js'
},
output: {
library: 'VirtualMachine',
libraryTarget: 'commonjs2',
path: __dirname,
filename: '[name].js'
}
}),
// Playground
defaultsDeep({}, base, {
entry: {
'vm': './src/index.js',
'vendor': [
// FPS counter
'stats.js/build/stats.min.js',
// Syntax highlighter
'highlightjs/highlight.pack.min.js',
// Scratch Blocks
'scratch-blocks/dist/vertical.js',
// Renderer
'scratch-render'
]
},
output: {
path: path.resolve(__dirname, 'playground'),
filename: '[name].js'
},
module: {
loaders: base.module.loaders.concat([
{
test: require.resolve('./src/index.js'),
loader: 'expose?VirtualMachine'
},
{
test: require.resolve('stats.js/build/stats.min.js'),
loader: 'script'
},
{
test: require.resolve('highlightjs/highlight.pack.min.js'),
loader: 'script'
},
{
test: require.resolve('scratch-blocks/dist/vertical.js'),
loader: 'expose?Blockly'
},
{
test: require.resolve('scratch-render'),
loader: 'expose?RenderWebGL'
}
])
},
plugins: base.plugins.concat([
new CopyWebpackPlugin([{
from: 'node_modules/scratch-blocks/media',
to: 'media'
}, {
from: 'node_modules/highlightjs/styles/zenburn.css'
}])
])
})
];