diff --git a/core/src/avm2/globals/flash/display/display_object.rs b/core/src/avm2/globals/flash/display/display_object.rs index ca70d41b1da7..d62581d1b974 100644 --- a/core/src/avm2/globals/flash/display/display_object.rs +++ b/core/src/avm2/globals/flash/display/display_object.rs @@ -37,7 +37,7 @@ pub fn initialize_for_allocator<'gc>( ) -> Object<'gc> { let obj = StageObject::for_display_object(context.gc(), dobj, class); dobj.set_placed_by_avm2_script(true); - dobj.set_object2(context, obj); + dobj.set_object2(context.gc(), obj); // [NA] Should these run for everything? dobj.post_instantiation(context, None, Instantiator::Avm2, false); @@ -51,6 +51,14 @@ pub fn initialize_for_allocator<'gc>( dobj.base().set_skip_next_enter_frame(true); dobj.on_construction_complete(context); + // All MovieClips constructed from ActionScript start out as orphans. If, + // at the end of a frame, this MovieClip is no longer an orphan, it will + // automatically be removed from the orphan list by + // `OrphanManager::cleanup_dead_orphans`. + if let Some(movie_clip) = dobj.as_movie_clip() { + context.orphan_manager.add_orphan_obj(movie_clip.into()); + } + obj.into() } diff --git a/core/src/avm2/globals/flash/display/simple_button.rs b/core/src/avm2/globals/flash/display/simple_button.rs index 5e5cc363aff3..51c8a8297ac3 100644 --- a/core/src/avm2/globals/flash/display/simple_button.rs +++ b/core/src/avm2/globals/flash/display/simple_button.rs @@ -34,7 +34,7 @@ pub fn simple_button_allocator<'gc>( button.post_instantiation(activation.context, None, Instantiator::Avm2, false); let display_object = button.into(); let obj = StageObject::for_display_object(activation.gc(), display_object, orig_class); - display_object.set_object2(activation.context, obj); + display_object.set_object2(activation.gc(), obj); return Ok(obj.into()); } diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 3ed5ac9b7710..67063741f035 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -1852,14 +1852,14 @@ pub trait TDisplayObject<'gc>: /// Retrieve the parent of this display object. /// /// This version of the function implements the concept of parenthood as - /// seen in AVM1. Notably, it disallows access to the `Stage` and to - /// non-AVM1 DisplayObjects; for an unfiltered concept of parent, - /// use the `parent` method. + /// seen in AVM1. Notably, it disallows access to the `Stage` and to a + /// `LoaderDisplay`; for an unfiltered concept of parent, use the `parent` + /// method. #[no_dynamic] fn avm1_parent(self) -> Option> { self.parent() .filter(|p| p.as_stage().is_none()) - .filter(|p| !p.movie().is_action_script_3()) + .filter(|p| p.as_loader_display().is_none()) } /// Retrieve the parent of this display object. @@ -2229,6 +2229,7 @@ pub trait TDisplayObject<'gc>: fn set_has_explicit_name(self, value: bool) { self.base().set_has_explicit_name(value); } + fn state(&self) -> Option { None } @@ -2251,6 +2252,20 @@ pub trait TDisplayObject<'gc>: /// as properties on the class fn construct_frame(self, _context: &mut UpdateContext<'gc>) {} + /// Whether this DisplayObject is an AVM2 orphan object. Objects that are + /// no longer AVM2 orphans (e.g. they have been adopted) will be + /// automatically removed from the global orphan list by + /// `OrphanManager::cleanup_dead_orphans`. + /// + /// There are two ways a DisplayObject can become an AVM2 orphan: + /// 1 - The clip has no parent + /// 2 - The clip has a parent, but the parent is from an AVM1 movie + #[no_dynamic] + fn is_avm2_orphan(self) -> bool { + self.parent() + .is_none_or(|p| !p.movie().is_action_script_3()) + } + /// To be called when an AVM2 display object has finished being constructed. /// /// This function must be called once and ONLY once, after the object's @@ -2532,7 +2547,7 @@ pub trait TDisplayObject<'gc>: fn object2(self) -> Option>; - fn set_object2(self, _context: &mut UpdateContext<'gc>, _to: Avm2StageObject<'gc>) {} + fn set_object2(self, _mc: &Mutation<'gc>, _to: Avm2StageObject<'gc>) {} #[no_dynamic] fn object2_or_null(self) -> Avm2Value<'gc> { @@ -2850,6 +2865,7 @@ impl<'gc> DisplayObject<'gc> { pub fn as_morph_shape for MorphShape; pub fn as_video for Video; pub fn as_bitmap for Bitmap; + pub fn as_loader_display for LoaderDisplay; } pub fn as_interactive(self) -> Option> { diff --git a/core/src/display_object/avm2_button.rs b/core/src/display_object/avm2_button.rs index 30ecc76f353c..7d9e8032f53f 100644 --- a/core/src/display_object/avm2_button.rs +++ b/core/src/display_object/avm2_button.rs @@ -658,8 +658,8 @@ impl<'gc> TDisplayObject<'gc> for Avm2Button<'gc> { self.0.object.get() } - fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) { - let write = Gc::write(context.gc(), self.0); + fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) { + let write = Gc::write(mc, self.0); unlock!(write, Avm2ButtonData, object).set(Some(to)); } diff --git a/core/src/display_object/bitmap.rs b/core/src/display_object/bitmap.rs index cafde0eefe10..9b92d7ec6b20 100644 --- a/core/src/display_object/bitmap.rs +++ b/core/src/display_object/bitmap.rs @@ -391,8 +391,8 @@ impl<'gc> TDisplayObject<'gc> for Bitmap<'gc> { self.0.avm2_object.get() } - fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) { - self.set_avm2_object(context.gc(), Some(to)); + fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) { + self.set_avm2_object(mc, Some(to)); } fn movie(self) -> Arc { diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index 91a557506a1a..5805ef6661d0 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -2574,8 +2574,8 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { self.0.object.get().and_then(|o| o.as_avm2_object()) } - fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) { - self.set_object(Some(to.into()), context.gc()); + fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) { + self.set_object(Some(to.into()), mc); } fn self_bounds(self) -> Rectangle { diff --git a/core/src/display_object/graphic.rs b/core/src/display_object/graphic.rs index be263fbbbb2b..b8a49aecd0bc 100644 --- a/core/src/display_object/graphic.rs +++ b/core/src/display_object/graphic.rs @@ -158,7 +158,7 @@ impl<'gc> TDisplayObject<'gc> for Graphic<'gc> { self.into(), class_object, ) { - Ok(object) => self.set_object2(activation.context, object), + Ok(object) => self.set_object2(activation.gc(), object), Err(e) => { tracing::error!("Got error when constructing AVM2 side of shape: {}", e) } @@ -249,8 +249,7 @@ impl<'gc> TDisplayObject<'gc> for Graphic<'gc> { self.0.avm2_object.get() } - fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) { - let mc = context.gc(); + fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) { unlock!(Gc::write(mc, self.0), GraphicData, avm2_object).set(Some(to)); } diff --git a/core/src/display_object/loader_display.rs b/core/src/display_object/loader_display.rs index 2d2c14bfc72a..c503865e0855 100644 --- a/core/src/display_object/loader_display.rs +++ b/core/src/display_object/loader_display.rs @@ -94,8 +94,7 @@ impl<'gc> TDisplayObject<'gc> for LoaderDisplay<'gc> { self.0.avm2_object.get() } - fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) { - let mc = context.gc(); + fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) { unlock!(Gc::write(mc, self.0), LoaderDisplayData, avm2_object).set(Some(to)) } diff --git a/core/src/display_object/morph_shape.rs b/core/src/display_object/morph_shape.rs index c0322efecc46..1dedc7d6939b 100644 --- a/core/src/display_object/morph_shape.rs +++ b/core/src/display_object/morph_shape.rs @@ -90,8 +90,7 @@ impl<'gc> TDisplayObject<'gc> for MorphShape<'gc> { self.0.object.get() } - fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) { - let mc = context.gc(); + fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) { unlock!(Gc::write(mc, self.0), MorphShapeData, object).set(Some(to)) } @@ -103,7 +102,7 @@ impl<'gc> TDisplayObject<'gc> for MorphShape<'gc> { // We don't need to call the initializer method, as AVM2 can't link // a custom class to a MorphShape, and the initializer method for // MorphShape itself is a no-op - self.set_object2(context, object); + self.set_object2(context.gc(), object); self.on_construction_complete(context); } diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index afdf5edab426..6fa800ae7acb 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -187,8 +187,6 @@ pub struct MovieClipData<'gc> { attached_audio: Lock>>, /// The next MovieClip in the AVM1 execution list. - /// - /// `None` in an AVM2 movie. next_avm1_clip: Lock>>, audio_stream: Cell>, @@ -421,8 +419,8 @@ impl<'gc> MovieClip<'gc> { /// Execute all other timeline actions on this object. pub fn run_frame_avm1(self, context: &mut UpdateContext<'gc>) { - if !self.movie().is_action_script_3() { - // Run my load/enterFrame clip event. + // Run my load/enterFrame clip event. + if !self.movie().is_action_script_3() || !context.root_swf.is_action_script_3() { let is_load_frame = !self.0.contains_flag(MovieClipFlags::INITIALIZED); if is_load_frame { self.event_dispatch(context, ClipEvent::Load); @@ -1995,7 +1993,11 @@ impl<'gc> MovieClip<'gc> { let object = Avm2StageObject::for_display_object(context.gc(), display_object, class_object); - self.set_object2(context, object); + self.set_object2(context.gc(), object); + + if self.is_avm2_orphan() { + context.orphan_manager.add_orphan_obj(self.into()); + } } /// Construct the AVM2 side of this object. @@ -2032,7 +2034,10 @@ impl<'gc> MovieClip<'gc> { let class_object = context.avm2.classes().avm1movie; let object = Avm2StageObject::for_display_object(context.gc(), self.into(), class_object); - self.set_object2(context, object); + self.set_object2(context.gc(), object); + + // No need to mark `self` as an orphan, as it's about to be adopted by + // the AVM2 `Loader` object } pub fn register_frame_script( @@ -2602,7 +2607,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { self.set_default_instance_name(context); - if !self.movie().is_action_script_3() { + if !self.movie().is_action_script_3() || !context.root_swf.is_action_script_3() { context.avm1.add_to_exec_list(context.gc(), self); self.construct_as_avm1_object(context, init_object, instantiated_by, run_frame); @@ -2617,12 +2622,9 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { self.0.object2.get() } - fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) { - let write = Gc::write(context.gc(), self.0); + fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) { + let write = Gc::write(mc, self.0); unlock!(write, MovieClipData, object2).set(Some(to)); - if self.parent().is_none() { - context.orphan_manager.add_orphan_obj(self.into()); - } } fn set_perspective_projection(self, mut perspective_projection: Option) { diff --git a/core/src/display_object/text.rs b/core/src/display_object/text.rs index 7f51f0646b01..352afe442651 100644 --- a/core/src/display_object/text.rs +++ b/core/src/display_object/text.rs @@ -267,7 +267,7 @@ impl<'gc> TDisplayObject<'gc> for Text<'gc> { // We don't need to call the initializer method, as AVM2 can't link // a custom class to a StaticText, and the initializer method for // StaticText itself is a no-op - self.set_object2(context, object); + self.set_object2(context.gc(), object); self.on_construction_complete(context); } @@ -281,8 +281,7 @@ impl<'gc> TDisplayObject<'gc> for Text<'gc> { self.0.avm2_object.get() } - fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) { - let mc = context.gc(); + fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) { unlock!(Gc::write(mc, self.0), TextData, avm2_object).set(Some(to)); } } diff --git a/core/src/display_object/video.rs b/core/src/display_object/video.rs index d5b1e953655b..3ec42c45b073 100644 --- a/core/src/display_object/video.rs +++ b/core/src/display_object/video.rs @@ -179,8 +179,7 @@ impl<'gc> Video<'gc> { )) } - fn set_object(&self, context: &mut UpdateContext<'gc>, to: AvmObject<'gc>) { - let mc = context.gc(); + fn set_object(&self, mc: &Mutation<'gc>, to: AvmObject<'gc>) { unlock!(Gc::write(mc, self.0), VideoData, object).set(Some(to)); } @@ -432,7 +431,7 @@ impl<'gc> TDisplayObject<'gc> for Video<'gc> { Some(context.avm1.prototypes(self.swf_version()).video), Avm1NativeObject::Video(self), ); - self.set_object(context, object.into()); + self.set_object(context.gc(), object.into()); } self.seek(context, starting_seek); @@ -448,7 +447,7 @@ impl<'gc> TDisplayObject<'gc> for Video<'gc> { // itself only sets the size of the Video- the Video already has the // correct size at this point. - self.set_object2(context, object); + self.set_object2(context.gc(), object); self.on_construction_complete(context); } @@ -552,8 +551,8 @@ impl<'gc> TDisplayObject<'gc> for Video<'gc> { self.0.object.get().and_then(|o| o.as_avm2_object()) } - fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) { - self.set_object(context, to.into()); + fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) { + self.set_object(mc, to.into()); } fn avm1_text_field_bindings(&self) -> Option]>> { diff --git a/core/src/frame_lifecycle.rs b/core/src/frame_lifecycle.rs index a53f7b16c924..1563bad3bebb 100644 --- a/core/src/frame_lifecycle.rs +++ b/core/src/frame_lifecycle.rs @@ -72,32 +72,41 @@ pub enum FramePhase { pub fn run_all_phases_avm2(context: &mut UpdateContext<'_>) { let stage = context.stage; - if !stage.movie().is_action_script_3() { - return; - } + // We may still have AVM2 orphans that we need to run frames for even though + // the root movie is AVM1. However, we don't run any stage phases if the + // root movie is AVM1. + let is_root_movie_as3 = stage.movie().is_action_script_3(); *context.frame_phase = FramePhase::Enter; OrphanManager::each_orphan_obj(context, |orphan, context| { orphan.enter_frame(context); }); - stage.enter_frame(context); + if is_root_movie_as3 { + stage.enter_frame(context); + } *context.frame_phase = FramePhase::Construct; OrphanManager::each_orphan_obj(context, |orphan, context| { orphan.construct_frame(context); }); - stage.construct_frame(context); - stage.frame_constructed(context); + if is_root_movie_as3 { + stage.construct_frame(context); + stage.frame_constructed(context); + } *context.frame_phase = FramePhase::FrameScripts; OrphanManager::each_orphan_obj(context, |orphan, context| { orphan.run_frame_scripts(context); }); - stage.run_frame_scripts(context); + if is_root_movie_as3 { + stage.run_frame_scripts(context); + } MovieClip::run_frame_script_cleanup(context); *context.frame_phase = FramePhase::Exit; - stage.exit_frame(context); + if is_root_movie_as3 { + 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. diff --git a/core/src/orphan_manager.rs b/core/src/orphan_manager.rs index 181879037ec1..8c8eac7b4cd9 100644 --- a/core/src/orphan_manager.rs +++ b/core/src/orphan_manager.rs @@ -65,6 +65,10 @@ impl<'gc> OrphanManager<'gc> { pub fn cleanup_dead_orphans(&mut self, mc: &Mutation<'gc>) { self.orphans_mut().retain(|d| { if let Some(dobj) = valid_orphan(*d, mc) { + let has_avm1_parent = dobj + .parent() + .is_some_and(|p| !p.movie().is_action_script_3()); + // All clips that become orphaned (have their parent removed, or start out with no parent) // get added to the orphan list. However, there's a distinction between clips // that are removed from a RemoveObject tag, and clips that are removed from ActionScript. @@ -78,14 +82,18 @@ impl<'gc> OrphanManager<'gc> { // indefinitely (if there are no remaining strong references, they will eventually // be garbage collected). // - // To detect this, we check 'placed_by_avm2_script'. This flag get set to 'true' + // To detect this, we check 'placed_by_script'. This flag get set to 'true' // for objects constructed from ActionScript, and for objects moved around // in the timeline (add/remove child, swap depths) by ActionScript. A // RemoveObject tag will only affect objects instantiated by the timeline, // which have not been moved in the displaylist by ActionScript. Therefore, - // any orphan we see that has 'placed_by_avm2_script()' should stay on the orphan + // any orphan we see that has 'placed_by_script()' should stay on the orphan // list, because it was not removed by a RemoveObject tag. - dobj.placed_by_avm2_script() + // + // Also, we consider AVM2 MovieClips loaded into an AVM1 parent to always + // be orphans, since we know they were placed by AVM1 code and not by + // the timeline. + dobj.placed_by_avm2_script() || has_avm1_parent } else { false } @@ -109,7 +117,7 @@ fn valid_orphan<'gc>( mc: &Mutation<'gc>, ) -> Option> { if let Some(dobj) = dobj.upgrade(mc) { - if dobj.parent().is_none() { + if dobj.is_avm2_orphan() { return Some(dobj); } } diff --git a/tests/tests/swfs/avm1/mixed_avm/avm2.swf b/tests/tests/swfs/avm1/mixed_avm/avm2.swf new file mode 100644 index 000000000000..2e79cdcb3414 Binary files /dev/null and b/tests/tests/swfs/avm1/mixed_avm/avm2.swf differ diff --git a/tests/tests/swfs/avm1/mixed_avm/output.txt b/tests/tests/swfs/avm1/mixed_avm/output.txt new file mode 100644 index 000000000000..769224495587 --- /dev/null +++ b/tests/tests/swfs/avm1/mixed_avm/output.txt @@ -0,0 +1,5 @@ +_level0.target +undefined +_level0.target.abc +_level0.target.abc +_level0.target.abc diff --git a/tests/tests/swfs/avm1/mixed_avm/test.swf b/tests/tests/swfs/avm1/mixed_avm/test.swf new file mode 100644 index 000000000000..3b777e03edbb Binary files /dev/null and b/tests/tests/swfs/avm1/mixed_avm/test.swf differ diff --git a/tests/tests/swfs/avm1/mixed_avm/test.toml b/tests/tests/swfs/avm1/mixed_avm/test.toml new file mode 100644 index 000000000000..1e15fdf27598 --- /dev/null +++ b/tests/tests/swfs/avm1/mixed_avm/test.toml @@ -0,0 +1 @@ +num_frames = 5