avm2: Partially implement Loader.load

This PR implements the `Loader.load` method, as well as
the associated `LoaderInfo` properties and events.

We can now load in an external AVM2 SWf: it will be added
as a child of `Loader` object, and will render properly
to the screen.

Limitations:
* The only supported `URLRequest` property is `url`
* `LoaderContext` is not supported at all - we always use the default
  behavior
* Only `Loader.load` is implemented - we do not yet support unloading.
* We fire a plain 'Event' for the 'progress' event, instead of using
  the (not yet implemented) 'ProgressEvent' class

The main changes in this PR are:
* The AVM2 `Loader` class now has an associated display object,
  `LoaderDisplay`. This is basically a stub, and just renders
  its single child (if it exists).
* `LoaderStream::Stage` is renamed to `LoaderStream::NotYetLoaded`.
  This is used for both the `Stage` and an 'uninitialized'
  `Loader.contentLoaderInfo`. In both cases, certain properties throw
  errors, while others return actual values.
* The rust `Loader` manager now handles both AVM1 and AVM2 movie loads.
This commit is contained in:
Aaron Hill 2022-07-24 23:09:05 -05:00 committed by Mike Welsh
parent 60b03bdcf2
commit 86e6983943
33 changed files with 970 additions and 316 deletions

View File

@ -99,7 +99,7 @@ jobs:
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: swf_images name: swf_images
path: tests/**/actual.png path: tests*/**/actual.png
check-required: check-required:
needs: changes needs: changes

View File

@ -1153,7 +1153,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
if url.is_empty() { if url.is_empty() {
//Blank URL on movie loads = unload! //Blank URL on movie loads = unload!
if let Some(mut mc) = level.as_movie_clip() { if let Some(mut mc) = level.as_movie_clip() {
mc.replace_with_movie(&mut self.context, None) mc.replace_with_movie(&mut self.context, None, None)
} }
} else { } else {
let future = self.context.load_manager.load_movie_into_clip( let future = self.context.load_manager.load_movie_into_clip(
@ -1269,7 +1269,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
if url.is_empty() { if url.is_empty() {
// Blank URL on movie loads = unload! // Blank URL on movie loads = unload!
if let Some(mut mc) = clip_target.as_movie_clip() { if let Some(mut mc) = clip_target.as_movie_clip() {
mc.replace_with_movie(&mut self.context, None) mc.replace_with_movie(&mut self.context, None, None)
} }
} else { } else {
let request = self.locals_into_request( let request = self.locals_into_request(
@ -1293,7 +1293,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
if url.is_empty() { if url.is_empty() {
// Blank URL on movie loads = unload! // Blank URL on movie loads = unload!
if let Some(mut mc) = clip_target.as_movie_clip() { if let Some(mut mc) = clip_target.as_movie_clip() {
mc.replace_with_movie(&mut self.context, None) mc.replace_with_movie(&mut self.context, None, None)
} }
} else { } else {
let future = self.context.load_manager.load_movie_into_clip( let future = self.context.load_manager.load_movie_into_clip(

View File

@ -1361,7 +1361,7 @@ fn unload_movie<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
target.unload(&mut activation.context); target.unload(&mut activation.context);
target.replace_with_movie(&mut activation.context, None); target.replace_with_movie(&mut activation.context, None, None);
Ok(Value::Undefined) Ok(Value::Undefined)
} }

View File

@ -10,6 +10,7 @@ use crate::avm1::property_decl::{define_properties_on, Declaration};
use crate::avm1::{ArrayObject, Object, Value}; use crate::avm1::{ArrayObject, Object, Value};
use crate::backend::navigator::Request; use crate::backend::navigator::Request;
use crate::display_object::{TDisplayObject, TDisplayObjectContainer}; use crate::display_object::{TDisplayObject, TDisplayObjectContainer};
use crate::loader::MovieLoaderEventHandler;
use gc_arena::MutationContext; use gc_arena::MutationContext;
const PROTO_DECLS: &[Declaration] = declare_properties! { const PROTO_DECLS: &[Declaration] = declare_properties! {
@ -63,7 +64,7 @@ fn load_clip<'gc>(
target, target,
Request::get(url.to_utf8_lossy().into_owned()), Request::get(url.to_utf8_lossy().into_owned()),
None, None,
Some(this), Some(MovieLoaderEventHandler::Avm1Broadcast(this)),
); );
activation.context.navigator.spawn_future(future); activation.context.navigator.spawn_future(future);
@ -99,7 +100,7 @@ fn unload_clip<'gc>(
if let Some(target) = target { if let Some(target) = target {
target.unload(&mut activation.context); target.unload(&mut activation.context);
if let Some(mut mc) = target.as_movie_clip() { if let Some(mut mc) = target.as_movie_clip() {
mc.replace_with_movie(&mut activation.context, None); mc.replace_with_movie(&mut activation.context, None, None);
} }
return Ok(true.into()); return Ok(true.into());
} }

View File

@ -89,6 +89,7 @@ pub struct SystemClasses<'gc> {
pub qname: ClassObject<'gc>, pub qname: ClassObject<'gc>,
pub sharedobject: ClassObject<'gc>, pub sharedobject: ClassObject<'gc>,
pub mouseevent: ClassObject<'gc>, pub mouseevent: ClassObject<'gc>,
pub progressevent: ClassObject<'gc>,
pub textevent: ClassObject<'gc>, pub textevent: ClassObject<'gc>,
pub errorevent: ClassObject<'gc>, pub errorevent: ClassObject<'gc>,
pub ioerrorevent: ClassObject<'gc>, pub ioerrorevent: ClassObject<'gc>,
@ -96,6 +97,8 @@ pub struct SystemClasses<'gc> {
pub transform: ClassObject<'gc>, pub transform: ClassObject<'gc>,
pub colortransform: ClassObject<'gc>, pub colortransform: ClassObject<'gc>,
pub matrix: ClassObject<'gc>, pub matrix: ClassObject<'gc>,
pub illegaloperationerror: ClassObject<'gc>,
pub eventdispatcher: ClassObject<'gc>,
} }
impl<'gc> SystemClasses<'gc> { impl<'gc> SystemClasses<'gc> {
@ -154,6 +157,7 @@ impl<'gc> SystemClasses<'gc> {
qname: object, qname: object,
sharedobject: object, sharedobject: object,
mouseevent: object, mouseevent: object,
progressevent: object,
textevent: object, textevent: object,
errorevent: object, errorevent: object,
ioerrorevent: object, ioerrorevent: object,
@ -161,6 +165,8 @@ impl<'gc> SystemClasses<'gc> {
transform: object, transform: object,
colortransform: object, colortransform: object,
matrix: object, matrix: object,
illegaloperationerror: object,
eventdispatcher: object,
} }
} }
} }
@ -454,11 +460,12 @@ pub fn load_player_globals<'gc>(
flash::events::ieventdispatcher::create_interface(mc), flash::events::ieventdispatcher::create_interface(mc),
script, script,
)?; )?;
class( avm2_system_class!(
eventdispatcher,
activation, activation,
flash::events::eventdispatcher::create_class(mc), flash::events::eventdispatcher::create_class(mc),
script, script
)?; );
// package `flash.utils` // package `flash.utils`
avm2_system_class!( avm2_system_class!(
@ -567,7 +574,6 @@ pub fn load_player_globals<'gc>(
flash::display::graphics::create_class(mc), flash::display::graphics::create_class(mc),
script script
); );
class(activation, flash::display::loader::create_class(mc), script)?;
avm2_system_class!( avm2_system_class!(
loaderinfo, loaderinfo,
activation, activation,
@ -734,9 +740,15 @@ fn load_playerglobal<'gc>(
script, script,
[ [
("flash.display", "Scene", scene), ("flash.display", "Scene", scene),
(
"flash.errors",
"IllegalOperationError",
illegaloperationerror
),
("flash.events", "Event", event), ("flash.events", "Event", event),
("flash.events", "TextEvent", textevent), ("flash.events", "TextEvent", textevent),
("flash.events", "ErrorEvent", errorevent), ("flash.events", "ErrorEvent", errorevent),
("flash.events", "ProgressEvent", progressevent),
("flash.events", "SecurityErrorEvent", securityerrorevent), ("flash.events", "SecurityErrorEvent", securityerrorevent),
("flash.events", "IOErrorEvent", ioerrorevent), ("flash.events", "IOErrorEvent", ioerrorevent),
("flash.events", "MouseEvent", mouseevent), ("flash.events", "MouseEvent", mouseevent),

View File

@ -1,5 +1,11 @@
// This is a stub - the actual class is defined in `displayobjectcontainer.rs` // This is a stub - the actual class is defined in `displayobjectcontainer.rs`
package flash.display { package flash.display {
public class DisplayObjectContainer { public class DisplayObjectContainer extends InteractiveObject {
} public native function addChild(child:DisplayObject):void;
public native function addChildAt(child:DisplayObject, index:int):void;
public native function removeChild(child:DisplayObject, index:int):void
public native function removeChildAt(index:int):void
public native function setChildIndex(child:DisplayObject, index:int):void
public native function getChildAt(index:int):DisplayObject;
}
} }

View File

@ -0,0 +1,50 @@
package flash.display {
public class Loader extends DisplayObjectContainer {
import flash.display.LoaderInfo;
import flash.display.DisplayObject;
import flash.errors.IllegalOperationError;
import flash.system.LoaderContext;
import flash.net.URLRequest;
private var _contentLoaderInfo: LoaderInfo;
public function get contentLoaderInfo():LoaderInfo {
return this._contentLoaderInfo;
}
private native function init();
public function Loader() {
this.init()
}
public function get content():DisplayObject {
if (this.numChildren == 0) {
return null;
}
return this.getChildAt(0)
}
public native function load(request: URLRequest, context: LoaderContext = null):void;
override public function addChild(child:DisplayObject):void {
throw new IllegalOperationError("Error #2069: The Loader class does not implement this method.", 2069);
}
override public function addChildAt(child:DisplayObject, index:int):void {
throw new IllegalOperationError("Error #2069: The Loader class does not implement this method.", 2069);
}
override public function removeChild(child:DisplayObject, index:int):void {
throw new IllegalOperationError("Error #2069: The Loader class does not implement this method.", 2069);
}
override public function removeChildAt(index:int):void {
throw new IllegalOperationError("Error #2069: The Loader class does not implement this method.", 2069);
}
override public function setChildIndex(child:DisplayObject, index:int):void {
throw new IllegalOperationError("Error #2069: The Loader class does not implement this method.", 2069);
}
}
}

View File

@ -0,0 +1,3 @@
package flash.display {
public class LoaderInfo {}
}

View File

@ -114,7 +114,7 @@ fn remove_child_from_displaylist<'gc>(
} }
/// Add the `child` to `parent`'s display list. /// Add the `child` to `parent`'s display list.
fn add_child_to_displaylist<'gc>( pub(super) fn add_child_to_displaylist<'gc>(
context: &mut UpdateContext<'_, 'gc, '_>, context: &mut UpdateContext<'_, 'gc, '_>,
parent: DisplayObject<'gc>, parent: DisplayObject<'gc>,
child: DisplayObject<'gc>, child: DisplayObject<'gc>,

View File

@ -1,71 +1,92 @@
//! `flash.display.Loader` builtin/prototype //! `flash.display.Loader` builtin/prototype
use crate::avm2::activation::Activation; use crate::avm2::activation::Activation;
use crate::avm2::class::{Class, ClassAttributes}; use crate::avm2::object::LoaderInfoObject;
use crate::avm2::method::{Method, NativeMethodImpl}; use crate::avm2::object::TObject;
use crate::avm2::value::Value; use crate::avm2::value::Value;
use crate::avm2::Multiname;
use crate::avm2::Namespace; use crate::avm2::Namespace;
use crate::avm2::QName; use crate::avm2::QName;
use crate::avm2::{Error, Object}; use crate::avm2::{Error, Object};
use gc_arena::{GcCell, MutationContext}; use crate::backend::navigator::Request;
use crate::display_object::LoaderDisplay;
use crate::display_object::MovieClip;
use crate::loader::MovieLoaderEventHandler;
use crate::tag_utils::SwfMovie;
use std::sync::Arc;
/// Implements `flash.display.Loader`'s class constructor. pub fn init<'gc>(
pub fn class_init<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
_this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
Ok(Value::Undefined)
}
/// Implements `flash.display.Loader`'s instance constructor.
pub fn instance_init<'gc>(
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>, this: Option<Object<'gc>>,
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(mut this) = this {
activation.super_init(this, &[])?; if this.as_display_object().is_none() {
let new_do = LoaderDisplay::new_with_avm2(activation.context.gc_context, this);
this.init_display_object(activation.context.gc_context, new_do.into());
}
// Some LoaderInfo properties (such as 'bytesLoaded' and 'bytesTotal') are always
// accessible, even before the 'init' event has fired. Using an empty movie gives
// us the correct value (0) for them.
let loader_info = LoaderInfoObject::not_yet_loaded(
activation,
Arc::new(SwfMovie::empty(activation.context.swf.version())),
Some(this),
)?;
this.set_property(
&QName::new(Namespace::private(""), "_contentLoaderInfo").into(),
loader_info.into(),
activation,
)?;
} }
Ok(Value::Undefined) Ok(Value::Undefined)
} }
// TODO: this is entirely stubbed, just to get addEventListener to not crash pub fn load<'gc>(
fn content_loader_info<'gc>( activation: &mut Activation<'_, 'gc, '_>,
_activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>, this: Option<Object<'gc>>,
_args: &[Value<'gc>], args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
log::warn!("Loader::contentLoaderInfo is a stub"); if let Some(this) = this {
Ok(this.unwrap().into()) let url_request = args[0].as_object().unwrap();
}
if let Some(context) = args.get(1) {
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> { if !matches!(context, Value::Null) {
let class = Class::new( log::warn!(
QName::new(Namespace::package("flash.display"), "Loader"), "Loader.load: 'context' argument is not yet implemented: {:?}",
Some( context
QName::new( );
Namespace::package("flash.display"), }
"DisplayObjectContainer", }
) let url = url_request
.into(), .get_property(&Multiname::public("url"), activation)?
), .coerce_to_string(activation)?;
Method::from_builtin(instance_init, "<Loader instance initializer>", mc),
Method::from_builtin(class_init, "<Loader class initializer>", mc), // This is a dummy MovieClip, which will get overwritten in `Loader`
mc, let content = MovieClip::new(
); Arc::new(SwfMovie::empty(activation.context.swf.version())),
activation.context.gc_context,
let mut write = class.write(mc); );
write.set_attributes(ClassAttributes::SEALED); let loader_info = this
.get_property(
const PUBLIC_INSTANCE_PROPERTIES: &[( &QName::new(Namespace::private(""), "_contentLoaderInfo").into(),
&str, activation,
Option<NativeMethodImpl>, )?
Option<NativeMethodImpl>, .as_object()
)] = &[("contentLoaderInfo", Some(content_loader_info), None)]; .unwrap();
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);
let future = activation.context.load_manager.load_movie_into_clip(
class activation.context.player.clone(),
content.into(),
// FIXME - set options from the `URLRequest`
Request::get(url.to_string()),
Some(url.to_string()),
Some(MovieLoaderEventHandler::Avm2LoaderInfo(loader_info)),
);
activation.context.navigator.spawn_future(future);
}
Ok(Value::Undefined)
} }

View File

@ -11,8 +11,13 @@ use crate::avm2::QName;
use crate::avm2::{AvmString, Error}; use crate::avm2::{AvmString, Error};
use crate::display_object::TDisplayObject; use crate::display_object::TDisplayObject;
use gc_arena::{GcCell, MutationContext}; use gc_arena::{GcCell, MutationContext};
use std::sync::Arc;
use swf::{write_swf, Compression}; use swf::{write_swf, Compression};
// FIXME - Throw an actual 'Error' with the proper code
const INSUFFICIENT: &str =
"Error #2099: The loading object is not sufficiently loaded to provide this information.";
/// Implements `flash.display.LoaderInfo`'s instance constructor. /// Implements `flash.display.LoaderInfo`'s instance constructor.
pub fn instance_init<'gc>( pub fn instance_init<'gc>(
_activation: &mut Activation<'_, 'gc, '_>, _activation: &mut Activation<'_, 'gc, '_>,
@ -51,10 +56,13 @@ pub fn action_script_version<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
match &*loader_stream { match &*loader_stream {
LoaderStream::Stage => { LoaderStream::NotYetLoaded(_) => {
return Err("Error: The stage's loader info does not have an AS version".into()) return Err(INSUFFICIENT.into());
} }
LoaderStream::Swf(movie, _) => { LoaderStream::Swf(movie, _) => {
let version = if movie.is_action_script_3() { 3 } else { 2 }; let version = if movie.is_action_script_3() { 3 } else { 2 };
@ -74,9 +82,12 @@ pub fn application_domain<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
match &*loader_stream { match &*loader_stream {
LoaderStream::Stage => { LoaderStream::NotYetLoaded(_) => {
return Ok(DomainObject::from_domain(activation, activation.domain())?.into()); return Ok(DomainObject::from_domain(activation, activation.domain())?.into());
} }
LoaderStream::Swf(movie, _) => { LoaderStream::Swf(movie, _) => {
@ -99,14 +110,17 @@ pub fn application_domain<'gc>(
/// TODO: This is also the getter for `bytesLoaded` as we don't yet support /// TODO: This is also the getter for `bytesLoaded` as we don't yet support
/// streaming loads yet. When we do, we'll need another property for this. /// streaming loads yet. When we do, we'll need another property for this.
pub fn bytes_total<'gc>( pub fn bytes_total<'gc>(
activation: &mut Activation<'_, 'gc, '_>, _activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>, this: Option<Object<'gc>>,
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
match &*loader_stream { match &*loader_stream {
LoaderStream::Stage => return Ok(activation.context.swf.compressed_len().into()), LoaderStream::NotYetLoaded(swf) => return Ok(swf.compressed_len().into()),
LoaderStream::Swf(movie, _) => { LoaderStream::Swf(movie, _) => {
return Ok(movie.compressed_len().into()); return Ok(movie.compressed_len().into());
} }
@ -124,9 +138,17 @@ pub fn content<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
match &*loader_stream { match &*loader_stream {
LoaderStream::Stage => return Ok(activation.context.stage.root_clip().object2()), LoaderStream::NotYetLoaded(swf) => {
if Arc::ptr_eq(swf, activation.context.swf) {
return Ok(activation.context.stage.root_clip().object2());
}
return Ok(Value::Null);
}
LoaderStream::Swf(_, root) => { LoaderStream::Swf(_, root) => {
return Ok(root.object2()); return Ok(root.object2());
} }
@ -144,9 +166,12 @@ pub fn content_type<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
match &*loader_stream { match &*loader_stream {
LoaderStream::Stage => return Ok(Value::Null), LoaderStream::NotYetLoaded(_) => return Ok(Value::Null),
LoaderStream::Swf(_, _) => { LoaderStream::Swf(_, _) => {
return Ok("application/x-shockwave-flash".into()); return Ok("application/x-shockwave-flash".into());
} }
@ -164,9 +189,12 @@ pub fn frame_rate<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
match &*loader_stream { match &*loader_stream {
LoaderStream::Stage => { LoaderStream::NotYetLoaded(_) => {
return Err("Error: The stage's loader info does not have a frame rate".into()) return Err("Error: The stage's loader info does not have a frame rate".into())
} }
LoaderStream::Swf(root, _) => { LoaderStream::Swf(root, _) => {
@ -186,9 +214,12 @@ pub fn height<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
match &*loader_stream { match &*loader_stream {
LoaderStream::Stage => { LoaderStream::NotYetLoaded(_) => {
return Err("Error: The stage's loader info does not have a height".into()) return Err("Error: The stage's loader info does not have a height".into())
} }
LoaderStream::Swf(root, _) => { LoaderStream::Swf(root, _) => {
@ -217,9 +248,12 @@ pub fn swf_version<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
match &*loader_stream { match &*loader_stream {
LoaderStream::Stage => { LoaderStream::NotYetLoaded(_) => {
return Err("Error: The stage's loader info does not have a SWF version".into()) return Err("Error: The stage's loader info does not have a SWF version".into())
} }
LoaderStream::Swf(root, _) => { LoaderStream::Swf(root, _) => {
@ -239,14 +273,19 @@ pub fn url<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
let root = match &*loader_stream { let root = match &*loader_stream {
LoaderStream::Stage => activation.context.swf, LoaderStream::NotYetLoaded(swf) => swf,
LoaderStream::Swf(root, _) => root, LoaderStream::Swf(root, _) => root,
}; };
let url = root.url().unwrap_or(""); let url = root.url().map_or(Value::Null, |url| {
return Ok(AvmString::new_utf8(activation.context.gc_context, url).into()); AvmString::new_utf8(activation.context.gc_context, url).into()
});
return Ok(url);
} }
} }
@ -260,9 +299,12 @@ pub fn width<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
match &*loader_stream { match &*loader_stream {
LoaderStream::Stage => { LoaderStream::NotYetLoaded(_) => {
return Err("Error: The stage's loader info does not have a width".into()) return Err("Error: The stage's loader info does not have a width".into())
} }
LoaderStream::Swf(root, _) => { LoaderStream::Swf(root, _) => {
@ -282,12 +324,19 @@ pub fn bytes<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
let root = match &*loader_stream { let root = match &*loader_stream {
LoaderStream::Stage => activation.context.swf, LoaderStream::NotYetLoaded(swf) => swf,
LoaderStream::Swf(root, _) => root, LoaderStream::Swf(root, _) => root,
}; };
if root.data().is_empty() {
return Ok(Value::Null);
}
let ba_class = activation.context.avm2.classes().bytearray; let ba_class = activation.context.avm2.classes().bytearray;
let ba = ba_class.construct(activation, &[])?; let ba = ba_class.construct(activation, &[])?;
@ -325,6 +374,19 @@ pub fn bytes<'gc>(
Ok(Value::Undefined) Ok(Value::Undefined)
} }
/// `loader` getter
pub fn loader<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(loader_info) = this.as_ref().and_then(|this| this.as_loader_info_object()) {
Ok(loader_info.loader().map_or(Value::Null, |v| v.into()))
} else {
Ok(Value::Undefined)
}
}
/// `loaderURL` getter /// `loaderURL` getter
pub fn loader_url<'gc>( pub fn loader_url<'gc>(
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
@ -332,9 +394,12 @@ pub fn loader_url<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
let root = match &*loader_stream { let root = match &*loader_stream {
LoaderStream::Stage => activation.context.swf, LoaderStream::NotYetLoaded(swf) => swf,
LoaderStream::Swf(root, _) => root, LoaderStream::Swf(root, _) => root,
}; };
@ -353,9 +418,12 @@ pub fn parameters<'gc>(
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> { ) -> Result<Value<'gc>, Error> {
if let Some(this) = this { if let Some(this) = this {
if let Some(loader_stream) = this.as_loader_stream() { if let Some(loader_stream) = this
.as_loader_info_object()
.and_then(|o| o.as_loader_stream())
{
let root = match &*loader_stream { let root = match &*loader_stream {
LoaderStream::Stage => activation.context.swf, LoaderStream::NotYetLoaded(_) => activation.context.swf,
LoaderStream::Swf(root, _) => root, LoaderStream::Swf(root, _) => root,
}; };
@ -383,6 +451,18 @@ pub fn parameters<'gc>(
Ok(Value::Undefined) Ok(Value::Undefined)
} }
/// `sharedEvents` getter
pub fn shared_events<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(loader_info) = this.as_ref().and_then(|this| this.as_loader_info_object()) {
return Ok(loader_info.shared_events().into());
}
Ok(Value::Undefined)
}
/// Construct `LoaderInfo`'s class. /// Construct `LoaderInfo`'s class.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> { pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new( let class = Class::new(
@ -421,8 +501,10 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
("url", Some(url), None), ("url", Some(url), None),
("width", Some(width), None), ("width", Some(width), None),
("bytes", Some(bytes), None), ("bytes", Some(bytes), None),
("loader", Some(loader), None),
("loaderURL", Some(loader_url), None), ("loaderURL", Some(loader_url), None),
("parameters", Some(parameters), None), ("parameters", Some(parameters), None),
("sharedEvents", Some(shared_events), None),
]; ];
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES); write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);

View File

@ -1,5 +1,4 @@
package flash.system { package flash.system {
import flash.display.DisplayObjectContainer; import flash.display.DisplayObjectContainer;
import flash.system.ApplicationDomain; import flash.system.ApplicationDomain;

View File

@ -19,6 +19,7 @@ include "flash/display/InterpolationMethod.as"
include "flash/display/JointStyle.as" include "flash/display/JointStyle.as"
include "flash/display/JPEGEncoderOptions.as" include "flash/display/JPEGEncoderOptions.as"
include "flash/display/JPEGXREncoderOptions.as" include "flash/display/JPEGXREncoderOptions.as"
include "flash/display/Loader.as"
include "flash/display/LineScaleMode.as" include "flash/display/LineScaleMode.as"
include "flash/display/NativeMenu.as" include "flash/display/NativeMenu.as"
include "flash/display/NativeMenuItem.as" include "flash/display/NativeMenuItem.as"

View File

@ -3,12 +3,15 @@
// `Object.as` must come first since the other classes depend on it. // `Object.as` must come first since the other classes depend on it.
include "Object.as" include "Object.as"
// List is ordered alphabetically. // List is ordered alphabetically, except where superclasses
// are listed before subclasses
include "Array.as" include "Array.as"
include "Boolean.as" include "Boolean.as"
include "flash/display/DisplayObject.as" include "flash/display/DisplayObject.as"
include "flash/display/DisplayObjectContainer.as"
include "flash/display/InteractiveObject.as" include "flash/display/InteractiveObject.as"
include "flash/display/DisplayObjectContainer.as"
include "flash/display/LoaderInfo.as"
include "flash/events/EventDispatcher.as" include "flash/events/EventDispatcher.as"
include "flash/system/ApplicationDomain.as" include "flash/system/ApplicationDomain.as"
include "Number.as" include "Number.as"

View File

@ -18,7 +18,6 @@ use crate::avm2::Namespace;
use crate::avm2::QName; use crate::avm2::QName;
use crate::backend::audio::{SoundHandle, SoundInstanceHandle}; use crate::backend::audio::{SoundHandle, SoundInstanceHandle};
use crate::bitmap::bitmap_data::BitmapData; use crate::bitmap::bitmap_data::BitmapData;
use crate::context::UpdateContext;
use crate::display_object::DisplayObject; use crate::display_object::DisplayObject;
use crate::html::TextFormat; use crate::html::TextFormat;
use crate::string::AvmString; use crate::string::AvmString;
@ -903,6 +902,10 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
None None
} }
fn as_loader_info_object(&self) -> Option<&LoaderInfoObject<'gc>> {
None
}
fn as_array_object(&self) -> Option<ArrayObject<'gc>> { fn as_array_object(&self) -> Option<ArrayObject<'gc>> {
None None
} }
@ -1011,13 +1014,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
None None
} }
fn loader_stream_init(&self, _context: &mut UpdateContext<'_, 'gc, '_>) {}
/// Unwrap this object's loader stream
fn as_loader_stream(&self) -> Option<Ref<LoaderStream<'gc>>> {
None
}
/// Unwrap this object's sound handle. /// Unwrap this object's sound handle.
fn as_sound(self) -> Option<SoundHandle> { fn as_sound(self) -> Option<SoundHandle> {
None None

View File

@ -26,7 +26,14 @@ pub fn loaderinfo_allocator<'gc>(
LoaderInfoObjectData { LoaderInfoObjectData {
base, base,
loaded_stream: None, loaded_stream: None,
init_fired: false, loader: None,
events_fired: false,
shared_events: activation
.context
.avm2
.classes()
.eventdispatcher
.construct(activation, &[])?,
}, },
)) ))
.into()) .into())
@ -36,12 +43,10 @@ pub fn loaderinfo_allocator<'gc>(
#[derive(Collect, Debug, Clone)] #[derive(Collect, Debug, Clone)]
#[collect(no_drop)] #[collect(no_drop)]
pub enum LoaderStream<'gc> { pub enum LoaderStream<'gc> {
/// The current stage.
///
/// While it makes no sense to actually retrieve loader info properties off /// While it makes no sense to actually retrieve loader info properties off
/// the stage, it's possible to do so. Some properties yield the /// the stage, it's possible to do so. Some properties yield the
/// not-yet-loaded error while others are pulled from the root SWF. /// not-yet-loaded error while others are pulled from the root SWF.
Stage, NotYetLoaded(Arc<SwfMovie>),
/// A loaded SWF movie. /// A loaded SWF movie.
/// ///
@ -64,8 +69,15 @@ pub struct LoaderInfoObjectData<'gc> {
/// The loaded stream that this gets it's info from. /// The loaded stream that this gets it's info from.
loaded_stream: Option<LoaderStream<'gc>>, loaded_stream: Option<LoaderStream<'gc>>,
/// Whether or not we've fired an 'init' event loader: Option<Object<'gc>>,
init_fired: bool,
/// Whether or not we've fired our 'init' and 'complete' events
events_fired: bool,
/// The `EventDispatcher` used for `LoaderInfo.sharedEvents`.
// FIXME: If we ever implement sandboxing, then ensure that we allow
// events to be fired across security boundaries using this object.
shared_events: Object<'gc>,
} }
impl<'gc> LoaderInfoObject<'gc> { impl<'gc> LoaderInfoObject<'gc> {
@ -74,6 +86,7 @@ impl<'gc> LoaderInfoObject<'gc> {
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
movie: Arc<SwfMovie>, movie: Arc<SwfMovie>,
root: DisplayObject<'gc>, root: DisplayObject<'gc>,
loader: Option<Object<'gc>>,
) -> Result<Object<'gc>, Error> { ) -> Result<Object<'gc>, Error> {
let class = activation.avm2().classes().loaderinfo; let class = activation.avm2().classes().loaderinfo;
let base = ScriptObjectData::new(class); let base = ScriptObjectData::new(class);
@ -84,7 +97,14 @@ impl<'gc> LoaderInfoObject<'gc> {
LoaderInfoObjectData { LoaderInfoObjectData {
base, base,
loaded_stream, loaded_stream,
init_fired: false, loader,
events_fired: false,
shared_events: activation
.context
.avm2
.classes()
.eventdispatcher
.construct(activation, &[])?,
}, },
)) ))
.into(); .into();
@ -96,7 +116,11 @@ impl<'gc> LoaderInfoObject<'gc> {
} }
/// Create a loader info object for the stage. /// Create a loader info object for the stage.
pub fn from_stage(activation: &mut Activation<'_, 'gc, '_>) -> Result<Object<'gc>, Error> { pub fn not_yet_loaded(
activation: &mut Activation<'_, 'gc, '_>,
movie: Arc<SwfMovie>,
loader: Option<Object<'gc>>,
) -> Result<Object<'gc>, Error> {
let class = activation.avm2().classes().loaderinfo; let class = activation.avm2().classes().loaderinfo;
let base = ScriptObjectData::new(class); let base = ScriptObjectData::new(class);
@ -104,10 +128,15 @@ impl<'gc> LoaderInfoObject<'gc> {
activation.context.gc_context, activation.context.gc_context,
LoaderInfoObjectData { LoaderInfoObjectData {
base, base,
loaded_stream: Some(LoaderStream::Stage), loaded_stream: Some(LoaderStream::NotYetLoaded(movie)),
// We never want to fire an "init" event for the special loader,
// Stagee loaderInfo events_fired: false,
init_fired: true, shared_events: activation
.context
.avm2
.classes()
.eventdispatcher
.construct(activation, &[])?,
}, },
)) ))
.into(); .into();
@ -117,6 +146,55 @@ impl<'gc> LoaderInfoObject<'gc> {
Ok(this) Ok(this)
} }
pub fn loader(&self) -> Option<Object<'gc>> {
return self.0.read().loader;
}
pub fn shared_events(&self) -> Object<'gc> {
return self.0.read().shared_events;
}
pub fn fire_init_and_complete_events(&self, context: &mut UpdateContext<'_, 'gc, '_>) {
if !self.0.read().events_fired {
self.0.write(context.gc_context).events_fired = true;
// TODO - 'init' should be fired earlier during the download.
// Right now, we fire it when downloading is fully completed.
let init_evt = EventObject::bare_default_event(context, "init");
if let Err(e) = Avm2::dispatch_event(context, init_evt, (*self).into()) {
log::error!(
"Encountered AVM2 error when broadcasting `init` event: {}",
e
);
}
let complete_evt = EventObject::bare_default_event(context, "complete");
if let Err(e) = Avm2::dispatch_event(context, complete_evt, (*self).into()) {
log::error!(
"Encountered AVM2 error when broadcasting `complete` event: {}",
e
);
}
}
}
/// Unwrap this object's loader stream
pub fn as_loader_stream(&self) -> Option<Ref<LoaderStream<'gc>>> {
if self.0.read().loaded_stream.is_some() {
Some(Ref::map(self.0.read(), |v: &LoaderInfoObjectData<'gc>| {
v.loaded_stream.as_ref().unwrap()
}))
} else {
None
}
}
pub fn set_loader_stream(&self, stream: LoaderStream<'gc>, mc: MutationContext<'gc, '_>) {
self.0.write(mc).loaded_stream = Some(stream);
}
} }
impl<'gc> TObject<'gc> for LoaderInfoObject<'gc> { impl<'gc> TObject<'gc> for LoaderInfoObject<'gc> {
@ -136,28 +214,7 @@ impl<'gc> TObject<'gc> for LoaderInfoObject<'gc> {
Ok(Value::Object((*self).into())) Ok(Value::Object((*self).into()))
} }
fn loader_stream_init(&self, context: &mut UpdateContext<'_, 'gc, '_>) { fn as_loader_info_object(&self) -> Option<&LoaderInfoObject<'gc>> {
if !self.0.read().init_fired { Some(self)
self.0.write(context.gc_context).init_fired = true;
let init_evt = EventObject::bare_default_event(context, "init");
if let Err(e) = Avm2::dispatch_event(context, init_evt, (*self).into()) {
log::error!(
"Encountered AVM2 error when broadcasting `init` event: {}",
e
);
}
}
}
/// Unwrap this object's loader stream
fn as_loader_stream(&self) -> Option<Ref<LoaderStream<'gc>>> {
if self.0.read().loaded_stream.is_some() {
Some(Ref::map(self.0.read(), |v| {
v.loaded_stream.as_ref().unwrap()
}))
} else {
None
}
} }
} }

View File

@ -28,6 +28,7 @@ mod container;
mod edit_text; mod edit_text;
mod graphic; mod graphic;
mod interactive; mod interactive;
mod loader_display;
mod morph_shape; mod morph_shape;
mod movie_clip; mod movie_clip;
mod stage; mod stage;
@ -44,6 +45,7 @@ pub use bitmap::Bitmap;
pub use edit_text::{AutoSizeMode, EditText, TextSelection}; pub use edit_text::{AutoSizeMode, EditText, TextSelection};
pub use graphic::Graphic; pub use graphic::Graphic;
pub use interactive::{InteractiveObject, TInteractiveObject}; pub use interactive::{InteractiveObject, TInteractiveObject};
pub use loader_display::LoaderDisplay;
pub use morph_shape::{MorphShape, MorphShapeStatic}; pub use morph_shape::{MorphShape, MorphShapeStatic};
pub use movie_clip::{MovieClip, Scene}; pub use movie_clip::{MovieClip, Scene};
pub use stage::{Stage, StageAlign, StageDisplayState, StageQuality, StageScaleMode, WindowMode}; pub use stage::{Stage, StageAlign, StageDisplayState, StageQuality, StageScaleMode, WindowMode};
@ -520,6 +522,7 @@ pub fn render_base<'gc>(this: DisplayObject<'gc>, context: &mut RenderContext<'_
MovieClip(MovieClip<'gc>), MovieClip(MovieClip<'gc>),
Text(Text<'gc>), Text(Text<'gc>),
Video(Video<'gc>), Video(Video<'gc>),
LoaderDisplay(LoaderDisplay<'gc>)
} }
)] )]
pub trait TDisplayObject<'gc>: pub trait TDisplayObject<'gc>:

View File

@ -3,6 +3,7 @@
use crate::avm2::{Avm2, EventObject as Avm2EventObject, Value as Avm2Value}; use crate::avm2::{Avm2, EventObject as Avm2EventObject, Value as Avm2Value};
use crate::context::{RenderContext, UpdateContext}; use crate::context::{RenderContext, UpdateContext};
use crate::display_object::avm1_button::Avm1Button; use crate::display_object::avm1_button::Avm1Button;
use crate::display_object::loader_display::LoaderDisplay;
use crate::display_object::movie_clip::MovieClip; use crate::display_object::movie_clip::MovieClip;
use crate::display_object::stage::Stage; use crate::display_object::stage::Stage;
use crate::display_object::{Depth, DisplayObject, TDisplayObject}; use crate::display_object::{Depth, DisplayObject, TDisplayObject};
@ -142,6 +143,7 @@ bitflags! {
Stage(Stage<'gc>), Stage(Stage<'gc>),
Avm1Button(Avm1Button<'gc>), Avm1Button(Avm1Button<'gc>),
MovieClip(MovieClip<'gc>), MovieClip(MovieClip<'gc>),
LoaderDisplay(LoaderDisplay<'gc>),
} }
)] )]
pub trait TDisplayObjectContainer<'gc>: pub trait TDisplayObjectContainer<'gc>:

View File

@ -7,6 +7,7 @@ use crate::context::UpdateContext;
use crate::display_object::avm1_button::Avm1Button; use crate::display_object::avm1_button::Avm1Button;
use crate::display_object::avm2_button::Avm2Button; use crate::display_object::avm2_button::Avm2Button;
use crate::display_object::edit_text::EditText; use crate::display_object::edit_text::EditText;
use crate::display_object::loader_display::LoaderDisplay;
use crate::display_object::movie_clip::MovieClip; use crate::display_object::movie_clip::MovieClip;
use crate::display_object::stage::Stage; use crate::display_object::stage::Stage;
use crate::display_object::{ use crate::display_object::{
@ -109,6 +110,7 @@ impl<'gc> Default for InteractiveObjectBase<'gc> {
Avm2Button(Avm2Button<'gc>), Avm2Button(Avm2Button<'gc>),
MovieClip(MovieClip<'gc>), MovieClip(MovieClip<'gc>),
EditText(EditText<'gc>), EditText(EditText<'gc>),
LoaderDisplay(LoaderDisplay<'gc>),
} }
)] )]
pub trait TInteractiveObject<'gc>: pub trait TInteractiveObject<'gc>:

View File

@ -0,0 +1,112 @@
use crate::avm2::Object as Avm2Object;
use crate::context::RenderContext;
use crate::context::UpdateContext;
use crate::display_object::InteractiveObject;
use crate::display_object::TInteractiveObject;
use crate::display_object::{DisplayObjectBase, DisplayObjectPtr, TDisplayObject};
use crate::events::{ClipEvent, ClipEventResult};
use crate::prelude::*;
use crate::display_object::container::ChildContainer;
use crate::display_object::interactive::InteractiveObjectBase;
use gc_arena::{Collect, GcCell, MutationContext};
use std::cell::{Ref, RefMut};
#[derive(Clone, Debug, Collect, Copy)]
#[collect(no_drop)]
pub struct LoaderDisplay<'gc>(GcCell<'gc, LoaderDisplayData<'gc>>);
#[derive(Clone, Debug, Collect)]
#[collect(no_drop)]
pub struct LoaderDisplayData<'gc> {
base: InteractiveObjectBase<'gc>,
container: ChildContainer<'gc>,
avm2_object: Avm2Object<'gc>,
}
impl<'gc> LoaderDisplay<'gc> {
pub fn new_with_avm2(
gc_context: MutationContext<'gc, '_>,
avm2_object: Avm2Object<'gc>,
) -> Self {
LoaderDisplay(GcCell::allocate(
gc_context,
LoaderDisplayData {
base: Default::default(),
container: ChildContainer::new(),
avm2_object,
},
))
}
}
impl<'gc> TDisplayObject<'gc> for LoaderDisplay<'gc> {
fn base(&self) -> Ref<DisplayObjectBase<'gc>> {
Ref::map(self.0.read(), |r| &r.base.base)
}
fn base_mut<'a>(&'a self, mc: MutationContext<'gc, '_>) -> RefMut<'a, DisplayObjectBase<'gc>> {
RefMut::map(self.0.write(mc), |w| &mut w.base.base)
}
fn instantiate(&self, gc_context: MutationContext<'gc, '_>) -> DisplayObject<'gc> {
Self(GcCell::allocate(gc_context, self.0.read().clone())).into()
}
fn as_ptr(&self) -> *const DisplayObjectPtr {
self.0.as_ptr() as *const DisplayObjectPtr
}
fn id(&self) -> CharacterId {
u16::MAX
}
fn render_self(&self, context: &mut RenderContext<'_, 'gc>) {
self.render_children(context);
}
fn self_bounds(&self) -> BoundingBox {
Default::default()
}
fn object2(&self) -> Avm2Value<'gc> {
self.0.read().avm2_object.into()
}
fn as_container(self) -> Option<DisplayObjectContainer<'gc>> {
Some(self.into())
}
fn as_interactive(self) -> Option<InteractiveObject<'gc>> {
Some(self.into())
}
}
impl<'gc> TInteractiveObject<'gc> for LoaderDisplay<'gc> {
fn ibase(&self) -> Ref<InteractiveObjectBase<'gc>> {
Ref::map(self.0.read(), |r| &r.base)
}
fn ibase_mut(&self, mc: MutationContext<'gc, '_>) -> RefMut<InteractiveObjectBase<'gc>> {
RefMut::map(self.0.write(mc), |w| &mut w.base)
}
fn as_displayobject(self) -> DisplayObject<'gc> {
self.into()
}
fn filter_clip_event(self, _event: ClipEvent) -> ClipEventResult {
ClipEventResult::NotHandled
}
fn event_dispatch(
self,
_context: &mut UpdateContext<'_, 'gc, '_>,
_event: ClipEvent<'gc>,
) -> ClipEventResult {
ClipEventResult::NotHandled
}
}
impl<'gc> TDisplayObjectContainer<'gc> for LoaderDisplay<'gc> {
impl_display_object_container!(container);
}

View File

@ -3,6 +3,7 @@ use crate::avm1::{
Avm1, Object as Avm1Object, StageObject, TObject as Avm1TObject, Value as Avm1Value, Avm1, Object as Avm1Object, StageObject, TObject as Avm1TObject, Value as Avm1Value,
}; };
use crate::avm2::object::LoaderInfoObject; use crate::avm2::object::LoaderInfoObject;
use crate::avm2::object::LoaderStream;
use crate::avm2::Activation as Avm2Activation; use crate::avm2::Activation as Avm2Activation;
use crate::avm2::{ use crate::avm2::{
Avm2, ClassObject as Avm2ClassObject, Error as Avm2Error, Namespace as Avm2Namespace, Avm2, ClassObject as Avm2ClassObject, Error as Avm2Error, Namespace as Avm2Namespace,
@ -177,6 +178,7 @@ impl<'gc> MovieClip<'gc> {
)) ))
} }
/// Constructs a non-root movie
pub fn new_with_data( pub fn new_with_data(
gc_context: MutationContext<'gc, '_>, gc_context: MutationContext<'gc, '_>,
id: CharacterId, id: CharacterId,
@ -189,7 +191,7 @@ impl<'gc> MovieClip<'gc> {
base: Default::default(), base: Default::default(),
static_data: Gc::allocate( static_data: Gc::allocate(
gc_context, gc_context,
MovieClipStatic::with_data(id, swf, num_frames, gc_context), MovieClipStatic::with_data(id, swf, num_frames, None, gc_context),
), ),
tag_stream_pos: 0, tag_stream_pos: 0,
current_frame: 0, current_frame: 0,
@ -218,20 +220,37 @@ impl<'gc> MovieClip<'gc> {
)) ))
} }
/// Construct a movie clip that represents an entire movie. /// Construct a movie clip that represents the root movie
pub fn from_movie(context: &mut UpdateContext<'_, 'gc, '_>, movie: Arc<SwfMovie>) -> Self { /// for the entire `Player`.
pub fn player_root_movie(
activation: &mut Avm2Activation<'_, 'gc, '_>,
movie: Arc<SwfMovie>,
) -> Self {
let num_frames = movie.num_frames(); let num_frames = movie.num_frames();
let loader_info = if movie.is_action_script_3() {
// The root movie doesn't have a `Loader`
// We will replace this with a `LoaderStream::Swf` later in this function
Some(
LoaderInfoObject::not_yet_loaded(activation, movie.clone(), None)
.expect("Failed to construct LoaderInfoObject"),
)
} else {
None
};
let mc = MovieClip(GcCell::allocate( let mc = MovieClip(GcCell::allocate(
context.gc_context, activation.context.gc_context,
MovieClipData { MovieClipData {
base: Default::default(), base: Default::default(),
static_data: Gc::allocate( static_data: Gc::allocate(
context.gc_context, activation.context.gc_context,
MovieClipStatic::with_data( MovieClipStatic::with_data(
0, 0,
movie.clone().into(), movie.clone().into(),
num_frames, num_frames,
context.gc_context, loader_info,
activation.context.gc_context,
), ),
), ),
tag_stream_pos: 0, tag_stream_pos: 0,
@ -259,8 +278,21 @@ impl<'gc> MovieClip<'gc> {
tag_frame_boundaries: Default::default(), tag_frame_boundaries: Default::default(),
}, },
)); ));
mc.set_is_root(context.gc_context, true);
mc.set_loader_info(context, movie); if movie.is_action_script_3() {
mc.0.read()
.static_data
.loader_info
.as_ref()
.unwrap()
.as_loader_info_object()
.unwrap()
.set_loader_stream(
LoaderStream::Swf(movie, mc.into()),
activation.context.gc_context,
);
}
mc.set_is_root(activation.context.gc_context, true);
mc mc
} }
@ -276,15 +308,27 @@ impl<'gc> MovieClip<'gc> {
&mut self, &mut self,
context: &mut UpdateContext<'_, 'gc, '_>, context: &mut UpdateContext<'_, 'gc, '_>,
movie: Option<Arc<SwfMovie>>, movie: Option<Arc<SwfMovie>>,
loader_info: Option<LoaderInfoObject<'gc>>,
) { ) {
let mut mc = self.0.write(context.gc_context); let mut mc = self.0.write(context.gc_context);
let is_swf = movie.is_some(); let is_swf = movie.is_some();
let movie = movie.unwrap_or_else(|| Arc::new(SwfMovie::empty(mc.movie().version()))); let movie = movie.unwrap_or_else(|| Arc::new(SwfMovie::empty(mc.movie().version())));
let total_frames = movie.num_frames(); let total_frames = movie.num_frames();
assert_eq!(
mc.static_data.loader_info, None,
"Called replace_movie on a clip with LoaderInfo set"
);
mc.base.base.reset_for_movie_load(); mc.base.base.reset_for_movie_load();
mc.static_data = Gc::allocate( mc.static_data = Gc::allocate(
context.gc_context, context.gc_context,
MovieClipStatic::with_data(0, movie.clone().into(), total_frames, context.gc_context), MovieClipStatic::with_data(
0,
movie.into(),
total_frames,
loader_info.map(|l| l.into()),
context.gc_context,
),
); );
mc.tag_stream_pos = 0; mc.tag_stream_pos = 0;
mc.flags = MovieClipFlags::PLAYING; mc.flags = MovieClipFlags::PLAYING;
@ -293,30 +337,6 @@ impl<'gc> MovieClip<'gc> {
mc.audio_stream = None; mc.audio_stream = None;
mc.container = ChildContainer::new(); mc.container = ChildContainer::new();
drop(mc); drop(mc);
self.set_loader_info(context, movie);
}
fn set_loader_info(&self, context: &mut UpdateContext<'_, 'gc, '_>, movie: Arc<SwfMovie>) {
if movie.is_action_script_3() {
let gc_context = context.gc_context;
let mc = self.0.write(gc_context);
if mc.base.base.is_root() {
let mut activation = Avm2Activation::from_nothing(context.reborrow());
match LoaderInfoObject::from_movie(&mut activation, movie, (*self).into()) {
Ok(loader_info) => {
*mc.static_data.loader_info.write(gc_context) = Some(loader_info);
}
Err(e) => {
log::error!(
"Error contructing LoaderInfoObject for movie {:?}: {:?}",
mc,
e
);
}
}
}
}
} }
pub fn preload(self, context: &mut UpdateContext<'_, 'gc, '_>) { pub fn preload(self, context: &mut UpdateContext<'_, 'gc, '_>) {
@ -1855,7 +1875,6 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
if context.is_action_script_3() { if context.is_action_script_3() {
let needs_construction = if matches!(self.object2(), Avm2Value::Undefined) { let needs_construction = if matches!(self.object2(), Avm2Value::Undefined) {
self.allocate_as_avm2_object(context, (*self).into()); self.allocate_as_avm2_object(context, (*self).into());
true true
} else { } else {
false false
@ -1988,8 +2007,12 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
// keeps track if an "init" event has already been fired, // keeps track if an "init" event has already been fired,
// so this becomes a no-op after the event has been fired. // so this becomes a no-op after the event has been fired.
if self.0.read().initialized() { if self.0.read().initialized() {
if let Some(loader_info) = self.loader_info() { if let Some(loader_info) = self
loader_info.loader_stream_init(context); .loader_info()
.as_ref()
.and_then(|o| o.as_loader_info_object())
{
loader_info.fire_init_and_complete_events(context);
} }
} }
@ -2150,7 +2173,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
} }
fn loader_info(&self) -> Option<Avm2Object<'gc>> { fn loader_info(&self) -> Option<Avm2Object<'gc>> {
*self.0.read().static_data.loader_info.read() self.0.read().static_data.loader_info
} }
fn allow_as_mask(&self) -> bool { fn allow_as_mask(&self) -> bool {
@ -3387,18 +3410,23 @@ struct MovieClipStatic<'gc> {
/// Only set if this MovieClip is the root movie in an SWF /// Only set if this MovieClip is the root movie in an SWF
/// (either the root SWF initially loaded by the player, /// (either the root SWF initially loaded by the player,
/// or an SWF dynamically loaded by `Loader`) /// or an SWF dynamically loaded by `Loader`)
loader_info: GcCell<'gc, Option<Avm2Object<'gc>>>, ///
/// This is always `None` for the AVM1 root movie.
/// However, it will be set for an AVM1 movie loaded from AVM2
/// via `Loader`
loader_info: Option<Avm2Object<'gc>>,
} }
impl<'gc> MovieClipStatic<'gc> { impl<'gc> MovieClipStatic<'gc> {
fn empty(movie: Arc<SwfMovie>, gc_context: MutationContext<'gc, '_>) -> Self { fn empty(movie: Arc<SwfMovie>, gc_context: MutationContext<'gc, '_>) -> Self {
Self::with_data(0, SwfSlice::empty(movie), 1, gc_context) Self::with_data(0, SwfSlice::empty(movie), 1, None, gc_context)
} }
fn with_data( fn with_data(
id: CharacterId, id: CharacterId,
swf: SwfSlice, swf: SwfSlice,
total_frames: FrameNumber, total_frames: FrameNumber,
loader_info: Option<Avm2Object<'gc>>,
gc_context: MutationContext<'gc, '_>, gc_context: MutationContext<'gc, '_>,
) -> Self { ) -> Self {
Self { Self {
@ -3410,7 +3438,7 @@ impl<'gc> MovieClipStatic<'gc> {
audio_stream_info: None, audio_stream_info: None,
audio_stream_handle: None, audio_stream_handle: None,
exported_name: GcCell::allocate(gc_context, None), exported_name: GcCell::allocate(gc_context, None),
loader_info: GcCell::allocate(gc_context, None), loader_info,
} }
} }
} }

View File

@ -1,7 +1,6 @@
//! Root stage impl //! Root stage impl
use crate::avm1::Object as Avm1Object; use crate::avm1::Object as Avm1Object;
use crate::avm2::object::LoaderInfoObject;
use crate::avm2::object::TObject; use crate::avm2::object::TObject;
use crate::avm2::{ use crate::avm2::{
Activation as Avm2Activation, EventObject as Avm2EventObject, Object as Avm2Object, Activation as Avm2Activation, EventObject as Avm2EventObject, Object as Avm2Object,
@ -174,6 +173,14 @@ impl<'gc> Stage<'gc> {
self.0.write(gc_context).movie_size = (width, height); self.0.write(gc_context).movie_size = (width, height);
} }
pub fn set_loader_info(
self,
gc_context: MutationContext<'gc, '_>,
loader_info: Avm2Object<'gc>,
) {
self.0.write(gc_context).loader_info = loader_info;
}
/// Returns the quality setting of the stage. /// Returns the quality setting of the stage.
/// ///
/// In the Flash Player, the quality setting affects anti-aliasing and smoothing of bitmaps. /// In the Flash Player, the quality setting affects anti-aliasing and smoothing of bitmaps.
@ -662,10 +669,6 @@ impl<'gc> TDisplayObject<'gc> for Stage<'gc> {
Ok(avm2_stage) => { Ok(avm2_stage) => {
let mut write = self.0.write(activation.context.gc_context); let mut write = self.0.write(activation.context.gc_context);
write.avm2_object = avm2_stage.into(); write.avm2_object = avm2_stage.into();
match LoaderInfoObject::from_stage(&mut activation) {
Ok(loader_info) => write.loader_info = loader_info,
Err(e) => log::error!("Unable to set AVM2 Stage loaderInfo: {}", e),
}
} }
Err(e) => log::error!("Unable to construct AVM2 Stage: {}", e), Err(e) => log::error!("Unable to construct AVM2 Stage: {}", e),
} }

View File

@ -6,7 +6,9 @@ use crate::avm1::{Avm1, Object, TObject, Value};
use crate::avm2::bytearray::ByteArrayStorage; use crate::avm2::bytearray::ByteArrayStorage;
use crate::avm2::object::ByteArrayObject; use crate::avm2::object::ByteArrayObject;
use crate::avm2::object::EventObject as Avm2EventObject; use crate::avm2::object::EventObject as Avm2EventObject;
use crate::avm2::object::LoaderStream;
use crate::avm2::object::TObject as _; use crate::avm2::object::TObject as _;
use crate::avm2::Multiname;
use crate::avm2::Namespace; use crate::avm2::Namespace;
use crate::avm2::{ use crate::avm2::{
Activation as Avm2Activation, Avm2, Domain as Avm2Domain, Object as Avm2Object, QName, Activation as Avm2Activation, Avm2, Domain as Avm2Domain, Object as Avm2Object, QName,
@ -119,7 +121,7 @@ pub enum Error {
#[error("Could not fetch: {0}")] #[error("Could not fetch: {0}")]
FetchError(String), FetchError(String),
#[error("Invalid SWF")] #[error("Invalid SWF: {0}")]
InvalidSwf(#[from] crate::tag_utils::Error), InvalidSwf(#[from] crate::tag_utils::Error),
#[error("Unexpected content of type {1}, expected {0}")] #[error("Unexpected content of type {1}, expected {0}")]
@ -209,12 +211,12 @@ impl<'gc> LoadManager<'gc> {
target_clip: DisplayObject<'gc>, target_clip: DisplayObject<'gc>,
request: Request, request: Request,
loader_url: Option<String>, loader_url: Option<String>,
target_broadcaster: Option<Object<'gc>>, event_handler: Option<MovieLoaderEventHandler<'gc>>,
) -> OwnedFuture<(), Error> { ) -> OwnedFuture<(), Error> {
let loader = Loader::Movie { let loader = Loader::Movie {
self_handle: None, self_handle: None,
target_clip, target_clip,
target_broadcaster, event_handler,
loader_status: LoaderStatus::Pending, loader_status: LoaderStatus::Pending,
}; };
let handle = self.add_loader(loader); let handle = self.add_loader(loader);
@ -314,6 +316,13 @@ pub enum LoaderStatus {
Failed, Failed,
} }
#[derive(Collect, Clone, Copy, Debug)]
#[collect(no_drop)]
pub enum MovieLoaderEventHandler<'gc> {
Avm1Broadcast(Object<'gc>),
Avm2LoaderInfo(Avm2Object<'gc>),
}
/// A struct that holds garbage-collected pointers for asynchronous code. /// A struct that holds garbage-collected pointers for asynchronous code.
#[derive(Collect)] #[derive(Collect)]
#[collect(no_drop)] #[collect(no_drop)]
@ -336,7 +345,7 @@ pub enum Loader<'gc> {
/// Event broadcaster (typically a `MovieClipLoader`) to fire events /// Event broadcaster (typically a `MovieClipLoader`) to fire events
/// into. /// into.
target_broadcaster: Option<Object<'gc>>, event_handler: Option<MovieLoaderEventHandler<'gc>>,
/// Indicates the completion status of this loader. /// Indicates the completion status of this loader.
/// ///
@ -458,78 +467,156 @@ impl<'gc> Loader<'gc> {
if let Some(mut mc) = clip.as_movie_clip() { if let Some(mut mc) = clip.as_movie_clip() {
mc.unload(uc); mc.unload(uc);
mc.replace_with_movie(uc, None); mc.replace_with_movie(uc, None, None);
} }
Loader::movie_loader_start(handle, uc) Loader::movie_loader_start(handle, uc)
})?; })?;
if let Ok(response) = fetch.await { match fetch.await {
let sniffed_type = ContentType::sniff(&response.body); Ok(response) => {
let mut length = response.body.len(); let sniffed_type = ContentType::sniff(&response.body);
let mut length = response.body.len();
if replacing_root_movie { if replacing_root_movie {
sniffed_type.expect(ContentType::Swf)?; sniffed_type.expect(ContentType::Swf)?;
let movie = let movie =
SwfMovie::from_data(&response.body, Some(response.url), loader_url)?; SwfMovie::from_data(&response.body, Some(response.url), loader_url)?;
player.lock().unwrap().set_root_movie(movie); player.lock().unwrap().set_root_movie(movie);
return Ok(()); return Ok(());
}
player.lock().unwrap().update(|uc| {
let clip = match uc.load_manager.get_loader(handle) {
Some(Loader::Movie { target_clip, .. }) => *target_clip,
None => return Err(Error::Cancelled),
_ => unreachable!(),
};
match sniffed_type {
ContentType::Swf => {
let movie = Arc::new(SwfMovie::from_data(
&response.body,
Some(response.url),
loader_url,
)?);
let mut activation = Avm2Activation::from_nothing(uc.reborrow());
let parent_domain = activation.avm2().global_domain();
let domain = Avm2Domain::movie_domain(&mut activation, parent_domain);
uc.library
.library_for_movie_mut(movie.clone())
.set_avm2_domain(domain);
if let Some(mut mc) = clip.as_movie_clip() {
mc.replace_with_movie(uc, Some(movie));
mc.post_instantiation(uc, None, Instantiator::Movie, false);
mc.preload(uc);
}
}
ContentType::Gif | ContentType::Jpeg | ContentType::Png => {
let bitmap = uc.renderer.register_bitmap_jpeg_2(&response.body)?;
let bitmap_obj =
Bitmap::new(uc, 0, bitmap.handle, bitmap.width, bitmap.height);
if let Some(mc) = clip.as_movie_clip() {
mc.replace_at_depth(uc, bitmap_obj.into(), 1);
}
}
ContentType::Unknown => {
length = 0;
}
} }
Loader::movie_loader_progress(handle, uc, length, length)?; player.lock().unwrap().update(|uc| {
let (clip, event_handler) = match uc.load_manager.get_loader(handle) {
Some(Loader::Movie {
target_clip,
event_handler,
..
}) => (*target_clip, *event_handler),
None => return Err(Error::Cancelled),
_ => unreachable!(),
};
Loader::movie_loader_complete(handle, uc)?; if let ContentType::Unknown = sniffed_type {
length = 0;
}
Ok(()) // FIXME - properly set 'bytesLoaded' and 'bytesTotal' on our `LoaderInfo` object
})?; //TODO: content sniffing errors need to be reported somehow // to match the values in the event.
} else { if let Some(MovieLoaderEventHandler::Avm2LoaderInfo(_)) = event_handler {
player // Flash always fires an initial 'progress' event with
.lock() // bytesLoaded=0 and bytesTotal set to the proper value.
.unwrap() // This only seems to happen for an AVM2 event handler
.update(|uc| -> Result<(), Error> { Loader::movie_loader_error(handle, uc) })?; Loader::movie_loader_progress(handle, uc, 0, length)?;
}
Loader::movie_loader_progress(handle, uc, length, length)?;
match sniffed_type {
ContentType::Swf => {
let movie = Arc::new(SwfMovie::from_data(
&response.body,
Some(response.url),
loader_url,
)?);
let mut activation = Avm2Activation::from_nothing(uc.reborrow());
let parent_domain = activation.avm2().global_domain();
let domain =
Avm2Domain::movie_domain(&mut activation, parent_domain);
activation
.context
.library
.library_for_movie_mut(movie.clone())
.set_avm2_domain(domain);
if let Some(mut mc) = clip.as_movie_clip() {
let loader_info = if let Some(
MovieLoaderEventHandler::Avm2LoaderInfo(loader_info),
) = event_handler
{
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()),
loader_info,
);
// FIXME - do we need to call 'set_place_frame'
mc.preload(&mut activation.context);
mc.construct_frame(&mut activation.context);
mc.post_instantiation(
&mut activation.context,
None,
Instantiator::Movie,
false,
);
if let Some(loader_info) = loader_info {
// Store the real movie into the `LoaderStream`, so that
// 'bytesTotal' starts returning the correct value
// (we previously had a fake empty SwfMovie).
// However, we still use `LoaderStream::NotYetLoaded`, since
// the actual MovieClip display object has not run its first
// frame yet.
loader_info.set_loader_stream(
LoaderStream::NotYetLoaded(movie),
activation.context.gc_context,
);
}
}
if let Some(MovieLoaderEventHandler::Avm2LoaderInfo(loader_info)) =
event_handler
{
let mut loader = loader_info
.get_property(
&Multiname::public("loader"),
&mut activation,
)?
.as_object()
.unwrap()
.as_display_object()
.unwrap()
.as_container()
.unwrap();
// 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
// clip does not yet exist.
loader.insert_at_index(&mut activation.context, clip, 0);
}
}
ContentType::Gif | ContentType::Jpeg | ContentType::Png => {
let bitmap = uc.renderer.register_bitmap_jpeg_2(&response.body)?;
let bitmap_obj =
Bitmap::new(uc, 0, bitmap.handle, bitmap.width, bitmap.height);
if let Some(mc) = clip.as_movie_clip() {
mc.replace_at_depth(uc, bitmap_obj.into(), 1);
}
}
ContentType::Unknown => {}
}
Loader::movie_loader_complete(handle, uc)?;
Ok(())
})?; //TODO: content sniffing errors need to be reported somehow
}
Err(e) => {
log::error!("Error during movie loading: {:?}", e);
player.lock().unwrap().update(|uc| -> Result<(), Error> {
Loader::movie_loader_error(handle, uc)
})?;
}
} }
Ok(()) Ok(())
@ -823,23 +910,37 @@ impl<'gc> Loader<'gc> {
let me = me.unwrap(); let me = me.unwrap();
let (clip, broadcaster) = match me { let (clip, event_handler) = match me {
Loader::Movie { Loader::Movie {
target_clip, target_clip,
target_broadcaster, event_handler,
.. ..
} => (*target_clip, *target_broadcaster), } => (*target_clip, *event_handler),
_ => unreachable!(), _ => unreachable!(),
}; };
if let Some(broadcaster) = broadcaster { match event_handler {
Avm1::run_stack_frame_for_method( Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) => {
clip, Avm1::run_stack_frame_for_method(
broadcaster, clip,
uc, broadcaster,
"broadcastMessage".into(), uc,
&["onLoadStart".into(), clip.object()], "broadcastMessage".into(),
); &["onLoadStart".into(), clip.object()],
);
}
Some(MovieLoaderEventHandler::Avm2LoaderInfo(loader_info)) => {
let mut activation = Avm2Activation::from_nothing(uc.reborrow());
let open_evt = Avm2EventObject::bare_default_event(&mut activation.context, "open");
if let Err(e) = Avm2::dispatch_event(uc, open_evt, loader_info) {
log::error!(
"Encountered AVM2 error when broadcasting `open` event: {}",
e
);
}
}
None => {}
} }
Ok(()) Ok(())
@ -859,28 +960,52 @@ impl<'gc> Loader<'gc> {
let me = me.unwrap(); let me = me.unwrap();
let (clip, broadcaster) = match me { let (clip, event_handler) = match me {
Loader::Movie { Loader::Movie {
target_clip, target_clip,
target_broadcaster, event_handler,
.. ..
} => (*target_clip, *target_broadcaster), } => (*target_clip, *event_handler),
_ => unreachable!(), _ => unreachable!(),
}; };
if let Some(broadcaster) = broadcaster { match event_handler {
Avm1::run_stack_frame_for_method( Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) => {
clip, Avm1::run_stack_frame_for_method(
broadcaster, clip,
uc, broadcaster,
"broadcastMessage".into(), uc,
&[ "broadcastMessage".into(),
"onLoadProgress".into(), &[
clip.object(), "onLoadProgress".into(),
cur_len.into(), clip.object(),
total_len.into(), cur_len.into(),
], total_len.into(),
); ],
);
}
Some(MovieLoaderEventHandler::Avm2LoaderInfo(loader_info)) => {
let mut activation = Avm2Activation::from_nothing(uc.reborrow());
let progress_evt = activation.avm2().classes().progressevent.construct(
&mut activation,
&[
"progress".into(),
false.into(),
false.into(),
cur_len.into(),
total_len.into(),
],
)?;
if let Err(e) = Avm2::dispatch_event(uc, progress_evt, loader_info) {
log::error!(
"Encountered AVM2 error when broadcasting `progress` event: {}",
e
);
}
}
None => {}
} }
Ok(()) Ok(())
@ -891,25 +1016,39 @@ impl<'gc> Loader<'gc> {
handle: Index, handle: Index,
uc: &mut UpdateContext<'_, 'gc, '_>, uc: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let (clip, broadcaster) = match uc.load_manager.get_loader_mut(handle) { let (clip, event_handler) = match uc.load_manager.get_loader_mut(handle) {
Some(Loader::Movie { Some(Loader::Movie {
target_clip, target_clip,
target_broadcaster, event_handler,
.. ..
}) => (*target_clip, *target_broadcaster), }) => (*target_clip, *event_handler),
None => return Err(Error::Cancelled), None => return Err(Error::Cancelled),
_ => unreachable!(), _ => unreachable!(),
}; };
if let Some(broadcaster) = broadcaster { match event_handler {
Avm1::run_stack_frame_for_method( Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) => {
clip, Avm1::run_stack_frame_for_method(
broadcaster, clip,
uc, broadcaster,
"broadcastMessage".into(), uc,
// TODO: Pass an actual httpStatus argument instead of 0. "broadcastMessage".into(),
&["onLoadComplete".into(), clip.object(), 0.into()], // TODO: Pass an actual httpStatus argument instead of 0.
); &["onLoadComplete".into(), clip.object(), 0.into()],
);
}
// This is fired after we process the movie's first frame,
// in `MovieClip.on_exit_frame`
Some(MovieLoaderEventHandler::Avm2LoaderInfo(loader_info)) => {
loader_info
.as_loader_info_object()
.unwrap()
.set_loader_stream(
LoaderStream::Swf(clip.as_movie_clip().unwrap().movie().unwrap(), clip),
uc.gc_context,
);
}
None => {}
} }
if let Loader::Movie { loader_status, .. } = uc.load_manager.get_loader_mut(handle).unwrap() if let Loader::Movie { loader_status, .. } = uc.load_manager.get_loader_mut(handle).unwrap()
@ -930,28 +1069,54 @@ impl<'gc> Loader<'gc> {
//error types we can actually inspect. //error types we can actually inspect.
//This also can get errors from decoding an invalid SWF file, //This also can get errors from decoding an invalid SWF file,
//too. We should distinguish those to player code. //too. We should distinguish those to player code.
let (clip, broadcaster) = match uc.load_manager.get_loader_mut(handle) { let (clip, event_handler) = match uc.load_manager.get_loader_mut(handle) {
Some(Loader::Movie { Some(Loader::Movie {
target_clip, target_clip,
target_broadcaster, event_handler,
.. ..
}) => (*target_clip, *target_broadcaster), }) => (*target_clip, *event_handler),
None => return Err(Error::Cancelled), None => return Err(Error::Cancelled),
_ => unreachable!(), _ => unreachable!(),
}; };
if let Some(broadcaster) = broadcaster { match event_handler {
Avm1::run_stack_frame_for_method( Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) => {
clip, Avm1::run_stack_frame_for_method(
broadcaster, clip,
uc, broadcaster,
"broadcastMessage".into(), uc,
&[ "broadcastMessage".into(),
"onLoadError".into(), &[
clip.object(), "onLoadError".into(),
"LoadNeverCompleted".into(), clip.object(),
], "LoadNeverCompleted".into(),
); ],
);
}
Some(MovieLoaderEventHandler::Avm2LoaderInfo(loader_info)) => {
let mut activation = Avm2Activation::from_nothing(uc.reborrow());
// FIXME - Match the exact error message generated by Flash
let io_error_evt_cls = activation.avm2().classes().ioerrorevent;
let io_error_evt = io_error_evt_cls.construct(
&mut activation,
&[
"ioError".into(),
false.into(),
false.into(),
"Movie loader error".into(),
0.into(),
],
)?;
if let Err(e) = Avm2::dispatch_event(uc, io_error_evt, loader_info) {
log::error!(
"Encountered AVM2 error when broadcasting `ioError` event: {}",
e
);
}
}
None => {}
} }
if let Loader::Movie { loader_status, .. } = uc.load_manager.get_loader_mut(handle).unwrap() if let Loader::Movie { loader_status, .. } = uc.load_manager.get_loader_mut(handle).unwrap()
@ -968,13 +1133,13 @@ impl<'gc> Loader<'gc> {
/// ///
/// Used to fire listener events on clips and terminate completed loaders. /// Used to fire listener events on clips and terminate completed loaders.
fn movie_clip_loaded(&mut self, queue: &mut ActionQueue<'gc>) -> bool { fn movie_clip_loaded(&mut self, queue: &mut ActionQueue<'gc>) -> bool {
let (clip, broadcaster, loader_status) = match self { let (clip, event_handler, loader_status) = match self {
Loader::Movie { Loader::Movie {
target_clip, target_clip,
target_broadcaster, event_handler,
loader_status, loader_status,
.. ..
} => (*target_clip, *target_broadcaster, *loader_status), } => (*target_clip, *event_handler, *loader_status),
_ => return false, _ => return false,
}; };
@ -982,7 +1147,8 @@ impl<'gc> Loader<'gc> {
LoaderStatus::Pending => false, LoaderStatus::Pending => false,
LoaderStatus::Failed => true, LoaderStatus::Failed => true,
LoaderStatus::Succeeded => { LoaderStatus::Succeeded => {
if let Some(broadcaster) = broadcaster { // AVM2 is handled separately
if let Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) = event_handler {
queue.queue_actions( queue.queue_actions(
clip, clip,
ActionType::Method { ActionType::Method {

View File

@ -4,6 +4,7 @@ use crate::avm1::globals::system::SystemProperties;
use crate::avm1::object::Object; use crate::avm1::object::Object;
use crate::avm1::property::Attribute; use crate::avm1::property::Attribute;
use crate::avm1::{Avm1, ScriptObject, TObject, Value}; use crate::avm1::{Avm1, ScriptObject, TObject, Value};
use crate::avm2::object::LoaderInfoObject;
use crate::avm2::{ use crate::avm2::{
Activation as Avm2Activation, Avm2, Domain as Avm2Domain, EventObject as Avm2EventObject, Activation as Avm2Activation, Avm2, Domain as Avm2Domain, EventObject as Avm2EventObject,
}; };
@ -290,15 +291,29 @@ impl Player {
let global_domain = activation.avm2().global_domain(); let global_domain = activation.avm2().global_domain();
let domain = Avm2Domain::movie_domain(&mut activation, global_domain); let domain = Avm2Domain::movie_domain(&mut activation, global_domain);
drop(activation); activation
.context
context
.library .library
.library_for_movie_mut(context.swf.clone()) .library_for_movie_mut(activation.context.swf.clone())
.set_avm2_domain(domain); .set_avm2_domain(domain);
context.ui.set_mouse_visible(true); activation.context.ui.set_mouse_visible(true);
let root: DisplayObject = MovieClip::from_movie(context, context.swf.clone()).into(); 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)
.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); root.set_depth(context.gc_context, 0);
let flashvars = if !context.swf.parameters().is_empty() { let flashvars = if !context.swf.parameters().is_empty() {
@ -1936,7 +1951,7 @@ impl PlayerBuilder {
let mut player_lock = player.lock().unwrap(); let mut player_lock = player.lock().unwrap();
player_lock.mutate_with_update_context(|context| { player_lock.mutate_with_update_context(|context| {
// Instantiate an empty root before the main movie loads. // Instantiate an empty root before the main movie loads.
let fake_root = MovieClip::from_movie(context, fake_movie); let fake_root = MovieClip::new(fake_movie, context.gc_context);
fake_root.post_instantiation(context, None, Instantiator::Movie, false); fake_root.post_instantiation(context, None, Instantiator::Movie, false);
context.stage.replace_at_depth(context, fake_root.into(), 0); context.stage.replace_at_depth(context, fake_root.into(), 0);
Avm2::load_player_globals(context).expect("Unable to load AVM2 globals"); Avm2::load_player_globals(context).expect("Unable to load AVM2 globals");

View File

@ -349,6 +349,7 @@ swf_tests! {
(as3_lazyinit, "avm2/lazyinit", 1), (as3_lazyinit, "avm2/lazyinit", 1),
(as3_lessequals, "avm2/lessequals", 1), (as3_lessequals, "avm2/lessequals", 1),
(as3_lessthan, "avm2/lessthan", 1), (as3_lessthan, "avm2/lessthan", 1),
(as3_loader_events, "avm2/loader_events", 3, img = true),
(as3_loaderinfo_events, "avm2/loaderinfo_events", 2), (as3_loaderinfo_events, "avm2/loaderinfo_events", 2),
(as3_loaderinfo_properties, "avm2/loaderinfo_properties", 2), (as3_loaderinfo_properties, "avm2/loaderinfo_properties", 2),
(as3_loaderinfo_root, "avm2/loaderinfo_root", 1), (as3_loaderinfo_root, "avm2/loaderinfo_root", 1),

View File

@ -0,0 +1,16 @@
package {
import flash.display.Stage;
import flash.display.Sprite;
public class Loadable {
public function Loadable(stage: Stage) {
trace("Hello from loaded SWF!");
var circle:Sprite = new Sprite();
circle.graphics.beginFill(0xFFCC00);
circle.graphics.drawCircle(50, 50, 50);
stage.addChild(circle);
}
}
}

View File

@ -0,0 +1,61 @@
package {
import flash.display.Stage;
public class Test {
public function Test(stage: Stage) {
import flash.display.Loader;
import flash.net.URLRequest;
import flash.errors.IllegalOperationError;
import flash.display.Sprite;
import flash.events.Event;
import flash.events.ProgressEvent;
var loader = new Loader();
stage.addChild(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 = " + loader.contentLoaderInfo.bytes);
trace("loader.contentLoaderInfo.url = " + loader.contentLoaderInfo.url);
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.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(Event.COMPLETE, function(e) {
dump(e);
});
loader.load(new URLRequest("./loadable.swf"));
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,14 @@
loader.content = null
loader.contentLoaderInfo.content = null
loader.contentLoaderInfo.bytesLoaded = 0
loader.contentLoaderInfo.bytesTotal = 0
loader.contentLoaderInfo.bytes = null
loader.contentLoaderInfo.url = null
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.url = null
Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=0 bytesTotal=674]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 0, loader.contentLoaderInfo.bytesTotal = 0, loader.contentLoaderInfo.url = null
Event [ProgressEvent type="progress" bubbles=false cancelable=false eventPhase=2 bytesLoaded=674 bytesTotal=674]: loader.numChildren = 0, loader.content = null, loader.contentLoaderInfo.bytesLoaded = 0, loader.contentLoaderInfo.bytesTotal = 0, loader.contentLoaderInfo.url = null
Hello from loaded SWF!
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 LoadableMainTimeline], loader.contentLoaderInfo.bytesLoaded = 674, loader.contentLoaderInfo.bytesTotal = 674, loader.contentLoaderInfo.url = file:///
Event [Event type="complete" bubbles=false cancelable=false eventPhase=2]: loader.numChildren = 1, loader.content = [object LoadableMainTimeline], loader.contentLoaderInfo.bytesLoaded = 674, loader.contentLoaderInfo.bytesTotal = 674, loader.contentLoaderInfo.url = file:///

Binary file not shown.

Binary file not shown.