diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index b3be41e19..104652438 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -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); diff --git a/core/src/frame_lifecycle.rs b/core/src/frame_lifecycle.rs index 016869f1e..4bcb822e5 100644 --- a/core/src/frame_lifecycle.rs +++ b/core/src/frame_lifecycle.rs @@ -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. /// diff --git a/tests/tests/swfs/avm2/goto_in_constructframe/Main.as b/tests/tests/swfs/avm2/goto_in_constructframe/Main.as new file mode 100644 index 000000000..8c048a214 --- /dev/null +++ b/tests/tests/swfs/avm2/goto_in_constructframe/Main.as @@ -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"); + } + } + } + +} diff --git a/tests/tests/swfs/avm2/goto_in_constructframe/MyChild.as b/tests/tests/swfs/avm2/goto_in_constructframe/MyChild.as new file mode 100644 index 000000000..1ecf9e0d8 --- /dev/null +++ b/tests/tests/swfs/avm2/goto_in_constructframe/MyChild.as @@ -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"); + } + } + +} diff --git a/tests/tests/swfs/avm2/goto_in_constructframe/OtherChild.as b/tests/tests/swfs/avm2/goto_in_constructframe/OtherChild.as new file mode 100644 index 000000000..e763c05cb --- /dev/null +++ b/tests/tests/swfs/avm2/goto_in_constructframe/OtherChild.as @@ -0,0 +1,14 @@ +package { + + import flash.display.MovieClip; + + + public class OtherChild extends MovieClip { + + + public function OtherChild() { + trace("Constructed OtherChild") + } + } + +} diff --git a/tests/tests/swfs/avm2/goto_in_constructframe/Test.as b/tests/tests/swfs/avm2/goto_in_constructframe/Test.as new file mode 100644 index 000000000..400e66292 --- /dev/null +++ b/tests/tests/swfs/avm2/goto_in_constructframe/Test.as @@ -0,0 +1,5 @@ +package { + public class Test { + + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/goto_in_constructframe/output.txt b/tests/tests/swfs/avm2/goto_in_constructframe/output.txt new file mode 100644 index 000000000..c7d355d34 --- /dev/null +++ b/tests/tests/swfs/avm2/goto_in_constructframe/output.txt @@ -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 diff --git a/tests/tests/swfs/avm2/goto_in_constructframe/test.fla b/tests/tests/swfs/avm2/goto_in_constructframe/test.fla new file mode 100644 index 000000000..7554d3d97 Binary files /dev/null and b/tests/tests/swfs/avm2/goto_in_constructframe/test.fla differ diff --git a/tests/tests/swfs/avm2/goto_in_constructframe/test.swf b/tests/tests/swfs/avm2/goto_in_constructframe/test.swf new file mode 100644 index 000000000..cf7981afe Binary files /dev/null and b/tests/tests/swfs/avm2/goto_in_constructframe/test.swf differ diff --git a/tests/tests/swfs/avm2/goto_in_constructframe/test.toml b/tests/tests/swfs/avm2/goto_in_constructframe/test.toml new file mode 100644 index 000000000..ded2e363a --- /dev/null +++ b/tests/tests/swfs/avm2/goto_in_constructframe/test.toml @@ -0,0 +1 @@ +num_frames = 2 diff --git a/tests/tests/swfs/avm2/goto_nested_framescript/Test.as b/tests/tests/swfs/avm2/goto_nested_framescript/Test.as new file mode 100644 index 000000000..a0cadc546 --- /dev/null +++ b/tests/tests/swfs/avm2/goto_nested_framescript/Test.as @@ -0,0 +1,5 @@ +package { + public class Test { + public static var frame3Count = 0; + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/goto_nested_framescript/output.txt b/tests/tests/swfs/avm2/goto_nested_framescript/output.txt new file mode 100644 index 000000000..8ca03f4c2 --- /dev/null +++ b/tests/tests/swfs/avm2/goto_nested_framescript/output.txt @@ -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 diff --git a/tests/tests/swfs/avm2/goto_nested_framescript/test.fla b/tests/tests/swfs/avm2/goto_nested_framescript/test.fla new file mode 100644 index 000000000..e6f2e5500 Binary files /dev/null and b/tests/tests/swfs/avm2/goto_nested_framescript/test.fla differ diff --git a/tests/tests/swfs/avm2/goto_nested_framescript/test.swf b/tests/tests/swfs/avm2/goto_nested_framescript/test.swf new file mode 100644 index 000000000..131a8f01f Binary files /dev/null and b/tests/tests/swfs/avm2/goto_nested_framescript/test.swf differ diff --git a/tests/tests/swfs/avm2/goto_nested_framescript/test.toml b/tests/tests/swfs/avm2/goto_nested_framescript/test.toml new file mode 100644 index 000000000..dbee897f5 --- /dev/null +++ b/tests/tests/swfs/avm2/goto_nested_framescript/test.toml @@ -0,0 +1 @@ +num_frames = 1 diff --git a/tests/tests/swfs/avm2/goto_on_orphan/Main.as b/tests/tests/swfs/avm2/goto_on_orphan/Main.as new file mode 100644 index 000000000..e72538b96 --- /dev/null +++ b/tests/tests/swfs/avm2/goto_on_orphan/Main.as @@ -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)); + } + } + } + +} diff --git a/tests/tests/swfs/avm2/goto_on_orphan/output.txt b/tests/tests/swfs/avm2/goto_on_orphan/output.txt new file mode 100644 index 000000000..66978b33e --- /dev/null +++ b/tests/tests/swfs/avm2/goto_on_orphan/output.txt @@ -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 diff --git a/tests/tests/swfs/avm2/goto_on_orphan/test.fla b/tests/tests/swfs/avm2/goto_on_orphan/test.fla new file mode 100644 index 000000000..50054c7d6 Binary files /dev/null and b/tests/tests/swfs/avm2/goto_on_orphan/test.fla differ diff --git a/tests/tests/swfs/avm2/goto_on_orphan/test.swf b/tests/tests/swfs/avm2/goto_on_orphan/test.swf new file mode 100644 index 000000000..461072f34 Binary files /dev/null and b/tests/tests/swfs/avm2/goto_on_orphan/test.swf differ diff --git a/tests/tests/swfs/avm2/goto_on_orphan/test.toml b/tests/tests/swfs/avm2/goto_on_orphan/test.toml new file mode 100644 index 000000000..ded2e363a --- /dev/null +++ b/tests/tests/swfs/avm2/goto_on_orphan/test.toml @@ -0,0 +1 @@ +num_frames = 2