core: Display image load when using Loader.loadBytes

The handling of images in Loader.loaderBytes is similar to
the handling of SWFs - some of the data is exposed immediately
following the 'Loader.loadBytes' call, but the DisplayObject isn't
loaded until later.
This commit is contained in:
Aaron Hill 2024-01-17 18:39:14 -05:00
parent 173efbb77a
commit 8dbcfe26f9
22 changed files with 496 additions and 131 deletions

View File

@ -6,9 +6,9 @@ use crate::avm2::error::error;
use crate::avm2::object::{DomainObject, LoaderStream, Object, TObject}; use crate::avm2::object::{DomainObject, LoaderStream, Object, TObject};
use crate::avm2::value::Value; use crate::avm2::value::Value;
use crate::avm2::{AvmString, Error}; use crate::avm2::{AvmString, Error};
use crate::avm2_stub_getter;
use crate::display_object::TDisplayObject; use crate::display_object::TDisplayObject;
use crate::loader::ContentType; use crate::loader::ContentType;
use crate::{avm2_stub_getter, avm2_stub_method};
use swf::{write_swf, Compression}; use swf::{write_swf, Compression};
pub use crate::avm2::object::loader_info_allocator; pub use crate::avm2::object::loader_info_allocator;
@ -118,23 +118,26 @@ pub fn get_bytes_loaded<'gc>(
this: Object<'gc>, this: Object<'gc>,
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
if let Some(loader_stream) = this let loader_info = this.as_loader_info_object().unwrap();
.as_loader_info_object() let loader_stream = loader_info.as_loader_stream().unwrap();
.and_then(|o| o.as_loader_stream()) match &*loader_stream {
{ LoaderStream::NotYetLoaded(swf, None, _) => {
match &*loader_stream { if loader_info.errored() {
LoaderStream::NotYetLoaded(_, None, _) => return Ok(0.into()), return Ok(swf.compressed_len().into());
LoaderStream::Swf(_, root) | LoaderStream::NotYetLoaded(_, Some(root), _) => {
return Ok(root
.as_movie_clip()
.map(|mc| mc.compressed_loaded_bytes())
.unwrap_or_default()
.into())
} }
}; Ok(0.into())
}
LoaderStream::Swf(swf, root) | LoaderStream::NotYetLoaded(swf, Some(root), _) => {
if root.as_bitmap().is_some() {
return Ok(swf.compressed_len().into());
}
Ok(root
.as_movie_clip()
.map(|mc| mc.compressed_loaded_bytes())
.unwrap_or_default()
.into())
}
} }
Ok(Value::Undefined)
} }
/// `content` getter /// `content` getter
@ -389,63 +392,77 @@ pub fn get_bytes<'gc>(
this: Object<'gc>, this: Object<'gc>,
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
if let Some(loader_stream) = this let loader_info = this.as_loader_info_object().unwrap();
.as_loader_info_object() let loader_stream = loader_info.as_loader_stream().unwrap();
.and_then(|o| o.as_loader_stream()) let (root, dobj) = match &*loader_stream {
{ LoaderStream::NotYetLoaded(_, None, _) => {
let root = match &*loader_stream { if loader_info.errored() {
LoaderStream::NotYetLoaded(_, None, _) => { return Ok(activation
// If we haven't even started loading yet (we have no root clip), .context
// then return null. FIXME - we should probably store the ByteArray .avm2
// in a field, and initialize it when we start loading. .classes()
return Ok(Value::Null); .bytearray
.construct(activation, &[])?
.into());
} }
LoaderStream::NotYetLoaded(swf, Some(_), _) => swf, // If we haven't even started loading yet (we have no root clip),
LoaderStream::Swf(root, _) => root, // then return null. FIXME - we should probably store the ByteArray
}; // in a field, and initialize it when we start loading.
return Ok(Value::Null);
let ba_class = activation.context.avm2.classes().bytearray;
let ba = ba_class.construct(activation, &[])?;
if root.data().is_empty() {
return Ok(ba.into());
} }
LoaderStream::NotYetLoaded(swf, Some(dobj), _) => (swf, dobj),
LoaderStream::Swf(root, dobj) => (root, dobj),
};
let mut ba_write = ba.as_bytearray_mut(activation.context.gc_context).unwrap(); let ba_class = activation.context.avm2.classes().bytearray;
let ba = ba_class.construct(activation, &[])?;
// First, write a fake header corresponding to an
// uncompressed SWF
let mut header = root.header().swf_header().clone();
header.compression = Compression::None;
write_swf(&header, &[], &mut *ba_write).unwrap();
// `swf` always writes an implicit end tag, let's cut that
// off. We scroll back 2 bytes before writing the actual
// datastream as it is guaranteed to at least be as long as
// the implicit end tag we want to get rid of.
let correct_header_length = ba_write.len() - 2;
ba_write.set_position(correct_header_length);
ba_write
.write_bytes(root.data())
.map_err(|e| e.to_avm(activation))?;
// `swf` wrote the wrong length (since we wrote the data
// ourselves), so we need to overwrite it ourselves.
ba_write.set_position(4);
ba_write.set_endian(Endian::Little);
ba_write
.write_unsigned_int((root.data().len() + correct_header_length) as u32)
.map_err(|e| e.to_avm(activation))?;
// Finally, reset the array to the correct state.
ba_write.set_position(0);
ba_write.set_endian(Endian::Big);
if root.data().is_empty() {
return Ok(ba.into()); return Ok(ba.into());
} }
Ok(Value::Undefined) if dobj.as_bitmap().is_some() {
// TODO - we need to construct a fake SWF that contains a 'Define' tag for the image data.
avm2_stub_method!(
activation,
"flash.display.LoaderInfo",
"bytes",
"with image"
);
}
let mut ba_write = ba.as_bytearray_mut(activation.context.gc_context).unwrap();
// First, write a fake header corresponding to an
// uncompressed SWF
let mut header = root.header().swf_header().clone();
header.compression = Compression::None;
write_swf(&header, &[], &mut *ba_write).unwrap();
// `swf` always writes an implicit end tag, let's cut that
// off. We scroll back 2 bytes before writing the actual
// datastream as it is guaranteed to at least be as long as
// the implicit end tag we want to get rid of.
let correct_header_length = ba_write.len() - 2;
ba_write.set_position(correct_header_length);
ba_write
.write_bytes(root.data())
.map_err(|e| e.to_avm(activation))?;
// `swf` wrote the wrong length (since we wrote the data
// ourselves), so we need to overwrite it ourselves.
ba_write.set_position(4);
ba_write.set_endian(Endian::Little);
ba_write
.write_unsigned_int((root.data().len() + correct_header_length) as u32)
.map_err(|e| e.to_avm(activation))?;
// Finally, reset the array to the correct state.
ba_write.set_position(0);
ba_write.set_endian(Endian::Big);
Ok(ba.into())
} }
/// `loader` getter /// `loader` getter

View File

@ -57,6 +57,15 @@ pub enum LoaderStream<'gc> {
Swf(Arc<SwfMovie>, DisplayObject<'gc>), Swf(Arc<SwfMovie>, DisplayObject<'gc>),
} }
impl<'gc> LoaderStream<'gc> {
pub fn movie(&self) -> &Arc<SwfMovie> {
match self {
LoaderStream::NotYetLoaded(movie, _, _) => movie,
LoaderStream::Swf(movie, _) => movie,
}
}
}
/// An Object which represents a loadable object, such as a SWF movie or image /// An Object which represents a loadable object, such as a SWF movie or image
/// resource. /// resource.
#[derive(Collect, Clone, Copy)] #[derive(Collect, Clone, Copy)]
@ -103,6 +112,8 @@ pub struct LoaderInfoObjectData<'gc> {
#[collect(require_static)] #[collect(require_static)]
content_type: ContentType, content_type: ContentType,
errored: bool,
} }
impl<'gc> LoaderInfoObject<'gc> { impl<'gc> LoaderInfoObject<'gc> {
@ -139,6 +150,7 @@ impl<'gc> LoaderInfoObject<'gc> {
.construct(activation, &[])?, .construct(activation, &[])?,
cached_avm1movie: None, cached_avm1movie: None,
content_type: ContentType::Swf, content_type: ContentType::Swf,
errored: false,
}, },
)) ))
.into(); .into();
@ -185,6 +197,7 @@ impl<'gc> LoaderInfoObject<'gc> {
.construct(activation, &[])?, .construct(activation, &[])?,
cached_avm1movie: None, cached_avm1movie: None,
content_type: ContentType::Unknown, content_type: ContentType::Unknown,
errored: false,
}, },
)) ))
.into(); .into();
@ -218,6 +231,14 @@ impl<'gc> LoaderInfoObject<'gc> {
} }
} }
pub fn set_errored(&self, val: bool, mc: &Mutation<'gc>) {
self.0.write(mc).errored = val;
}
pub fn errored(&self) -> bool {
self.0.read().errored
}
pub fn fire_init_and_complete_events( pub fn fire_init_and_complete_events(
&self, &self,
context: &mut UpdateContext<'_, 'gc>, context: &mut UpdateContext<'_, 'gc>,
@ -321,6 +342,7 @@ impl<'gc> LoaderInfoObject<'gc> {
let empty_swf = Arc::new(SwfMovie::empty(activation.context.swf.version())); let empty_swf = Arc::new(SwfMovie::empty(activation.context.swf.version()));
let loader_stream = LoaderStream::NotYetLoaded(empty_swf, None, false); let loader_stream = LoaderStream::NotYetLoaded(empty_swf, None, false);
self.set_loader_stream(loader_stream, activation.context.gc_context); self.set_loader_stream(loader_stream, activation.context.gc_context);
self.set_errored(false, activation.context.gc_context);
} }
} }

View File

@ -29,6 +29,7 @@ use crate::loader::LoadManager;
use crate::local_connection::LocalConnections; use crate::local_connection::LocalConnections;
use crate::net_connection::NetConnections; use crate::net_connection::NetConnections;
use crate::player::Player; use crate::player::Player;
use crate::player::PostFrameCallback;
use crate::prelude::*; use crate::prelude::*;
use crate::socket::Sockets; use crate::socket::Sockets;
use crate::streams::StreamManager; use crate::streams::StreamManager;
@ -252,7 +253,7 @@ pub struct UpdateContext<'a, 'gc> {
/// These functions are run at the end of each frame execution. /// These functions are run at the end of each frame execution.
/// Currently, this is just used for handling `Loader.loadBytes` /// Currently, this is just used for handling `Loader.loadBytes`
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
pub post_frame_callbacks: &'a mut Vec<Box<dyn FnOnce(&mut UpdateContext<'_, '_>)>>, pub post_frame_callbacks: &'a mut Vec<PostFrameCallback<'gc>>,
} }
/// Convenience methods for controlling audio. /// Convenience methods for controlling audio.

View File

@ -348,6 +348,9 @@ impl<'gc> TDisplayObject<'gc> for Bitmap<'gc> {
.expect("can't throw from post_instantiation -_-"); .expect("can't throw from post_instantiation -_-");
self.0.write(mc).avm2_object = Some(bitmap.into()); self.0.write(mc).avm2_object = Some(bitmap.into());
// Use a dummy BitmapData when calling the constructor on the user subclass
// - the constructor should see an invalid BitmapData before calling 'super',
// even if it's linked to an image.
let bitmap_data_obj = Avm2BitmapDataObject::from_bitmap_data_internal( let bitmap_data_obj = Avm2BitmapDataObject::from_bitmap_data_internal(
&mut activation, &mut activation,
BitmapDataWrapper::dummy(mc), BitmapDataWrapper::dummy(mc),
@ -355,6 +358,14 @@ impl<'gc> TDisplayObject<'gc> for Bitmap<'gc> {
) )
.expect("can't throw from post_instantiation -_-"); .expect("can't throw from post_instantiation -_-");
if bitmap_data_obj.as_bitmap_data().unwrap().disposed() {
// Set the real bitmapdata, in case this Bitmap was constructed from a Loader
// (it will have real data that doesn't come from a linked class)
bitmap_data_obj.init_bitmap_data(
activation.context.gc_context,
self.bitmap_data_wrapper(),
);
}
self.set_bitmap_data( self.set_bitmap_data(
&mut activation.context, &mut activation.context,
bitmap_data_obj.as_bitmap_data().unwrap(), bitmap_data_obj.as_bitmap_data().unwrap(),

View File

@ -6,7 +6,7 @@ use crate::avm1::{ExecutionReason, NativeObject};
use crate::avm1::{Object, SoundObject, TObject, Value}; use crate::avm1::{Object, SoundObject, TObject, Value};
use crate::avm2::bytearray::ByteArrayStorage; use crate::avm2::bytearray::ByteArrayStorage;
use crate::avm2::object::{ use crate::avm2::object::{
BitmapDataObject, ByteArrayObject, EventObject as Avm2EventObject, LoaderStream, TObject as _, ByteArrayObject, EventObject as Avm2EventObject, LoaderStream, TObject as _,
}; };
use crate::avm2::{ use crate::avm2::{
Activation as Avm2Activation, Avm2, Domain as Avm2Domain, Object as Avm2Object, Activation as Avm2Activation, Avm2, Domain as Avm2Domain, Object as Avm2Object,
@ -18,12 +18,12 @@ use crate::bitmap::bitmap_data::Color;
use crate::bitmap::bitmap_data::{BitmapData, BitmapDataWrapper}; use crate::bitmap::bitmap_data::{BitmapData, BitmapDataWrapper};
use crate::context::{ActionQueue, ActionType, UpdateContext}; use crate::context::{ActionQueue, ActionType, UpdateContext};
use crate::display_object::{ use crate::display_object::{
DisplayObject, MovieClip, TDisplayObject, TDisplayObjectContainer, TInteractiveObject, Bitmap, DisplayObject, MovieClip, TDisplayObject, TDisplayObjectContainer, TInteractiveObject,
}; };
use crate::events::ClipEvent; use crate::events::ClipEvent;
use crate::frame_lifecycle::catchup_display_object_to_frame; use crate::frame_lifecycle::catchup_display_object_to_frame;
use crate::limits::ExecutionLimit; use crate::limits::ExecutionLimit;
use crate::player::Player; use crate::player::{Player, PostFrameCallback};
use crate::streams::NetStream; use crate::streams::NetStream;
use crate::string::AvmString; use crate::string::AvmString;
use crate::tag_utils::SwfMovie; use crate::tag_utils::SwfMovie;
@ -1688,9 +1688,9 @@ impl<'gc> Loader<'gc> {
.as_loader_info_object() .as_loader_info_object()
.unwrap() .unwrap()
.set_content_type(sniffed_type, activation.context.gc_context); .set_content_type(sniffed_type, activation.context.gc_context);
let fake_movie = Arc::new(SwfMovie::empty_fake_compressed_len( let fake_movie = Arc::new(SwfMovie::fake_with_compressed_len(
activation.context.swf.version(), activation.context.swf.version(),
length, data.len(),
)); ));
// Expose 'bytesTotal' (via the fake movie) during the first 'progress' event, // Expose 'bytesTotal' (via the fake movie) during the first 'progress' event,
@ -1764,23 +1764,16 @@ impl<'gc> Loader<'gc> {
mc.compressed_loaded_bytes() as usize, mc.compressed_loaded_bytes() as usize,
mc.compressed_total_bytes() as usize, mc.compressed_total_bytes() as usize,
)?; )?;
let cb: Box<dyn FnOnce(&mut UpdateContext<'_, '_>)> = Box::new(move |uc| { uc.post_frame_callbacks.push(PostFrameCallback {
let target_clip = match uc.load_manager.get_loader(handle) { callback: Box::new(move |uc, dobj: DisplayObject<'_>| {
Some(Loader::Movie { target_clip, .. }) => *target_clip, if let Err(e) =
None => return, Loader::movie_loader_complete(handle, uc, Some(dobj), 0, false)
_ => unreachable!(), {
}; tracing::error!("Error finishing loading of Loader.loadBytes movie {dobj:?}: {e:?}");
if let Err(e) = Loader::movie_loader_complete( }
handle, }),
uc, data: clip,
Some(target_clip),
0,
false,
) {
tracing::error!("Error finishing loading of Loader.loadBytes movie {target_clip:?}: {e:?}");
}
}); });
uc.post_frame_callbacks.push(cb);
} }
} }
@ -1798,7 +1791,10 @@ impl<'gc> Loader<'gc> {
return Ok(()); return Ok(());
} }
ContentType::Gif | ContentType::Jpeg | ContentType::Png => { ContentType::Gif | ContentType::Jpeg | ContentType::Png => {
let library = activation.context.library.library_for_movie_mut(movie); let library = activation
.context
.library
.library_for_movie_mut(movie.clone());
library.set_avm2_domain(domain); library.set_avm2_domain(domain);
@ -1807,38 +1803,92 @@ impl<'gc> Loader<'gc> {
let bitmap = ruffle_render::utils::decode_define_bits_jpeg(data, None)?; let bitmap = ruffle_render::utils::decode_define_bits_jpeg(data, None)?;
let transparency = true; let transparency = true;
let bitmap_data = BitmapData::new_with_pixels( let bitmap_data = BitmapDataWrapper::new(GcCell::new(
bitmap.width(), activation.context.gc_context,
bitmap.height(), BitmapData::new_with_pixels(
transparency, bitmap.width(),
bitmap.as_colors().map(Color::from).collect(), bitmap.height(),
transparency,
bitmap.as_colors().map(Color::from).collect(),
),
));
let bitmap_dobj = Bitmap::new_with_bitmap_data(
activation.context.gc_context,
0,
bitmap_data,
false,
&activation.caller_movie_or_root(),
); );
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 if let MovieLoaderVMData::Avm2 { loader_info, .. } = vm_data {
.avm2() let fake_movie = Arc::new(SwfMovie::fake_with_compressed_len(
.classes() activation.context.swf.version(),
.bitmap data.len(),
.construct(&mut activation, &[bitmapdata_avm2.into()]) ));
.unwrap();
let bitmap_obj = bitmap_avm2.as_display_object().unwrap(); loader_info
.as_loader_info_object()
.unwrap()
.set_loader_stream(
LoaderStream::NotYetLoaded(fake_movie, Some(bitmap_dobj.into()), false),
activation.context.gc_context,
);
}
Loader::movie_loader_progress(handle, &mut activation.context, length, length)?; Loader::movie_loader_progress(handle, &mut activation.context, length, length)?;
Loader::movie_loader_complete(
handle, if let MovieLoaderVMData::Avm2 { loader_info, .. } = vm_data {
&mut activation.context, let fake_movie = Arc::new(SwfMovie::fake_with_compressed_data(
Some(bitmap_obj), activation.context.swf.version(),
status, data.to_vec(),
redirected, ));
)?;
loader_info
.as_loader_info_object()
.unwrap()
.set_loader_stream(
LoaderStream::NotYetLoaded(fake_movie, Some(bitmap_dobj.into()), false),
activation.context.gc_context,
);
}
if from_bytes {
// Note - flash player seems to delay this for *two* frames for some reason
uc.post_frame_callbacks.push(PostFrameCallback {
callback: Box::new(move |uc, bitmap_obj| {
uc.post_frame_callbacks.push(PostFrameCallback {
callback: Box::new(move |uc, bitmap_obj| {
bitmap_obj.post_instantiation(uc, None, Instantiator::Movie, true);
if let Err(e) = Loader::movie_loader_complete(
handle,
uc,
Some(bitmap_obj),
status,
redirected,
) {
tracing::error!("Error finishing loading of Loader.loadBytes image {bitmap_obj:?}: {e:?}");
}
}),
data: bitmap_obj,
})
}),
data: bitmap_dobj.into(),
});
} else {
bitmap_dobj.post_instantiation(
&mut activation.context,
None,
Instantiator::Movie,
true,
);
Loader::movie_loader_complete(
handle,
&mut activation.context,
Some(bitmap_dobj.into()),
status,
redirected,
)?;
}
} }
ContentType::Unknown => { ContentType::Unknown => {
match vm_data { match vm_data {
@ -1858,20 +1908,35 @@ impl<'gc> Loader<'gc> {
redirected, redirected,
)?; )?;
} }
MovieLoaderVMData::Avm2 { .. } => { MovieLoaderVMData::Avm2 { loader_info, .. } => {
let fake_movie = Arc::new(SwfMovie::fake_with_compressed_len(
activation.context.swf.version(),
data.len(),
));
let loader_info = loader_info.as_loader_info_object().unwrap();
loader_info.set_errored(true, activation.context.gc_context);
loader_info.set_loader_stream(
LoaderStream::NotYetLoaded(fake_movie, None, false),
activation.context.gc_context,
);
Loader::movie_loader_progress( Loader::movie_loader_progress(
handle, handle,
&mut activation.context, &mut activation.context,
length, length,
length, length,
)?; )?;
let mut error = "Error #2124: Loaded file is an unknown type.".to_string();
if !from_bytes {
error += &format!(" URL: {url}");
}
Loader::movie_loader_error( Loader::movie_loader_error(
handle, handle,
uc, uc,
AvmString::new_utf8( AvmString::new_utf8(uc.gc_context, error),
uc.gc_context,
&format!("Error #2124: Loaded file is an unknown type. URL: {url}"),
),
status, status,
redirected, redirected,
url, url,
@ -2097,8 +2162,9 @@ impl<'gc> Loader<'gc> {
// in `MovieClip.on_exit_frame` // in `MovieClip.on_exit_frame`
MovieLoaderVMData::Avm2 { loader_info, .. } => { MovieLoaderVMData::Avm2 { loader_info, .. } => {
let loader_info_obj = loader_info.as_loader_info_object().unwrap(); let loader_info_obj = loader_info.as_loader_info_object().unwrap();
let current_movie = { loader_info_obj.as_loader_stream().unwrap().movie().clone() };
loader_info_obj.set_loader_stream( loader_info_obj.set_loader_stream(
LoaderStream::Swf(target_clip.as_movie_clip().unwrap().movie(), dobj.unwrap()), LoaderStream::Swf(current_movie, dobj.unwrap()),
uc.gc_context, uc.gc_context,
); );

View File

@ -177,9 +177,16 @@ struct GcRootData<'gc> {
/// Dynamic root for allowing handles to GC objects to exist outside of the GC. /// Dynamic root for allowing handles to GC objects to exist outside of the GC.
dynamic_root: DynamicRootSet<'gc>, dynamic_root: DynamicRootSet<'gc>,
post_frame_callbacks: Vec<PostFrameCallback<'gc>>,
}
#[derive(Collect)]
#[collect(no_drop)]
pub struct PostFrameCallback<'gc> {
#[collect(require_static)] #[collect(require_static)]
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
post_frame_callbacks: Vec<Box<dyn FnOnce(&mut UpdateContext<'_, '_>) + 'static>>, pub callback: Box<dyn for<'b> FnOnce(&mut UpdateContext<'_, 'b>, DisplayObject<'b>) + 'static>,
pub data: DisplayObject<'gc>,
} }
impl<'gc> GcRootData<'gc> { impl<'gc> GcRootData<'gc> {
@ -208,7 +215,7 @@ impl<'gc> GcRootData<'gc> {
&mut Sockets<'gc>, &mut Sockets<'gc>,
&mut NetConnections<'gc>, &mut NetConnections<'gc>,
&mut LocalConnections<'gc>, &mut LocalConnections<'gc>,
&mut Vec<Box<dyn FnOnce(&mut UpdateContext<'_, '_>) + 'static>>, &mut Vec<PostFrameCallback<'gc>>,
DynamicRootSet<'gc>, DynamicRootSet<'gc>,
) { ) {
( (
@ -1569,8 +1576,8 @@ impl Player {
// Only run the current list of callbacks - any callbacks added during callback execution // Only run the current list of callbacks - any callbacks added during callback execution
// will be run at the end of the *next* frame. // will be run at the end of the *next* frame.
for callback in std::mem::take(context.post_frame_callbacks) { for cb in std::mem::take(context.post_frame_callbacks) {
callback(context); (cb.callback)(context, cb.data);
} }
}); });

View File

@ -91,15 +91,30 @@ impl SwfMovie {
/// This is used by `Loader` when firing an initial `progress` event: /// This is used by `Loader` when firing an initial `progress` event:
/// `LoaderInfo.bytesTotal` is set to the actual value, but no data is available, /// `LoaderInfo.bytesTotal` is set to the actual value, but no data is available,
/// and `LoaderInfo.parameters` is empty. /// and `LoaderInfo.parameters` is empty.
pub fn empty_fake_compressed_len(swf_version: u8, compressed_len: usize) -> Self { pub fn fake_with_compressed_len(swf_version: u8, compressed_len: usize) -> Self {
Self { Self {
header: HeaderExt::default_with_swf_version(swf_version), header: HeaderExt::default_with_swf_version(swf_version),
data: vec![], compressed_len,
data: Vec::new(),
url: "file:///".into(),
loader_url: None,
parameters: Vec::new(),
encoding: swf::UTF_8,
is_movie: false,
}
}
/// Like `fake_with_compressed_len`, but uses actual data.
/// This is used when loading a Bitmap to expose the underlying content
pub fn fake_with_compressed_data(swf_version: u8, compressed_data: Vec<u8>) -> Self {
Self {
header: HeaderExt::default_with_swf_version(swf_version),
compressed_len: compressed_data.len(),
data: compressed_data,
url: "file:///".into(), url: "file:///".into(),
loader_url: None, loader_url: None,
parameters: Vec::new(), parameters: Vec::new(),
encoding: swf::UTF_8, encoding: swf::UTF_8,
compressed_len,
is_movie: false, is_movie: false,
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,121 @@
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="5000x5000.png", mimeType="application/octet-stream")]
private static var LOADABLE_IMAGE_BYTES: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
// TODO - enable this when we correctly construct a fake SWF for the image
//+ ", 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) {
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_IMAGE_BYTES()));
trace("Directly after load:");
this.dumpLoader(loader);
// Enable this to print and save the fake SWF
/*var bytes = loader.contentLoaderInfo.bytes;
new FileReference().save(bytes);
var readBack = [];
for (var i = 0; i < 64; i++) {
readBack.push(bytes[i]);;
}
trace(readBack);*/
return loader;
}
}
}

View File

@ -0,0 +1,23 @@
loader.content = null
loader.contentLoaderInfo.content = null
loader.contentLoaderInfo.bytesLoaded = 0
loader.contentLoaderInfo.bytesTotal = 0
loader.contentLoaderInfo.url = null
loader.contentLoaderInfo.parameters = [object Object]
Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=0 bytesTotal=28417]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 0, loader.contentLoaderInfo.bytesTotal = 28417, loader.contentLoaderInfo.url = null
Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=28417 bytesTotal=28417]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 28417, loader.contentLoaderInfo.bytesTotal = 28417, loader.contentLoaderInfo.url = null
Directly after load:
loader.content = null
loader.contentLoaderInfo.content = null
loader.contentLoaderInfo.bytesLoaded = 28417
loader.contentLoaderInfo.bytesTotal = 28417
loader.contentLoaderInfo.url = null
loader.contentLoaderInfo.parameters = [object Object]
Calling super() in Main()
Called super() in Main()
exitFrame in Test
exitFrame in Test
Event [Event type="init" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 1, loader.content = [object Bitmap], loader.contentLoaderInfo.bytesLoaded = 28417, loader.contentLoaderInfo.bytesTotal = 28417, loader.contentLoaderInfo.url = file:///
Event [Event type="complete" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 1, loader.content = [object Bitmap], loader.contentLoaderInfo.bytesLoaded = 28417, loader.contentLoaderInfo.bytesTotal = 28417, loader.contentLoaderInfo.url = file:///
exitFrame in Test
exitFrame in Test

View File

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

View File

@ -0,0 +1,47 @@
package {
import flash.display.Loader;
import flash.events.Event;
import flash.events.ProgressEvent;
import flash.events.IOErrorEvent;
import flash.net.URLRequest;
import flash.display.MovieClip;
public class Test {
[Embed(source = "data.txt", mimeType="application/octet-stream")]
public static var DATA: Class;
public function Test(main: MovieClip) {
var loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.OPEN, function(e) {
printEvent(Event.OPEN, e, loader);
});
loader.contentLoaderInfo.addEventListener(Event.INIT, function(e) {
printEvent(Event.INIT, e, loader);
});
loader.contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, function(e) {
printEvent(IOErrorEvent.IO_ERROR, e, loader);
});
loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, function(e) {
printEvent(ProgressEvent.PROGRESS, e, loader);
});
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, function(e) {
printEvent(Event.COMPLETE, e, loader);
});
main.addChild(loader);
trace("Calling loadBytes");
loader.loadBytes(new DATA());
trace("Immediately after loadBytes:");
trace("loader.contentLoaderInfo.bytesLoaded = " + loader.contentLoaderInfo.bytesLoaded);
trace("loader.contentLoaderInfo.bytesTotal = " + loader.contentLoaderInfo.bytesTotal);
trace("loader.contentLoaderInfo.bytes.length = " + loader.contentLoaderInfo.bytes.length);
}
private function printEvent(name: String, event: Event, loader: Loader) {
var eventString = event.toString();
trace("Event: " + name + " event: " + eventString);
// FIXME - print 'bytesLoaded' and 'bytesTotal' when Ruffle properly matches Flash Player
trace("Content: " + loader.content);
trace("Bytes length: " + loader.contentLoaderInfo.bytes.length);
}
}
}

View File

@ -0,0 +1 @@
This is some data

View File

@ -0,0 +1,14 @@
Calling loadBytes
Event: progress event: [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=0 bytesTotal=20]
Content: null
Bytes length: 0
Event: progress event: [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=20 bytesTotal=20]
Content: null
Bytes length: 0
Event: ioError event: [IOErrorEvent type="ioError" bubbles=false cancelable=false eventPhase=2 text="Error #2124: Loaded file is an unknown type."]
Content: null
Bytes length: 0
Immediately after loadBytes:
loader.contentLoaderInfo.bytesLoaded = 20
loader.contentLoaderInfo.bytesTotal = 20
loader.contentLoaderInfo.bytes.length = 0

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -36,9 +36,15 @@
eventString = eventString.substr(0, index) + "file:///[[RUFFLE PATH]]"; eventString = eventString.substr(0, index) + "file:///[[RUFFLE PATH]]";
} }
trace("Event: " + name + " event: " + eventString); trace("Event: " + name + " event: " + eventString);
// FIXME - print 'bytesLoaded' and 'bytesTotal' when Ruffle properly matches Flash Player
trace("Content: " + loader.content); trace("Content: " + loader.content);
trace("Bytes length: " + loader.contentLoaderInfo.bytes.length); trace("Bytes length: " + loader.contentLoaderInfo.bytes.length);
trace("loader.contentLoaderInfo.bytesLoaded = " + loader.contentLoaderInfo.bytesLoaded);
trace("loader.contentLoaderInfo.bytesTotal = " + loader.contentLoaderInfo.bytesTotal);
try {
trace("loader.contentLoaderInfo.frameRate = " + loader.contentLoaderInfo.frameRate);
} catch (e) {
trace("Caught error: " + e);
}
} }
} }
} }

View File

@ -1,12 +1,24 @@
Event: open event: [Event type="open" bubbles=false cancelable=false eventPhase=2] Event: open event: [Event type="open" bubbles=false cancelable=false eventPhase=2]
Content: null Content: null
Bytes length: 0 Bytes length: 0
loader.contentLoaderInfo.bytesLoaded = 0
loader.contentLoaderInfo.bytesTotal = 0
Caught error: Error: Error #2099: The loading object is not sufficiently loaded to provide this information.
Event: progress event: [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=0 bytesTotal=20] Event: progress event: [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=0 bytesTotal=20]
Content: null Content: null
Bytes length: 0 Bytes length: 0
loader.contentLoaderInfo.bytesLoaded = 0
loader.contentLoaderInfo.bytesTotal = 20
Caught error: Error: Error #2099: The loading object is not sufficiently loaded to provide this information.
Event: progress event: [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=20 bytesTotal=20] Event: progress event: [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=20 bytesTotal=20]
Content: null Content: null
Bytes length: 0 Bytes length: 0
loader.contentLoaderInfo.bytesLoaded = 20
loader.contentLoaderInfo.bytesTotal = 20
Caught error: Error: Error #2099: The loading object is not sufficiently loaded to provide this information.
Event: ioError event: [IOErrorEvent type="ioError" bubbles=false cancelable=false eventPhase=2 text="Error #2124: Loaded file is an unknown type. URL: file:///[[RUFFLE PATH]] Event: ioError event: [IOErrorEvent type="ioError" bubbles=false cancelable=false eventPhase=2 text="Error #2124: Loaded file is an unknown type. URL: file:///[[RUFFLE PATH]]
Content: null Content: null
Bytes length: 0 Bytes length: 0
loader.contentLoaderInfo.bytesLoaded = 20
loader.contentLoaderInfo.bytesTotal = 20
Caught error: Error: Error #2099: The loading object is not sufficiently loaded to provide this information.