avm2: Run 'nested goto frame' for entire Stage and orphans

When we run a 'goto', a weird "nested frame" gets triggered.
Previously, we were only calling `construct_frame` on the target
MovieClip as part of this "nested frame". However, Flash Player
seems to treat this (in some ways) like a normal frame - *all*
objects on the Stage (and orphans) have `construct_frame` called.

In particular, `gotoAndStop`/`gotoAndPlay` is called during
an "enterFrame" event handler, then unrelated objects on the Stage
will have their children constructed during the execution of
`gotoAndStop`/`gotoAndPlay`. The same logic holds for frame scripts.

This fixes a bug in Steamlands, which relies on children on the main
timeline being constructed immediately following a call to `gotoAndStop`
on an orphan (originally triggered from an "enterFrame" handler).
This commit is contained in:
Aaron Hill 2023-05-28 21:40:34 -05:00
parent 650f872b19
commit 59b219cda6
20 changed files with 207 additions and 18 deletions

View File

@ -9,6 +9,7 @@ use crate::avm2::{
};
use crate::backend::audio::{SoundHandle, SoundInstanceHandle};
use crate::backend::ui::MouseCursor;
use crate::frame_lifecycle::run_inner_goto_frame;
use bitflags::bitflags;
use crate::avm1::Avm1;
@ -1020,13 +1021,7 @@ impl<'gc> MovieClip<'gc> {
}
} else if context.is_action_script_3() {
// Pretend we actually did a goto, but don't do anything.
self.construct_frame(context);
self.frame_constructed(context);
self.avm2_root(context)
.unwrap_or_else(|| self.into())
.run_frame_scripts(context);
self.exit_frame(context);
run_inner_goto_frame(context, &[]);
}
}
@ -1948,17 +1943,7 @@ impl<'gc> MovieClip<'gc> {
//
// Our queued place tags will now run at this time, too.
if !is_implicit {
self.construct_frame(context);
self.frame_constructed(context);
self.avm2_root(context)
.unwrap_or_else(|| self.into())
.run_frame_scripts(context);
for child in removed_frame_scripts {
child.run_frame_scripts(context);
}
self.exit_frame(context);
run_inner_goto_frame(context, &removed_frame_scripts);
}
self.assert_expected_tag_end(context, hit_target_frame);

View File

@ -106,6 +106,56 @@ pub fn run_all_phases_avm2(context: &mut UpdateContext<'_, '_>) {
*context.frame_phase = FramePhase::Idle;
}
/// Like `run_all_phases_avm2`, but specialized for the "nested frame" triggered
/// by a goto. This is different enough to not be worth combining into a single
/// method with extra parameters.
///
/// During a goto, we run frame construction, framescripts, and frame exits for the *entire stage*.
/// This even extends to orphans - for example, calling `gotoAndStop` on an orphan will
/// cause frame construction to get run for the *current frame* of other objects on the timeline
/// (even if the goto was called from an enterFrame event handler).
pub fn run_inner_goto_frame<'gc>(
context: &mut UpdateContext<'_, 'gc>,
removed_frame_scripts: &[DisplayObject<'gc>],
) {
let stage = context.stage;
let old_phase = *context.frame_phase;
// Note - we do *not* call `enter_frame` or dispatch an `enterFrame` event
*context.frame_phase = FramePhase::Construct;
Avm2::each_orphan_obj(context, |orphan, context| {
orphan.construct_frame(context);
});
stage.construct_frame(context);
stage.frame_constructed(context);
*context.frame_phase = FramePhase::FrameScripts;
stage.run_frame_scripts(context);
Avm2::each_orphan_obj(context, |orphan, context| {
orphan.run_frame_scripts(context);
});
for child in removed_frame_scripts {
child.run_frame_scripts(context);
}
*context.frame_phase = FramePhase::Exit;
Avm2::each_orphan_obj(context, |orphan, context| {
orphan.on_exit_frame(context);
});
stage.exit_frame(context);
// We cannot easily remove dead `GcWeak` instances from the orphan list
// inside `each_orphan_movie`, since the callback may modify the orphan list.
// Instead, we do one cleanup at the end of the frame.
// This performs special handling of clips which became orphaned as
// a result of a RemoveObject tag - see `cleanup_dead_orphans` for details.
Avm2::cleanup_dead_orphans(context);
*context.frame_phase = old_phase;
}
/// Run all previously-executed frame phases on a newly-constructed display
/// object.
///

View File

@ -0,0 +1,29 @@
package {
import flash.display.MovieClip;
import flash.display.DisplayObject;
import flash.events.Event;
public class Main extends MovieClip {
private var runIt = true;
public var myOtherChild:DisplayObject;
public function Main() {
this.addEventListener(Event.ENTER_FRAME, this.onEnterFrame);
}
private function onEnterFrame(event: Event) {
if (this.runIt) {
this.runIt = false;
trace("Enter frame!");
this.gotoAndStop(3);
trace("Enter frame done");
}
}
}
}

View File

@ -0,0 +1,24 @@
package {
import flash.display.MovieClip;
import flash.events.Event;
public class MyChild extends MovieClip {
public function MyChild() {
trace("Constructed MyChild");
this.addEventListener(Event.ADDED, this.onAdded);
}
private function onAdded(event: Event) {
trace("In MyChild.onAdded - this.parent.getChildAt(1) = " + this.parent.getChildAt(1));
trace("Child added! Running this.parent.gotoAndStop(3)");
MovieClip(this.parent).gotoAndStop(3);
trace("Child done");
}
}
}

View File

@ -0,0 +1,14 @@
package {
import flash.display.MovieClip;
public class OtherChild extends MovieClip {
public function OtherChild() {
trace("Constructed OtherChild")
}
}
}

View File

@ -0,0 +1,5 @@
package {
public class Test {
}
}

View File

@ -0,0 +1,12 @@
Frame 1
Enter frame!
Constructed MyChild
In MyChild.onAdded - this.parent.getChildAt(1) = null
Child added! Running this.parent.gotoAndStop(3)
Constructed OtherChild
Num children: 2
Frame 3: this.getChildAt(1) = myOtherChild this.myOtherChild = [object OtherChild]
Frame 3 - running this.gotoAndStop(2)
Frame 3 - ran gotoAndStop
Child done
Enter frame done

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_frames = 2

View File

@ -0,0 +1,5 @@
package {
public class Test {
public static var frame3Count = 0;
}
}

View File

@ -0,0 +1,9 @@
In frame 1 - running this.gotoAndStop(2) - this.getChildAt(0)['text'] = Frame 1 text object
Frame 1 - finished this.gotoAndStop(2)
In frame 2 - running this.gotoAndStop(3) - this.getChildAt(0)['text'] = Frame 2 text object
Frame 2 - finished this.gotoAndStop(3)
In frame 3 - running goto - this.getChildAt(0)['text'] = Frame 3 text object
Frame 3 - Finished gotoAndStop

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_frames = 1

View File

@ -0,0 +1,38 @@
package {
import flash.display.MovieClip;
import flash.events.Event;
public class Main extends MovieClip {
public var runIt = true;
public var myOrphan = new MyOrphan();
public function Main() {
var self = this;
this.addEventListener(Event.ENTER_FRAME, function(e) {
trace("Main - enterFrame in frame " + self.currentFrame);
if (self.currentFrame == 2 && self.runIt) {
self.runIt = false;
self.dumpChildren();
trace("Running self.myOrphan.gotoAndStop(3)");
self.myOrphan.gotoAndStop(3);
trace("Finished self.myOrphan.gotoAndStop(3)");
self.dumpChildren();
}
});
this.addEventListener(Event.FRAME_CONSTRUCTED, function(e) {
trace("Main - frameConstructed");
});
}
private function dumpChildren() {
trace("Main Children: " + this.numChildren);
for (var i = 0; i < this.numChildren; i++) {
trace(i + ": " + this.getChildAt(i));
}
}
}
}

View File

@ -0,0 +1,15 @@
Main - frameConstructed
Orphan frame exectuion
Main - enterFrame in frame 2
Main Children: 2
0: null
1: null
Running self.myOrphan.gotoAndStop(3)
Main - frameConstructed
Main framescript 2
Orphan frame 3
Finished self.myOrphan.gotoAndStop(3)
Main Children: 2
0: [object Shape]
1: [object TextField]
Main - frameConstructed

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_frames = 2