diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 509db118dbfb..a9ee35c6ee46 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -58,7 +58,7 @@ pub use graphic::Graphic; pub use interactive::{Avm2MousePick, InteractiveObject, TInteractiveObject}; pub use loader_display::LoaderDisplay; pub use morph_shape::MorphShape; -pub use movie_clip::{MovieClip, MovieClipWeak, Scene}; +pub use movie_clip::{MovieClip, MovieClipHandle, MovieClipWeak, Scene}; use ruffle_render::backend::{BitmapCacheEntry, RenderBackend}; use ruffle_render::bitmap::{BitmapHandle, BitmapInfo, PixelSnapping}; use ruffle_render::blend::ExtendedBlendMode; diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 48c85c47677b..5922392dbb74 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -42,7 +42,7 @@ use bitflags::bitflags; use core::fmt; use gc_arena::barrier::unlock; use gc_arena::lock::{Lock, RefLock}; -use gc_arena::{Collect, Gc, GcWeak, Mutation}; +use gc_arena::{Collect, DynamicRoot, Gc, GcWeak, Mutation, Rootable}; use ruffle_macros::istr; use ruffle_render::perspective_projection::PerspectiveProjection; use smallvec::SmallVec; @@ -147,6 +147,19 @@ impl<'gc> MovieClipWeak<'gc> { } } +#[derive(Clone)] +pub struct MovieClipHandle(DynamicRoot]>); + +impl MovieClipHandle { + pub fn stash<'gc>(uc: &UpdateContext<'gc>, this: MovieClip<'gc>) -> Self { + Self(uc.dynamic_root.stash(uc.gc(), this.0)) + } + + pub fn fetch<'gc>(&self, uc: &UpdateContext<'gc>) -> MovieClip<'gc> { + MovieClip(uc.dynamic_root.fetch(&self.0)) + } +} + #[derive(Clone, Collect, HasPrefixField)] #[collect(no_drop)] #[repr(C, align(8))] @@ -157,9 +170,6 @@ pub struct MovieClipData<'gc> { tag_stream_pos: Cell, - // If this movie was loaded from ImportAssets(2), this will be the parent movie. - importer_movie: Option>, - // Unlike most other DisplayObjects, a MovieClip can have an AVM1 // side and an AVM2 side simultaneously. object1: Lock>>, @@ -250,7 +260,6 @@ impl<'gc> MovieClipData<'gc> { hit_area: Lock::new(None), attached_audio: Lock::new(None), next_avm1_clip: Lock::new(None), - importer_movie: None, } } @@ -290,7 +299,7 @@ impl<'gc> MovieClip<'gc> { swf: SwfSlice, num_frames: u16, ) -> Self { - let shared = MovieClipShared::with_data(id, swf, num_frames, None); + let shared = MovieClipShared::with_data(id, swf, num_frames, None, None); let data = MovieClipData::new(shared, mc); data.flags.set(MovieClipFlags::PLAYING); MovieClip(Gc::new(mc, data)) @@ -299,15 +308,15 @@ impl<'gc> MovieClip<'gc> { pub fn new_import_assets( context: &mut UpdateContext<'gc>, movie: Arc, - parent: Arc, + parent: MovieClip<'gc>, ) -> Self { let num_frames = movie.num_frames(); let loader_info = None; - let shared = MovieClipShared::with_data(0, movie.into(), num_frames, loader_info); + let shared = + MovieClipShared::with_data(0, movie.into(), num_frames, loader_info, Some(parent)); - let mut data = MovieClipData::new(shared, context.gc()); + let data = MovieClipData::new(shared, context.gc()); data.flags.set(MovieClipFlags::PLAYING); - data.importer_movie = Some(parent); MovieClip(Gc::new(context.gc(), data)) } @@ -330,8 +339,13 @@ impl<'gc> MovieClip<'gc> { None }; - let shared = - MovieClipShared::with_data(0, movie.clone().into(), movie.num_frames(), loader_info); + let shared = MovieClipShared::with_data( + 0, + movie.clone().into(), + movie.num_frames(), + loader_info, + None, + ); let data = MovieClipData::new(shared, activation.gc()); data.flags.set(MovieClipFlags::PLAYING); data.base.base.set_is_root(true); @@ -374,7 +388,7 @@ impl<'gc> MovieClip<'gc> { unlock!(write, MovieClipData, shared).set(Gc::new( context.gc(), - MovieClipShared::with_data(0, movie.into(), total_frames, loader_info), + MovieClipShared::with_data(0, movie.into(), total_frames, loader_info, None), )); write.tag_stream_pos.set(0); write.flags.set(MovieClipFlags::PLAYING); @@ -508,9 +522,7 @@ impl<'gc> MovieClip<'gc> { TagCode::DefineText2 => shared.define_text(context, reader, 2), TagCode::DoInitAction => self.do_init_action(context, reader, tag_len), TagCode::DefineSceneAndFrameLabelData => shared.scene_and_frame_labels(reader), - TagCode::ExportAssets => { - shared.export_assets(context, reader, self.0.importer_movie.as_ref()) - } + TagCode::ExportAssets => shared.export_assets(context, reader), TagCode::FrameLabel => shared.frame_label(reader), TagCode::JpegTables => shared.jpeg_tables(context, reader), TagCode::ShowFrame => shared.show_frame(reader, tag_len), @@ -519,8 +531,8 @@ impl<'gc> MovieClip<'gc> { TagCode::SoundStreamHead2 => shared.sound_stream_head(reader, 2), TagCode::VideoFrame => shared.preload_video_frame(context, reader), TagCode::DefineBinaryData => shared.define_binary_data(context, reader), - TagCode::ImportAssets => shared.import_assets(context, reader, chunk_limit), - TagCode::ImportAssets2 => shared.import_assets_2(context, reader, chunk_limit), + TagCode::ImportAssets => self.import_assets(context, reader, chunk_limit, 1), + TagCode::ImportAssets2 => self.import_assets(context, reader, chunk_limit, 2), TagCode::DoAbc | TagCode::DoAbc2 => shared.preload_bytecode_tag(tag_code, reader), TagCode::SymbolClass => shared.preload_symbol_class(reader), TagCode::End => { @@ -545,9 +557,7 @@ impl<'gc> MovieClip<'gc> { }; let is_finished = end_tag_found || result.is_err() || !result.unwrap_or_default(); - if let Some(importer_movie) = self.0.importer_movie.clone() { - shared.import_exports_of_importer(context, importer_movie); - } + shared.import_exports_of_importer(context); if is_finished { if progress.cur_preload_frame.get() == 1 { @@ -573,15 +583,29 @@ impl<'gc> MovieClip<'gc> { reader: &mut SwfStream<'_>, tag_len: usize, ) -> Result<(), Error> { - if self.movie().is_action_script_3() { - tracing::warn!("DoInitAction tag in AVM2 movie"); - return Ok(()); + let mut target = self; + loop { + let shared = target.0.shared.get(); + if shared.movie().is_action_script_3() { + tracing::warn!("DoInitAction tag in AVM2 movie"); + return Ok(()); + } + + // `DoInitAction`s always execute in the context of their importer movie. + let Some(parent) = shared.importer_movie else { + break; + }; + + target = parent; } let start = reader.as_slice(); - // Queue the init actions. + // TODO: Init actions are supposed to be executed once, and it gives a // sprite ID... how does that work? + // TODO: what happens with `DoInitAction` blocks nested in a `DefineSprite`? + // The SWF spec forbids this, but Ruffle will currently execute them in the context + // of the character itself, which is probably nonsense. let _sprite_id = reader.read_u16()?; let num_read = reader.pos(start); @@ -593,7 +617,7 @@ impl<'gc> MovieClip<'gc> { .resize_to_reader(reader, tag_len - num_read); if !slice.is_empty() { - Avm1::run_stack_frame_for_init_action(self.into(), slice, context); + Avm1::run_stack_frame_for_init_action(target.into(), slice, context); } Ok(()) @@ -670,6 +694,43 @@ impl<'gc> MovieClip<'gc> { Ok(None) } + #[inline] + fn import_assets( + self, + context: &mut UpdateContext<'gc>, + reader: &mut SwfStream<'_>, + _chunk_limit: &mut ExecutionLimit, + version: u8, + ) -> Result<(), Error> { + let (url, exported_assets) = match version { + 1 => reader.read_import_assets()?, + 2 => reader.read_import_assets_2()?, + _ => unreachable!(), + }; + + let mc = context.gc(); + let library = context.library.library_for_movie_mut(self.movie()); + + let asset_url = url.to_string_lossy(UTF_8); + + let request = Request::get(asset_url); + + for asset in exported_assets { + let name = asset.name.decode(reader.encoding()); + let name = AvmString::new(mc, name); + let id = asset.id; + tracing::debug!("Importing asset: {} (ID: {})", name, id); + + library.register_import(name, id); + } + + let fut = LoadManager::load_asset_movie(context, request, self); + + context.navigator.spawn_future(fut); + + Ok(()) + } + pub fn playing(self) -> bool { self.0.playing() } @@ -3733,12 +3794,11 @@ impl<'gc, 'a> MovieClipShared<'gc> { } #[inline] - fn import_exports_of_importer( - &self, - context: &mut UpdateContext<'gc>, - importer: Arc, - ) { - let Some(importer_library) = context.library.library_for_movie(importer) else { + fn import_exports_of_importer(&self, context: &mut UpdateContext<'gc>) { + let Some(importer_library) = self + .importer_movie + .and_then(|mc| context.library.library_for_movie(mc.movie())) + else { return; }; @@ -3760,73 +3820,6 @@ impl<'gc, 'a> MovieClipShared<'gc> { } } - #[inline] - fn import_assets( - &self, - context: &mut UpdateContext<'gc>, - reader: &mut SwfStream<'a>, - chunk_limit: &mut ExecutionLimit, - ) -> Result<(), Error> { - let import_assets = reader.read_import_assets()?; - self.import_assets_load( - context, - reader, - import_assets.0, - import_assets.1, - chunk_limit, - ) - } - - #[inline] - fn import_assets_2( - &self, - context: &mut UpdateContext<'gc>, - reader: &mut SwfStream<'a>, - chunk_limit: &mut ExecutionLimit, - ) -> Result<(), Error> { - let import_assets = reader.read_import_assets_2()?; - self.import_assets_load( - context, - reader, - import_assets.0, - import_assets.1, - chunk_limit, - ) - } - - #[inline] - fn import_assets_load( - &self, - context: &mut UpdateContext<'gc>, - reader: &mut SwfStream<'a>, - url: &swf::SwfStr, - exported_assets: Vec, - _chunk_limit: &mut ExecutionLimit, - ) -> Result<(), Error> { - let mc = context.gc(); - let library = self.library_mut(context); - - let asset_url = url.to_string_lossy(UTF_8); - - let request = Request::get(asset_url); - - for asset in exported_assets { - let name = asset.name.decode(reader.encoding()); - let name = AvmString::new(mc, name); - let id = asset.id; - tracing::debug!("Importing asset: {} (ID: {})", name, id); - - library.register_import(name, id); - } - - let player = context.player.clone(); - let fut = LoadManager::load_asset_movie(player, request, self.movie()); - - context.navigator.spawn_future(fut); - - Ok(()) - } - #[inline] fn script_limits(&self, reader: &mut SwfStream<'a>, avm: &mut Avm1<'gc>) -> Result<(), Error> { let max_recursion_depth = reader.read_u16()?; @@ -3869,9 +3862,9 @@ impl<'gc, 'a> MovieClipShared<'gc> { &self, context: &mut UpdateContext<'gc>, reader: &mut SwfStream<'a>, - importer_movie: Option<&Arc>, ) -> Result<(), Error> { let exports = reader.read_export_assets()?; + let importer_movie = self.importer_movie.map(|mc| mc.movie()); for export in exports { let name = export.name.decode(reader.encoding()); let name = AvmString::new(context.gc(), name); @@ -3883,7 +3876,7 @@ impl<'gc, 'a> MovieClipShared<'gc> { Self::register_export(context, export.id, name, self.movie()); tracing::debug!("register_export asset: {} (ID: {})", name, export.id); - if let Some(parent) = importer_movie { + if let Some(parent) = &importer_movie { let parent_library = context.library.library_for_movie_mut(parent.clone()); if let Some(id) = parent_library.character_id_by_import_name(name) { @@ -4486,6 +4479,9 @@ struct MovieClipShared<'gc> { /// However, it will be set for an AVM1 movie loaded from AVM2 /// via `Loader` loader_info: Option>, + + // If this movie was loaded from ImportAssets(2), this will be the root MovieClip of the parent movie. + importer_movie: Option>, } #[derive(Default)] @@ -4514,7 +4510,7 @@ struct EagerTags { impl<'gc> MovieClipShared<'gc> { fn empty(movie: Arc) -> Self { - let mut s = Self::with_data(0, SwfSlice::empty(movie), 1, None); + let mut s = Self::with_data(0, SwfSlice::empty(movie), 1, None, None); *s.preload_progress.cur_preload_frame.get_mut() = s.header_frames + 1; @@ -4526,6 +4522,7 @@ impl<'gc> MovieClipShared<'gc> { swf: SwfSlice, header_frames: FrameNumber, loader_info: Option>, + importer_movie: Option>, ) -> Self { Self { cell: Default::default(), @@ -4536,6 +4533,7 @@ impl<'gc> MovieClipShared<'gc> { exported_name: Lock::new(None), avm2_class: Lock::new(None), loader_info, + importer_movie, } } diff --git a/core/src/loader.rs b/core/src/loader.rs index b2fdf46cba53..f6f90deeb9e0 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -21,7 +21,8 @@ use crate::bitmap::bitmap_data::BitmapData; use crate::bitmap::bitmap_data::Color; use crate::context::{ActionQueue, ActionType, UpdateContext}; use crate::display_object::{ - DisplayObject, MovieClip, TDisplayObject, TDisplayObjectContainer, TInteractiveObject, + DisplayObject, MovieClip, MovieClipHandle, TDisplayObject, TDisplayObjectContainer, + TInteractiveObject, }; use crate::events::ClipEvent; use crate::frame_lifecycle::catchup_display_object_to_frame; @@ -367,14 +368,17 @@ impl<'gc> LoadManager<'gc> { } pub fn load_asset_movie( - player: Weak>, + uc: &UpdateContext<'gc>, request: Request, - importer_movie: Arc, + importer_movie: MovieClip<'gc>, ) -> OwnedFuture<(), Error> { - let player = player + let player = uc + .player .upgrade() .expect("Could not upgrade weak reference to player"); + let importer_movie = MovieClipHandle::stash(uc, importer_movie); + Box::pin(async move { let fetch = player.lock().unwrap().navigator().fetch(request); @@ -390,6 +394,7 @@ impl<'gc> LoadManager<'gc> { let movie = Arc::new(movie); player.lock().unwrap().mutate_with_update_context(|uc| { + let importer_movie = importer_movie.fetch(uc); let clip = MovieClip::new_import_assets(uc, movie, importer_movie); clip.set_cur_preload_frame(0); diff --git a/tests/tests/swfs/avm1/do_init_action_child/assets.swf b/tests/tests/swfs/avm1/do_init_action_child/assets.swf new file mode 100644 index 000000000000..df5a0973dd81 Binary files /dev/null and b/tests/tests/swfs/avm1/do_init_action_child/assets.swf differ diff --git a/tests/tests/swfs/avm1/do_init_action_child/child.swf b/tests/tests/swfs/avm1/do_init_action_child/child.swf new file mode 100644 index 000000000000..fb5ad6245a95 Binary files /dev/null and b/tests/tests/swfs/avm1/do_init_action_child/child.swf differ diff --git a/tests/tests/swfs/avm1/do_init_action_child/output.txt b/tests/tests/swfs/avm1/do_init_action_child/output.txt new file mode 100644 index 000000000000..23d9aff4b18e --- /dev/null +++ b/tests/tests/swfs/avm1/do_init_action_child/output.txt @@ -0,0 +1,12 @@ +=== DoInitAction #1 +this: _level0 +is root: true +=== Child DoInitAction #1 +this: _level0.child +is root: false +=== Assets DoInitAction #1 (not imported) +this: _level0.child +is root: false +=== Assets DoInitAction #2 +this: _level0.child +is root: false diff --git a/tests/tests/swfs/avm1/do_init_action_child/test.swf b/tests/tests/swfs/avm1/do_init_action_child/test.swf new file mode 100644 index 000000000000..355ed619835d Binary files /dev/null and b/tests/tests/swfs/avm1/do_init_action_child/test.swf differ diff --git a/tests/tests/swfs/avm1/do_init_action_child/test.toml b/tests/tests/swfs/avm1/do_init_action_child/test.toml new file mode 100644 index 000000000000..cd84be37d7e3 --- /dev/null +++ b/tests/tests/swfs/avm1/do_init_action_child/test.toml @@ -0,0 +1,7 @@ +# Tests DoInitAction behavior in child movies, both when +# loaded through a `MovieClipLoader` and through `ImportAssets2`. +# +# .swf files were handcrafted using the JPEXS Decompiler and have +# no corresponding sources files. + +num_frames = 2