export default function ({ scaffolding }) { // This file is based on the `pause` addon from Scratch Addons // All major portions of this file were written by me, so the GPL does not apply const STATUS_PROMISE_WAIT = 1; const STATUS_DONE = 4; const vm = scaffolding.vm; let paused = false; let pausedThreadState = new WeakMap(); let audioContextStateChange = Promise.resolve(); const setPaused = (_paused) => { paused = _paused; if (paused) { audioContextStateChange = audioContextStateChange.then(() => { return vm.runtime.audioEngine.audioContext.suspend(); }); if (!vm.runtime.ioDevices.clock._paused) { vm.runtime.ioDevices.clock.pause(); } for (const thread of vm.runtime.threads) { if (!thread.updateMonitor && !pausedThreadState.has(thread)) { const pauseState = { pauseTime: vm.runtime.currentMSecs, status: thread.status, }; pausedThreadState.set(thread, pauseState); thread.status = STATUS_PROMISE_WAIT; } } // Immediately emit project stop // Scratch will do this automatically, but there may be a slight delay. vm.runtime.emit("PROJECT_RUN_STOP"); } else { audioContextStateChange = audioContextStateChange.then(() => { return vm.runtime.audioEngine.audioContext.resume(); }); vm.runtime.ioDevices.clock.resume(); const now = Date.now(); for (const thread of vm.runtime.threads) { const pauseState = pausedThreadState.get(thread); if (pauseState) { const stackFrame = thread.peekStackFrame(); if (stackFrame && stackFrame.executionContext && stackFrame.executionContext.timer) { const dt = now - pauseState.pauseTime; stackFrame.executionContext.timer.startTime += dt; } // Compiler state is stored differently if (thread.timer) { const dt = now - pauseState.pauseTime; thread.timer.startTime += dt; } thread.status = pauseState.status; } } pausedThreadState = new WeakMap(); } vm.emit('P4_PAUSE', paused); }; const ensurePausedThreadIsStillPaused = (thread) => { if (thread.status === STATUS_DONE) { return; } const pauseState = pausedThreadState.get(thread); if (pauseState) { if (thread.status !== STATUS_PROMISE_WAIT) { // We'll record the change so we can properly resume the thread, but the thread must still be paused for now. pauseState.status = thread.status; thread.status = STATUS_PROMISE_WAIT; } } }; const originalStepThreads = vm.runtime.sequencer.stepThreads; vm.runtime.sequencer.stepThreads = function () { if (paused) { for (const thread of this.runtime.threads) { ensurePausedThreadIsStillPaused(thread); } } return originalStepThreads.call(this); }; const originalGreenFlag = vm.runtime.greenFlag; vm.runtime.greenFlag = function () { setPaused(false); return originalGreenFlag.call(this); }; // Disable edge-activated hats and hats like "when key pressed" while paused. const originalStartHats = vm.runtime.startHats; vm.runtime.startHats = function (...args) { if (paused) { return []; } return originalStartHats.apply(this, args); }; // Paused threads should not be counted as running when updating GUI state. const originalGetMonitorThreadCount = vm.runtime._getMonitorThreadCount; vm.runtime._getMonitorThreadCount = function (threads) { let count = originalGetMonitorThreadCount.call(this, threads); if (paused) { for (const thread of threads) { if (pausedThreadState.has(thread)) { count++; } } } return count; }; vm.setPaused = setPaused; vm.isPaused = () => paused; }