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.
This commit is contained in:
Aaron Hill 2022-06-27 22:56:14 -05:00 committed by Mike Welsh
parent df07f610e7
commit 49d1a985ca
16 changed files with 198 additions and 80 deletions

View File

@ -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(

View File

@ -1352,7 +1352,7 @@ fn unload_movie<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, 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)
}

View File

@ -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());
}

View File

@ -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<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, 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)
}

View File

@ -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<Object<'gc>> + Clone + Copy
None
}
fn loader_stream_init(&self, _context: &mut UpdateContext<'_, 'gc, '_>) {}
/// Unwrap this object's loader stream
fn as_loader_stream(&self) -> Option<Ref<LoaderStream<'gc>>> {
None

View File

@ -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<LoaderStream<'gc>>,
/// 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<Value<'gc>, 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<Value<'gc>, 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
);
}
}
}

View File

@ -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<Avm2Object<'gc>> {
None
}
fn instantiate(&self, gc_context: MutationContext<'gc, '_>) -> DisplayObject<'gc>;
fn as_ptr(&self) -> *const DisplayObjectPtr;

View File

@ -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<SwfMovie>) -> Self {
pub fn from_movie(context: &mut UpdateContext<'_, 'gc, '_>, movie: Arc<SwfMovie>) -> 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<Arc<SwfMovie>>,
) {
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<SwfMovie>) {
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<Avm2Object<'gc>> {
*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<Arc<SwfMovie>>,
) {
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<AvmString<'gc>>>,
/// 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<Avm2Object<'gc>>>,
}
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),
}
}
}

View File

@ -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<Avm2Object<'gc>> {
Some(self.0.read().loader_info)
}
}
impl<'gc> TDisplayObjectContainer<'gc> for Stage<'gc> {

View File

@ -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);
}

View File

@ -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");

View File

@ -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),

View File

@ -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");
}
}
}

View File

@ -0,0 +1,6 @@
Called constructor
Called added
Called addedToStage
Called exitFrame
Called init!
Called enterFrame

Binary file not shown.

Binary file not shown.