mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-06-07 19:14:40 -04:00
Interpreter fixes, enhancements, features (#280)
* Thread stepping rework; interp.redraw equivalent * Add turbo mode and pause mode * Yielding behavior to match Scratch 2.0 * Implement warp-mode procedure threads * Add check for recursive call * Inline wait block timer * Revert to setInterval and always drawing * Restore yielding in glide * 30TPS compatibility mode * 5-call count recursion limit * Removing dead primitive code * To simplify, access runtime.threads inline in `stepThreads`. * Warp mode/timer fixes; recursive check fixes; clean-up * Add basic single-stepping * Add single-stepping speed slider * Allow yielding threads to run in single-stepping * Restore inactive threads tracking for block glows * Add clock pausing during pause mode * Documentation and clean-up throughout * Don't look for block glows in `thread.topBlock`. * Add null check for block glows; rename `_updateScriptGlows` to reflect block glowing * Use the current executed block for glow, instead of stack * Add more comments to `stepToProcedure`, and re-arrange to match 2.0 * Tweak to Blocks.prototype.getTopLevelScript * Revert previous * Fix threads array to be resilient to changes during `stepThreads` * Restore inactive threads filtering * Fix typo in "procedure" * !! instead of == true
This commit is contained in:
parent
060d1ab2a5
commit
e49f076fa1
14 changed files with 705 additions and 281 deletions
|
@ -15,6 +15,19 @@
|
|||
<button id="greenflag">Green flag</button>
|
||||
<button id="stopall">Stop</button>
|
||||
</div>
|
||||
<div>
|
||||
Turbo: <input id='turbomode' type='checkbox' />
|
||||
</div>
|
||||
<div>
|
||||
Pause: <input id='pausemode' type='checkbox' />
|
||||
</div>
|
||||
<div>
|
||||
Compatibility (30 TPS): <input id='compatmode' type='checkbox' />
|
||||
</div>
|
||||
<div>
|
||||
Single stepping: <input id='singlestepmode' type='checkbox' />
|
||||
<input id='singlestepspeed' type='range' min='1' max='20' value='10' />
|
||||
</div>
|
||||
<br />
|
||||
<ul id="playgroundLinks">
|
||||
<li><a id="renderexplorer-link" href="#">Renderer</a></li>
|
||||
|
|
|
@ -245,6 +245,29 @@ window.onload = function() {
|
|||
document.getElementById('stopall').addEventListener('click', function() {
|
||||
vm.stopAll();
|
||||
});
|
||||
document.getElementById('turbomode').addEventListener('change', function() {
|
||||
var turboOn = document.getElementById('turbomode').checked;
|
||||
vm.setTurboMode(turboOn);
|
||||
});
|
||||
document.getElementById('pausemode').addEventListener('change', function() {
|
||||
var pauseOn = document.getElementById('pausemode').checked;
|
||||
vm.setPauseMode(pauseOn);
|
||||
});
|
||||
document.getElementById('compatmode').addEventListener('change',
|
||||
function() {
|
||||
var compatibilityMode = document.getElementById('compatmode').checked;
|
||||
vm.setCompatibilityMode(compatibilityMode);
|
||||
});
|
||||
document.getElementById('singlestepmode').addEventListener('change',
|
||||
function() {
|
||||
var singleStep = document.getElementById('singlestepmode').checked;
|
||||
vm.setSingleSteppingMode(singleStep);
|
||||
});
|
||||
document.getElementById('singlestepspeed').addEventListener('input',
|
||||
function() {
|
||||
var speed = document.getElementById('singlestepspeed').value;
|
||||
vm.setSingleSteppingSpeed(speed);
|
||||
});
|
||||
|
||||
var tabBlockExplorer = document.getElementById('tab-blockexplorer');
|
||||
var tabThreadExplorer = document.getElementById('tab-threadexplorer');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var Cast = require('../util/cast');
|
||||
var Promise = require('promise');
|
||||
var Timer = require('../util/timer');
|
||||
|
||||
function Scratch3ControlBlocks(runtime) {
|
||||
/**
|
||||
|
@ -23,7 +23,6 @@ Scratch3ControlBlocks.prototype.getPrimitives = function() {
|
|||
'control_if': this.if,
|
||||
'control_if_else': this.ifElse,
|
||||
'control_stop': this.stop,
|
||||
'control_create_clone_of_menu': this.createCloneMenu,
|
||||
'control_create_clone_of': this.createClone,
|
||||
'control_delete_this_clone': this.deleteClone
|
||||
};
|
||||
|
@ -46,90 +45,60 @@ Scratch3ControlBlocks.prototype.repeat = function(args, util) {
|
|||
// Only execute once per frame.
|
||||
// When the branch finishes, `repeat` will be executed again and
|
||||
// the second branch will be taken, yielding for the rest of the frame.
|
||||
if (!util.stackFrame.executedInFrame) {
|
||||
util.stackFrame.executedInFrame = true;
|
||||
// Decrease counter
|
||||
util.stackFrame.loopCounter--;
|
||||
// If we still have some left, start the branch.
|
||||
if (util.stackFrame.loopCounter >= 0) {
|
||||
util.startBranch();
|
||||
}
|
||||
} else {
|
||||
util.stackFrame.executedInFrame = false;
|
||||
util.yieldFrame();
|
||||
// Decrease counter
|
||||
util.stackFrame.loopCounter--;
|
||||
// If we still have some left, start the branch.
|
||||
if (util.stackFrame.loopCounter >= 0) {
|
||||
util.startBranch(1, true);
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.repeatUntil = function(args, util) {
|
||||
var condition = Cast.toBoolean(args.CONDITION);
|
||||
// Only execute once per frame.
|
||||
// When the branch finishes, `repeat` will be executed again and
|
||||
// the second branch will be taken, yielding for the rest of the frame.
|
||||
if (!util.stackFrame.executedInFrame) {
|
||||
util.stackFrame.executedInFrame = true;
|
||||
// If the condition is true, start the branch.
|
||||
if (!condition) {
|
||||
util.startBranch();
|
||||
}
|
||||
} else {
|
||||
util.stackFrame.executedInFrame = false;
|
||||
util.yieldFrame();
|
||||
// If the condition is true, start the branch.
|
||||
if (!condition) {
|
||||
util.startBranch(1, true);
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.waitUntil = function(args, util) {
|
||||
var condition = Cast.toBoolean(args.CONDITION);
|
||||
// Only execute once per frame.
|
||||
if (!condition) {
|
||||
util.yieldFrame();
|
||||
util.yield();
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.forever = function(args, util) {
|
||||
// Only execute once per frame.
|
||||
// When the branch finishes, `forever` will be executed again and
|
||||
// the second branch will be taken, yielding for the rest of the frame.
|
||||
if (!util.stackFrame.executedInFrame) {
|
||||
util.stackFrame.executedInFrame = true;
|
||||
util.startBranch();
|
||||
} else {
|
||||
util.stackFrame.executedInFrame = false;
|
||||
util.yieldFrame();
|
||||
}
|
||||
util.startBranch(1, true);
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.wait = function(args) {
|
||||
var duration = Cast.toNumber(args.DURATION);
|
||||
return new Promise(function(resolve) {
|
||||
setTimeout(function() {
|
||||
resolve();
|
||||
}, 1000 * duration);
|
||||
});
|
||||
Scratch3ControlBlocks.prototype.wait = function(args, util) {
|
||||
if (!util.stackFrame.timer) {
|
||||
util.stackFrame.timer = new Timer();
|
||||
util.stackFrame.timer.start();
|
||||
util.yield();
|
||||
this.runtime.requestRedraw();
|
||||
} else {
|
||||
var duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));
|
||||
if (util.stackFrame.timer.timeElapsed() < duration) {
|
||||
util.yield();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.if = function(args, util) {
|
||||
var condition = Cast.toBoolean(args.CONDITION);
|
||||
// Only execute one time. `if` will be returned to
|
||||
// when the branch finishes, but it shouldn't execute again.
|
||||
if (util.stackFrame.executedInFrame === undefined) {
|
||||
util.stackFrame.executedInFrame = true;
|
||||
if (condition) {
|
||||
util.startBranch();
|
||||
}
|
||||
if (condition) {
|
||||
util.startBranch(1, false);
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ControlBlocks.prototype.ifElse = function(args, util) {
|
||||
var condition = Cast.toBoolean(args.CONDITION);
|
||||
// Only execute one time. `ifElse` will be returned to
|
||||
// when the branch finishes, but it shouldn't execute again.
|
||||
if (util.stackFrame.executedInFrame === undefined) {
|
||||
util.stackFrame.executedInFrame = true;
|
||||
if (condition) {
|
||||
util.startBranch(1);
|
||||
} else {
|
||||
util.startBranch(2);
|
||||
}
|
||||
if (condition) {
|
||||
util.startBranch(1, false);
|
||||
} else {
|
||||
util.startBranch(2, false);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -145,11 +114,6 @@ Scratch3ControlBlocks.prototype.stop = function(args, util) {
|
|||
}
|
||||
};
|
||||
|
||||
// @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_') {
|
||||
|
|
|
@ -82,7 +82,7 @@ Scratch3EventBlocks.prototype.broadcastAndWait = function (args, util) {
|
|||
return instance.runtime.isActiveThread(thread);
|
||||
});
|
||||
if (waiting) {
|
||||
util.yieldFrame();
|
||||
util.yield();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -153,7 +153,7 @@ Scratch3LooksBlocks.prototype.switchBackdropAndWait = function (args, util) {
|
|||
return instance.runtime.isActiveThread(thread);
|
||||
});
|
||||
if (waiting) {
|
||||
util.yieldFrame();
|
||||
util.yield();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ Scratch3MotionBlocks.prototype.glide = function (args, util) {
|
|||
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
|
||||
return;
|
||||
}
|
||||
util.yieldFrame();
|
||||
util.yield();
|
||||
} else {
|
||||
var timeElapsed = util.stackFrame.timer.timeElapsed();
|
||||
if (timeElapsed < util.stackFrame.duration * 1000) {
|
||||
|
@ -131,7 +131,7 @@ Scratch3MotionBlocks.prototype.glide = function (args, util) {
|
|||
util.stackFrame.startX + dx,
|
||||
util.stackFrame.startY + dy
|
||||
);
|
||||
util.yieldFrame();
|
||||
util.yield();
|
||||
} else {
|
||||
// Finished: move to final position.
|
||||
util.target.setXY(util.stackFrame.endX, util.stackFrame.endY);
|
||||
|
|
|
@ -24,23 +24,20 @@ Scratch3ProcedureBlocks.prototype.defNoReturn = function () {
|
|||
|
||||
Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) {
|
||||
if (!util.stackFrame.executed) {
|
||||
var procedureName = args.mutation.proccode;
|
||||
var paramNames = util.getProcedureParamNames(procedureName);
|
||||
var procedureCode = args.mutation.proccode;
|
||||
var paramNames = util.getProcedureParamNames(procedureCode);
|
||||
for (var i = 0; i < paramNames.length; i++) {
|
||||
if (args.hasOwnProperty('input' + i)) {
|
||||
util.pushParam(paramNames[i], args['input' + i]);
|
||||
}
|
||||
}
|
||||
util.stackFrame.executed = true;
|
||||
util.startProcedure(procedureName);
|
||||
util.startProcedure(procedureCode);
|
||||
}
|
||||
};
|
||||
|
||||
Scratch3ProcedureBlocks.prototype.param = function (args, util) {
|
||||
var value = util.getParam(args.mutation.paramname);
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ var execute = function (sequencer, thread) {
|
|||
runtime.visualReport(currentBlockId, resolvedValue);
|
||||
}
|
||||
// Finished any yields.
|
||||
thread.setStatus(Thread.STATUS_RUNNING);
|
||||
thread.status = Thread.STATUS_RUNNING;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -133,15 +133,22 @@ var execute = function (sequencer, thread) {
|
|||
var input = inputs[inputName];
|
||||
var inputBlockId = input.block;
|
||||
// Is there no value for this input waiting in the stack frame?
|
||||
if (typeof currentStackFrame.reported[inputName] === 'undefined') {
|
||||
if (typeof currentStackFrame.reported[inputName] === 'undefined'
|
||||
&& inputBlockId) {
|
||||
// If there's not, we need to evaluate the block.
|
||||
var reporterYielded = (
|
||||
sequencer.stepToReporter(thread, inputBlockId, inputName)
|
||||
);
|
||||
// If the reporter yielded, return immediately;
|
||||
// it needs time to finish and report its value.
|
||||
if (reporterYielded) {
|
||||
// Push to the stack to evaluate the reporter block.
|
||||
thread.pushStack(inputBlockId);
|
||||
// Save name of input for `Thread.pushReportedValue`.
|
||||
currentStackFrame.waitingReporter = inputName;
|
||||
// Actually execute the block.
|
||||
execute(sequencer, thread);
|
||||
if (thread.status === Thread.STATUS_PROMISE_WAIT) {
|
||||
return;
|
||||
} else {
|
||||
// Execution returned immediately,
|
||||
// and presumably a value was reported, so pop the stack.
|
||||
currentStackFrame.waitingReporter = null;
|
||||
thread.popStack();
|
||||
}
|
||||
}
|
||||
argValues[inputName] = currentStackFrame.reported[inputName];
|
||||
|
@ -164,17 +171,10 @@ var execute = function (sequencer, thread) {
|
|||
stackFrame: currentStackFrame.executionContext,
|
||||
target: target,
|
||||
yield: function() {
|
||||
thread.setStatus(Thread.STATUS_YIELD);
|
||||
thread.status = Thread.STATUS_YIELD;
|
||||
},
|
||||
yieldFrame: function() {
|
||||
thread.setStatus(Thread.STATUS_YIELD_FRAME);
|
||||
},
|
||||
done: function() {
|
||||
thread.setStatus(Thread.STATUS_RUNNING);
|
||||
sequencer.proceedThread(thread);
|
||||
},
|
||||
startBranch: function (branchNum) {
|
||||
sequencer.stepToBranch(thread, branchNum);
|
||||
startBranch: function (branchNum, isLoop) {
|
||||
sequencer.stepToBranch(thread, branchNum, isLoop);
|
||||
},
|
||||
stopAll: function () {
|
||||
runtime.stopAll();
|
||||
|
@ -185,11 +185,11 @@ var execute = function (sequencer, thread) {
|
|||
stopThread: function() {
|
||||
sequencer.retireThread(thread);
|
||||
},
|
||||
startProcedure: function (procedureName) {
|
||||
sequencer.stepToProcedure(thread, procedureName);
|
||||
startProcedure: function (procedureCode) {
|
||||
sequencer.stepToProcedure(thread, procedureCode);
|
||||
},
|
||||
getProcedureParamNames: function (procedureName) {
|
||||
return blockContainer.getProcedureParamNames(procedureName);
|
||||
getProcedureParamNames: function (procedureCode) {
|
||||
return blockContainer.getProcedureParamNames(procedureCode);
|
||||
},
|
||||
pushParam: function (paramName, paramValue) {
|
||||
thread.pushParam(paramName, paramValue);
|
||||
|
@ -221,18 +221,24 @@ var execute = function (sequencer, thread) {
|
|||
if (isPromise(primitiveReportedValue)) {
|
||||
if (thread.status === Thread.STATUS_RUNNING) {
|
||||
// Primitive returned a promise; automatically yield thread.
|
||||
thread.setStatus(Thread.STATUS_YIELD);
|
||||
thread.status = Thread.STATUS_PROMISE_WAIT;
|
||||
}
|
||||
// Promise handlers
|
||||
primitiveReportedValue.then(function(resolvedValue) {
|
||||
handleReport(resolvedValue);
|
||||
sequencer.proceedThread(thread);
|
||||
if (typeof resolvedValue !== 'undefined') {
|
||||
thread.popStack();
|
||||
} else {
|
||||
var popped = thread.popStack();
|
||||
var nextBlockId = thread.target.blocks.getNextBlock(popped);
|
||||
thread.pushStack(nextBlockId);
|
||||
}
|
||||
}, function(rejectionReason) {
|
||||
// Promise rejected: the primitive had some error.
|
||||
// Log it and proceed.
|
||||
console.warn('Primitive rejected promise: ', rejectionReason);
|
||||
thread.setStatus(Thread.STATUS_RUNNING);
|
||||
sequencer.proceedThread(thread);
|
||||
thread.status = Thread.STATUS_RUNNING;
|
||||
thread.popStack();
|
||||
});
|
||||
} else if (thread.status === Thread.STATUS_RUNNING) {
|
||||
handleReport(primitiveReportedValue);
|
||||
|
|
|
@ -27,8 +27,6 @@ function Runtime () {
|
|||
// Bind event emitter
|
||||
EventEmitter.call(this);
|
||||
|
||||
// State for the runtime
|
||||
|
||||
/**
|
||||
* Target management and storage.
|
||||
* @type {Array.<!Target>}
|
||||
|
@ -45,33 +43,131 @@ function Runtime () {
|
|||
/** @type {!Sequencer} */
|
||||
this.sequencer = new Sequencer(this);
|
||||
|
||||
/**
|
||||
* Storage container for flyout blocks.
|
||||
* These will execute on `_editingTarget.`
|
||||
* @type {!Blocks}
|
||||
*/
|
||||
this.flyoutBlocks = new Blocks();
|
||||
|
||||
/**
|
||||
* Currently known editing target for the VM.
|
||||
* @type {?Target}
|
||||
*/
|
||||
this._editingTarget = null;
|
||||
|
||||
/**
|
||||
* Map to look up a block primitive's implementation function by its opcode.
|
||||
* This is a two-step lookup: package name first, then primitive name.
|
||||
* @type {Object.<string, Function>}
|
||||
*/
|
||||
this._primitives = {};
|
||||
|
||||
/**
|
||||
* Map to look up hat blocks' metadata.
|
||||
* Keys are opcode for hat, values are metadata objects.
|
||||
* @type {Object.<string, Object>}
|
||||
*/
|
||||
this._hats = {};
|
||||
|
||||
/**
|
||||
* Currently known values for edge-activated hats.
|
||||
* Keys are block ID for the hat; values are the currently known values.
|
||||
* @type {Object.<string, *>}
|
||||
*/
|
||||
this._edgeActivatedHatValues = {};
|
||||
|
||||
/**
|
||||
* A list of script block IDs that were glowing during the previous frame.
|
||||
* @type {!Array.<!string>}
|
||||
*/
|
||||
this._scriptGlowsPreviousFrame = [];
|
||||
|
||||
/**
|
||||
* A list of block IDs that were glowing during the previous frame.
|
||||
* @type {!Array.<!string>}
|
||||
*/
|
||||
this._blockGlowsPreviousFrame = [];
|
||||
|
||||
/**
|
||||
* Currently known number of clones, used to enforce clone limit.
|
||||
* @type {number}
|
||||
*/
|
||||
this._cloneCounter = 0;
|
||||
|
||||
/**
|
||||
* Whether the project is in "turbo mode."
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.turboMode = false;
|
||||
|
||||
/**
|
||||
* Whether the project is in "pause mode."
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.pauseMode = false;
|
||||
|
||||
/**
|
||||
* Whether the project is in "compatibility mode" (30 TPS).
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.compatibilityMode = false;
|
||||
|
||||
/**
|
||||
* Whether the project is in "single stepping mode."
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.singleStepping = false;
|
||||
|
||||
/**
|
||||
* How fast in ms "single stepping mode" should run, in ms.
|
||||
* Can be updated dynamically.
|
||||
* @type {!number}
|
||||
*/
|
||||
this.singleStepInterval = 1000 / 10;
|
||||
|
||||
/**
|
||||
* A reference to the current runtime stepping interval, set
|
||||
* by a `setInterval`.
|
||||
* @type {!number}
|
||||
*/
|
||||
this._steppingInterval = null;
|
||||
|
||||
/**
|
||||
* Current length of a step.
|
||||
* Changes as mode switches, and used by the sequencer to calculate
|
||||
* WORK_TIME.
|
||||
* @type {!number}
|
||||
*/
|
||||
this.currentStepTime = null;
|
||||
|
||||
/**
|
||||
* Whether any primitive has requested a redraw.
|
||||
* Affects whether `Sequencer.stepThreads` will yield
|
||||
* after stepping each thread.
|
||||
* Reset on every frame.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.redrawRequested = false;
|
||||
|
||||
// Register all given block packages.
|
||||
this._registerBlockPackages();
|
||||
|
||||
// Register and initialize "IO devices", containers for processing
|
||||
// I/O related data.
|
||||
/** @type {Object.<string, Object>} */
|
||||
this.ioDevices = {
|
||||
'clock': new Clock(),
|
||||
'keyboard': new Keyboard(this),
|
||||
'mouse': new Mouse(this)
|
||||
};
|
||||
|
||||
this._scriptGlowsPreviousFrame = [];
|
||||
this._editingTarget = null;
|
||||
/**
|
||||
* Currently known number of clones.
|
||||
* @type {number}
|
||||
*/
|
||||
this._cloneCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inherit from EventEmitter
|
||||
*/
|
||||
util.inherits(Runtime, EventEmitter);
|
||||
|
||||
/**
|
||||
* Width of the stage, in pixels.
|
||||
* @const {number}
|
||||
|
@ -115,15 +211,15 @@ Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF';
|
|||
Runtime.VISUAL_REPORT = 'VISUAL_REPORT';
|
||||
|
||||
/**
|
||||
* Inherit from EventEmitter
|
||||
*/
|
||||
util.inherits(Runtime, EventEmitter);
|
||||
|
||||
/**
|
||||
* How rapidly we try to step threads, in ms.
|
||||
* How rapidly we try to step threads by default, in ms.
|
||||
*/
|
||||
Runtime.THREAD_STEP_INTERVAL = 1000 / 60;
|
||||
|
||||
/**
|
||||
* In compatibility mode, how rapidly we try to step threads, in ms.
|
||||
*/
|
||||
Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY = 1000 / 30;
|
||||
|
||||
/**
|
||||
* How many clones can be created at a time.
|
||||
* @const {number}
|
||||
|
@ -175,9 +271,6 @@ Runtime.prototype.getOpcodeFunction = function (opcode) {
|
|||
return this._primitives[opcode];
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return whether an opcode represents a hat block.
|
||||
* @param {!string} opcode The opcode to look up.
|
||||
|
@ -235,7 +328,7 @@ Runtime.prototype.attachRenderer = function (renderer) {
|
|||
*/
|
||||
Runtime.prototype._pushThread = function (id, target) {
|
||||
var thread = new Thread(id);
|
||||
thread.setTarget(target);
|
||||
thread.target = target;
|
||||
thread.pushStack(id);
|
||||
this.threads.push(thread);
|
||||
return thread;
|
||||
|
@ -449,6 +542,10 @@ Runtime.prototype.stopAll = function () {
|
|||
* inactive threads after each iteration.
|
||||
*/
|
||||
Runtime.prototype._step = function () {
|
||||
if (this.pauseMode) {
|
||||
// Don't do any execution while in pause mode.
|
||||
return;
|
||||
}
|
||||
// Find all edge-activated hats, and add them to threads to be evaluated.
|
||||
for (var hatType in this._hats) {
|
||||
var hat = this._hats[hatType];
|
||||
|
@ -456,37 +553,113 @@ Runtime.prototype._step = function () {
|
|||
this.startHats(hatType);
|
||||
}
|
||||
}
|
||||
var inactiveThreads = this.sequencer.stepThreads(this.threads);
|
||||
this._updateScriptGlows();
|
||||
for (var i = 0; i < inactiveThreads.length; i++) {
|
||||
this._removeThread(inactiveThreads[i]);
|
||||
this.redrawRequested = false;
|
||||
var inactiveThreads = this.sequencer.stepThreads();
|
||||
this._updateGlows(inactiveThreads);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the current editing target known by the runtime.
|
||||
* @param {!Target} editingTarget New editing target.
|
||||
*/
|
||||
Runtime.prototype.setEditingTarget = function (editingTarget) {
|
||||
this._editingTarget = editingTarget;
|
||||
// Script glows must be cleared.
|
||||
this._scriptGlowsPreviousFrame = [];
|
||||
this._updateGlows();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether we are in pause mode.
|
||||
* @param {boolean} pauseModeOn True iff in pause mode.
|
||||
*/
|
||||
Runtime.prototype.setPauseMode = function (pauseModeOn) {
|
||||
// Inform the project clock/timer to pause/resume its time.
|
||||
if (this.ioDevices.clock) {
|
||||
if (pauseModeOn && !this.pauseMode) {
|
||||
this.ioDevices.clock.pause();
|
||||
}
|
||||
if (!pauseModeOn && this.pauseMode) {
|
||||
this.ioDevices.clock.resume();
|
||||
}
|
||||
}
|
||||
this.pauseMode = pauseModeOn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether we are in 30 TPS compatibility mode.
|
||||
* @param {boolean} compatibilityModeOn True iff in compatibility mode.
|
||||
*/
|
||||
Runtime.prototype.setCompatibilityMode = function (compatibilityModeOn) {
|
||||
this.compatibilityMode = compatibilityModeOn;
|
||||
if (this._steppingInterval) {
|
||||
self.clearInterval(this._steppingInterval);
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
|
||||
Runtime.prototype.setEditingTarget = function (editingTarget) {
|
||||
this._scriptGlowsPreviousFrame = [];
|
||||
this._editingTarget = editingTarget;
|
||||
this._updateScriptGlows();
|
||||
/**
|
||||
* Set whether we are in single-stepping mode.
|
||||
* @param {boolean} singleSteppingOn True iff in single-stepping mode.
|
||||
*/
|
||||
Runtime.prototype.setSingleSteppingMode = function (singleSteppingOn) {
|
||||
this.singleStepping = singleSteppingOn;
|
||||
if (this._steppingInterval) {
|
||||
self.clearInterval(this._steppingInterval);
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
|
||||
Runtime.prototype._updateScriptGlows = function () {
|
||||
/**
|
||||
* Set the speed during single-stepping mode.
|
||||
* @param {number} speed Interval length to step threads, in ms.
|
||||
*/
|
||||
Runtime.prototype.setSingleSteppingSpeed = function (speed) {
|
||||
this.singleStepInterval = 1000 / speed;
|
||||
if (this._steppingInterval) {
|
||||
self.clearInterval(this._steppingInterval);
|
||||
this.start();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit glows/glow clears for blocks and scripts after a single tick.
|
||||
* Looks at `this.threads` and notices which have turned on/off new glows.
|
||||
* @param {Array.<Thread>=} opt_extraThreads Optional list of inactive threads.
|
||||
*/
|
||||
Runtime.prototype._updateGlows = function (opt_extraThreads) {
|
||||
var searchThreads = [];
|
||||
searchThreads.push.apply(searchThreads, this.threads);
|
||||
if (opt_extraThreads) {
|
||||
searchThreads.push.apply(searchThreads, opt_extraThreads);
|
||||
}
|
||||
// Set of scripts that request a glow this frame.
|
||||
var requestedGlowsThisFrame = [];
|
||||
var requestedBlockGlowsThisFrame = [];
|
||||
// Final set of scripts glowing during this frame.
|
||||
var finalScriptGlows = [];
|
||||
var finalBlockGlows = [];
|
||||
// Find all scripts that should be glowing.
|
||||
for (var i = 0; i < this.threads.length; i++) {
|
||||
var thread = this.threads[i];
|
||||
for (var i = 0; i < searchThreads.length; i++) {
|
||||
var thread = searchThreads[i];
|
||||
var target = thread.target;
|
||||
if (thread.requestScriptGlowInFrame && target == this._editingTarget) {
|
||||
var blockForThread = thread.peekStack() || thread.topBlock;
|
||||
var script = target.blocks.getTopLevelScript(blockForThread);
|
||||
if (!script) {
|
||||
// Attempt to find in flyout blocks.
|
||||
script = this.flyoutBlocks.getTopLevelScript(blockForThread);
|
||||
if (target == this._editingTarget) {
|
||||
var blockForThread = thread.blockGlowInFrame;
|
||||
if (thread.requestScriptGlowInFrame) {
|
||||
var script = target.blocks.getTopLevelScript(blockForThread);
|
||||
if (!script) {
|
||||
// Attempt to find in flyout blocks.
|
||||
script = this.flyoutBlocks.getTopLevelScript(
|
||||
blockForThread
|
||||
);
|
||||
}
|
||||
if (script) {
|
||||
requestedGlowsThisFrame.push(script);
|
||||
}
|
||||
}
|
||||
if (script) {
|
||||
requestedGlowsThisFrame.push(script);
|
||||
// Only show block glows in single-stepping mode.
|
||||
if (this.singleStepping && blockForThread) {
|
||||
requestedBlockGlowsThisFrame.push(blockForThread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -509,7 +682,30 @@ Runtime.prototype._updateScriptGlows = function () {
|
|||
finalScriptGlows.push(currentFrameGlow);
|
||||
}
|
||||
}
|
||||
for (var m = 0; m < this._blockGlowsPreviousFrame.length; m++) {
|
||||
var previousBlockGlow = this._blockGlowsPreviousFrame[m];
|
||||
if (requestedBlockGlowsThisFrame.indexOf(previousBlockGlow) < 0) {
|
||||
// Glow turned off.
|
||||
try {
|
||||
this.glowBlock(previousBlockGlow, false);
|
||||
} catch (e) {
|
||||
// Block has been removed.
|
||||
}
|
||||
} else {
|
||||
// Still glowing.
|
||||
finalBlockGlows.push(previousBlockGlow);
|
||||
}
|
||||
}
|
||||
for (var p = 0; p < requestedBlockGlowsThisFrame.length; p++) {
|
||||
var currentBlockFrameGlow = requestedBlockGlowsThisFrame[p];
|
||||
if (this._blockGlowsPreviousFrame.indexOf(currentBlockFrameGlow) < 0) {
|
||||
// Glow turned on.
|
||||
this.glowBlock(currentBlockFrameGlow, true);
|
||||
finalBlockGlows.push(currentBlockFrameGlow);
|
||||
}
|
||||
}
|
||||
this._scriptGlowsPreviousFrame = finalScriptGlows;
|
||||
this._blockGlowsPreviousFrame = finalBlockGlows;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -617,22 +813,38 @@ Runtime.prototype.getTargetForStage = function () {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tell the runtime to request a redraw.
|
||||
* Use after a clone/sprite has completed some visible operation on the stage.
|
||||
*/
|
||||
Runtime.prototype.requestRedraw = function () {
|
||||
this.redrawRequested = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle an animation frame from the main thread.
|
||||
*/
|
||||
Runtime.prototype.animationFrame = function () {
|
||||
if (this.renderer) {
|
||||
// @todo: Only render when this.redrawRequested or clones rendered.
|
||||
this.renderer.draw();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set up timers to repeatedly step in a browser
|
||||
* Set up timers to repeatedly step in a browser.
|
||||
*/
|
||||
Runtime.prototype.start = function () {
|
||||
self.setInterval(function() {
|
||||
var interval = Runtime.THREAD_STEP_INTERVAL;
|
||||
if (this.singleStepping) {
|
||||
interval = this.singleStepInterval;
|
||||
} else if (this.compatibilityMode) {
|
||||
interval = Runtime.THREAD_STEP_INTERVAL_COMPATIBILITY;
|
||||
}
|
||||
this.currentStepTime = interval;
|
||||
this._steppingInterval = self.setInterval(function() {
|
||||
this._step();
|
||||
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
|
||||
}.bind(this), interval);
|
||||
};
|
||||
|
||||
module.exports = Runtime;
|
||||
|
|
|
@ -17,87 +17,163 @@ function Sequencer (runtime) {
|
|||
}
|
||||
|
||||
/**
|
||||
* The sequencer does as much work as it can within WORK_TIME milliseconds,
|
||||
* then yields. This is essentially a rate-limiter for blocks.
|
||||
* In Scratch 2.0, this is set to 75% of the target stage frame-rate (30fps).
|
||||
* @const {!number}
|
||||
* Time to run a warp-mode thread, in ms.
|
||||
* @type {number}
|
||||
*/
|
||||
Sequencer.WORK_TIME = 10;
|
||||
Sequencer.WARP_TIME = 500;
|
||||
|
||||
/**
|
||||
* Step through all threads in `this.threads`, running them in order.
|
||||
* @param {Array.<Thread>} threads List of which threads to step.
|
||||
* @return {Array.<Thread>} All threads which have finished in this iteration.
|
||||
* Step through all threads in `this.runtime.threads`, running them in order.
|
||||
* @return {Array.<!Thread>} List of inactive threads after stepping.
|
||||
*/
|
||||
Sequencer.prototype.stepThreads = function (threads) {
|
||||
// Start counting toward WORK_TIME
|
||||
Sequencer.prototype.stepThreads = function () {
|
||||
// Work time is 75% of the thread stepping interval.
|
||||
var WORK_TIME = 0.75 * this.runtime.currentStepTime;
|
||||
// Start counting toward WORK_TIME.
|
||||
this.timer.start();
|
||||
// List of threads which have been killed by this step.
|
||||
// Count of active threads.
|
||||
var numActiveThreads = Infinity;
|
||||
// Whether `stepThreads` has run through a full single tick.
|
||||
var ranFirstTick = false;
|
||||
var inactiveThreads = [];
|
||||
// If all of the threads are yielding, we should yield.
|
||||
var numYieldingThreads = 0;
|
||||
// Clear all yield statuses that were for the previous frame.
|
||||
for (var t = 0; t < threads.length; t++) {
|
||||
if (threads[t].status === Thread.STATUS_YIELD_FRAME) {
|
||||
threads[t].setStatus(Thread.STATUS_RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
// While there are still threads to run and we are within WORK_TIME,
|
||||
// continue executing threads.
|
||||
while (threads.length > 0 &&
|
||||
threads.length > numYieldingThreads &&
|
||||
this.timer.timeElapsed() < Sequencer.WORK_TIME) {
|
||||
// New threads at the end of the iteration.
|
||||
var newThreads = [];
|
||||
// Reset yielding thread count.
|
||||
numYieldingThreads = 0;
|
||||
// Attempt to run each thread one time
|
||||
for (var i = 0; i < threads.length; i++) {
|
||||
var activeThread = threads[i];
|
||||
if (activeThread.status === Thread.STATUS_RUNNING) {
|
||||
// Normal-mode thread: step.
|
||||
this.startThread(activeThread);
|
||||
} else if (activeThread.status === Thread.STATUS_YIELD ||
|
||||
activeThread.status === Thread.STATUS_YIELD_FRAME) {
|
||||
// Yielding thread: do nothing for this step.
|
||||
numYieldingThreads++;
|
||||
}
|
||||
if (activeThread.stack.length === 0 &&
|
||||
// Conditions for continuing to stepping threads:
|
||||
// 1. We must have threads in the list, and some must be active.
|
||||
// 2. Time elapsed must be less than WORK_TIME.
|
||||
// 3. Either turbo mode, or no redraw has been requested by a primitive.
|
||||
while (this.runtime.threads.length > 0 &&
|
||||
numActiveThreads > 0 &&
|
||||
this.timer.timeElapsed() < WORK_TIME &&
|
||||
(this.runtime.turboMode || !this.runtime.redrawRequested)) {
|
||||
numActiveThreads = 0;
|
||||
// Inline copy of the threads, updated on each step.
|
||||
var threadsCopy = this.runtime.threads.slice();
|
||||
// Attempt to run each thread one time.
|
||||
for (var i = 0; i < threadsCopy.length; i++) {
|
||||
var activeThread = threadsCopy[i];
|
||||
if (activeThread.stack.length === 0 ||
|
||||
activeThread.status === Thread.STATUS_DONE) {
|
||||
// Finished with this thread - tell runtime to clean it up.
|
||||
inactiveThreads.push(activeThread);
|
||||
} else {
|
||||
// Keep this thead in the loop.
|
||||
newThreads.push(activeThread);
|
||||
// Finished with this thread.
|
||||
if (inactiveThreads.indexOf(activeThread) < 0) {
|
||||
inactiveThreads.push(activeThread);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (activeThread.status === Thread.STATUS_YIELD_TICK &&
|
||||
!ranFirstTick) {
|
||||
// Clear single-tick yield from the last call of `stepThreads`.
|
||||
activeThread.status = Thread.STATUS_RUNNING;
|
||||
}
|
||||
if (activeThread.status === Thread.STATUS_RUNNING ||
|
||||
activeThread.status === Thread.STATUS_YIELD) {
|
||||
// Normal-mode thread: step.
|
||||
this.stepThread(activeThread);
|
||||
activeThread.warpTimer = null;
|
||||
}
|
||||
if (activeThread.status === Thread.STATUS_RUNNING) {
|
||||
// After stepping, status is still running.
|
||||
// If we're in single-stepping mode, mark the thread as
|
||||
// a single-tick yield so it doesn't re-execute
|
||||
// until the next frame.
|
||||
if (this.runtime.singleStepping) {
|
||||
activeThread.status = Thread.STATUS_YIELD_TICK;
|
||||
}
|
||||
numActiveThreads++;
|
||||
}
|
||||
}
|
||||
// Effectively filters out threads that have stopped.
|
||||
threads = newThreads;
|
||||
// We successfully ticked once. Prevents running STATUS_YIELD_TICK
|
||||
// threads on the next tick.
|
||||
ranFirstTick = true;
|
||||
}
|
||||
// Filter inactive threads from `this.runtime.threads`.
|
||||
this.runtime.threads = this.runtime.threads.filter(function(thread) {
|
||||
if (inactiveThreads.indexOf(thread) > -1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return inactiveThreads;
|
||||
};
|
||||
|
||||
/**
|
||||
* Step the requested thread
|
||||
* @param {!Thread} thread Thread object to step
|
||||
* Step the requested thread for as long as necessary.
|
||||
* @param {!Thread} thread Thread object to step.
|
||||
*/
|
||||
Sequencer.prototype.startThread = function (thread) {
|
||||
Sequencer.prototype.stepThread = function (thread) {
|
||||
var currentBlockId = thread.peekStack();
|
||||
if (!currentBlockId) {
|
||||
// A "null block" - empty branch.
|
||||
// Yield for the frame.
|
||||
thread.popStack();
|
||||
thread.setStatus(Thread.STATUS_YIELD_FRAME);
|
||||
return;
|
||||
}
|
||||
// Execute the current block
|
||||
execute(this, thread);
|
||||
// If the block executed without yielding and without doing control flow,
|
||||
// move to done.
|
||||
if (thread.status === Thread.STATUS_RUNNING &&
|
||||
thread.peekStack() === currentBlockId) {
|
||||
this.proceedThread(thread);
|
||||
while (thread.peekStack()) {
|
||||
var isWarpMode = thread.peekStackFrame().warpMode;
|
||||
if (isWarpMode && !thread.warpTimer) {
|
||||
// Initialize warp-mode timer if it hasn't been already.
|
||||
// This will start counting the thread toward `Sequencer.WARP_TIME`.
|
||||
thread.warpTimer = new Timer();
|
||||
thread.warpTimer.start();
|
||||
}
|
||||
// Execute the current block.
|
||||
// Save the current block ID to notice if we did control flow.
|
||||
currentBlockId = thread.peekStack();
|
||||
execute(this, thread);
|
||||
thread.blockGlowInFrame = currentBlockId;
|
||||
// If the thread has yielded or is waiting, yield to other threads.
|
||||
if (thread.status === Thread.STATUS_YIELD) {
|
||||
// Mark as running for next iteration.
|
||||
thread.status = Thread.STATUS_RUNNING;
|
||||
// In warp mode, yielded blocks are re-executed immediately.
|
||||
if (isWarpMode &&
|
||||
thread.warpTimer.timeElapsed() <= Sequencer.WARP_TIME) {
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
} else if (thread.status === Thread.STATUS_PROMISE_WAIT) {
|
||||
// A promise was returned by the primitive. Yield the thread
|
||||
// until the promise resolves. Promise resolution should reset
|
||||
// thread.status to Thread.STATUS_RUNNING.
|
||||
return;
|
||||
}
|
||||
// If no control flow has happened, switch to next block.
|
||||
if (thread.peekStack() === currentBlockId) {
|
||||
thread.goToNextBlock();
|
||||
}
|
||||
// If no next block has been found at this point, look on the stack.
|
||||
while (!thread.peekStack()) {
|
||||
thread.popStack();
|
||||
if (thread.stack.length === 0) {
|
||||
// No more stack to run!
|
||||
thread.status = Thread.STATUS_DONE;
|
||||
return;
|
||||
}
|
||||
if (thread.peekStackFrame().isLoop) {
|
||||
// The current level of the stack is marked as a loop.
|
||||
// Return to yield for the frame/tick in general.
|
||||
// Unless we're in warp mode - then only return if the
|
||||
// warp timer is up.
|
||||
if (!isWarpMode ||
|
||||
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
|
||||
// Don't do anything to the stack, since loops need
|
||||
// to be re-executed.
|
||||
return;
|
||||
} else {
|
||||
// Don't go to the next block for this level of the stack,
|
||||
// since loops need to be re-executed.
|
||||
continue;
|
||||
}
|
||||
} else if (thread.peekStackFrame().waitingReporter) {
|
||||
// This level of the stack was waiting for a value.
|
||||
// This means a reporter has just returned - so don't go
|
||||
// to the next block for this level of the stack.
|
||||
return;
|
||||
}
|
||||
// Get next block of existing block on the stack.
|
||||
thread.goToNextBlock();
|
||||
}
|
||||
// In single-stepping mode, force `stepThread` to only run one block
|
||||
// at a time.
|
||||
if (this.runtime.singleStepping) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -105,8 +181,9 @@ Sequencer.prototype.startThread = function (thread) {
|
|||
* Step a thread into a block's branch.
|
||||
* @param {!Thread} thread Thread object to step to branch.
|
||||
* @param {Number} branchNum Which branch to step to (i.e., 1, 2).
|
||||
* @param {Boolean} isLoop Whether this block is a loop.
|
||||
*/
|
||||
Sequencer.prototype.stepToBranch = function (thread, branchNum) {
|
||||
Sequencer.prototype.stepToBranch = function (thread, branchNum, isLoop) {
|
||||
if (!branchNum) {
|
||||
branchNum = 1;
|
||||
}
|
||||
|
@ -115,11 +192,11 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
|
|||
currentBlockId,
|
||||
branchNum
|
||||
);
|
||||
thread.peekStackFrame().isLoop = isLoop;
|
||||
if (branchId) {
|
||||
// Push branch ID to the thread's stack.
|
||||
thread.pushStack(branchId);
|
||||
} else {
|
||||
// Push null, so we come back to the current block.
|
||||
thread.pushStack(null);
|
||||
}
|
||||
};
|
||||
|
@ -127,56 +204,39 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
|
|||
/**
|
||||
* Step a procedure.
|
||||
* @param {!Thread} thread Thread object to step to procedure.
|
||||
* @param {!string} procedureName Name of procedure defined in this target.
|
||||
* @param {!string} procedureCode Procedure code of procedure to step to.
|
||||
*/
|
||||
Sequencer.prototype.stepToProcedure = function (thread, procedureName) {
|
||||
var definition = thread.target.blocks.getProcedureDefinition(procedureName);
|
||||
Sequencer.prototype.stepToProcedure = function (thread, procedureCode) {
|
||||
var definition = thread.target.blocks.getProcedureDefinition(procedureCode);
|
||||
if (!definition) {
|
||||
return;
|
||||
}
|
||||
// Check if the call is recursive.
|
||||
// If so, set the thread to yield after pushing.
|
||||
var isRecursive = thread.isRecursiveCall(procedureCode);
|
||||
// To step to a procedure, we put its definition on the stack.
|
||||
// Execution for the thread will proceed through the definition hat
|
||||
// and on to the main definition of the procedure.
|
||||
// When that set of blocks finishes executing, it will be popped
|
||||
// from the stack by the sequencer, returning control to the caller.
|
||||
thread.pushStack(definition);
|
||||
// Check if the call is recursive. If so, yield.
|
||||
// @todo: Have behavior match Scratch 2.0.
|
||||
if (thread.stack.indexOf(definition) > -1) {
|
||||
thread.setStatus(Thread.STATUS_YIELD_FRAME);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Step a thread into an input reporter, and manage its status appropriately.
|
||||
* @param {!Thread} thread Thread object to step to reporter.
|
||||
* @param {!string} blockId ID of reporter block.
|
||||
* @param {!string} inputName Name of input on parent block.
|
||||
* @return {boolean} True if yielded, false if it finished immediately.
|
||||
*/
|
||||
Sequencer.prototype.stepToReporter = function (thread, blockId, inputName) {
|
||||
var currentStackFrame = thread.peekStackFrame();
|
||||
// Push to the stack to evaluate the reporter block.
|
||||
thread.pushStack(blockId);
|
||||
// Save name of input for `Thread.pushReportedValue`.
|
||||
currentStackFrame.waitingReporter = inputName;
|
||||
// Actually execute the block.
|
||||
this.startThread(thread);
|
||||
// If a reporter yielded, caller must wait for it to unyield.
|
||||
// The value will be populated once the reporter unyields,
|
||||
// and passed up to the currentStackFrame on next execution.
|
||||
return thread.status === Thread.STATUS_YIELD;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finish stepping a thread and proceed it to the next block.
|
||||
* @param {!Thread} thread Thread object to proceed.
|
||||
*/
|
||||
Sequencer.prototype.proceedThread = function (thread) {
|
||||
var currentBlockId = thread.peekStack();
|
||||
// Mark the status as done and proceed to the next block.
|
||||
// Pop from the stack - finished this level of execution.
|
||||
thread.popStack();
|
||||
// Push next connected block, if there is one.
|
||||
var nextBlockId = thread.target.blocks.getNextBlock(currentBlockId);
|
||||
if (nextBlockId) {
|
||||
thread.pushStack(nextBlockId);
|
||||
}
|
||||
// If we can't find a next block to run, mark the thread as done.
|
||||
if (!thread.peekStack()) {
|
||||
thread.setStatus(Thread.STATUS_DONE);
|
||||
// In known warp-mode threads, only yield when time is up.
|
||||
if (thread.peekStackFrame().warpMode &&
|
||||
thread.warpTimer.timeElapsed() > Sequencer.WARP_TIME) {
|
||||
thread.status = Thread.STATUS_YIELD;
|
||||
} else {
|
||||
// Look for warp-mode flag on definition, and set the thread
|
||||
// to warp-mode if needed.
|
||||
var definitionBlock = thread.target.blocks.getBlock(definition);
|
||||
var doWarp = definitionBlock.mutation.warp;
|
||||
if (doWarp) {
|
||||
thread.peekStackFrame().warpMode = true;
|
||||
} else {
|
||||
// In normal-mode threads, yield any time we have a recursive call.
|
||||
if (isRecursive) {
|
||||
thread.status = Thread.STATUS_YIELD;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -188,7 +248,7 @@ Sequencer.prototype.retireThread = function (thread) {
|
|||
thread.stack = [];
|
||||
thread.stackFrame = [];
|
||||
thread.requestScriptGlowInFrame = false;
|
||||
thread.setStatus(Thread.STATUS_DONE);
|
||||
thread.status = Thread.STATUS_DONE;
|
||||
};
|
||||
|
||||
module.exports = Sequencer;
|
||||
|
|
|
@ -40,6 +40,19 @@ function Thread (firstBlock) {
|
|||
* @type {boolean}
|
||||
*/
|
||||
this.requestScriptGlowInFrame = false;
|
||||
|
||||
/**
|
||||
* Which block ID should glow during this frame, if any.
|
||||
* @type {?string}
|
||||
*/
|
||||
this.blockGlowInFrame = null;
|
||||
|
||||
/**
|
||||
* A timer for when the thread enters warp mode.
|
||||
* Substitutes the sequencer's count toward WORK_TIME on a per-thread basis.
|
||||
* @type {?Timer}
|
||||
*/
|
||||
this.warpTimer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,25 +64,31 @@ function Thread (firstBlock) {
|
|||
Thread.STATUS_RUNNING = 0;
|
||||
|
||||
/**
|
||||
* Thread status for a yielded thread.
|
||||
* Threads are in this state when a primitive has yielded; execution is paused
|
||||
* until the relevant primitive unyields.
|
||||
* Threads are in this state when a primitive is waiting on a promise;
|
||||
* execution is paused until the promise changes thread status.
|
||||
* @const
|
||||
*/
|
||||
Thread.STATUS_YIELD = 1;
|
||||
Thread.STATUS_PROMISE_WAIT = 1;
|
||||
|
||||
/**
|
||||
* Thread status for a single-frame yield.
|
||||
* Thread status for yield.
|
||||
* @const
|
||||
*/
|
||||
Thread.STATUS_YIELD_FRAME = 2;
|
||||
Thread.STATUS_YIELD = 2;
|
||||
|
||||
/**
|
||||
* Thread status for a single-tick yield. This will be cleared when the
|
||||
* thread is resumed.
|
||||
* @const
|
||||
*/
|
||||
Thread.STATUS_YIELD_TICK = 3;
|
||||
|
||||
/**
|
||||
* Thread status for a finished/done thread.
|
||||
* Thread is in this state when there are no more blocks to execute.
|
||||
* @const
|
||||
*/
|
||||
Thread.STATUS_DONE = 3;
|
||||
Thread.STATUS_DONE = 4;
|
||||
|
||||
/**
|
||||
* Push stack and update stack frames appropriately.
|
||||
|
@ -80,7 +99,14 @@ Thread.prototype.pushStack = function (blockId) {
|
|||
// Push an empty stack frame, if we need one.
|
||||
// Might not, if we just popped the stack.
|
||||
if (this.stack.length > this.stackFrames.length) {
|
||||
// Copy warp mode from any higher level.
|
||||
var warpMode = false;
|
||||
if (this.stackFrames[this.stackFrames.length - 1]) {
|
||||
warpMode = this.stackFrames[this.stackFrames.length - 1].warpMode;
|
||||
}
|
||||
this.stackFrames.push({
|
||||
isLoop: false, // Whether this level of the stack is a loop.
|
||||
warpMode: warpMode, // Whether this level is in warp mode.
|
||||
reported: {}, // Collects reported input values.
|
||||
waitingReporter: null, // Name of waiting reporter.
|
||||
params: {}, // Procedure parameters.
|
||||
|
@ -125,22 +151,32 @@ Thread.prototype.peekParentStackFrame = function () {
|
|||
|
||||
/**
|
||||
* Push a reported value to the parent of the current stack frame.
|
||||
* @param {!Any} value Reported value to push.
|
||||
* @param {*} value Reported value to push.
|
||||
*/
|
||||
Thread.prototype.pushReportedValue = function (value) {
|
||||
var parentStackFrame = this.peekParentStackFrame();
|
||||
if (parentStackFrame) {
|
||||
var waitingReporter = parentStackFrame.waitingReporter;
|
||||
parentStackFrame.reported[waitingReporter] = value;
|
||||
parentStackFrame.waitingReporter = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a parameter to the stack frame.
|
||||
* Use when calling a procedure with parameter values.
|
||||
* @param {!string} paramName Name of parameter.
|
||||
* @param {*} value Value to set for parameter.
|
||||
*/
|
||||
Thread.prototype.pushParam = function (paramName, value) {
|
||||
var stackFrame = this.peekStackFrame();
|
||||
stackFrame.params[paramName] = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a parameter at the lowest possible level of the stack.
|
||||
* @param {!string} paramName Name of parameter.
|
||||
* @return {*} value Value for parameter.
|
||||
*/
|
||||
Thread.prototype.getParam = function (paramName) {
|
||||
for (var i = this.stackFrames.length - 1; i >= 0; i--) {
|
||||
var frame = this.stackFrames[i];
|
||||
|
@ -159,28 +195,43 @@ Thread.prototype.atStackTop = function () {
|
|||
return this.peekStack() === this.topBlock;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set thread status.
|
||||
* @param {number} status Enum representing thread status.
|
||||
* Switch the thread to the next block at the current level of the stack.
|
||||
* For example, this is used in a standard sequence of blocks,
|
||||
* where execution proceeds from one block to the next.
|
||||
*/
|
||||
Thread.prototype.setStatus = function (status) {
|
||||
this.status = status;
|
||||
Thread.prototype.goToNextBlock = function () {
|
||||
var nextBlockId = this.target.blocks.getNextBlock(this.peekStack());
|
||||
// Copy warp mode to next block.
|
||||
var warpMode = this.peekStackFrame().warpMode;
|
||||
// The current block is on the stack - pop it and push the next.
|
||||
// Note that this could push `null` - that is handled by the sequencer.
|
||||
this.popStack();
|
||||
this.pushStack(nextBlockId);
|
||||
if (this.peekStackFrame()) {
|
||||
this.peekStackFrame().warpMode = warpMode;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set thread target.
|
||||
* @param {?Target} target Target for this thread.
|
||||
* Attempt to determine whether a procedure call is recursive,
|
||||
* by examining the stack.
|
||||
* @param {!string} procedureCode Procedure code of procedure being called.
|
||||
* @return {boolean} True if the call appears recursive.
|
||||
*/
|
||||
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;
|
||||
Thread.prototype.isRecursiveCall = function (procedureCode) {
|
||||
var callCount = 5; // Max number of enclosing procedure calls to examine.
|
||||
var sp = this.stack.length - 1;
|
||||
for (var i = sp - 1; i >= 0; i--) {
|
||||
var block = this.target.blocks.getBlock(this.stack[i]);
|
||||
if (block.opcode == 'procedures_callnoreturn' &&
|
||||
block.mutation.proccode == procedureCode) {
|
||||
return true;
|
||||
}
|
||||
if (--callCount < 0) return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
module.exports = Thread;
|
||||
|
|
47
src/index.js
47
src/index.js
|
@ -66,6 +66,53 @@ VirtualMachine.prototype.greenFlag = function () {
|
|||
this.runtime.greenFlag();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether the VM is in "turbo mode."
|
||||
* When true, loops don't yield to redraw.
|
||||
* @param {Boolean} turboModeOn Whether turbo mode should be set.
|
||||
*/
|
||||
VirtualMachine.prototype.setTurboMode = function (turboModeOn) {
|
||||
this.runtime.turboMode = !!turboModeOn;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether the VM is in "pause mode."
|
||||
* When true, nothing is stepped.
|
||||
* @param {Boolean} pauseModeOn Whether pause mode should be set.
|
||||
*/
|
||||
VirtualMachine.prototype.setPauseMode = function (pauseModeOn) {
|
||||
this.runtime.setPauseMode(!!pauseModeOn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether the VM is in 2.0 "compatibility mode."
|
||||
* When true, ticks go at 2.0 speed (30 TPS).
|
||||
* @param {Boolean} compatibilityModeOn Whether compatibility mode is set.
|
||||
*/
|
||||
VirtualMachine.prototype.setCompatibilityMode = function (compatibilityModeOn) {
|
||||
this.runtime.setCompatibilityMode(!!compatibilityModeOn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set whether the VM is in "single stepping mode."
|
||||
* When true, blocks execute slowly and are highlighted visually.
|
||||
* @param {Boolean} singleSteppingOn Whether single-stepping mode is set.
|
||||
*/
|
||||
VirtualMachine.prototype.setSingleSteppingMode = function (singleSteppingOn) {
|
||||
this.runtime.setSingleSteppingMode(!!singleSteppingOn);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set single-stepping mode speed.
|
||||
* When in single-stepping mode, adjusts the speed of execution.
|
||||
* @param {Number} speed Interval length in ms.
|
||||
*/
|
||||
VirtualMachine.prototype.setSingleSteppingSpeed = function (speed) {
|
||||
this.runtime.setSingleSteppingSpeed(speed);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Stop all threads and running activities.
|
||||
*/
|
||||
|
|
|
@ -1,14 +1,35 @@
|
|||
var Timer = require('../util/timer');
|
||||
|
||||
function Clock () {
|
||||
function Clock (runtime) {
|
||||
this._projectTimer = new Timer();
|
||||
this._projectTimer.start();
|
||||
this._pausedTime = null;
|
||||
this._paused = false;
|
||||
/**
|
||||
* Reference to the owning Runtime.
|
||||
* @type{!Runtime}
|
||||
*/
|
||||
this.runtime = runtime;
|
||||
}
|
||||
|
||||
Clock.prototype.projectTimer = function () {
|
||||
if (this._paused) {
|
||||
return this._pausedTime / 1000;
|
||||
}
|
||||
return this._projectTimer.timeElapsed() / 1000;
|
||||
};
|
||||
|
||||
Clock.prototype.pause = function () {
|
||||
this._paused = true;
|
||||
this._pausedTime = this._projectTimer.timeElapsed();
|
||||
};
|
||||
|
||||
Clock.prototype.resume = function () {
|
||||
this._paused = false;
|
||||
var dt = this._projectTimer.timeElapsed() - this._pausedTime;
|
||||
this._projectTimer.startTime += dt;
|
||||
};
|
||||
|
||||
Clock.prototype.resetProjectTimer = function () {
|
||||
this._projectTimer.start();
|
||||
};
|
||||
|
|
|
@ -152,6 +152,9 @@ Clone.prototype.setXY = function (x, y) {
|
|||
this.renderer.updateDrawableProperties(this.drawableID, {
|
||||
position: [this.x, this.y]
|
||||
});
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -191,6 +194,9 @@ Clone.prototype.setDirection = function (direction) {
|
|||
direction: renderedDirectionScale.direction,
|
||||
scale: renderedDirectionScale.scale
|
||||
});
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -224,6 +230,9 @@ Clone.prototype.setVisible = function (visible) {
|
|||
this.renderer.updateDrawableProperties(this.drawableID, {
|
||||
visible: this.visible
|
||||
});
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -243,6 +252,9 @@ Clone.prototype.setSize = function (size) {
|
|||
direction: renderedDirectionScale.direction,
|
||||
scale: renderedDirectionScale.scale
|
||||
});
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -258,6 +270,9 @@ Clone.prototype.setEffect = function (effectName, value) {
|
|||
var props = {};
|
||||
props[effectName] = this.effects[effectName];
|
||||
this.renderer.updateDrawableProperties(this.drawableID, props);
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -270,6 +285,9 @@ Clone.prototype.clearEffects = function () {
|
|||
}
|
||||
if (this.renderer) {
|
||||
this.renderer.updateDrawableProperties(this.drawableID, this.effects);
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -287,6 +305,9 @@ Clone.prototype.setCostume = function (index) {
|
|||
this.renderer.updateDrawableProperties(this.drawableID, {
|
||||
skin: this.sprite.costumes[this.currentCostume].skin
|
||||
});
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -308,6 +329,9 @@ Clone.prototype.setRotationStyle = function (rotationStyle) {
|
|||
direction: renderedDirectionScale.direction,
|
||||
scale: renderedDirectionScale.scale
|
||||
});
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -339,6 +363,9 @@ Clone.prototype.updateAllDrawableProperties = function () {
|
|||
visible: this.visible,
|
||||
skin: this.sprite.costumes[this.currentCostume].skin
|
||||
});
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -552,6 +579,9 @@ Clone.prototype.dispose = function () {
|
|||
this.runtime.changeCloneCounter(-1);
|
||||
if (this.renderer && this.drawableID !== null) {
|
||||
this.renderer.destroyDrawable(this.drawableID);
|
||||
if (this.visible) {
|
||||
this.runtime.requestRedraw();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue