This block is commonly used to get exact timing in Scratch, so it is worthwhile to compile.
537 lines
15 KiB
JavaScript
537 lines
15 KiB
JavaScript
const Thread = require('../engine/thread');
|
|
const Timer = require('../util/timer');
|
|
const Cast = require('../util/cast');
|
|
const log = require('../util/log');
|
|
|
|
/* eslint-disable no-unused-vars */
|
|
|
|
const compatibilityLayerBlockUtility = require('./compat-block-utility');
|
|
|
|
|
|
/**
|
|
* The currently running thread.
|
|
* @type {Thread}
|
|
*/
|
|
let thread;
|
|
|
|
// All the functions defined here will be available to compiled scripts.
|
|
// The JSDoc annotations define the function's contract.
|
|
// Most of these functions are only used at runtime by generated scripts. Despite what your editor may say, they are not unused.
|
|
|
|
let stuckCounter = 0;
|
|
/**
|
|
* Determine whether the current tick is likely stuck.
|
|
* @returns {boolean} true if the current tick is likely stuck.
|
|
*/
|
|
const isStuck = () => {
|
|
stuckCounter++;
|
|
if (stuckCounter === 10) {
|
|
stuckCounter = 0;
|
|
return thread.target.runtime.sequencer.timer.timeElapsed() > 500;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Start hats by opcode.
|
|
* @param {string} requestedHat The opcode of the hat to start.
|
|
* @param {*} optMatchFields Fields to match.
|
|
* @returns {Array} A list of threads that were started.
|
|
*/
|
|
const startHats = (requestedHat, optMatchFields) => {
|
|
const threads = thread.target.runtime.startHats(requestedHat, optMatchFields, undefined);
|
|
return threads;
|
|
};
|
|
|
|
/**
|
|
* Implements "thread waiting", where scripts are halted until all the scripts have finished executing.
|
|
* @param {Array} threads The list of threads.
|
|
*/
|
|
const waitThreads = function*(threads) {
|
|
const runtime = thread.target.runtime;
|
|
|
|
while (true) {
|
|
// determine whether any threads are running
|
|
var anyRunning = false;
|
|
for (var i = 0; i < threads.length; i++) {
|
|
if (runtime.threads.indexOf(threads[i]) !== -1) {
|
|
anyRunning = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!anyRunning) {
|
|
// all threads are finished, can resume
|
|
return;
|
|
}
|
|
|
|
var allWaiting = true;
|
|
for (var i = 0; i < threads.length; i++) {
|
|
if (!runtime.isWaitingThread(threads[i])) {
|
|
allWaiting = false;
|
|
break;
|
|
}
|
|
}
|
|
if (allWaiting) {
|
|
thread.status = 3; // STATUS_YIELD_TICK
|
|
}
|
|
|
|
yield;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Wait until a Promise resolves or rejects before continuing.
|
|
* @param {Promise} promise The promise to wait for.
|
|
* @returns {*} the value that the promise resolves to, otherwise undefined if the promise rejects
|
|
*/
|
|
const waitPromise = function*(promise) {
|
|
// TODO: there's quite a lot going on in engine/execute.js's handlePromise. We should see how much of that matters to us
|
|
|
|
const _thread = thread; // need to store reference to current thread, as promise handlers won't be called from the tick loop
|
|
let returnValue = undefined;
|
|
|
|
promise
|
|
.then(value => {
|
|
returnValue = value;
|
|
_thread.status = Thread.STATUS_RUNNING;
|
|
})
|
|
.catch(error => {
|
|
_thread.status = Thread.STATUS_RUNNING;
|
|
log.warn('Promise rejected in compiled script:', error);
|
|
});
|
|
|
|
// enter STATUS_PROMISE_WAIT and yield, this will stop script execution until the promise handlers reset the thread status
|
|
thread.status = Thread.STATUS_PROMISE_WAIT;
|
|
yield;
|
|
|
|
return returnValue;
|
|
};
|
|
|
|
/**
|
|
* Execute a scratch-vm primitive.
|
|
* @param {*} inputs The inputs to pass to the block.
|
|
* @param {function} blockFunction The primitive's function.
|
|
* @returns {*} the value returned by the block, if any.
|
|
*/
|
|
const executeInCompatibilityLayer = function*(inputs, blockFunction) {
|
|
// reset the stackframe
|
|
// we only ever use one stackframe at a time, so this shouldn't cause issues
|
|
thread.stackFrames[thread.stackFrames.length - 1].reuse(thread.warp > 0);
|
|
|
|
const executeBlock = () => {
|
|
compatibilityLayerBlockUtility.thread = thread;
|
|
compatibilityLayerBlockUtility.sequencer = thread.target.runtime.sequencer;
|
|
return blockFunction(inputs, compatibilityLayerBlockUtility);
|
|
};
|
|
|
|
const isPromise = (value) => {
|
|
// see engine/execute.js
|
|
return (
|
|
value !== null &&
|
|
typeof value === 'object' &&
|
|
typeof value.then === 'function'
|
|
);
|
|
};
|
|
|
|
let returnValue = executeBlock();
|
|
|
|
if (isPromise(returnValue)) {
|
|
return yield* waitPromise(returnValue);
|
|
}
|
|
|
|
while (thread.status === Thread.STATUS_YIELD || thread.status === Thread.STATUS_YIELD_TICK) {
|
|
// Yielded threads will run next iteration.
|
|
if (thread.status === Thread.STATUS_YIELD) {
|
|
thread.status = Thread.STATUS_RUNNING;
|
|
// Yield back to the event loop when stuck or not in warp mode.
|
|
if (thread.warp === 0 || isStuck()) {
|
|
yield;
|
|
}
|
|
} else {
|
|
// status is STATUS_YIELD_TICK, always yield to the event loop
|
|
yield;
|
|
}
|
|
|
|
returnValue = executeBlock();
|
|
|
|
if (isPromise(returnValue)) {
|
|
return yield* waitPromise(returnValue);
|
|
}
|
|
}
|
|
|
|
// todo: do we have to do anything extra if status is STATUS_DONE?
|
|
|
|
return returnValue;
|
|
};
|
|
|
|
/**
|
|
* End the current script.
|
|
*/
|
|
const retire = () => {
|
|
thread.target.runtime.sequencer.retireThread(thread);
|
|
};
|
|
|
|
/**
|
|
* Scratch cast to boolean.
|
|
* Similar to Cast.toBoolean()
|
|
* @param {*} value The value to cast
|
|
* @returns {boolean} The value cast to a boolean
|
|
*/
|
|
const toBoolean = value => {
|
|
if (typeof value === 'boolean') {
|
|
return value;
|
|
}
|
|
if (typeof value === 'string') {
|
|
if (value === '' || value === '0' || value.toLowerCase() === 'false') {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return !!value;
|
|
};
|
|
|
|
/**
|
|
* Check if a value is considered whitespace.
|
|
* Similar to Cast.isWhiteSpace()
|
|
* @param {*} val Value to check
|
|
* @returns {boolean} true if the value is whitespace
|
|
*/
|
|
const isWhiteSpace = val => {
|
|
return val === null || (typeof val === 'string' && val.trim().length === 0);
|
|
};
|
|
|
|
/**
|
|
* Determine if two values are equal.
|
|
* @param {*} v1
|
|
* @param {*} v2
|
|
* @returns {boolean} true if v1 is equal to v2
|
|
*/
|
|
const compareEqual = (v1, v2) => {
|
|
let n1 = +v1;
|
|
let n2 = +v2;
|
|
if (n1 === 0 && isWhiteSpace(v1)) {
|
|
n1 = NaN;
|
|
} else if (n2 === 0 && isWhiteSpace(v2)) {
|
|
n2 = NaN;
|
|
}
|
|
if (isNaN(n1) || isNaN(n2)) {
|
|
const s1 = ('' + v1).toLowerCase();
|
|
const s2 = ('' + v2).toLowerCase();
|
|
return s1 === s2;
|
|
}
|
|
return n1 === n2;
|
|
};
|
|
|
|
/**
|
|
* Determine if one value is greater than another.
|
|
* @param {*} v1
|
|
* @param {*} v2
|
|
* @returns {boolean} true if v1 is greater than v2
|
|
*/
|
|
const compareGreaterThan = (v1, v2) => {
|
|
let n1 = +v1;
|
|
let n2 = +v2;
|
|
if (n1 === 0 && isWhiteSpace(v1)) {
|
|
n1 = NaN;
|
|
} else if (n2 === 0 && isWhiteSpace(v2)) {
|
|
n2 = NaN;
|
|
}
|
|
if (isNaN(n1) || isNaN(n2)) {
|
|
const s1 = ('' + v1).toLowerCase();
|
|
const s2 = ('' + v2).toLowerCase();
|
|
return s1 > s2;
|
|
}
|
|
return n1 > n2;
|
|
};
|
|
|
|
/**
|
|
* Determine if one value is less than another.
|
|
* @param {*} v1
|
|
* @param {*} v2
|
|
* @returns {boolean} true if v1 is less than v2
|
|
*/
|
|
const compareLessThan = (v1, v2) => {
|
|
let n1 = +v1;
|
|
let n2 = +v2;
|
|
if (n1 === 0 && isWhiteSpace(v1)) {
|
|
n1 = NaN;
|
|
} else if (n2 === 0 && isWhiteSpace(v2)) {
|
|
n2 = NaN;
|
|
}
|
|
if (isNaN(n1) || isNaN(n2)) {
|
|
const s1 = ('' + v1).toLowerCase();
|
|
const s2 = ('' + v2).toLowerCase();
|
|
return s1 < s2;
|
|
}
|
|
return n1 < n2;
|
|
};
|
|
|
|
/**
|
|
* Generate a random integer.
|
|
* @param {number} low Lower bound
|
|
* @param {number} high Upper bound
|
|
* @returns {number} A random integer between low and high, inclusive.
|
|
*/
|
|
const randomInt = (low, high) => {
|
|
return low + Math.floor(Math.random() * ((high + 1) - low));
|
|
};
|
|
|
|
/**
|
|
* Generate a random float.
|
|
* @param {number} low Lower bound
|
|
* @param {number} high Upper bound
|
|
* @returns {number} A random floating point number between low and high.
|
|
*/
|
|
const randomFloat = (low, high) => {
|
|
return (Math.random() * (high - low)) + low;
|
|
};
|
|
|
|
/**
|
|
* Perform an IO query
|
|
* @param {string} device The name of the device to query
|
|
* @param {string} func The function of the device to query
|
|
* @param {*} args The arguments to pass to the device
|
|
* @returns {*} The value returned by the IO device
|
|
*/
|
|
const ioQuery = (device, func, args) => {
|
|
// We will assume that the device always exists.
|
|
const devObject = thread.target.runtime.ioDevices[device];
|
|
return devObject[func].apply(devObject, args);
|
|
};
|
|
|
|
// nowObj used for timers.
|
|
const timerNowObj = {
|
|
now: () => thread.target.runtime.currentMSecs
|
|
};
|
|
|
|
/**
|
|
* Create and start a timer.
|
|
* @returns {Timer} A started timer
|
|
*/
|
|
const timer = () => {
|
|
const t = new Timer(timerNowObj);
|
|
t.start();
|
|
return t;
|
|
};
|
|
|
|
// This is the "epoch" for the daysSince2000() function.
|
|
// Storing this in a variable is faster than constantly recreating it.
|
|
const daysSince2000Epoch = new Date(2000, 0, 1);
|
|
|
|
/**
|
|
* Returns the amount of days since January 1st, 2000.
|
|
* @returns {number} Days since 2000.
|
|
*/
|
|
const daysSince2000 = () => {
|
|
const today = new Date();
|
|
const dstAdjust = today.getTimezoneOffset() - daysSince2000Epoch.getTimezoneOffset();
|
|
let mSecsSinceStart = today.valueOf() - daysSince2000Epoch.valueOf();
|
|
mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000);
|
|
return mSecsSinceStart / (24 * 60 * 60 * 1000);
|
|
};
|
|
|
|
/**
|
|
* Convert a Scratch list index to a JavaScript list index.
|
|
* "all" is not considered as a list index.
|
|
* Similar to Cast.toListIndex()
|
|
* @param {number} index Scratch list index.
|
|
* @param {number} length Length of the list.
|
|
* @returns {number} 0 based list index, or -1 if invalid.
|
|
*/
|
|
const listIndex = (index, length) => {
|
|
if (typeof index !== 'number') {
|
|
if (index === 'last') {
|
|
if (length > 0) {
|
|
return length - 1;
|
|
}
|
|
return -1;
|
|
} else if (index === 'random' || index === '*') {
|
|
if (length > 0) {
|
|
return Math.floor(Math.random() * length);
|
|
}
|
|
return -1;
|
|
}
|
|
index = +index || 0;
|
|
}
|
|
index = Math.floor(index);
|
|
if (index < 1 || index > length) {
|
|
return -1;
|
|
}
|
|
return index - 1;
|
|
};
|
|
|
|
/**
|
|
* Get a value from a list.
|
|
* @param {Array} list The list
|
|
* @param {*} idx The 1-indexed index in the list.
|
|
* @returns The list item, otherwise empty string if it does not exist.
|
|
*/
|
|
const listGet = (list, idx) => {
|
|
const index = listIndex(idx, list.length);
|
|
if (index === -1) {
|
|
return '';
|
|
}
|
|
return list[index];
|
|
};
|
|
|
|
/**
|
|
* listGet() optimized for number arguments.
|
|
* @param {Array} list The list
|
|
* @param {number} idx The 1-indexed index in the list.
|
|
* @returns The list item, otherwise empty string if it does not exist.
|
|
*/
|
|
const listGetFast = (list, idx) => {
|
|
if (idx < 1 || idx > list.length) {
|
|
return '';
|
|
}
|
|
return list[Math.floor(idx) - 1];
|
|
};
|
|
|
|
/**
|
|
* Replace a value in a list.
|
|
* @param {import('../engine/variable')} list The list
|
|
* @param {*} idx List index, Scratch style.
|
|
* @param {*} value The new value.
|
|
*/
|
|
const listReplace = (list, idx, value) => {
|
|
const index = listIndex(idx, list.value.length);
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
list.value[index] = value;
|
|
list._monitorUpToDate = false;
|
|
};
|
|
|
|
/**
|
|
* Insert a value in a list.
|
|
* @param {import('../engine/variable')} list The list.
|
|
* @param {*} idx The Scratch index in the list.
|
|
* @param {*} value The value to insert.
|
|
*/
|
|
const listInsert = (list, idx, value) => {
|
|
const index = listIndex(idx, list.value.length + 1);
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
list.value.splice(index, 0, value);
|
|
list._monitorUpToDate = false;
|
|
};
|
|
|
|
/**
|
|
* Delete a value from a list.
|
|
* @param {import('../engine/variable')} list The list.
|
|
* @param {*} idx The Scratch index in the list.
|
|
*/
|
|
const listDelete = (list, idx) => {
|
|
if (idx === 'all') {
|
|
list.value = [];
|
|
return;
|
|
}
|
|
const index = listIndex(idx, list.value.length);
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
list.value.splice(index, 1);
|
|
list._monitorUpToDate = false;
|
|
};
|
|
|
|
/**
|
|
* Return whether a list contains a value.
|
|
* @param {import('../engine/variable')} list The list.
|
|
* @param {*} item The value to search for.
|
|
* @returns {boolean} True if the list contains the item
|
|
*/
|
|
const listContains = (list, item) => {
|
|
// TODO: evaluate whether indexOf is worthwhile here
|
|
if (list.value.indexOf(item) !== -1) {
|
|
return true;
|
|
}
|
|
for (let i = 0; i < list.value.length; i++) {
|
|
if (compareEqual(list.value[i], item)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Find the 1-indexed index of an item in a list.
|
|
* @param {import('../engine/variable')} list The list.
|
|
* @param {*} item The item to search for
|
|
* @returns {number} The 1-indexed index of the item in the list, otherwise 0
|
|
*/
|
|
const listIndexOf = (list, item) => {
|
|
for (var i = 0; i < list.value.length; i++) {
|
|
if (compareEqual(list.value[i], item)) {
|
|
return i + 1;
|
|
}
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
/**
|
|
* Get the stringified form of a list.
|
|
* @param {import('../engine/variable')} list The list.
|
|
* @returns {string} Stringified form of the list.
|
|
*/
|
|
const listContents = list => {
|
|
for (let i = 0; i < list.value.length; i++) {
|
|
const listItem = list.value[i];
|
|
// this is an intentional break from what scratch 3 does to address our automatic string -> number conversions
|
|
// it fixes more than it breaks
|
|
if ((listItem + '').length !== 1) {
|
|
return list.value.join(' ');
|
|
}
|
|
}
|
|
return list.value.join('');
|
|
};
|
|
|
|
/**
|
|
* Convert a color to an RGB list
|
|
* @param {*} color The color value to convert
|
|
* @return {Array.<number>} [r,g,b], values between 0-255.
|
|
*/
|
|
const colorToList = color => {
|
|
// TODO: remove Cast dependency
|
|
return Cast.toRgbColorList(color);
|
|
};
|
|
|
|
/**
|
|
* Implements Scratch modulo (floored division instead of truncated division)
|
|
* @param {number} n
|
|
* @param {number} modulus
|
|
* @returns {number}
|
|
*/
|
|
const mod = (n, modulus) => {
|
|
let result = n % modulus;
|
|
if (result / modulus < 0) result += modulus;
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Step a compiled thread.
|
|
* @param {Thread} _thread
|
|
*/
|
|
const execute = (_thread) => {
|
|
thread = _thread;
|
|
_thread.generator.next();
|
|
};
|
|
|
|
/**
|
|
* eval() some JS
|
|
* @param {string} source
|
|
*/
|
|
const scopedEval = source => {
|
|
try {
|
|
// eslint-disable-next-line no-eval
|
|
return eval(source);
|
|
} catch (e) {
|
|
console.error('was unable to compile script', source);
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
execute.scopedEval = scopedEval;
|
|
|
|
module.exports = execute;
|