From fe08638d2606136d8e0fd95db133806266f07350 Mon Sep 17 00:00:00 2001 From: Marco Bartoli Date: Tue, 2 Jul 2024 13:41:48 +0200 Subject: [PATCH] Implement ImportAssets/ImportAssets2 (#16420) --- core/src/display_object/movie_clip.rs | 267 +++++++++++++++++++++++--- core/src/library.rs | 10 + core/src/loader.rs | 59 ++++++ swf/src/read.rs | 59 ++++-- 4 files changed, 344 insertions(+), 51 deletions(-) diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 60c0b6320..c26453414 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -9,6 +9,7 @@ use crate::avm2::{ QName as Avm2QName, StageObject as Avm2StageObject, TObject as Avm2TObject, Value as Avm2Value, }; use crate::backend::audio::{SoundHandle, SoundInstanceHandle}; +use crate::backend::navigator::Request; use crate::backend::ui::MouseCursor; use crate::frame_lifecycle::run_inner_goto_frame; use bitflags::bitflags; @@ -18,7 +19,6 @@ use crate::avm1::{Activation as Avm1Activation, ActivationIdentifier}; use crate::binary_data::BinaryData; use crate::character::{Character, CompressedBitmap}; use crate::context::{ActionType, RenderContext, UpdateContext}; -use crate::context_stub; use crate::display_object::container::{dispatch_removed_event, ChildContainer}; use crate::display_object::interactive::{ InteractiveObject, InteractiveObjectBase, TInteractiveObject, @@ -31,8 +31,8 @@ use crate::drawing::Drawing; use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult}; use crate::font::{Font, FontType}; use crate::limits::ExecutionLimit; -use crate::loader::Loader; use crate::loader::{self, ContentType}; +use crate::loader::{LoadManager, Loader}; use crate::prelude::*; use crate::streams::NetStream; use crate::string::{AvmString, SwfStrExt as _, WStr, WString}; @@ -47,7 +47,7 @@ use std::cmp::max; use std::collections::HashMap; use std::sync::Arc; use swf::extensions::ReadSwfExt; -use swf::{ClipEventFlag, DefineBitsLossless, FrameLabelData, TagCode}; +use swf::{ClipEventFlag, DefineBitsLossless, FrameLabelData, TagCode, UTF_8}; use super::interactive::Avm2MousePick; use super::BitmapClass; @@ -188,6 +188,9 @@ pub struct MovieClipData<'gc> { /// Attached audio (AVM1) attached_audio: Option>, + + // If this movie was loaded from ImportAssets(2), this will be the parent movie. + importer_movie: Option>, } impl<'gc> MovieClip<'gc> { @@ -223,6 +226,7 @@ impl<'gc> MovieClip<'gc> { tag_frame_boundaries: Default::default(), queued_tags: HashMap::new(), attached_audio: None, + importer_movie: None, }, )) } @@ -264,6 +268,7 @@ impl<'gc> MovieClip<'gc> { tag_frame_boundaries: Default::default(), queued_tags: HashMap::new(), attached_audio: None, + importer_movie: None, }, )); clip.set_avm2_class(gc_context, Some(class)); @@ -308,6 +313,7 @@ impl<'gc> MovieClip<'gc> { tag_frame_boundaries: Default::default(), queued_tags: HashMap::new(), attached_audio: None, + importer_movie: None, }, )) } @@ -316,6 +322,59 @@ impl<'gc> MovieClip<'gc> { MovieClipWeak(GcCell::downgrade(self.0)) } + pub fn new_import_assets( + context: &mut UpdateContext<'_, 'gc>, + movie: Arc, + parent: Arc, + ) -> Self { + let num_frames = movie.num_frames(); + + let loader_info = None; + + let mc = MovieClip(GcCell::new( + context.gc_context, + MovieClipData { + base: Default::default(), + static_data: Gc::new( + context.gc_context, + MovieClipStatic::with_data( + 0, + movie.clone().into(), + num_frames, + loader_info, + context.gc_context, + ), + ), + tag_stream_pos: 0, + current_frame: 0, + audio_stream: None, + container: ChildContainer::new(movie.clone()), + object: None, + clip_event_handlers: Vec::new(), + clip_event_flags: ClipEventFlag::empty(), + frame_scripts: Vec::new(), + flags: MovieClipFlags::PLAYING, + drawing: Drawing::new(), + avm2_enabled: true, + avm2_use_hand_cursor: true, + button_mode: false, + last_queued_script_frame: None, + queued_script_frame: None, + queued_goto_frame: None, + drop_target: None, + hit_area: None, + + #[cfg(feature = "timeline_debug")] + tag_frame_boundaries: Default::default(), + queued_tags: HashMap::new(), + attached_audio: None, + importer_movie: Some(parent.clone()), + }, + )); + + mc + } + /// Construct a movie clip that represents the root movie /// for the entire `Player`. pub fn player_root_movie( @@ -375,6 +434,7 @@ impl<'gc> MovieClip<'gc> { tag_frame_boundaries: Default::default(), queued_tags: HashMap::new(), attached_audio: None, + importer_movie: None, }, )); @@ -696,14 +756,16 @@ impl<'gc> MovieClip<'gc> { .0 .write(context.gc_context) .define_binary_data(context, reader), - TagCode::ImportAssets => self - .0 - .write(context.gc_context) - .import_assets(context, reader), - TagCode::ImportAssets2 => self - .0 - .write(context.gc_context) - .import_assets_2(context, reader), + TagCode::ImportAssets => { + self.0 + .write(context.gc_context) + .import_assets(context, reader, chunk_limit) + } + TagCode::ImportAssets2 => { + self.0 + .write(context.gc_context) + .import_assets_2(context, reader, chunk_limit) + } TagCode::DoAbc | TagCode::DoAbc2 => self.preload_bytecode_tag( tag_code, reader, @@ -736,6 +798,10 @@ impl<'gc> MovieClip<'gc> { }; let is_finished = end_tag_found || result.is_err() || !result.unwrap_or_default(); + self.0 + .write(context.gc_context) + .import_exports_of_importer(context); + // These variables will be persisted to be picked back up in the next // chunk. { @@ -4027,24 +4093,104 @@ impl<'gc, 'a> MovieClipData<'gc> { Ok(()) } + #[inline] + fn get_exported_from_importer( + &self, + context: &mut UpdateContext<'_, 'gc>, + importer_movie: Arc, + ) -> HashMap, (CharacterId, Character<'gc>)> { + let mut map: HashMap, (CharacterId, Character<'gc>)> = HashMap::new(); + let library = context.library.library_for_movie_mut(importer_movie); + + library.export_characters().iter().for_each(|(name, id)| { + let character = library.character_by_id(*id).unwrap(); + map.insert(name, (*id, character.clone())); + }); + map + } + + #[inline] + fn import_exports_of_importer(&mut self, context: &mut UpdateContext<'_, 'gc>) { + if let Some(importer_movie) = self.importer_movie.as_ref() { + let exported_from_importer = + { self.get_exported_from_importer(context, importer_movie.clone()) }; + + let self_library = context.library.library_for_movie_mut(self.movie().clone()); + + exported_from_importer + .iter() + .for_each(|(name, (id, character))| { + let id = *id; + if self_library.character_by_id(id).is_none() { + self_library.register_character(id, character.clone()); + self_library.register_export(id, *name); + } + }); + } + } + #[inline] fn import_assets( &mut self, context: &mut UpdateContext<'_, 'gc>, - _reader: &mut SwfStream<'a>, + reader: &mut SwfStream<'a>, + chunk_limit: &mut ExecutionLimit, ) -> Result<(), Error> { - context_stub!(context, "ImportAssets tag"); - - Ok(()) + 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( &mut self, context: &mut UpdateContext<'_, 'gc>, - _reader: &mut SwfStream<'a>, + reader: &mut SwfStream<'a>, + chunk_limit: &mut ExecutionLimit, ) -> Result<(), Error> { - context_stub!(context, "ImportAssets2 tag"); + 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( + &mut self, + context: &mut UpdateContext<'_, 'gc>, + reader: &mut SwfStream<'a>, + url: &swf::SwfStr, + exported_assets: Vec, + _chunk_limit: &mut ExecutionLimit, + ) -> Result<(), Error> { + 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(context.gc_context, 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(()) } @@ -4063,6 +4209,57 @@ impl<'gc, 'a> MovieClipData<'gc> { Ok(()) } + #[inline] + fn get_registered_character_by_id( + &mut self, + context: &mut UpdateContext<'_, 'gc>, + id: CharacterId, + ) -> Option> { + let library_for_movie = context.library.library_for_movie(self.movie()); + + if let Some(library) = library_for_movie { + if let Some(character) = library.character_by_id(id) { + return Some(character.clone()); + } + } + None + } + + fn register_export( + &mut self, + context: &mut UpdateContext<'_, 'gc>, + id: CharacterId, + name: &AvmString<'gc>, + movie: Arc, + ) { + let library = context.library.library_for_movie_mut(movie); + library.register_export(id, *name); + + // TODO: do other types of Character need to know their exported name? + if let Some(character) = library.character_by_id(id) { + if let Character::MovieClip(movie_clip) = character { + *movie_clip + .0 + .read() + .static_data + .exported_name + .write(context.gc_context) = Some(*name); + } else { + tracing::warn!( + "Registering export for non-movie clip: {} (ID: {})", + name, + id + ); + } + } else { + tracing::warn!( + "Can't register export {}: Character ID {} doesn't exist", + name, + id, + ); + } + } + #[inline] fn export_assets( &mut self, @@ -4073,24 +4270,32 @@ impl<'gc, 'a> MovieClipData<'gc> { for export in exports { let name = export.name.decode(reader.encoding()); let name = AvmString::new(context.gc_context, name); - let library = context.library.library_for_movie_mut(self.movie()); - library.register_export(export.id, name); - // TODO: do other types of Character need to know their exported name? - if let Some(character) = library.character_by_id(export.id) { - if let Character::MovieClip(movie_clip) = character { - *movie_clip - .0 - .read() - .static_data - .exported_name - .write(context.gc_context) = Some(name); + if let Some(character) = self.get_registered_character_by_id(context, export.id) { + self.register_export(context, export.id, &name, self.movie()); + tracing::debug!("register_export asset: {} (ID: {})", name, export.id); + + if self.importer_movie.is_some() { + let parent = self.importer_movie.as_ref().unwrap().clone(); + let parent_library = context.library.library_for_movie_mut(parent.clone()); + + if let Some(id) = parent_library.character_id_by_import_name(name) { + parent_library.register_character(id, character); + + self.register_export(context, id, &name, parent); + tracing::debug!( + "Registering parent asset: {} (Parent ID: {})(ID: {})", + name, + id, + export.id + ); + } } } else { - tracing::warn!( - "Can't register export {}: Character ID {} doesn't exist", + tracing::error!( + "Export asset: {} (ID: {}) not found in library", name, - export.id, + export.id ); } } diff --git a/core/src/library.rs b/core/src/library.rs index 35a2e6ae8..5b5a5e7a8 100644 --- a/core/src/library.rs +++ b/core/src/library.rs @@ -126,6 +126,7 @@ pub struct MovieLibrary<'gc> { swf: Arc, characters: HashMap>, export_characters: Avm1PropertyMap<'gc, CharacterId>, + imported_assets: HashMap, CharacterId>, jpeg_tables: Option>, fonts: FontMap<'gc>, avm2_domain: Option>, @@ -136,6 +137,7 @@ impl<'gc> MovieLibrary<'gc> { Self { swf, characters: HashMap::new(), + imported_assets: HashMap::new(), export_characters: Avm1PropertyMap::new(), jpeg_tables: None, fonts: Default::default(), @@ -190,6 +192,14 @@ impl<'gc> MovieLibrary<'gc> { None } + pub fn character_id_by_import_name(&self, name: AvmString<'gc>) -> Option { + self.imported_assets.get(&name).copied() + } + + pub fn register_import(&mut self, name: AvmString<'gc>, id: CharacterId) { + self.imported_assets.insert(name, id); + } + /// Instantiates the library item with the given character ID into a display object. /// The object must then be post-instantiated before being used. pub fn instantiate_by_id( diff --git a/core/src/loader.rs b/core/src/loader.rs index 0bcde60d2..700445243 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -343,6 +343,65 @@ impl<'gc> LoadManager<'gc> { loader.movie_loader(player, request, loader_url) } + pub fn load_asset_movie( + player: Weak>, + request: Request, + importer_movie: Arc, + ) -> OwnedFuture<(), Error> { + let player = player + .upgrade() + .expect("Could not upgrade weak reference to player"); + + Box::pin(async move { + let fetch = player.lock().unwrap().navigator().fetch(request); + + match Loader::wait_for_full_response(fetch).await { + Ok((body, url, _status, _redirected)) => { + let content_type = ContentType::sniff(&body); + tracing::info!("Loading imported movie: {:?}", url); + match content_type { + ContentType::Swf => { + let movie = SwfMovie::from_data(&body, url.clone(), Some(url.clone())) + .expect("Could not load movie"); + + let movie = Arc::new(movie); + + player.lock().unwrap().mutate_with_update_context(|uc| { + let clip = MovieClip::new_import_assets(uc, movie, importer_movie); + + clip.set_cur_preload_frame(uc.gc_context, 0); + let mut execution_limit = ExecutionLimit::none(); + + tracing::debug!("Preloading swf to run exports {:?}", url); + + // Create library for exports before preloading + uc.library.library_for_movie_mut(clip.movie()); + let res = clip.preload(uc, &mut execution_limit); + tracing::debug!( + "Preloaded swf to run exports result {:?} {}", + url, + res + ); + }); + Ok(()) + } + _ => { + tracing::warn!( + "Unsupported content type for ImportAssets: {:?}", + content_type + ); + Ok(()) + } + } + } + Err(e) => Err(Error::FetchError(format!( + "Could not fetch: {:?} because {:?}", + e.url, e.error + ))), + } + }) + } + /// Kick off a movie clip load. /// /// Returns the loader's async process, which you will need to spawn. diff --git a/swf/src/read.rs b/swf/src/read.rs index 82883e900..aafd36b33 100644 --- a/swf/src/read.rs +++ b/swf/src/read.rs @@ -470,30 +470,18 @@ impl<'a> Reader<'a> { Tag::EnableTelemetry { password_hash } } TagCode::ImportAssets => { - let url = tag_reader.read_str()?; - let num_imports = tag_reader.read_u16()?; - let mut imports = Vec::with_capacity(num_imports as usize); - for _ in 0..num_imports { - imports.push(ExportedAsset { - id: tag_reader.read_u16()?, - name: tag_reader.read_str()?, - }); + let import_assets = tag_reader.read_import_assets()?; + Tag::ImportAssets { + url: import_assets.0, + imports: import_assets.1, } - Tag::ImportAssets { url, imports } } TagCode::ImportAssets2 => { - let url = tag_reader.read_str()?; - tag_reader.read_u8()?; // Reserved; must be 1 - tag_reader.read_u8()?; // Reserved; must be 0 - let num_imports = tag_reader.read_u16()?; - let mut imports = Vec::with_capacity(num_imports as usize); - for _ in 0..num_imports { - imports.push(ExportedAsset { - id: tag_reader.read_u16()?, - name: tag_reader.read_str()?, - }); + let import_assets = tag_reader.read_import_assets_2()?; + Tag::ImportAssets { + url: import_assets.0, + imports: import_assets.1, } - Tag::ImportAssets { url, imports } } TagCode::JpegTables => { @@ -1855,6 +1843,37 @@ impl<'a> Reader<'a> { Ok(exports) } + pub fn read_import_assets(&mut self) -> Result<(&'a SwfStr, ExportAssets<'a>)> { + let url = self.read_str()?; + let num_imports = self.read_u16()?; + let mut imports = Vec::with_capacity(num_imports as usize); + for _ in 0..num_imports { + imports.push(ExportedAsset { + id: self.read_u16()?, + name: self.read_str()?, + }); + } + + Ok((url, imports)) + } + + pub fn read_import_assets_2(&mut self) -> Result<(&'a SwfStr, ExportAssets<'a>)> { + let url = self.read_str()?; + self.read_u8()?; // Reserved; must be 1 + self.read_u8()?; // Reserved; must be 0 + let num_imports = self.read_u16()?; + let mut imports = Vec::with_capacity(num_imports as usize); + + for _ in 0..num_imports { + imports.push(ExportedAsset { + id: self.read_u16()?, + name: self.read_str()?, + }); + } + + Ok((url, imports)) + } + pub fn read_place_object(&mut self) -> Result> { Ok(PlaceObject { version: 1,