avm2: Run most of Loader.loadBytes immediately

This requires moving `set_root_movie` into `UpdateContext`.

Now, we preload the entire movieclip immediately - Flash Player
does this regardless of the size of the SWF.
The 'Loader::load_complete' is delayed to the end of the frame
(which is when the root class is constructed for the loaded clip).
This commit is contained in:
Aaron Hill 2023-12-16 18:03:41 -05:00 committed by Nathan Adams
parent b5f28f6caa
commit 6c420fa5d5
38 changed files with 953 additions and 423 deletions

View File

@ -16,6 +16,7 @@ use crate::avm2_stub_method;
use crate::backend::navigator::{NavigationMethod, Request};
use crate::display_object::LoaderDisplay;
use crate::display_object::MovieClip;
use crate::loader::LoadManager;
use crate::loader::MovieLoaderVMData;
use crate::tag_utils::SwfMovie;
use std::sync::Arc;
@ -227,19 +228,24 @@ pub fn load_bytes<'gc>(
.as_object()
.unwrap();
let future = activation.context.load_manager.load_movie_into_clip_bytes(
activation.context.player.clone(),
let default_domain = activation
.caller_domain()
.expect("Missing caller domain in Loader.loadBytes");
if let Err(e) = LoadManager::load_movie_into_clip_bytes(
&mut activation.context,
content.into(),
bytearray.bytes().to_vec(),
MovieLoaderVMData::Avm2 {
loader_info,
context,
default_domain: activation
.caller_domain()
.expect("Missing caller domain in Loader.loadBytes"),
default_domain,
},
);
activation.context.navigator.spawn_future(future);
) {
return Err(Error::RustError(
format!("Error in Loader.loadBytes: {e:?}").into(),
));
}
Ok(Value::Undefined)
}

View File

@ -14,7 +14,7 @@ pub fn get_domain<'gc>(
_this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let movie = activation.context.swf;
let movie = &activation.context.swf;
let domain = if let Ok(url) = url::Url::parse(movie.url()) {
if url.scheme() == "file" {

View File

@ -9,7 +9,7 @@ pub fn get_domain<'gc>(
_this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let movie = activation.context.swf;
let movie = &activation.context.swf;
let domain = if let Ok(url) = url::Url::parse(movie.url()) {
if url.scheme() == "file" {

View File

@ -236,33 +236,37 @@ impl<'gc> LoaderInfoObject<'gc> {
if !self.0.read().complete_event_fired {
// NOTE: We have to check load progress here because this function
// is called unconditionally at the end of every frame.
let should_complete = match self.0.read().loaded_stream {
Some(LoaderStream::Swf(_, root)) => root
.as_movie_clip()
.map(|mc| mc.loaded_bytes() as i32 >= mc.total_bytes())
.unwrap_or(true),
_ => false,
let (should_complete, from_url) = match self.0.read().loaded_stream {
Some(LoaderStream::Swf(ref movie, root)) => (
root.as_movie_clip()
.map(|mc| mc.loaded_bytes() as i32 >= mc.total_bytes())
.unwrap_or(true),
movie.loader_url().is_some(),
),
_ => (false, false),
};
if should_complete {
let mut activation = Activation::from_nothing(context.reborrow());
let http_status_evt = activation
.avm2()
.classes()
.httpstatusevent
.construct(
&mut activation,
&[
"httpStatus".into(),
false.into(),
false.into(),
status.into(),
redirected.into(),
],
)
.unwrap();
if from_url {
let http_status_evt = activation
.avm2()
.classes()
.httpstatusevent
.construct(
&mut activation,
&[
"httpStatus".into(),
false.into(),
false.into(),
status.into(),
redirected.into(),
],
)
.unwrap();
Avm2::dispatch_event(context, http_status_evt, (*self).into());
Avm2::dispatch_event(context, http_status_evt, (*self).into());
}
self.0.write(context.gc_context).complete_event_fired = true;
let complete_evt = EventObject::bare_default_event(context, "complete");

View File

@ -1,8 +1,16 @@
//! Contexts and helper types passed between functions.
use crate::avm1::Activation;
use crate::avm1::ActivationIdentifier;
use crate::avm1::Attribute;
use crate::avm1::Avm1;
use crate::avm1::ScriptObject;
use crate::avm1::SystemProperties;
use crate::avm1::TObject;
use crate::avm1::{Object as Avm1Object, Value as Avm1Value};
use crate::avm2::api_version::ApiVersion;
use crate::avm2::object::LoaderInfoObject;
use crate::avm2::Activation as Avm2Activation;
use crate::avm2::{Avm2, Object as Avm2Object, SoundChannelObject};
use crate::backend::{
audio::{AudioBackend, AudioManager, SoundHandle, SoundInstanceHandle},
@ -24,10 +32,12 @@ use crate::player::Player;
use crate::prelude::*;
use crate::socket::Sockets;
use crate::streams::StreamManager;
use crate::string::AvmString;
use crate::string::AvmStringInterner;
use crate::stub::StubCollection;
use crate::tag_utils::{SwfMovie, SwfSlice};
use crate::timer::Timers;
use crate::vminterface::Instantiator;
use core::fmt;
use gc_arena::{Collect, Mutation};
use rand::rngs::SmallRng;
@ -101,7 +111,7 @@ pub struct UpdateContext<'a, 'gc> {
pub needs_render: &'a mut bool,
/// The root SWF file.
pub swf: &'a Arc<SwfMovie>,
pub swf: &'a mut Arc<SwfMovie>,
/// The audio backend, used by display objects and AVM to play audio.
pub audio: &'a mut dyn AudioBackend,
@ -238,6 +248,11 @@ pub struct UpdateContext<'a, 'gc> {
/// Dynamic root for allowing handles to GC objects to exist outside of the GC.
pub dynamic_root: gc_arena::DynamicRootSet<'gc>,
/// These functions are run at the end of each frame execution.
/// Currently, this is just used for handling `Loader.loadBytes`
#[allow(clippy::type_complexity)]
pub post_frame_callbacks: &'a mut Vec<Box<dyn FnOnce(&mut UpdateContext<'_, '_>)>>,
}
/// Convenience methods for controlling audio.
@ -326,6 +341,112 @@ impl<'a, 'gc> UpdateContext<'a, 'gc> {
pub fn set_sound_transforms_dirty(&mut self) {
self.audio_manager.set_sound_transforms_dirty()
}
/// Change the root movie.
///
/// This should only be called once, as it makes no attempt at removing
/// previous stage contents. If you need to load a new root movie, you
/// should destroy and recreate the player instance.
pub fn set_root_movie(&mut self, movie: SwfMovie) {
if !self.forced_frame_rate {
*self.frame_rate = movie.frame_rate().into();
}
info!(
"Loaded SWF version {}, resolution {}x{} @ {} FPS",
movie.version(),
movie.width(),
movie.height(),
self.frame_rate,
);
*self.swf = Arc::new(movie);
*self.instance_counter = 0;
if self.swf.is_action_script_3() {
self.avm2.root_api_version =
ApiVersion::from_swf_version(self.swf.version(), self.avm2.player_runtime)
.unwrap_or_else(|| panic!("Unknown SWF version {}", self.swf.version()));
}
self.stage.set_movie_size(
self.gc_context,
self.swf.width().to_pixels() as u32,
self.swf.height().to_pixels() as u32,
);
self.stage.set_movie(self.gc_context, self.swf.clone());
let stage_domain = self.avm2.stage_domain();
let mut activation = Avm2Activation::from_domain(self.reborrow(), stage_domain);
activation
.context
.library
.library_for_movie_mut(activation.context.swf.clone())
.set_avm2_domain(stage_domain);
activation.context.ui.set_mouse_visible(true);
let swf = activation.context.swf.clone();
let root: DisplayObject = MovieClip::player_root_movie(&mut activation, swf.clone()).into();
// The Stage `LoaderInfo` is permanently in the 'not yet loaded' state,
// and has no associated `Loader` instance.
// However, some properties are always accessible, and take their values
// from the root SWF.
let stage_loader_info =
LoaderInfoObject::not_yet_loaded(&mut activation, swf, None, Some(root), true)
.expect("Failed to construct Stage LoaderInfo");
activation
.context
.stage
.set_loader_info(activation.context.gc_context, stage_loader_info);
drop(activation);
root.set_depth(self.gc_context, 0);
let flashvars = if !self.swf.parameters().is_empty() {
let object = ScriptObject::new(self.gc_context, None);
for (key, value) in self.swf.parameters().iter() {
object.define_value(
self.gc_context,
AvmString::new_utf8(self.gc_context, key),
AvmString::new_utf8(self.gc_context, value).into(),
Attribute::empty(),
);
}
Some(object.into())
} else {
None
};
root.post_instantiation(self, flashvars, Instantiator::Movie, false);
root.set_default_root_name(self);
self.stage.replace_at_depth(self, root, 0);
// Set the version parameter on the root.
let mut activation = Activation::from_stub(
self.reborrow(),
ActivationIdentifier::root("[Version Setter]"),
);
let object = root.object().coerce_to_object(&mut activation);
let version_string = activation
.context
.system
.get_version_string(activation.context.avm1);
object.define_value(
activation.context.gc_context,
"$version",
AvmString::new_utf8(activation.context.gc_context, version_string).into(),
Attribute::empty(),
);
let stage = activation.context.stage;
stage.build_matrices(&mut activation.context);
drop(activation);
self.audio.set_frame_rate(*self.frame_rate);
}
}
impl<'a, 'gc> UpdateContext<'a, 'gc> {
@ -400,6 +521,7 @@ impl<'a, 'gc> UpdateContext<'a, 'gc> {
net_connections: self.net_connections,
local_connections: self.local_connections,
dynamic_root: self.dynamic_root,
post_frame_callbacks: self.post_frame_callbacks,
}
}

View File

@ -28,6 +28,7 @@ use crate::streams::NetStream;
use crate::string::AvmString;
use crate::tag_utils::SwfMovie;
use crate::vminterface::Instantiator;
use crate::{avm2_stub_method, avm2_stub_method_context};
use encoding_rs::UTF_8;
use gc_arena::{Collect, GcCell};
use generational_arena::{Arena, Index};
@ -305,6 +306,7 @@ impl<'gc> LoadManager<'gc> {
target_clip,
vm_data,
loader_status: LoaderStatus::Pending,
from_bytes: false,
movie: None,
};
let handle = self.add_loader(loader);
@ -316,22 +318,21 @@ impl<'gc> LoadManager<'gc> {
///
/// Returns the loader's async process, which you will need to spawn.
pub fn load_movie_into_clip_bytes(
&mut self,
player: Weak<Mutex<Player>>,
context: &mut UpdateContext<'_, 'gc>,
target_clip: DisplayObject<'gc>,
bytes: Vec<u8>,
vm_data: MovieLoaderVMData<'gc>,
) -> OwnedFuture<(), Error> {
) -> Result<(), Error> {
let loader = Loader::Movie {
self_handle: None,
target_clip,
vm_data,
loader_status: LoaderStatus::Pending,
movie: None,
from_bytes: true,
};
let handle = self.add_loader(loader);
let loader = self.get_loader_mut(handle).unwrap();
loader.movie_loader_bytes(player, bytes)
let handle = context.load_manager.add_loader(loader);
Loader::movie_loader_bytes(handle, context, bytes)
}
/// Fires the `onLoad` listener event for every MovieClip that has been
@ -621,6 +622,9 @@ pub enum Loader<'gc> {
/// completed and we expect the Player to periodically tick preload
/// until loading completes.
movie: Option<Arc<SwfMovie>>,
/// Whether or not this was loaded as a result of a `Loader.loadBytes` call
from_bytes: bool,
},
/// Loader that is loading form data into an AVM1 object scope.
@ -745,13 +749,21 @@ impl<'gc> Loader<'gc> {
) -> Result<bool, Error> {
let mc = match context.load_manager.get_loader_mut(handle) {
Some(Self::Movie {
target_clip, movie, ..
target_clip,
movie,
from_bytes,
..
}) => {
if movie.is_none() {
//Non-SWF load or file not loaded yet
return Ok(false);
}
// Loader.loadBytes movies never participate in preloading
if *from_bytes {
return Ok(true);
}
if target_clip.as_movie_clip().is_none() {
// Non-movie-clip loads should not be handled in preload_tick
tracing::error!("Cannot preload non-movie-clip loader");
@ -829,7 +841,9 @@ impl<'gc> Loader<'gc> {
let mut movie = SwfMovie::from_data(&response.body, spoofed_or_swf_url, None)?;
on_metadata(movie.header());
movie.append_parameters(parameters);
player.lock().unwrap().set_root_movie(movie);
player.lock().unwrap().mutate_with_update_context(|uc| {
uc.set_root_movie(movie);
});
Ok(())
})
}
@ -893,19 +907,23 @@ impl<'gc> Loader<'gc> {
ContentType::sniff(&response.body).expect(ContentType::Swf)?;
let movie = SwfMovie::from_data(&response.body, response.url, loader_url)?;
player.lock().unwrap().set_root_movie(movie);
player.lock().unwrap().mutate_with_update_context(|uc| {
uc.set_root_movie(movie);
});
return Ok(());
}
Ok(response) => {
Loader::movie_loader_data(
handle,
player,
&response.body,
response.url,
response.status,
response.redirected,
loader_url,
)?;
player.lock().unwrap().mutate_with_update_context(|uc| {
Loader::movie_loader_data(
handle,
uc,
&response.body,
response.url,
response.status,
response.redirected,
loader_url,
)
})?;
}
Err(response) => {
tracing::error!(
@ -939,57 +957,45 @@ impl<'gc> Loader<'gc> {
})
}
fn movie_loader_bytes(
&mut self,
player: Weak<Mutex<Player>>,
pub fn movie_loader_bytes(
handle: Handle,
uc: &mut UpdateContext<'_, 'gc>,
bytes: Vec<u8>,
) -> OwnedFuture<(), Error> {
let handle = match self {
Loader::Movie { self_handle, .. } => self_handle.expect("Loader not self-introduced"),
_ => return Box::pin(async { Err(Error::NotMovieLoader) }),
) -> Result<(), Error> {
let clip = match uc.load_manager.get_loader(handle) {
Some(Loader::Movie { target_clip, .. }) => *target_clip,
None => return Err(Error::Cancelled),
_ => unreachable!(),
};
let player = player
.upgrade()
.expect("Could not upgrade weak reference to player");
let replacing_root_movie = uc
.stage
.root_clip()
.map(|root| DisplayObject::ptr_eq(clip, root))
.unwrap_or(false);
Box::pin(async move {
let mut replacing_root_movie = false;
player.lock().unwrap().update(|uc| -> Result<(), Error> {
let clip = match uc.load_manager.get_loader(handle) {
Some(Loader::Movie { target_clip, .. }) => *target_clip,
None => return Err(Error::Cancelled),
_ => unreachable!(),
};
replacing_root_movie = uc
.stage
.root_clip()
.map(|root| DisplayObject::ptr_eq(clip, root))
.unwrap_or(false);
if let Some(mc) = clip.as_movie_clip() {
if !mc.movie().is_action_script_3() {
mc.avm1_unload(uc);
}
mc.replace_with_movie(uc, None, false, None);
}
// NOTE: We do NOT call `movie_loader_start` as `loadBytes` does
// not emit `open`
Ok(())
})?;
if replacing_root_movie {
ContentType::sniff(&bytes).expect(ContentType::Swf)?;
let movie = SwfMovie::from_data(&bytes, "file:///".into(), None)?;
player.lock().unwrap().set_root_movie(movie);
return Ok(());
if let Some(mc) = clip.as_movie_clip() {
if !mc.movie().is_action_script_3() {
mc.avm1_unload(uc);
}
mc.replace_with_movie(uc, None, false, None);
}
Loader::movie_loader_data(handle, player, &bytes, "file:///".into(), 0, false, None)
})
if replacing_root_movie {
ContentType::sniff(&bytes).expect(ContentType::Swf)?;
let movie = SwfMovie::from_data(&bytes, "file:///".into(), None)?;
avm2_stub_method_context!(
uc,
"flash.display.Loader",
"loadBytes",
"replacing root movie"
);
uc.set_root_movie(movie);
return Ok(());
}
Loader::movie_loader_data(handle, uc, &bytes, "file:///".into(), 0, false, None)
}
fn form_loader(
@ -1593,7 +1599,7 @@ impl<'gc> Loader<'gc> {
/// Load data into a movie loader.
fn movie_loader_data(
handle: Handle,
player: Arc<Mutex<Player>>,
uc: &mut UpdateContext<'_, 'gc>,
data: &[u8],
url: String,
status: u16,
@ -1606,143 +1612,172 @@ impl<'gc> Loader<'gc> {
if sniffed_type == ContentType::Unknown {
if let Ok(data) = extract_swz(data) {
return Self::movie_loader_data(
handle, player, &data, url, status, redirected, loader_url,
handle, uc, &data, url, status, redirected, loader_url,
);
}
}
player.lock().unwrap().update(|uc| {
let (clip, vm_data) = match uc.load_manager.get_loader(handle) {
Some(Loader::Movie {
target_clip,
vm_data,
..
}) => (*target_clip, *vm_data),
None => return Err(Error::Cancelled),
_ => unreachable!(),
};
let mut activation = Avm2Activation::from_nothing(uc.reborrow());
let domain = if let MovieLoaderVMData::Avm2 {
context,
default_domain,
let (clip, vm_data, from_bytes) = match uc.load_manager.get_loader(handle) {
Some(Loader::Movie {
target_clip,
vm_data,
from_bytes,
..
} = vm_data
{
let domain = context
.and_then(|o| {
o.get_public_property("applicationDomain", &mut activation)
.ok()
})
.and_then(|v| v.coerce_to_object(&mut activation).ok())
.and_then(|o| o.as_application_domain())
.unwrap_or_else(|| {
let parent_domain = default_domain;
Avm2Domain::movie_domain(&mut activation, parent_domain)
});
domain
} else {
// This is necessary when the MovieLoaderData is AVM1,
// but loaded an AVM2 SWF (mixed AVM).
activation.context.avm2.stage_domain()
};
}) => (*target_clip, *vm_data, *from_bytes),
None => return Err(Error::Cancelled),
_ => unreachable!(),
};
let movie = match sniffed_type {
ContentType::Swf => Arc::new(SwfMovie::from_data(data, url.clone(), loader_url)?),
ContentType::Gif | ContentType::Jpeg | ContentType::Png => {
Arc::new(SwfMovie::from_loaded_image(url.clone(), length))
}
ContentType::Unknown => Arc::new(SwfMovie::error_movie(url.clone())),
};
let mut activation = Avm2Activation::from_nothing(uc.reborrow());
match activation.context.load_manager.get_loader_mut(handle) {
Some(Loader::Movie {
movie: old,
loader_status,
..
}) => {
*loader_status = LoaderStatus::Parsing;
*old = Some(movie.clone())
}
_ => unreachable!(),
};
let domain = if let MovieLoaderVMData::Avm2 {
context,
default_domain,
..
} = vm_data
{
let domain = context
.and_then(|o| {
o.get_public_property("applicationDomain", &mut activation)
.ok()
})
.and_then(|v| v.coerce_to_object(&mut activation).ok())
.and_then(|o| o.as_application_domain())
.unwrap_or_else(|| {
let parent_domain = default_domain;
Avm2Domain::movie_domain(&mut activation, parent_domain)
});
domain
} else {
// This is necessary when the MovieLoaderData is AVM1,
// but loaded an AVM2 SWF (mixed AVM).
activation.context.avm2.stage_domain()
};
if let MovieLoaderVMData::Avm2 { loader_info, .. } = vm_data {
loader_info
.as_loader_info_object()
.unwrap()
.set_content_type(sniffed_type, activation.context.gc_context);
let fake_movie = Arc::new(SwfMovie::empty_fake_compressed_len(
activation.context.swf.version(),
length,
));
// Expose 'bytesTotal' (via the fake movie) during the first 'progress' event,
// but nothing else (in particular, the `parameters` and `url` properties are not set
// to their real values)
loader_info
.as_loader_info_object()
.unwrap()
.set_loader_stream(
LoaderStream::NotYetLoaded(fake_movie, Some(clip), false),
activation.context.gc_context,
);
// Flash always fires an initial 'progress' event with
// bytesLoaded=0 and bytesTotal set to the proper value.
// This only seems to happen for an AVM2 event handler
Loader::movie_loader_progress(handle, &mut activation.context, 0, length)?;
// Update the LoaderStream - we now have a real SWF movie and a real target clip
// This is intentionally set *after* the first 'progress' event, to match Flash's behavior
// (`LoaderInfo.parameters` is always empty during the first 'progress' event)
loader_info
.as_loader_info_object()
.unwrap()
.set_loader_stream(
LoaderStream::NotYetLoaded(movie.clone(), Some(clip), false),
activation.context.gc_context,
);
let movie = match sniffed_type {
ContentType::Swf => {
Arc::new(SwfMovie::from_data(data, url.clone(), loader_url.clone())?)
}
ContentType::Gif | ContentType::Jpeg | ContentType::Png => {
Arc::new(SwfMovie::from_loaded_image(url.clone(), length))
}
ContentType::Unknown => Arc::new(SwfMovie::error_movie(url.clone())),
};
match sniffed_type {
ContentType::Swf => {
let library = activation
.context
.library
.library_for_movie_mut(movie.clone());
match activation.context.load_manager.get_loader_mut(handle) {
Some(Loader::Movie {
movie: old,
loader_status,
..
}) => {
*loader_status = LoaderStatus::Parsing;
*old = Some(movie.clone())
}
_ => unreachable!(),
};
library.set_avm2_domain(domain);
if let MovieLoaderVMData::Avm2 { loader_info, .. } = vm_data {
loader_info
.as_loader_info_object()
.unwrap()
.set_content_type(sniffed_type, activation.context.gc_context);
let fake_movie = Arc::new(SwfMovie::empty_fake_compressed_len(
activation.context.swf.version(),
length,
));
if let Some(mc) = clip.as_movie_clip() {
let loader_info =
if let MovieLoaderVMData::Avm2 { loader_info, .. } = vm_data {
Some(*loader_info.as_loader_info_object().unwrap())
} else {
None
};
// Expose 'bytesTotal' (via the fake movie) during the first 'progress' event,
// but nothing else (in particular, the `parameters` and `url` properties are not set
// to their real values)
loader_info
.as_loader_info_object()
.unwrap()
.set_loader_stream(
LoaderStream::NotYetLoaded(fake_movie, Some(clip), false),
activation.context.gc_context,
);
// Store our downloaded `SwfMovie` into our target `MovieClip`,
// and initialize it.
// Flash always fires an initial 'progress' event with
// bytesLoaded=0 and bytesTotal set to the proper value.
// This only seems to happen for an AVM2 event handler
Loader::movie_loader_progress(handle, &mut activation.context, 0, length)?;
mc.replace_with_movie(
&mut activation.context,
Some(movie.clone()),
true,
loader_info,
);
// Update the LoaderStream - we now have a real SWF movie and a real target clip
// This is intentionally set *after* the first 'progress' event, to match Flash's behavior
// (`LoaderInfo.parameters` is always empty during the first 'progress' event)
loader_info
.as_loader_info_object()
.unwrap()
.set_loader_stream(
LoaderStream::NotYetLoaded(movie.clone(), Some(clip), false),
activation.context.gc_context,
);
}
if matches!(vm_data, MovieLoaderVMData::Avm2 { .. })
&& !movie.is_action_script_3()
{
// When an AVM2 movie loads an AVM1 movie, we need to call `post_instantiation` here.
mc.post_instantiation(uc, None, Instantiator::Movie, false);
match sniffed_type {
ContentType::Swf => {
let library = activation
.context
.library
.library_for_movie_mut(movie.clone());
mc.set_depth(uc.gc_context, LOADER_INSERTED_AVM1_DEPTH);
}
library.set_avm2_domain(domain);
if let Some(mc) = clip.as_movie_clip() {
let loader_info = if let MovieLoaderVMData::Avm2 { loader_info, .. } = vm_data {
Some(*loader_info.as_loader_info_object().unwrap())
} else {
None
};
// Store our downloaded `SwfMovie` into our target `MovieClip`,
// and initialize it.
mc.replace_with_movie(
&mut activation.context,
Some(movie.clone()),
true,
loader_info,
);
if matches!(vm_data, MovieLoaderVMData::Avm2 { .. })
&& !movie.is_action_script_3()
{
// When an AVM2 movie loads an AVM1 movie, we need to call `post_instantiation` here.
mc.post_instantiation(uc, None, Instantiator::Movie, false);
mc.set_depth(uc.gc_context, LOADER_INSERTED_AVM1_DEPTH);
}
// NOTE: Certain tests specifically expect small files to preload immediately
if from_bytes {
mc.preload(uc, &mut ExecutionLimit::none());
Loader::movie_loader_progress(
handle,
uc,
mc.compressed_loaded_bytes() as usize,
mc.compressed_total_bytes() as usize,
)?;
let cb: Box<dyn FnOnce(&mut UpdateContext<'_, '_>)> = Box::new(move |uc| {
let target_clip = match uc.load_manager.get_loader(handle) {
Some(Loader::Movie { target_clip, .. }) => *target_clip,
None => return,
_ => unreachable!(),
};
if let Err(e) = Loader::movie_loader_complete(
handle,
uc,
Some(target_clip),
0,
false,
) {
tracing::error!("Error finishing loading of Loader.loadBytes movie {target_clip:?}: {e:?}");
}
});
uc.post_frame_callbacks.push(cb);
}
}
// NOTE: Certain tests specifically expect small files to preload immediately
if !from_bytes {
Loader::preload_tick(
handle,
uc,
@ -1750,103 +1785,96 @@ impl<'gc> Loader<'gc> {
status,
redirected,
)?;
};
return Ok(());
}
ContentType::Gif | ContentType::Jpeg | ContentType::Png => {
let library = activation.context.library.library_for_movie_mut(movie);
return Ok(());
}
ContentType::Gif | ContentType::Jpeg | ContentType::Png => {
let library = activation.context.library.library_for_movie_mut(movie);
library.set_avm2_domain(domain);
library.set_avm2_domain(domain);
// This will construct AVM2-side objects even under AVM1, but it doesn't matter,
// since Bitmap and BitmapData never have AVM1-side objects.
let bitmap = ruffle_render::utils::decode_define_bits_jpeg(data, None)?;
// This will construct AVM2-side objects even under AVM1, but it doesn't matter,
// since Bitmap and BitmapData never have AVM1-side objects.
let bitmap = ruffle_render::utils::decode_define_bits_jpeg(data, None)?;
let transparency = true;
let bitmap_data = BitmapData::new_with_pixels(
bitmap.width(),
bitmap.height(),
transparency,
bitmap.as_colors().map(Color::from).collect(),
);
let bitmapdata_wrapper = BitmapDataWrapper::new(GcCell::new(
activation.context.gc_context,
bitmap_data,
));
let bitmapdata_class = activation.context.avm2.classes().bitmapdata;
let bitmapdata_avm2 = BitmapDataObject::from_bitmap_data_internal(
&mut activation,
bitmapdata_wrapper,
bitmapdata_class,
)
let transparency = true;
let bitmap_data = BitmapData::new_with_pixels(
bitmap.width(),
bitmap.height(),
transparency,
bitmap.as_colors().map(Color::from).collect(),
);
let bitmapdata_wrapper =
BitmapDataWrapper::new(GcCell::new(activation.context.gc_context, bitmap_data));
let bitmapdata_class = activation.context.avm2.classes().bitmapdata;
let bitmapdata_avm2 = BitmapDataObject::from_bitmap_data_internal(
&mut activation,
bitmapdata_wrapper,
bitmapdata_class,
)
.unwrap();
let bitmap_avm2 = activation
.avm2()
.classes()
.bitmap
.construct(&mut activation, &[bitmapdata_avm2.into()])
.unwrap();
let bitmap_obj = bitmap_avm2.as_display_object().unwrap();
let bitmap_avm2 = activation
.avm2()
.classes()
.bitmap
.construct(&mut activation, &[bitmapdata_avm2.into()])
.unwrap();
let bitmap_obj = bitmap_avm2.as_display_object().unwrap();
Loader::movie_loader_progress(handle, &mut activation.context, length, length)?;
Loader::movie_loader_complete(
handle,
&mut activation.context,
Some(bitmap_obj),
status,
redirected,
)?;
}
ContentType::Unknown => {
match vm_data {
MovieLoaderVMData::Avm1 { .. } => {
// If the file is no valid supported file, the MovieClip enters the error state
if let Some(mut mc) = clip.as_movie_clip() {
Loader::load_error_swf(
&mut mc,
&mut activation.context,
url.clone(),
);
}
// AVM1 fires the event with the current and total length as 0
Loader::movie_loader_progress(handle, &mut activation.context, 0, 0)?;
Loader::movie_loader_complete(
handle,
&mut activation.context,
None,
status,
redirected,
)?;
}
MovieLoaderVMData::Avm2 { .. } => {
Loader::movie_loader_progress(
handle,
&mut activation.context,
length,
length,
)?;
Loader::movie_loader_error(
handle,
uc,
AvmString::new_utf8(
uc.gc_context,
&format!(
"Error #2124: Loaded file is an unknown type. URL: {url}"
),
),
status,
redirected,
url,
)?;
Loader::movie_loader_progress(handle, &mut activation.context, length, length)?;
Loader::movie_loader_complete(
handle,
&mut activation.context,
Some(bitmap_obj),
status,
redirected,
)?;
}
ContentType::Unknown => {
match vm_data {
MovieLoaderVMData::Avm1 { .. } => {
// If the file is no valid supported file, the MovieClip enters the error state
if let Some(mut mc) = clip.as_movie_clip() {
Loader::load_error_swf(&mut mc, &mut activation.context, url.clone());
}
// AVM1 fires the event with the current and total length as 0
Loader::movie_loader_progress(handle, &mut activation.context, 0, 0)?;
Loader::movie_loader_complete(
handle,
&mut activation.context,
None,
status,
redirected,
)?;
}
MovieLoaderVMData::Avm2 { .. } => {
Loader::movie_loader_progress(
handle,
&mut activation.context,
length,
length,
)?;
Loader::movie_loader_error(
handle,
uc,
AvmString::new_utf8(
uc.gc_context,
&format!("Error #2124: Loaded file is an unknown type. URL: {url}"),
),
status,
redirected,
url,
)?;
}
}
}
}
Ok(())
}) //TODO: content sniffing errors need to be reported somehow
//TODO: content sniffing errors need to be reported somehow
Ok(())
}
/// Report a movie loader progress event to script code.
@ -2005,6 +2033,16 @@ impl<'gc> Loader<'gc> {
.as_container()
.unwrap();
// This isn't completely correct - the 'large_preload' test observes the child
// being set after an 'enterFrame' call. However, our current logic should
// hopefully be good enough.
avm2_stub_method!(
activation,
"flash.display.Loader",
"load",
"addChild at the correct time"
);
// Note that we do *not* use the 'addChild' method here:
// Per the flash docs, our implementation always throws
// an 'unsupported' error. Also, the AVM2 side of our movie

View File

@ -5,11 +5,9 @@ use crate::avm1::Object;
use crate::avm1::SystemProperties;
use crate::avm1::VariableDumper;
use crate::avm1::{Activation, ActivationIdentifier};
use crate::avm1::{ScriptObject, TObject, Value};
use crate::avm2::api_version::ApiVersion;
use crate::avm1::{TObject, Value};
use crate::avm2::{
object::LoaderInfoObject, object::TObject as _, Activation as Avm2Activation, Avm2, CallStack,
Object as Avm2Object,
object::TObject as _, Activation as Avm2Activation, Avm2, CallStack, Object as Avm2Object,
};
use crate::backend::ui::FontDefinition;
use crate::backend::{
@ -28,7 +26,7 @@ use crate::context_menu::{
};
use crate::display_object::Avm2MousePick;
use crate::display_object::{
EditText, InteractiveObject, MovieClip, Stage, StageAlign, StageDisplayState, StageScaleMode,
EditText, InteractiveObject, Stage, StageAlign, StageDisplayState, StageScaleMode,
TInteractiveObject, WindowMode,
};
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, MouseButton, PlayerEvent};
@ -66,7 +64,7 @@ use std::rc::{Rc, Weak as RcWeak};
use std::str::FromStr;
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;
use tracing::{info, instrument};
use tracing::instrument;
use web_time::Instant;
/// The newest known Flash Player version, serves as a default to
@ -178,6 +176,10 @@ struct GcRootData<'gc> {
/// Dynamic root for allowing handles to GC objects to exist outside of the GC.
dynamic_root: DynamicRootSet<'gc>,
#[collect(require_static)]
#[allow(clippy::type_complexity)]
post_frame_callbacks: Vec<Box<dyn FnOnce(&mut UpdateContext<'_, '_>) + 'static>>,
}
impl<'gc> GcRootData<'gc> {
@ -206,6 +208,7 @@ impl<'gc> GcRootData<'gc> {
&mut Sockets<'gc>,
&mut NetConnections<'gc>,
&mut LocalConnections<'gc>,
&mut Vec<Box<dyn FnOnce(&mut UpdateContext<'_, '_>) + 'static>>,
DynamicRootSet<'gc>,
) {
(
@ -228,6 +231,7 @@ impl<'gc> GcRootData<'gc> {
&mut self.sockets,
&mut self.net_connections,
&mut self.local_connections,
&mut self.post_frame_callbacks,
self.dynamic_root,
)
}
@ -375,117 +379,6 @@ impl Player {
});
}
/// Change the root movie.
///
/// This should only be called once, as it makes no attempt at removing
/// previous stage contents. If you need to load a new root movie, you
/// should destroy and recreate the player instance.
pub fn set_root_movie(&mut self, movie: SwfMovie) {
if !self.forced_frame_rate {
self.frame_rate = movie.frame_rate().into();
}
info!(
"Loaded SWF version {}, resolution {}x{} @ {} FPS",
movie.version(),
movie.width(),
movie.height(),
self.frame_rate(),
);
self.swf = Arc::new(movie);
self.instance_counter = 0;
self.mutate_with_update_context(|context| {
if context.swf.is_action_script_3() {
context.avm2.root_api_version = ApiVersion::from_swf_version(
context.swf.version(),
context.avm2.player_runtime,
)
.unwrap_or_else(|| panic!("Unknown SWF version {}", context.swf.version()));
}
context.stage.set_movie_size(
context.gc_context,
context.swf.width().to_pixels() as u32,
context.swf.height().to_pixels() as u32,
);
context
.stage
.set_movie(context.gc_context, context.swf.clone());
let stage_domain = context.avm2.stage_domain();
let mut activation = Avm2Activation::from_domain(context.reborrow(), stage_domain);
activation
.context
.library
.library_for_movie_mut(activation.context.swf.clone())
.set_avm2_domain(stage_domain);
activation.context.ui.set_mouse_visible(true);
let swf = activation.context.swf.clone();
let root: DisplayObject =
MovieClip::player_root_movie(&mut activation, swf.clone()).into();
// The Stage `LoaderInfo` is permanently in the 'not yet loaded' state,
// and has no associated `Loader` instance.
// However, some properties are always accessible, and take their values
// from the root SWF.
let stage_loader_info =
LoaderInfoObject::not_yet_loaded(&mut activation, swf, None, Some(root), true)
.expect("Failed to construct Stage LoaderInfo");
activation
.context
.stage
.set_loader_info(activation.context.gc_context, stage_loader_info);
drop(activation);
root.set_depth(context.gc_context, 0);
let flashvars = if !context.swf.parameters().is_empty() {
let object = ScriptObject::new(context.gc_context, None);
for (key, value) in context.swf.parameters().iter() {
object.define_value(
context.gc_context,
AvmString::new_utf8(context.gc_context, key),
AvmString::new_utf8(context.gc_context, value).into(),
Attribute::empty(),
);
}
Some(object.into())
} else {
None
};
root.post_instantiation(context, flashvars, Instantiator::Movie, false);
root.set_default_root_name(context);
context.stage.replace_at_depth(context, root, 0);
// Set the version parameter on the root.
let mut activation = Activation::from_stub(
context.reborrow(),
ActivationIdentifier::root("[Version Setter]"),
);
let object = root.object().coerce_to_object(&mut activation);
let version_string = activation
.context
.system
.get_version_string(activation.context.avm1);
object.define_value(
activation.context.gc_context,
"$version",
AvmString::new_utf8(activation.context.gc_context, version_string).into(),
Attribute::empty(),
);
let stage = activation.context.stage;
stage.build_matrices(&mut activation.context);
});
self.audio.set_frame_rate(self.frame_rate);
}
/// Get rough estimate of the max # of times we can update the frame.
///
/// In some cases, we might want to update several times in a row.
@ -1673,6 +1566,12 @@ impl Player {
run_all_phases_avm2(context);
Avm1::run_frame(context);
AudioManager::update_sounds(context);
// Only run the current list of callbacks - any callbacks added during callback execution
// will be run at the end of the *next* frame.
for callback in std::mem::take(context.post_frame_callbacks) {
callback(context);
}
});
self.needs_render = true;
@ -1916,12 +1815,13 @@ impl Player {
sockets,
net_connections,
local_connections,
post_frame_callbacks,
dynamic_root,
) = root_data.update_context_params();
let mut update_context = UpdateContext {
player_version: self.player_version,
swf: &self.swf,
swf: &mut self.swf,
library,
rng: &mut self.rng,
renderer: self.renderer.deref_mut(),
@ -1971,6 +1871,7 @@ impl Player {
net_connections,
local_connections,
dynamic_root,
post_frame_callbacks,
};
let prev_frame_rate = *update_context.frame_rate;
@ -2487,6 +2388,7 @@ impl PlayerBuilder {
net_connections: NetConnections::default(),
local_connections: LocalConnections::empty(),
dynamic_root,
post_frame_callbacks: Vec::new(),
},
),
}
@ -2645,7 +2547,9 @@ impl PlayerBuilder {
if let Some(url) = self.spoofed_url.clone() {
movie.set_url(url);
}
player_lock.set_root_movie(movie);
player_lock.mutate_with_update_context(|context| {
context.set_root_movie(movie);
});
}
drop(player_lock);
player

View File

@ -0,0 +1,113 @@
package {
import flash.display.Stage;
import flash.display.Loader;
import flash.display.Loader;
import flash.net.URLRequest;
import flash.errors.IllegalOperationError;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.ProgressEvent;
import flash.events.HTTPStatusEvent;
import flash.display.MovieClip;
import flash.utils.ByteArray;
public class Main extends MovieClip {
[Embed(source="large_bytearray/test.swf", mimeType="application/octet-stream")]
private static var LOADABLE_SWF:Class;
private var loader: Loader;
public function Main() {
this.setupLoader();
trace("Calling super() in Main()");
super();
trace("Called super() in Main()");
var self = this;
this.addEventListener(Event.ENTER_FRAME, function(e) {
// FIXME - re-enable this when the timing of 'content' being
// set in Ruffle matches Flash Player
//trace("enterFrame in Test: this.loader.content = " + self.loader.content);
});
this.addEventListener(Event.EXIT_FRAME, function(e) {
trace("exitFrame in Test");
});
}
private function dumpParams(obj: Object) {
var out = []
for (var key in obj) {
out.push(key + " = " + obj[key]);
}
out.sort();
trace("Parameters: (len=" + out.length + ")");
trace(out);
}
private function dumpLoader(loader: Loader) {
trace("loader.content = " + loader.content);
trace("loader.contentLoaderInfo.content = " + loader.contentLoaderInfo.content);
trace("loader.contentLoaderInfo.bytesLoaded = " + loader.contentLoaderInfo.bytesLoaded);
trace("loader.contentLoaderInfo.bytesTotal = " + loader.contentLoaderInfo.bytesTotal);
trace("loader.contentLoaderInfo.bytes?.length = " + (loader.contentLoaderInfo.bytes ? loader.contentLoaderInfo.bytes.length : null));
trace("loader.contentLoaderInfo.url = " + loader.contentLoaderInfo.url);
trace("loader.contentLoaderInfo.parameters = " + loader.contentLoaderInfo.parameters);
}
function setupLoader() {
this.loader = new Loader();
this.addChild(loader);
this.dumpLoader(loader);
function dump(event:Event) {
var url = loader.contentLoaderInfo.url;
if (url) {
// This truncates the path to 'file:///' to make the output
// reproducible across deifferent machines
url = url.substr(0, 8);
}
trace("Event " + event + ": "
+ "loader.numChildren = " + loader.numChildren
+ ", loader.content = " + loader.content
+ ", loader.contentLoaderInfo.bytesLoaded = " + loader.contentLoaderInfo.bytesLoaded
+ ", loader.contentLoaderInfo.bytesTotal = " + loader.contentLoaderInfo.bytesTotal
+ ", loader.contentLoaderInfo.bytes.length = " + loader.contentLoaderInfo.bytes.length
+ ", loader.contentLoaderInfo.url = " + url);
}
loader.contentLoaderInfo.addEventListener(Event.OPEN, function(e) {
dump(e);
});
loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, function(e) {
// FIXME - the 'bytesLoaded' and 'bytesTotal' values printed here are wrong,
// as they are not properly implemented in Ruffle. Once the implementation is fixed,
// the output of this test will change.
dump(e);
});
loader.contentLoaderInfo.addEventListener(Event.INIT, function(e) {
trace("loader.contentLoaderInfo === loader.content.loaderInfo : " + (loader.contentLoaderInfo === loader.content.loaderInfo).toString());
trace("loader.contentLoaderInfo.content === loader.content : " + (loader.contentLoaderInfo.content == loader.content).toString());
dump(e);
});
loader.contentLoaderInfo.addEventListener(HTTPStatusEvent.HTTP_STATUS, function(e) {
dump(e);
});
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(e) {
dump(e);
});
loader.loadBytes(ByteArray(new LOADABLE_SWF()));
//loader.load(new URLRequest("./large_bytearray/test.swf"));
trace("Directly after load:");
this.dumpLoader(loader);
return loader;
}
}
}

View File

@ -0,0 +1,104 @@
package {
import flash.display.MovieClip;
import flash.display.Loader;
import flash.events.*;
import flash.utils.ByteArray;
public class LargeSWF extends MovieClip {
private var loader: Loader;
[Embed(source = "data1.bin", mimeType="application/octet-stream")]
public static var DATA1: Class;
[Embed(source = "data2.bin", mimeType="application/octet-stream")]
public static var DATA2: Class;
[Embed(source = "data3.bin", mimeType="application/octet-stream")]
public static var DATA3: Class;
[Embed(source = "data4.bin", mimeType="application/octet-stream")]
public static var DATA4: Class;
[Embed(source = "data5.bin", mimeType="application/octet-stream")]
public static var DATA5: Class;
[Embed(source= "../nested_load/test.swf", mimeType="application/octet-stream")]
public static var NESTED_LOAD: Class;
public function LargeSWF() {
trace("Calling super() in LargeSWF()");
super();
trace("Called super() in LargeSWF()");
trace("Loading ../nested_load/test.swf from bytes");
this.setupLoader();
}
private function dumpLoader(loader: Loader) {
trace("LargeSWF loader.content = " + loader.content);
trace("LargeSWF loader.contentLoaderInfo.content = " + loader.contentLoaderInfo.content);
trace("LargeSWF loader.contentLoaderInfo.bytesLoaded = " + loader.contentLoaderInfo.bytesLoaded);
trace("LargeSWF loader.contentLoaderInfo.bytesTotal = " + loader.contentLoaderInfo.bytesTotal);
trace("LargeSWF loader.contentLoaderInfo.bytes?.length = " + (loader.contentLoaderInfo.bytes ? loader.contentLoaderInfo.bytes.length : null));
trace("LargeSWF loader.contentLoaderInfo.url = " + loader.contentLoaderInfo.url);
trace("LargeSWF loader.contentLoaderInfo.parameters = " + loader.contentLoaderInfo.parameters);
}
private function setupLoader() {
this.loader = new Loader();
this.addChild(loader);
this.dumpLoader(loader);
function dump(event:Event) {
var url = loader.contentLoaderInfo.url;
if (url) {
// This truncates the path to 'file:///' to make the output
// reproducible across deifferent machines
url = url.substr(0, 8);
}
trace("LargeSWF Event " + event + ": "
+ "loader.numChildren = " + loader.numChildren
+ ", loader.content = " + loader.content
+ ", loader.contentLoaderInfo.bytesLoaded = " + loader.contentLoaderInfo.bytesLoaded
+ ", loader.contentLoaderInfo.bytesTotal = " + loader.contentLoaderInfo.bytesTotal
+ ", loader.contentLoaderInfo.bytes.length = " + loader.contentLoaderInfo.bytes.length
+ ", loader.contentLoaderInfo.url = " + url);
}
loader.contentLoaderInfo.addEventListener(Event.OPEN, function(e) {
dump(e);
});
loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, function(e) {
// FIXME - the 'bytesLoaded' and 'bytesTotal' values printed here are wrong,
// as they are not properly implemented in Ruffle. Once the implementation is fixed,
// the output of this test will change.
dump(e);
});
loader.contentLoaderInfo.addEventListener(Event.INIT, function(e) {
trace("LargeSWF loader.contentLoaderInfo === loader.content.loaderInfo : " + (loader.contentLoaderInfo === loader.content.loaderInfo).toString());
trace("LargeSWF loader.contentLoaderInfo.content === loader.content : " + (loader.contentLoaderInfo.content == loader.content).toString());
dump(e);
});
loader.contentLoaderInfo.addEventListener(HTTPStatusEvent.HTTP_STATUS, function(e) {
dump(e);
});
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(e) {
dump(e);
});
loader.loadBytes(ByteArray(new NESTED_LOAD()));
trace("LargeSWF: Directly after load:");
this.dumpLoader(loader);
return loader;
}
}
}

View File

@ -0,0 +1,14 @@
package {
import flash.display.MovieClip;
public class Test extends MovieClip {
public function Test() {
trace("Constucted nested_load/test.swf");
}
}
}

View File

@ -0,0 +1,51 @@
loader.content = null
loader.contentLoaderInfo.content = null
loader.contentLoaderInfo.bytesLoaded = 0
loader.contentLoaderInfo.bytesTotal = 0
loader.contentLoaderInfo.bytes?.length = null
loader.contentLoaderInfo.url = null
loader.contentLoaderInfo.parameters = [object Object]
Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=0 bytesTotal=503316]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 0, loader.contentLoaderInfo.bytesTotal = 503316, loader.contentLoaderInfo.bytes.length = 0, loader.contentLoaderInfo.url = null
Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=503316 bytesTotal=503316]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 503316, loader.contentLoaderInfo.bytesTotal = 503316, loader.contentLoaderInfo.bytes.length = 505316, loader.contentLoaderInfo.url = null
Directly after load:
loader.content = null
loader.contentLoaderInfo.content = null
loader.contentLoaderInfo.bytesLoaded = 503316
loader.contentLoaderInfo.bytesTotal = 503316
loader.contentLoaderInfo.bytes?.length = 505316
loader.contentLoaderInfo.url = null
loader.contentLoaderInfo.parameters = [object Object]
Calling super() in Main()
Called super() in Main()
exitFrame in Test
Calling super() in LargeSWF()
Called super() in LargeSWF()
Loading ../nested_load/test.swf from bytes
LargeSWF loader.content = null
LargeSWF loader.contentLoaderInfo.content = null
LargeSWF loader.contentLoaderInfo.bytesLoaded = 0
LargeSWF loader.contentLoaderInfo.bytesTotal = 0
LargeSWF loader.contentLoaderInfo.bytes?.length = null
LargeSWF loader.contentLoaderInfo.url = null
LargeSWF loader.contentLoaderInfo.parameters = [object Object]
LargeSWF Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=0 bytesTotal=424]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 0, loader.contentLoaderInfo.bytesTotal = 424, loader.contentLoaderInfo.bytes.length = 0, loader.contentLoaderInfo.url = null
LargeSWF Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=424 bytesTotal=424]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 424, loader.contentLoaderInfo.bytesTotal = 424, loader.contentLoaderInfo.bytes.length = 541, loader.contentLoaderInfo.url = null
LargeSWF: Directly after load:
LargeSWF loader.content = null
LargeSWF loader.contentLoaderInfo.content = null
LargeSWF loader.contentLoaderInfo.bytesLoaded = 424
LargeSWF loader.contentLoaderInfo.bytesTotal = 424
LargeSWF loader.contentLoaderInfo.bytes?.length = 541
LargeSWF loader.contentLoaderInfo.url = null
LargeSWF loader.contentLoaderInfo.parameters = [object Object]
exitFrame in Test
loader.contentLoaderInfo === loader.content.loaderInfo : true
loader.contentLoaderInfo.content === loader.content : true
Event [Event type="init" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 1, loader.content = [object LargeSWF], loader.contentLoaderInfo.bytesLoaded = 503316, loader.contentLoaderInfo.bytesTotal = 503316, loader.contentLoaderInfo.bytes.length = 505316, loader.contentLoaderInfo.url = file:///
Event [Event type="complete" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 1, loader.content = [object LargeSWF], loader.contentLoaderInfo.bytesLoaded = 503316, loader.contentLoaderInfo.bytesTotal = 503316, loader.contentLoaderInfo.bytes.length = 505316, loader.contentLoaderInfo.url = file:///
Constucted nested_load/test.swf
exitFrame in Test
LargeSWF loader.contentLoaderInfo === loader.content.loaderInfo : true
LargeSWF loader.contentLoaderInfo.content === loader.content : true
LargeSWF Event [Event type="init" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 1, loader.content = [object Test], loader.contentLoaderInfo.bytesLoaded = 424, loader.contentLoaderInfo.bytesTotal = 424, loader.contentLoaderInfo.bytes.length = 541, loader.contentLoaderInfo.url = file:///
LargeSWF Event [Event type="complete" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 1, loader.content = [object Test], loader.contentLoaderInfo.bytesLoaded = 424, loader.contentLoaderInfo.bytesTotal = 424, loader.contentLoaderInfo.bytes.length = 541, loader.contentLoaderInfo.url = file:///

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_ticks = 3

View File

@ -0,0 +1,113 @@
package {
import flash.display.Stage;
import flash.display.Loader;
import flash.display.Loader;
import flash.net.URLRequest;
import flash.errors.IllegalOperationError;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.ProgressEvent;
import flash.events.HTTPStatusEvent;
import flash.display.MovieClip;
import flash.utils.ByteArray;
public class Test extends MovieClip {
private var loader: Loader;
public function Test() {
this.setupLoader();
trace("Calling super() in Test()");
super();
trace("Called super() in Test()");
var self = this;
this.addEventListener(Event.ENTER_FRAME, function(e) {
// FIXME - re-enable this when the timing of 'content' being
// set in Ruffle matches Flash Player
//trace("enterFrame in Test: this.loader.content = " + self.loader.content);
});
this.addEventListener(Event.EXIT_FRAME, function(e) {
// FIXME - Flash Player has two 'exitFrame' events in a
// row. Until we can figure out why, don't log them.
//trace("exitFrame in Test");
});
}
private function dumpParams(obj: Object) {
var out = []
for (var key in obj) {
out.push(key + " = " + obj[key]);
}
out.sort();
trace("Parameters: (len=" + out.length + ")");
trace(out);
}
private function dumpLoader(loader: Loader) {
trace("loader.content = " + loader.content);
trace("loader.contentLoaderInfo.content = " + loader.contentLoaderInfo.content);
trace("loader.contentLoaderInfo.bytesLoaded = " + loader.contentLoaderInfo.bytesLoaded);
trace("loader.contentLoaderInfo.bytesTotal = " + loader.contentLoaderInfo.bytesTotal);
trace("loader.contentLoaderInfo.bytes?.length = " + (loader.contentLoaderInfo.bytes ? loader.contentLoaderInfo.bytes.length : null));
trace("loader.contentLoaderInfo.url = " + loader.contentLoaderInfo.url);
trace("loader.contentLoaderInfo.parameters = " + loader.contentLoaderInfo.parameters);
}
function setupLoader() {
this.loader = new Loader();
this.addChild(loader);
this.dumpLoader(loader);
function dump(event:Event) {
var url = loader.contentLoaderInfo.url;
if (url) {
// This truncates the path to 'file:///' to make the output
// reproducible across deifferent machines
url = url.substr(0, 8);
}
trace("Event " + event + ": "
+ "loader.numChildren = " + loader.numChildren
+ ", loader.content = " + loader.content
+ ", loader.contentLoaderInfo.bytesLoaded = " + loader.contentLoaderInfo.bytesLoaded
+ ", loader.contentLoaderInfo.bytesTotal = " + loader.contentLoaderInfo.bytesTotal
+ ", loader.contentLoaderInfo.bytes.length = " + loader.contentLoaderInfo.bytes.length
+ ", loader.contentLoaderInfo.url = " + url);
}
loader.contentLoaderInfo.addEventListener(Event.OPEN, function(e) {
dump(e);
});
loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, function(e: ProgressEvent) {
// Note - the exact number of bytes loaded during each event is different
// between Flash and Ruffle, so we only print the start and end events
if (e.bytesLoaded == 0 || e.bytesLoaded == e.bytesTotal) {
dump(e);
}
});
loader.contentLoaderInfo.addEventListener(Event.INIT, function(e) {
trace("loader.contentLoaderInfo === loader.content.loaderInfo : " + (loader.contentLoaderInfo === loader.content.loaderInfo).toString());
trace("loader.contentLoaderInfo.content === loader.content : " + (loader.contentLoaderInfo.content == loader.content).toString());
dump(e);
});
loader.contentLoaderInfo.addEventListener(HTTPStatusEvent.HTTP_STATUS, function(e) {
dump(e);
});
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(e) {
dump(e);
});
loader.load(new URLRequest("./large_bytearray/test.swf"));
trace("Directly after load:");
this.dumpLoader(loader);
return loader;
}
}
}

View File

@ -0,0 +1,30 @@
package {
import flash.display.MovieClip;
public class LargeSWF extends MovieClip {
[Embed(source = "data1.bin", mimeType="application/octet-stream")]
public static var DATA1: Class;
[Embed(source = "data2.bin", mimeType="application/octet-stream")]
public static var DATA2: Class;
[Embed(source = "data3.bin", mimeType="application/octet-stream")]
public static var DATA3: Class;
[Embed(source = "data4.bin", mimeType="application/octet-stream")]
public static var DATA4: Class;
[Embed(source = "data5.bin", mimeType="application/octet-stream")]
public static var DATA5: Class;
public function LargeSWF() {
trace("Calling super() in LargeSWF()");
super();
trace("Called super() in LargeSWF()");
}
}
}

View File

@ -0,0 +1,27 @@
loader.content = null
loader.contentLoaderInfo.content = null
loader.contentLoaderInfo.bytesLoaded = 0
loader.contentLoaderInfo.bytesTotal = 0
loader.contentLoaderInfo.bytes?.length = null
loader.contentLoaderInfo.url = null
loader.contentLoaderInfo.parameters = [object Object]
Directly after load:
loader.content = null
loader.contentLoaderInfo.content = null
loader.contentLoaderInfo.bytesLoaded = 0
loader.contentLoaderInfo.bytesTotal = 0
loader.contentLoaderInfo.bytes?.length = 0
loader.contentLoaderInfo.url = null
loader.contentLoaderInfo.parameters = [object Object]
Calling super() in Test()
Called super() in Test()
Event [Event type="open" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 0, loader.contentLoaderInfo.bytesTotal = 0, loader.contentLoaderInfo.bytes.length = 0, loader.contentLoaderInfo.url = null
Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=0 bytesTotal=501655]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 0, loader.contentLoaderInfo.bytesTotal = 501655, loader.contentLoaderInfo.bytes.length = 0, loader.contentLoaderInfo.url = null
Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=501655 bytesTotal=501655]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 501655, loader.contentLoaderInfo.bytesTotal = 501655, loader.contentLoaderInfo.bytes.length = 502418, loader.contentLoaderInfo.url = null
Calling super() in LargeSWF()
Called super() in LargeSWF()
loader.contentLoaderInfo === loader.content.loaderInfo : true
loader.contentLoaderInfo.content === loader.content : true
Event [Event type="init" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 1, loader.content = [object LargeSWF], loader.contentLoaderInfo.bytesLoaded = 501655, loader.contentLoaderInfo.bytesTotal = 501655, loader.contentLoaderInfo.bytes.length = 502418, loader.contentLoaderInfo.url = file:///
Event [HTTPStatusEvent type="httpStatus" bubbles=false cancelable=false eventPhase=2 status=0 redirected=false responseURL=null]: loader.numChildren = 1, loader.content = [object LargeSWF], loader.contentLoaderInfo.bytesLoaded = 501655, loader.contentLoaderInfo.bytesTotal = 501655, loader.contentLoaderInfo.bytes.length = 502418, loader.contentLoaderInfo.url = file:///
Event [Event type="complete" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 1, loader.content = [object LargeSWF], loader.contentLoaderInfo.bytesLoaded = 501655, loader.contentLoaderInfo.bytesTotal = 501655, loader.contentLoaderInfo.bytes.length = 502418, loader.contentLoaderInfo.url = file:///

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_ticks = 2

View File

@ -1 +1 @@
num_frames = 3
num_frames = 4

View File

@ -408,7 +408,9 @@ impl Ruffle {
self.on_metadata(movie.header());
let _ = self.with_core_mut(move |core| {
core.set_root_movie(movie);
core.update(|uc| {
uc.set_root_movie(movie);
});
});
Ok(())