Implement ImportAssets/ImportAssets2 (#16420)

This commit is contained in:
Marco Bartoli 2024-07-02 13:41:48 +02:00 committed by GitHub
parent 617cb3330d
commit fe08638d26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 344 additions and 51 deletions

View File

@ -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<NetStream<'gc>>,
// If this movie was loaded from ImportAssets(2), this will be the parent movie.
importer_movie: Option<Arc<SwfMovie>>,
}
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<SwfMovie>,
parent: Arc<SwfMovie>,
) -> 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
TagCode::ImportAssets => {
self.0
.write(context.gc_context)
.import_assets(context, reader),
TagCode::ImportAssets2 => self
.0
.import_assets(context, reader, chunk_limit)
}
TagCode::ImportAssets2 => {
self.0
.write(context.gc_context)
.import_assets_2(context, reader),
.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<SwfMovie>,
) -> HashMap<AvmString<'gc>, (CharacterId, Character<'gc>)> {
let mut map: HashMap<AvmString<'gc>, (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<swf::ExportedAsset>,
_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<Character<'gc>> {
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<SwfMovie>,
) {
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
);
}
}

View File

@ -126,6 +126,7 @@ pub struct MovieLibrary<'gc> {
swf: Arc<SwfMovie>,
characters: HashMap<CharacterId, Character<'gc>>,
export_characters: Avm1PropertyMap<'gc, CharacterId>,
imported_assets: HashMap<AvmString<'gc>, CharacterId>,
jpeg_tables: Option<Vec<u8>>,
fonts: FontMap<'gc>,
avm2_domain: Option<Avm2Domain<'gc>>,
@ -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<CharacterId> {
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(

View File

@ -343,6 +343,65 @@ impl<'gc> LoadManager<'gc> {
loader.movie_loader(player, request, loader_url)
}
pub fn load_asset_movie(
player: Weak<Mutex<Player>>,
request: Request,
importer_movie: Arc<SwfMovie>,
) -> 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.

View File

@ -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<PlaceObject<'a>> {
Ok(PlaceObject {
version: 1,