Skip to content

Fix warped threads' event threads not inheriting their parents' warp state/timer #4035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: warped threads' events inherit warp state
  • Loading branch information
Sanae6 committed Jul 6, 2023
commit bea81b41f36d803b66157c1d2d5fb1680631950e
38 changes: 5 additions & 33 deletions src/blocks/scratch3_event.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,40 +96,12 @@ class Scratch3EventBlocks {
}
if (util.stackFrame.broadcastVar) {
const broadcastOption = util.stackFrame.broadcastVar.name;
// Have we run before, starting threads?
if (!util.stackFrame.startedThreads) {
// No - start hats for this broadcast.
util.stackFrame.startedThreads = util.startHats(
'event_whenbroadcastreceived', {
BROADCAST_OPTION: broadcastOption
}
);
if (util.stackFrame.startedThreads.length === 0) {
// Nothing was started.
return;
}
}
// We've run before; check if the wait is still going on.
const instance = this;
// Scratch 2 considers threads to be waiting if they are still in
// runtime.threads. Threads that have run all their blocks, or are
// marked done but still in runtime.threads are still considered to
// be waiting.
const waiting = util.stackFrame.startedThreads
.some(thread => instance.runtime.threads.indexOf(thread) !== -1);
if (waiting) {
// If all threads are waiting for the next tick or later yield
// for a tick as well. Otherwise yield until the next loop of
// the threads.
if (
util.stackFrame.startedThreads
.every(thread => instance.runtime.isWaitingThread(thread))
) {
util.yieldTick();
} else {
util.yield();

util.startHatsAndWait(
'event_whenbroadcastreceived', {
BROADCAST_OPTION: broadcastOption
}
}
);
}
}
}
Expand Down
62 changes: 21 additions & 41 deletions src/blocks/scratch3_looks.js
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,9 @@ class Scratch3LooksBlocks {
* @param {!Target} stage Target to set backdrop to.
* @param {Any} requestedBackdrop Backdrop requested, e.g., 0, 'name', etc.
* @param {boolean=} optZeroIndex Set to zero-index the requestedBackdrop.
* @return {Array.<!Thread>} Any threads started by this switch.
* @param {boolean=} optSkipEvent Skips firing the backdrop changed event.
*/
_setBackdrop (stage, requestedBackdrop, optZeroIndex) {
_setBackdrop (stage, requestedBackdrop, optZeroIndex, optSkipEvent) {
if (typeof requestedBackdrop === 'number') {
// Numbers should be treated as backdrop indices, always
stage.setCostume(optZeroIndex ? requestedBackdrop : requestedBackdrop - 1);
Expand Down Expand Up @@ -455,10 +455,12 @@ class Scratch3LooksBlocks {
}
}

const newName = stage.getCostumes()[stage.currentCostume].name;
return this.runtime.startHats('event_whenbackdropswitchesto', {
BACKDROP: newName
});
if (!optSkipEvent) {
const newName = stage.getCostumes()[stage.currentCostume].name;
this.runtime.startHats('event_whenbackdropswitchesto', {
BACKDROP: newName
});
}
}

switchCostume (args, util) {
Expand All @@ -476,41 +478,19 @@ class Scratch3LooksBlocks {
}

switchBackdropAndWait (args, util) {
// Have we run before, starting threads?
if (!util.stackFrame.startedThreads) {
// No - switch the backdrop.
util.stackFrame.startedThreads = (
this._setBackdrop(
this.runtime.getTargetForStage(),
args.BACKDROP
)
);
if (util.stackFrame.startedThreads.length === 0) {
// Nothing was started.
return;
}
}
// We've run before; check if the wait is still going on.
const instance = this;
// Scratch 2 considers threads to be waiting if they are still in
// runtime.threads. Threads that have run all their blocks, or are
// marked done but still in runtime.threads are still considered to
// be waiting.
const waiting = util.stackFrame.startedThreads
.some(thread => instance.runtime.threads.indexOf(thread) !== -1);
if (waiting) {
// If all threads are waiting for the next tick or later yield
// for a tick as well. Otherwise yield until the next loop of
// the threads.
if (
util.stackFrame.startedThreads
.every(thread => instance.runtime.isWaitingThread(thread))
) {
util.yieldTick();
} else {
util.yield();
}
}
const stage = this.runtime.getTargetForStage();

this._setBackdrop(
stage,
args.BACKDROP,
false,
true // Skip firing event so we can wait on it.
);

const newName = stage.getCostumes()[stage.currentCostume].name;
util.startHatsAndWait('event_whenbackdropswitchesto', {
BACKDROP: newName
});
}

nextBackdrop () {
Expand Down
74 changes: 74 additions & 0 deletions src/engine/block-utility.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,80 @@ class BlockUtility {
return result;
}

/**
* Start all relevant hats, and wait for their completion in warp mode.
* @param {!string} requestedHat Opcode of hats to start.
* @param {object=} optMatchFields Optionally, fields to match on the hat.
* @param {Target=} optTarget Optionally, a target to restrict to.
*/
startHatsAndWait (requestedHat, optMatchFields, optTarget) {
// Have we run before, starting threads?
if (!this.stackFrame.startedThreads) {
// No - start threads.
this.stackFrame.startedThreads = this.startHats(
requestedHat, optMatchFields, optTarget
);
if (this.stackFrame.startedThreads.length === 0) {
// Nothing was started.
return;
}
}

// We've run before; check if the wait is still going on.
// Scratch 2 considers threads to be waiting if they are still in
// runtime.threads. Threads that have run all their blocks, or are
// marked done but still in runtime.threads are still considered to
// be waiting.
if (
this.stackFrame.startedThreads
.every(thread => this.runtime.threads.indexOf(thread) === -1)
) {
return;
}

if (this.thread.peekStackFrame().warpMode) {
const threads = this.stackFrame.startedThreads;

// Make threads inherit warpMode and warpTimer if they haven't already.
for (let i = 0; i < threads.length; i++) {
if (!threads[i].warpTimer) {
threads[i].peekStackFrame().warpMode = true;
threads[i].warpTimer = this.thread.warpTimer;
}
}

// Workaround for cyclic dependency chain introduced by importing Sequencer
const warpTime = this.sequencer.constructor.WARP_TIME;
// Don't step the threads once the timer is up.
if (this.thread.warpTimer.timeElapsed() <= warpTime) {
// Step threads one by one in warp mode. They will execute until completion, or until
// the inherited warpTimer is up.
for (let i = 0; i < threads.length; i++) {
this.sequencer.stepThread(threads[i]);
}

// End the call when all threads are completed.
if (threads.every(thread => thread.status === Thread.STATUS_DONE)) {
return;
}
}

// If we still need to execute the threads, and the warp timer is up, yield.
}

// If all threads are waiting for the next tick or later yield
// for a tick as well. Otherwise yield until the next loop of
// the threads.
if (
this.stackFrame.startedThreads
.every(thread => this.runtime.isWaitingThread(thread))
) {
this.yieldTick();
} else {
this.yield();
}
}

/**
* Query a named IO device.
* @param {string} device The name of like the device, like keyboard.
Expand Down