From 49d1a985cae454d900c599af9c8c6f53304703e3 Mon Sep 17 00:00:00 2001 From: Aaron Hill Date: Mon, 27 Jun 2022 22:56:14 -0500 Subject: [PATCH] avm2: Store `LoaderInfo` object on `MovieClip` and `Stage` Previously, we would create a fresh `LoaderInfo` object each time the `loaderInfo` property was accessed. However, users can add event handlers to a `LoaderInfo`, so we need to create and store exactly one `LoaderInfo` object per movie (and stage). To verify that we're correctly handling the storage of `LoaderInfo`, I've implemented firing the "init" event. This required a new `on_frame_exit` hook, so that we can properly fire the "init" event after the "exitFrame" for the initial frame but before the "enterFrame" of the next frame. --- core/src/avm1/activation.rs | 4 +- core/src/avm1/globals/movie_clip.rs | 2 +- core/src/avm1/globals/movie_clip_loader.rs | 2 +- .../globals/flash/display/displayobject.rs | 21 +-- core/src/avm2/object.rs | 3 + core/src/avm2/object/loaderinfo_object.rs | 37 +++-- core/src/display_object.rs | 14 ++ core/src/display_object/movie_clip.rs | 132 ++++++++++++------ core/src/display_object/stage.rs | 16 ++- core/src/loader.rs | 4 +- core/src/player.rs | 5 +- tests/tests/regression_tests.rs | 1 + .../tests/swfs/avm2/loaderinfo_events/Main.as | 31 ++++ .../swfs/avm2/loaderinfo_events/output.txt | 6 + .../swfs/avm2/loaderinfo_events/test.fla | Bin 0 -> 3868 bytes .../swfs/avm2/loaderinfo_events/test.swf | Bin 0 -> 654 bytes 16 files changed, 198 insertions(+), 80 deletions(-) create mode 100644 tests/tests/swfs/avm2/loaderinfo_events/Main.as create mode 100644 tests/tests/swfs/avm2/loaderinfo_events/output.txt create mode 100644 tests/tests/swfs/avm2/loaderinfo_events/test.fla create mode 100644 tests/tests/swfs/avm2/loaderinfo_events/test.swf diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index 824120c42..2d0f5bced 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -1148,7 +1148,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { if url.is_empty() { //Blank URL on movie loads = unload! if let Some(mut mc) = level.as_movie_clip() { - mc.replace_with_movie(self.context.gc_context, None) + mc.replace_with_movie(&mut self.context, None) } } else { let future = self.context.load_manager.load_movie_into_clip( @@ -1264,7 +1264,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> { if url.is_empty() { // Blank URL on movie loads = unload! if let Some(mut mc) = clip_target.as_movie_clip() { - mc.replace_with_movie(self.context.gc_context, None) + mc.replace_with_movie(&mut self.context, None) } } else { let request = self.locals_into_request( diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index c1125d498..8e3a4507c 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -1352,7 +1352,7 @@ fn unload_movie<'gc>( _args: &[Value<'gc>], ) -> Result, Error<'gc>> { target.unload(&mut activation.context); - target.replace_with_movie(activation.context.gc_context, None); + target.replace_with_movie(&mut activation.context, None); Ok(Value::Undefined) } diff --git a/core/src/avm1/globals/movie_clip_loader.rs b/core/src/avm1/globals/movie_clip_loader.rs index e5f3b53f2..f164954f6 100644 --- a/core/src/avm1/globals/movie_clip_loader.rs +++ b/core/src/avm1/globals/movie_clip_loader.rs @@ -99,7 +99,7 @@ fn unload_clip<'gc>( if let Some(target) = target { target.unload(&mut activation.context); if let Some(mut mc) = target.as_movie_clip() { - mc.replace_with_movie(activation.context.gc_context, None); + mc.replace_with_movie(&mut activation.context, None); } return Ok(true.into()); } diff --git a/core/src/avm2/globals/flash/display/displayobject.rs b/core/src/avm2/globals/flash/display/displayobject.rs index 08e846bba..c56ab2319 100644 --- a/core/src/avm2/globals/flash/display/displayobject.rs +++ b/core/src/avm2/globals/flash/display/displayobject.rs @@ -4,11 +4,11 @@ use crate::avm2::activation::Activation; use crate::avm2::class::Class; use crate::avm2::method::{Method, NativeMethodImpl}; use crate::avm2::names::{Namespace, QName}; -use crate::avm2::object::{stage_allocator, LoaderInfoObject, Object, TObject}; +use crate::avm2::object::{stage_allocator, Object, TObject}; use crate::avm2::value::Value; use crate::avm2::ArrayObject; use crate::avm2::Error; -use crate::display_object::{DisplayObject, HitTestOptions, TDisplayObject}; +use crate::display_object::{HitTestOptions, TDisplayObject}; use crate::types::{Degrees, Percent}; use crate::vminterface::Instantiator; use gc_arena::{GcCell, MutationContext}; @@ -574,26 +574,15 @@ pub fn hit_test_object<'gc>( /// Implements `loaderInfo` getter pub fn loader_info<'gc>( - activation: &mut Activation<'_, 'gc, '_>, + _activation: &mut Activation<'_, 'gc, '_>, this: Option>, _args: &[Value<'gc>], ) -> Result, Error> { if let Some(dobj) = this.and_then(|this| this.as_display_object()) { - if let Some(root) = dobj.avm2_root(&mut activation.context) { - let movie = dobj.movie(); - - if let Some(movie) = movie { - let obj = LoaderInfoObject::from_movie(activation, movie, root)?; - - return Ok(obj.into()); - } - } - - if DisplayObject::ptr_eq(dobj, activation.context.stage.into()) { - return Ok(LoaderInfoObject::from_stage(activation)?.into()); + if let Some(loader_info) = dobj.loader_info() { + return Ok(loader_info.into()); } } - Ok(Value::Undefined) } diff --git a/core/src/avm2/object.rs b/core/src/avm2/object.rs index 194c15bab..bff864a8b 100644 --- a/core/src/avm2/object.rs +++ b/core/src/avm2/object.rs @@ -16,6 +16,7 @@ use crate::avm2::vtable::{ClassBoundMethod, VTable}; use crate::avm2::Error; use crate::backend::audio::{SoundHandle, SoundInstanceHandle}; use crate::bitmap::bitmap_data::BitmapData; +use crate::context::UpdateContext; use crate::display_object::DisplayObject; use crate::html::TextFormat; use crate::string::AvmString; @@ -1003,6 +1004,8 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy None } + fn loader_stream_init(&self, _context: &mut UpdateContext<'_, 'gc, '_>) {} + /// Unwrap this object's loader stream fn as_loader_stream(&self) -> Option>> { None diff --git a/core/src/avm2/object/loaderinfo_object.rs b/core/src/avm2/object/loaderinfo_object.rs index 7462a862e..305283217 100644 --- a/core/src/avm2/object/loaderinfo_object.rs +++ b/core/src/avm2/object/loaderinfo_object.rs @@ -4,9 +4,11 @@ use crate::avm2::activation::Activation; use crate::avm2::object::script_object::ScriptObjectData; use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject}; use crate::avm2::value::Value; +use crate::avm2::Avm2; use crate::avm2::Error; +use crate::avm2::{Event, EventData}; +use crate::context::UpdateContext; use crate::display_object::DisplayObject; -use crate::string::AvmString; use crate::tag_utils::SwfMovie; use gc_arena::{Collect, GcCell, MutationContext}; use std::cell::{Ref, RefMut}; @@ -24,6 +26,7 @@ pub fn loaderinfo_allocator<'gc>( LoaderInfoObjectData { base, loaded_stream: None, + init_fired: false, }, )) .into()) @@ -60,6 +63,9 @@ pub struct LoaderInfoObjectData<'gc> { /// The loaded stream that this gets it's info from. loaded_stream: Option>, + + /// Whether or not we've fired an 'init' event + init_fired: bool, } impl<'gc> LoaderInfoObject<'gc> { @@ -78,6 +84,7 @@ impl<'gc> LoaderInfoObject<'gc> { LoaderInfoObjectData { base, loaded_stream, + init_fired: false, }, )) .into(); @@ -98,6 +105,9 @@ impl<'gc> LoaderInfoObject<'gc> { LoaderInfoObjectData { base, loaded_stream: Some(LoaderStream::Stage), + // We never want to fire an "init" event for the special + // Stagee loaderInfo + init_fired: true, }, )) .into(); @@ -122,14 +132,23 @@ impl<'gc> TObject<'gc> for LoaderInfoObject<'gc> { self.0.as_ptr() as *const ObjectPtr } - fn value_of(&self, mc: MutationContext<'gc, '_>) -> Result, Error> { - if let Some(class) = self.instance_of_class_definition() { - Ok( - AvmString::new_utf8(mc, format!("[object {}]", class.read().name().local_name())) - .into(), - ) - } else { - Ok("[object Object]".into()) + fn value_of(&self, _mc: MutationContext<'gc, '_>) -> Result, Error> { + Ok(Value::Object((*self).into())) + } + + fn loader_stream_init(&self, context: &mut UpdateContext<'_, 'gc, '_>) { + if !self.0.read().init_fired { + self.0.write(context.gc_context).init_fired = true; + let mut init_evt = Event::new("init", EventData::Empty); + init_evt.set_bubbles(false); + init_evt.set_cancelable(false); + + if let Err(e) = Avm2::dispatch_event(context, init_evt, (*self).into()) { + log::error!( + "Encountered AVM2 error when broadcasting `init` event: {}", + e + ); + } } } diff --git a/core/src/display_object.rs b/core/src/display_object.rs index f0b0b5701..535b2130f 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -1165,6 +1165,16 @@ pub trait TDisplayObject<'gc>: e ); } + + self.on_exit_frame(context); + } + + fn on_exit_frame(&self, context: &mut UpdateContext<'_, 'gc, '_>) { + if let Some(container) = self.as_container() { + for child in container.iter_render_list() { + child.on_exit_frame(context); + } + } } fn render_self(&self, _context: &mut RenderContext<'_, 'gc>) {} @@ -1338,6 +1348,10 @@ pub trait TDisplayObject<'gc>: self.parent().and_then(|p| p.movie()) } + fn loader_info(&self) -> Option> { + None + } + fn instantiate(&self, gc_context: MutationContext<'gc, '_>) -> DisplayObject<'gc>; fn as_ptr(&self) -> *const DisplayObjectPtr; diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 45461207a..382920848 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -2,6 +2,7 @@ use crate::avm1::{ Avm1, Object as Avm1Object, StageObject, TObject as Avm1TObject, Value as Avm1Value, }; +use crate::avm2::object::LoaderInfoObject; use crate::avm2::Activation as Avm2Activation; use crate::avm2::{ Avm2, ClassObject as Avm2ClassObject, Error as Avm2Error, Namespace as Avm2Namespace, @@ -205,15 +206,20 @@ impl<'gc> MovieClip<'gc> { } /// Construct a movie clip that represents an entire movie. - pub fn from_movie(gc_context: MutationContext<'gc, '_>, movie: Arc) -> Self { + pub fn from_movie(context: &mut UpdateContext<'_, 'gc, '_>, movie: Arc) -> Self { let num_frames = movie.num_frames(); let mc = MovieClip(GcCell::allocate( - gc_context, + context.gc_context, MovieClipData { base: Default::default(), static_data: Gc::allocate( - gc_context, - MovieClipStatic::with_data(0, movie.into(), num_frames, gc_context), + context.gc_context, + MovieClipStatic::with_data( + 0, + movie.clone().into(), + num_frames, + context.gc_context, + ), ), tag_stream_pos: 0, current_frame: 0, @@ -237,23 +243,64 @@ impl<'gc> MovieClip<'gc> { drop_target: None, }, )); - mc.set_is_root(gc_context, true); + mc.set_is_root(context.gc_context, true); + mc.set_loader_info(context, movie); mc } - /// Replace the current MovieClip with a completely new SwfMovie. + /// Replace the current MovieClipData with a completely new SwfMovie. /// /// Playback will start at position zero, any existing streamed audio will - /// be terminated, and so on. Children and AVM data will be kept across the - /// load boundary. + /// be terminated, and so on. Children and AVM data will NOT be kept across + /// the load boundary. + /// + /// If no movie is provided, then the movie clip will be replaced with an + /// empty movie of the same SWF version. pub fn replace_with_movie( &mut self, - gc_context: MutationContext<'gc, '_>, + context: &mut UpdateContext<'_, 'gc, '_>, movie: Option>, ) { - self.0 - .write(gc_context) - .replace_with_movie(gc_context, movie) + let mut mc = self.0.write(context.gc_context); + let is_swf = movie.is_some(); + let movie = movie.unwrap_or_else(|| Arc::new(SwfMovie::empty(mc.movie().version()))); + let total_frames = movie.num_frames(); + mc.base.base.reset_for_movie_load(); + mc.static_data = Gc::allocate( + context.gc_context, + MovieClipStatic::with_data(0, movie.clone().into(), total_frames, context.gc_context), + ); + mc.tag_stream_pos = 0; + mc.flags = MovieClipFlags::PLAYING; + mc.base.base.set_is_root(is_swf); + mc.current_frame = 0; + mc.audio_stream = None; + mc.container = ChildContainer::new(); + drop(mc); + + self.set_loader_info(context, movie); + } + + fn set_loader_info(&self, context: &mut UpdateContext<'_, 'gc, '_>, movie: Arc) { + if movie.avm_type() == AvmType::Avm2 { + let gc_context = context.gc_context; + let mc = self.0.write(gc_context); + if mc.base.base.is_root() { + let mut activation = Avm2Activation::from_nothing(context.reborrow()); + match LoaderInfoObject::from_movie(&mut activation, movie, (*self).into()) { + Ok(loader_info) => { + *mc.static_data.loader_info.write(gc_context) = Some(loader_info); + } + Err(e) => { + log::error!( + "Error contructing LoaderInfoObject for movie {:?}: {:?}", + mc, + e + ); + } + } + } + } } pub fn preload(self, context: &mut UpdateContext<'_, 'gc, '_>) { @@ -1771,9 +1818,8 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { // we can't fire the events until we run our first frame, so we // have to actually check if we've just built the root and act // like it just got added to the timeline. - let root = self.avm2_root(context); let self_dobj: DisplayObject<'gc> = (*self).into(); - if DisplayObject::option_ptr_eq(Some(self_dobj), root) { + if self_dobj.is_root() { dispatch_added_event_only(self_dobj, context); dispatch_added_to_stage_event_only(self_dobj, context); } @@ -1783,7 +1829,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { fn run_frame(&self, context: &mut UpdateContext<'_, 'gc, '_>) { // Run my load/enterFrame clip event. - let is_load_frame = !self.0.read().initialized(); + let is_load_frame = !self.0.read().flags.contains(MovieClipFlags::INITIALIZED); if is_load_frame { self.event_dispatch(context, ClipEvent::Load); self.0.write(context.gc_context).set_initialized(true); @@ -1866,6 +1912,23 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { } } + fn on_exit_frame(&self, context: &mut UpdateContext<'_, 'gc, '_>) { + // Attempt to fire an "init" event on our `LoaderInfo`. + // This fires after we've exited our first frame, but before + // but before we enter a new frame. `loader_strean_init` + // keeps track if an "init" event has already been fired, + // so this becomes a no-op after the event has been fired. + if self.0.read().initialized() { + if let Some(loader_info) = self.loader_info() { + loader_info.loader_stream_init(context); + } + } + + for child in self.iter_render_list() { + child.on_exit_frame(context); + } + } + fn render_self(&self, context: &mut RenderContext<'_, 'gc>) { self.0.read().drawing.render(context); self.render_children(context); @@ -2017,6 +2080,10 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { self.set_removed(context.gc_context, true); } + fn loader_info(&self) -> Option> { + *self.0.read().static_data.loader_info.read() + } + fn allow_as_mask(&self) -> bool { !self.is_empty() } @@ -2225,36 +2292,6 @@ impl<'gc> TInteractiveObject<'gc> for MovieClip<'gc> { } impl<'gc> MovieClipData<'gc> { - /// Replace the current MovieClipData with a completely new SwfMovie. - /// - /// Playback will start at position zero, any existing streamed audio will - /// be terminated, and so on. Children and AVM data will NOT be kept across - /// the load boundary. - /// - /// If no movie is provided, then the movie clip will be replaced with an - /// empty movie of the same SWF version. - pub fn replace_with_movie( - &mut self, - gc_context: MutationContext<'gc, '_>, - movie: Option>, - ) { - let is_swf = movie.is_some(); - let movie = movie.unwrap_or_else(|| Arc::new(SwfMovie::empty(self.movie().version()))); - let total_frames = movie.num_frames(); - - self.base.base.reset_for_movie_load(); - self.static_data = Gc::allocate( - gc_context, - MovieClipStatic::with_data(0, movie.into(), total_frames, gc_context), - ); - self.tag_stream_pos = 0; - self.flags = MovieClipFlags::PLAYING; - self.base.base.set_is_root(is_swf); - self.current_frame = 0; - self.audio_stream = None; - self.container = ChildContainer::new(); - } - fn id(&self) -> CharacterId { self.static_data.id } @@ -3283,6 +3320,10 @@ struct MovieClipStatic<'gc> { /// The last known symbol name under which this movie clip was exported. /// Used for looking up constructors registered with `Object.registerClass`. exported_name: GcCell<'gc, Option>>, + /// Only set if this MovieClip is the root movie in an SWF + /// (either the root SWF initially loaded by the player, + /// or an SWF dynamically loaded by `Loader`) + loader_info: GcCell<'gc, Option>>, } impl<'gc> MovieClipStatic<'gc> { @@ -3305,6 +3346,7 @@ impl<'gc> MovieClipStatic<'gc> { audio_stream_info: None, audio_stream_handle: None, exported_name: GcCell::allocate(gc_context, None), + loader_info: GcCell::allocate(gc_context, None), } } } diff --git a/core/src/display_object/stage.rs b/core/src/display_object/stage.rs index abdbb0c49..06b2d9740 100644 --- a/core/src/display_object/stage.rs +++ b/core/src/display_object/stage.rs @@ -1,6 +1,7 @@ //! Root stage impl use crate::avm1::Object as Avm1Object; +use crate::avm2::object::LoaderInfoObject; use crate::avm2::{ Activation as Avm2Activation, Event as Avm2Event, EventData as Avm2EventData, Object as Avm2Object, ScriptObject as Avm2ScriptObject, StageObject as Avm2StageObject, @@ -105,6 +106,9 @@ pub struct StageData<'gc> { /// The AVM2 view of this stage object. avm2_object: Avm2Object<'gc>, + + /// The AVM2 'LoaderInfo' object for this stage object + loader_info: Avm2Object<'gc>, } impl<'gc> Stage<'gc> { @@ -138,6 +142,7 @@ impl<'gc> Stage<'gc> { window_mode: Default::default(), show_menu: true, avm2_object: Avm2ScriptObject::custom_object(gc_context, None, None), + loader_info: Avm2ScriptObject::custom_object(gc_context, None, None), }, )); stage.set_is_root(gc_context, true); @@ -671,7 +676,12 @@ impl<'gc> TDisplayObject<'gc> for Stage<'gc> { match avm2_stage { Ok(avm2_stage) => { - self.0.write(activation.context.gc_context).avm2_object = avm2_stage.into() + let mut write = self.0.write(activation.context.gc_context); + write.avm2_object = avm2_stage.into(); + match LoaderInfoObject::from_stage(&mut activation) { + Ok(loader_info) => write.loader_info = loader_info, + Err(e) => log::error!("Unable to set AVM2 Stage loaderInfo: {}", e), + } } Err(e) => log::error!("Unable to construct AVM2 Stage: {}", e), } @@ -729,6 +739,10 @@ impl<'gc> TDisplayObject<'gc> for Stage<'gc> { fn object2(&self) -> Avm2Value<'gc> { self.0.read().avm2_object.into() } + + fn loader_info(&self) -> Option> { + Some(self.0.read().loader_info) + } } impl<'gc> TDisplayObjectContainer<'gc> for Stage<'gc> { diff --git a/core/src/loader.rs b/core/src/loader.rs index c6688ca95..6b31d74dc 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -457,7 +457,7 @@ impl<'gc> Loader<'gc> { if let Some(mut mc) = clip.as_movie_clip() { mc.unload(uc); - mc.replace_with_movie(uc.gc_context, None); + mc.replace_with_movie(uc, None); } Loader::movie_loader_start(handle, uc) @@ -499,7 +499,7 @@ impl<'gc> Loader<'gc> { .set_avm2_domain(domain); if let Some(mut mc) = clip.as_movie_clip() { - mc.replace_with_movie(uc.gc_context, Some(movie)); + mc.replace_with_movie(uc, Some(movie)); mc.post_instantiation(uc, None, Instantiator::Movie, false); mc.preload(uc); } diff --git a/core/src/player.rs b/core/src/player.rs index 6094d64bc..464763d69 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -294,8 +294,7 @@ impl Player { .set_avm2_domain(domain); context.ui.set_mouse_visible(true); - let root: DisplayObject = - MovieClip::from_movie(context.gc_context, context.swf.clone()).into(); + let root: DisplayObject = MovieClip::from_movie(context, context.swf.clone()).into(); root.set_depth(context.gc_context, 0); let flashvars = if !context.swf.parameters().is_empty() { @@ -1922,7 +1921,7 @@ impl PlayerBuilder { let mut player_lock = player.lock().unwrap(); player_lock.mutate_with_update_context(|context| { // Instantiate an empty root before the main movie loads. - let fake_root = MovieClip::from_movie(context.gc_context, fake_movie); + let fake_root = MovieClip::from_movie(context, fake_movie); fake_root.post_instantiation(context, None, Instantiator::Movie, false); context.stage.replace_at_depth(context, fake_root.into(), 0); Avm2::load_player_globals(context).expect("Unable to load AVM2 globals"); diff --git a/tests/tests/regression_tests.rs b/tests/tests/regression_tests.rs index 9867ef3fe..918a1e00e 100644 --- a/tests/tests/regression_tests.rs +++ b/tests/tests/regression_tests.rs @@ -332,6 +332,7 @@ swf_tests! { (as3_lazyinit, "avm2/lazyinit", 1), (as3_lessequals, "avm2/lessequals", 1), (as3_lessthan, "avm2/lessthan", 1), + (as3_loaderinfo_events, "avm2/loaderinfo_events", 2), (as3_loaderinfo_properties, "avm2/loaderinfo_properties", 2), (as3_loaderinfo_quine, "avm2/loaderinfo_quine", 2), (as3_lshift, "avm2/lshift", 1), diff --git a/tests/tests/swfs/avm2/loaderinfo_events/Main.as b/tests/tests/swfs/avm2/loaderinfo_events/Main.as new file mode 100644 index 000000000..7963f7abf --- /dev/null +++ b/tests/tests/swfs/avm2/loaderinfo_events/Main.as @@ -0,0 +1,31 @@ + package { + import flash.display.MovieClip; + public class Main extends MovieClip { + + public function Main() { + loaderInfo.addEventListener("init", function() { + trace("Called init!"); + }); + this.addEventListener("addedToStage", function() { + trace("Called addedToStage"); + }); + this.addEventListener("added", function() { + trace("Called added"); + }); + + var clip = this; + this.addEventListener("enterFrame", function() { + trace("Called enterFrame"); + }); + + this.addEventListener("exitFrame", function() { + trace("Called exitFrame"); + }); + + stage.loaderInfo.addEventListener("init", function() { + trace("ERROR: Stage loaderInfo should not fire 'init'"); + }); + trace("Called constructor"); + } + } +} \ No newline at end of file diff --git a/tests/tests/swfs/avm2/loaderinfo_events/output.txt b/tests/tests/swfs/avm2/loaderinfo_events/output.txt new file mode 100644 index 000000000..ba45b16c5 --- /dev/null +++ b/tests/tests/swfs/avm2/loaderinfo_events/output.txt @@ -0,0 +1,6 @@ +Called constructor +Called added +Called addedToStage +Called exitFrame +Called init! +Called enterFrame diff --git a/tests/tests/swfs/avm2/loaderinfo_events/test.fla b/tests/tests/swfs/avm2/loaderinfo_events/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..a2829536fcbbbf00214d6960e01e1affaa3a103a GIT binary patch literal 3868 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 literal 0 HcmV?d00001 diff --git a/tests/tests/swfs/avm2/loaderinfo_events/test.swf b/tests/tests/swfs/avm2/loaderinfo_events/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..ff70b5840c69c20f350ef6b37809738c3d10bdc2 GIT binary patch literal 654 zcmV;90&)FAS5qt90{{ScoOP1jPTMdP$8G1sP1BZ^Lirwcr}&JddWfzUEW zok|WN)FyX#cbnzwmHJNoV!Q4&pMMK^XZYqt`{exM<-zNBUB7Lk+eH&pi$N7baS*!cu;~ZsRp@>-j^a_k+F@|@?Bnh!XDLsoWBO%~ zp1PyUr>9};`YZ{eL43wWEaK_jo;axKrW#LNj}_W(7&6}qqJTfHP7~Mnng2dM=I+IP z!Gu+&2^_?dgT%dL}f1e)a!7giL0PiJ>4WU=CvyBLk3; zS2Ft$5W%Kd`mK-YC#8|=DR`Va6g#Ih57ZFU1ggwv5olh}5>RzU=Yg7nR)K0WS_4`T zbP1?FqpLuRg02Cj3NecusFv9$Pk!3Ja0zLUbkSIM)JD05dm4ddBWujJh>QzaquNqr zTxK}?eu>AGtg+BiC0ooIi!DvYD~>)8$LKO$2kj-Hy>W;z6%OSH7_yz&u&6NPV ooxJUswo|a3qU{j4KcEeH4KdUa==)XGm^v-~Mbv`e56CtDY9-r3nE(I) literal 0 HcmV?d00001