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
with:
name: swf_images
path: tests/**/actual.png
path: tests*/**/actual.png
check-required:
needs: changes

View File

@ -1153,7 +1153,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
if url.is_empty() {
//Blank URL on movie loads = unload!
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 {
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() {
// Blank URL on movie loads = unload!
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 {
let request = self.locals_into_request(
@ -1293,7 +1293,7 @@ impl<'a, 'gc, 'gc_context> Activation<'a, 'gc, 'gc_context> {
if url.is_empty() {
// Blank URL on movie loads = unload!
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 {
let future = self.context.load_manager.load_movie_into_clip(

View File

@ -1361,7 +1361,7 @@ fn unload_movie<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
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)
}

View File

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

View File

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

View File

@ -1,5 +1,11 @@
// This is a stub - the actual class is defined in `displayobjectcontainer.rs`
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.
fn add_child_to_displaylist<'gc>(
pub(super) fn add_child_to_displaylist<'gc>(
context: &mut UpdateContext<'_, 'gc, '_>,
parent: DisplayObject<'gc>,
child: DisplayObject<'gc>,

View File

@ -1,71 +1,92 @@
//! `flash.display.Loader` builtin/prototype
use crate::avm2::activation::Activation;
use crate::avm2::class::{Class, ClassAttributes};
use crate::avm2::method::{Method, NativeMethodImpl};
use crate::avm2::object::LoaderInfoObject;
use crate::avm2::object::TObject;
use crate::avm2::value::Value;
use crate::avm2::Multiname;
use crate::avm2::Namespace;
use crate::avm2::QName;
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 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>(
pub fn init<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Some(this) = this {
activation.super_init(this, &[])?;
if let Some(mut this) = 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)
}
// TODO: this is entirely stubbed, just to get addEventListener to not crash
fn content_loader_info<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
pub fn load<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
log::warn!("Loader::contentLoaderInfo is a stub");
Ok(this.unwrap().into())
}
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
QName::new(Namespace::package("flash.display"), "Loader"),
Some(
QName::new(
Namespace::package("flash.display"),
"DisplayObjectContainer",
)
.into(),
),
Method::from_builtin(instance_init, "<Loader instance initializer>", mc),
Method::from_builtin(class_init, "<Loader class initializer>", mc),
mc,
);
let mut write = class.write(mc);
write.set_attributes(ClassAttributes::SEALED);
const PUBLIC_INSTANCE_PROPERTIES: &[(
&str,
Option<NativeMethodImpl>,
Option<NativeMethodImpl>,
)] = &[("contentLoaderInfo", Some(content_loader_info), None)];
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);
class
if let Some(this) = this {
let url_request = args[0].as_object().unwrap();
if let Some(context) = args.get(1) {
if !matches!(context, Value::Null) {
log::warn!(
"Loader.load: 'context' argument is not yet implemented: {:?}",
context
);
}
}
let url = url_request
.get_property(&Multiname::public("url"), activation)?
.coerce_to_string(activation)?;
// This is a dummy MovieClip, which will get overwritten in `Loader`
let content = MovieClip::new(
Arc::new(SwfMovie::empty(activation.context.swf.version())),
activation.context.gc_context,
);
let loader_info = this
.get_property(
&QName::new(Namespace::private(""), "_contentLoaderInfo").into(),
activation,
)?
.as_object()
.unwrap();
let future = activation.context.load_manager.load_movie_into_clip(
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::display_object::TDisplayObject;
use gc_arena::{GcCell, MutationContext};
use std::sync::Arc;
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.
pub fn instance_init<'gc>(
_activation: &mut Activation<'_, 'gc, '_>,
@ -51,10 +56,13 @@ pub fn action_script_version<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => {
return Err("Error: The stage's loader info does not have an AS version".into())
LoaderStream::NotYetLoaded(_) => {
return Err(INSUFFICIENT.into());
}
LoaderStream::Swf(movie, _) => {
let version = if movie.is_action_script_3() { 3 } else { 2 };
@ -74,9 +82,12 @@ pub fn application_domain<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => {
LoaderStream::NotYetLoaded(_) => {
return Ok(DomainObject::from_domain(activation, activation.domain())?.into());
}
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
/// streaming loads yet. When we do, we'll need another property for this.
pub fn bytes_total<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
_activation: &mut Activation<'_, 'gc, '_>,
this: Option<Object<'gc>>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => return Ok(activation.context.swf.compressed_len().into()),
LoaderStream::NotYetLoaded(swf) => return Ok(swf.compressed_len().into()),
LoaderStream::Swf(movie, _) => {
return Ok(movie.compressed_len().into());
}
@ -124,9 +138,17 @@ pub fn content<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
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) => {
return Ok(root.object2());
}
@ -144,9 +166,12 @@ pub fn content_type<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => return Ok(Value::Null),
LoaderStream::NotYetLoaded(_) => return Ok(Value::Null),
LoaderStream::Swf(_, _) => {
return Ok("application/x-shockwave-flash".into());
}
@ -164,9 +189,12 @@ pub fn frame_rate<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => {
LoaderStream::NotYetLoaded(_) => {
return Err("Error: The stage's loader info does not have a frame rate".into())
}
LoaderStream::Swf(root, _) => {
@ -186,9 +214,12 @@ pub fn height<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => {
LoaderStream::NotYetLoaded(_) => {
return Err("Error: The stage's loader info does not have a height".into())
}
LoaderStream::Swf(root, _) => {
@ -217,9 +248,12 @@ pub fn swf_version<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => {
LoaderStream::NotYetLoaded(_) => {
return Err("Error: The stage's loader info does not have a SWF version".into())
}
LoaderStream::Swf(root, _) => {
@ -239,14 +273,19 @@ pub fn url<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => activation.context.swf,
LoaderStream::NotYetLoaded(swf) => swf,
LoaderStream::Swf(root, _) => root,
};
let url = root.url().unwrap_or("");
return Ok(AvmString::new_utf8(activation.context.gc_context, url).into());
let url = root.url().map_or(Value::Null, |url| {
AvmString::new_utf8(activation.context.gc_context, url).into()
});
return Ok(url);
}
}
@ -260,9 +299,12 @@ pub fn width<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => {
LoaderStream::NotYetLoaded(_) => {
return Err("Error: The stage's loader info does not have a width".into())
}
LoaderStream::Swf(root, _) => {
@ -282,12 +324,19 @@ pub fn bytes<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => activation.context.swf,
LoaderStream::NotYetLoaded(swf) => swf,
LoaderStream::Swf(root, _) => root,
};
if root.data().is_empty() {
return Ok(Value::Null);
}
let ba_class = activation.context.avm2.classes().bytearray;
let ba = ba_class.construct(activation, &[])?;
@ -325,6 +374,19 @@ pub fn bytes<'gc>(
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
pub fn loader_url<'gc>(
activation: &mut Activation<'_, 'gc, '_>,
@ -332,9 +394,12 @@ pub fn loader_url<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => activation.context.swf,
LoaderStream::NotYetLoaded(swf) => swf,
LoaderStream::Swf(root, _) => root,
};
@ -353,9 +418,12 @@ pub fn parameters<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
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 {
LoaderStream::Stage => activation.context.swf,
LoaderStream::NotYetLoaded(_) => activation.context.swf,
LoaderStream::Swf(root, _) => root,
};
@ -383,6 +451,18 @@ pub fn parameters<'gc>(
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.
pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> {
let class = Class::new(
@ -421,8 +501,10 @@ pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>
("url", Some(url), None),
("width", Some(width), None),
("bytes", Some(bytes), None),
("loader", Some(loader), None),
("loaderURL", Some(loader_url), None),
("parameters", Some(parameters), None),
("sharedEvents", Some(shared_events), None),
];
write.define_public_builtin_instance_properties(mc, PUBLIC_INSTANCE_PROPERTIES);

View File

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

View File

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

View File

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

View File

@ -18,7 +18,6 @@ use crate::avm2::Namespace;
use crate::avm2::QName;
use crate::backend::audio::{SoundHandle, SoundInstanceHandle};
use crate::bitmap::bitmap_data::BitmapData;
use crate::context::UpdateContext;
use crate::display_object::DisplayObject;
use crate::html::TextFormat;
use crate::string::AvmString;
@ -903,6 +902,10 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
None
}
fn as_loader_info_object(&self) -> Option<&LoaderInfoObject<'gc>> {
None
}
fn as_array_object(&self) -> Option<ArrayObject<'gc>> {
None
}
@ -1011,13 +1014,6 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
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.
fn as_sound(self) -> Option<SoundHandle> {
None

View File

@ -26,7 +26,14 @@ pub fn loaderinfo_allocator<'gc>(
LoaderInfoObjectData {
base,
loaded_stream: None,
init_fired: false,
loader: None,
events_fired: false,
shared_events: activation
.context
.avm2
.classes()
.eventdispatcher
.construct(activation, &[])?,
},
))
.into())
@ -36,12 +43,10 @@ pub fn loaderinfo_allocator<'gc>(
#[derive(Collect, Debug, Clone)]
#[collect(no_drop)]
pub enum LoaderStream<'gc> {
/// The current stage.
///
/// While it makes no sense to actually retrieve loader info properties off
/// the stage, it's possible to do so. Some properties yield the
/// not-yet-loaded error while others are pulled from the root SWF.
Stage,
NotYetLoaded(Arc<SwfMovie>),
/// A loaded SWF movie.
///
@ -64,8 +69,15 @@ pub struct LoaderInfoObjectData<'gc> {
/// The loaded stream that this gets it's info from.
loaded_stream: Option<LoaderStream<'gc>>,
/// Whether or not we've fired an 'init' event
init_fired: bool,
loader: Option<Object<'gc>>,
/// 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> {
@ -74,6 +86,7 @@ impl<'gc> LoaderInfoObject<'gc> {
activation: &mut Activation<'_, 'gc, '_>,
movie: Arc<SwfMovie>,
root: DisplayObject<'gc>,
loader: Option<Object<'gc>>,
) -> Result<Object<'gc>, Error> {
let class = activation.avm2().classes().loaderinfo;
let base = ScriptObjectData::new(class);
@ -84,7 +97,14 @@ impl<'gc> LoaderInfoObject<'gc> {
LoaderInfoObjectData {
base,
loaded_stream,
init_fired: false,
loader,
events_fired: false,
shared_events: activation
.context
.avm2
.classes()
.eventdispatcher
.construct(activation, &[])?,
},
))
.into();
@ -96,7 +116,11 @@ impl<'gc> LoaderInfoObject<'gc> {
}
/// 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 base = ScriptObjectData::new(class);
@ -104,10 +128,15 @@ impl<'gc> LoaderInfoObject<'gc> {
activation.context.gc_context,
LoaderInfoObjectData {
base,
loaded_stream: Some(LoaderStream::Stage),
// We never want to fire an "init" event for the special
// Stagee loaderInfo
init_fired: true,
loaded_stream: Some(LoaderStream::NotYetLoaded(movie)),
loader,
events_fired: false,
shared_events: activation
.context
.avm2
.classes()
.eventdispatcher
.construct(activation, &[])?,
},
))
.into();
@ -117,6 +146,55 @@ impl<'gc> LoaderInfoObject<'gc> {
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> {
@ -136,28 +214,7 @@ impl<'gc> TObject<'gc> for LoaderInfoObject<'gc> {
Ok(Value::Object((*self).into()))
}
fn loader_stream_init(&self, context: &mut UpdateContext<'_, 'gc, '_>) {
if !self.0.read().init_fired {
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
}
fn as_loader_info_object(&self) -> Option<&LoaderInfoObject<'gc>> {
Some(self)
}
}

View File

@ -28,6 +28,7 @@ mod container;
mod edit_text;
mod graphic;
mod interactive;
mod loader_display;
mod morph_shape;
mod movie_clip;
mod stage;
@ -44,6 +45,7 @@ pub use bitmap::Bitmap;
pub use edit_text::{AutoSizeMode, EditText, TextSelection};
pub use graphic::Graphic;
pub use interactive::{InteractiveObject, TInteractiveObject};
pub use loader_display::LoaderDisplay;
pub use morph_shape::{MorphShape, MorphShapeStatic};
pub use movie_clip::{MovieClip, Scene};
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>),
Text(Text<'gc>),
Video(Video<'gc>),
LoaderDisplay(LoaderDisplay<'gc>)
}
)]
pub trait TDisplayObject<'gc>:

View File

@ -3,6 +3,7 @@
use crate::avm2::{Avm2, EventObject as Avm2EventObject, Value as Avm2Value};
use crate::context::{RenderContext, UpdateContext};
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::stage::Stage;
use crate::display_object::{Depth, DisplayObject, TDisplayObject};
@ -142,6 +143,7 @@ bitflags! {
Stage(Stage<'gc>),
Avm1Button(Avm1Button<'gc>),
MovieClip(MovieClip<'gc>),
LoaderDisplay(LoaderDisplay<'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::avm2_button::Avm2Button;
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::stage::Stage;
use crate::display_object::{
@ -109,6 +110,7 @@ impl<'gc> Default for InteractiveObjectBase<'gc> {
Avm2Button(Avm2Button<'gc>),
MovieClip(MovieClip<'gc>),
EditText(EditText<'gc>),
LoaderDisplay(LoaderDisplay<'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,
};
use crate::avm2::object::LoaderInfoObject;
use crate::avm2::object::LoaderStream;
use crate::avm2::Activation as Avm2Activation;
use crate::avm2::{
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(
gc_context: MutationContext<'gc, '_>,
id: CharacterId,
@ -189,7 +191,7 @@ impl<'gc> MovieClip<'gc> {
base: Default::default(),
static_data: Gc::allocate(
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,
current_frame: 0,
@ -218,20 +220,37 @@ impl<'gc> MovieClip<'gc> {
))
}
/// Construct a movie clip that represents an entire movie.
pub fn from_movie(context: &mut UpdateContext<'_, 'gc, '_>, movie: Arc<SwfMovie>) -> Self {
/// Construct a movie clip that represents the root movie
/// for the entire `Player`.
pub fn player_root_movie(
activation: &mut Avm2Activation<'_, 'gc, '_>,
movie: Arc<SwfMovie>,
) -> Self {
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(
context.gc_context,
activation.context.gc_context,
MovieClipData {
base: Default::default(),
static_data: Gc::allocate(
context.gc_context,
activation.context.gc_context,
MovieClipStatic::with_data(
0,
movie.clone().into(),
num_frames,
context.gc_context,
loader_info,
activation.context.gc_context,
),
),
tag_stream_pos: 0,
@ -259,8 +278,21 @@ impl<'gc> MovieClip<'gc> {
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
}
@ -276,15 +308,27 @@ impl<'gc> MovieClip<'gc> {
&mut self,
context: &mut UpdateContext<'_, 'gc, '_>,
movie: Option<Arc<SwfMovie>>,
loader_info: Option<LoaderInfoObject<'gc>>,
) {
let mut mc = self.0.write(context.gc_context);
let is_swf = movie.is_some();
let movie = movie.unwrap_or_else(|| Arc::new(SwfMovie::empty(mc.movie().version())));
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.static_data = Gc::allocate(
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.flags = MovieClipFlags::PLAYING;
@ -293,30 +337,6 @@ impl<'gc> MovieClip<'gc> {
mc.audio_stream = None;
mc.container = ChildContainer::new();
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, '_>) {
@ -1855,7 +1875,6 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
if context.is_action_script_3() {
let needs_construction = if matches!(self.object2(), Avm2Value::Undefined) {
self.allocate_as_avm2_object(context, (*self).into());
true
} else {
false
@ -1988,8 +2007,12 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
// keeps track if an "init" event has already been fired,
// so this becomes a no-op after the event has been fired.
if self.0.read().initialized() {
if let Some(loader_info) = self.loader_info() {
loader_info.loader_stream_init(context);
if let Some(loader_info) = self
.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>> {
*self.0.read().static_data.loader_info.read()
self.0.read().static_data.loader_info
}
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
/// (either the root SWF initially loaded by the player,
/// 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> {
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(
id: CharacterId,
swf: SwfSlice,
total_frames: FrameNumber,
loader_info: Option<Avm2Object<'gc>>,
gc_context: MutationContext<'gc, '_>,
) -> Self {
Self {
@ -3410,7 +3438,7 @@ impl<'gc> MovieClipStatic<'gc> {
audio_stream_info: None,
audio_stream_handle: 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
use crate::avm1::Object as Avm1Object;
use crate::avm2::object::LoaderInfoObject;
use crate::avm2::object::TObject;
use crate::avm2::{
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);
}
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.
///
/// 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) => {
let mut write = self.0.write(activation.context.gc_context);
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),
}

View File

@ -6,7 +6,9 @@ use crate::avm1::{Avm1, Object, TObject, Value};
use crate::avm2::bytearray::ByteArrayStorage;
use crate::avm2::object::ByteArrayObject;
use crate::avm2::object::EventObject as Avm2EventObject;
use crate::avm2::object::LoaderStream;
use crate::avm2::object::TObject as _;
use crate::avm2::Multiname;
use crate::avm2::Namespace;
use crate::avm2::{
Activation as Avm2Activation, Avm2, Domain as Avm2Domain, Object as Avm2Object, QName,
@ -119,7 +121,7 @@ pub enum Error {
#[error("Could not fetch: {0}")]
FetchError(String),
#[error("Invalid SWF")]
#[error("Invalid SWF: {0}")]
InvalidSwf(#[from] crate::tag_utils::Error),
#[error("Unexpected content of type {1}, expected {0}")]
@ -209,12 +211,12 @@ impl<'gc> LoadManager<'gc> {
target_clip: DisplayObject<'gc>,
request: Request,
loader_url: Option<String>,
target_broadcaster: Option<Object<'gc>>,
event_handler: Option<MovieLoaderEventHandler<'gc>>,
) -> OwnedFuture<(), Error> {
let loader = Loader::Movie {
self_handle: None,
target_clip,
target_broadcaster,
event_handler,
loader_status: LoaderStatus::Pending,
};
let handle = self.add_loader(loader);
@ -314,6 +316,13 @@ pub enum LoaderStatus {
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.
#[derive(Collect)]
#[collect(no_drop)]
@ -336,7 +345,7 @@ pub enum Loader<'gc> {
/// Event broadcaster (typically a `MovieClipLoader`) to fire events
/// into.
target_broadcaster: Option<Object<'gc>>,
event_handler: Option<MovieLoaderEventHandler<'gc>>,
/// Indicates the completion status of this loader.
///
@ -458,78 +467,156 @@ impl<'gc> Loader<'gc> {
if let Some(mut mc) = clip.as_movie_clip() {
mc.unload(uc);
mc.replace_with_movie(uc, None);
mc.replace_with_movie(uc, None, None);
}
Loader::movie_loader_start(handle, uc)
})?;
if let Ok(response) = fetch.await {
let sniffed_type = ContentType::sniff(&response.body);
let mut length = response.body.len();
match fetch.await {
Ok(response) => {
let sniffed_type = ContentType::sniff(&response.body);
let mut length = response.body.len();
if replacing_root_movie {
sniffed_type.expect(ContentType::Swf)?;
if replacing_root_movie {
sniffed_type.expect(ContentType::Swf)?;
let movie =
SwfMovie::from_data(&response.body, Some(response.url), loader_url)?;
player.lock().unwrap().set_root_movie(movie);
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;
}
let movie =
SwfMovie::from_data(&response.body, Some(response.url), loader_url)?;
player.lock().unwrap().set_root_movie(movie);
return Ok(());
}
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(())
})?; //TODO: content sniffing errors need to be reported somehow
} else {
player
.lock()
.unwrap()
.update(|uc| -> Result<(), Error> { Loader::movie_loader_error(handle, uc) })?;
// FIXME - properly set 'bytesLoaded' and 'bytesTotal' on our `LoaderInfo` object
// to match the values in the event.
if let Some(MovieLoaderEventHandler::Avm2LoaderInfo(_)) = event_handler {
// Flash always fires an initial 'progress' event with
// bytesLoaded=0 and bytesTotal set to the proper value.
// This only seems to happen for an AVM2 event handler
Loader::movie_loader_progress(handle, 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(())
@ -823,23 +910,37 @@ impl<'gc> Loader<'gc> {
let me = me.unwrap();
let (clip, broadcaster) = match me {
let (clip, event_handler) = match me {
Loader::Movie {
target_clip,
target_broadcaster,
event_handler,
..
} => (*target_clip, *target_broadcaster),
} => (*target_clip, *event_handler),
_ => unreachable!(),
};
if let Some(broadcaster) = broadcaster {
Avm1::run_stack_frame_for_method(
clip,
broadcaster,
uc,
"broadcastMessage".into(),
&["onLoadStart".into(), clip.object()],
);
match event_handler {
Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) => {
Avm1::run_stack_frame_for_method(
clip,
broadcaster,
uc,
"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(())
@ -859,28 +960,52 @@ impl<'gc> Loader<'gc> {
let me = me.unwrap();
let (clip, broadcaster) = match me {
let (clip, event_handler) = match me {
Loader::Movie {
target_clip,
target_broadcaster,
event_handler,
..
} => (*target_clip, *target_broadcaster),
} => (*target_clip, *event_handler),
_ => unreachable!(),
};
if let Some(broadcaster) = broadcaster {
Avm1::run_stack_frame_for_method(
clip,
broadcaster,
uc,
"broadcastMessage".into(),
&[
"onLoadProgress".into(),
clip.object(),
cur_len.into(),
total_len.into(),
],
);
match event_handler {
Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) => {
Avm1::run_stack_frame_for_method(
clip,
broadcaster,
uc,
"broadcastMessage".into(),
&[
"onLoadProgress".into(),
clip.object(),
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(())
@ -891,25 +1016,39 @@ impl<'gc> Loader<'gc> {
handle: Index,
uc: &mut UpdateContext<'_, 'gc, '_>,
) -> 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 {
target_clip,
target_broadcaster,
event_handler,
..
}) => (*target_clip, *target_broadcaster),
}) => (*target_clip, *event_handler),
None => return Err(Error::Cancelled),
_ => unreachable!(),
};
if let Some(broadcaster) = broadcaster {
Avm1::run_stack_frame_for_method(
clip,
broadcaster,
uc,
"broadcastMessage".into(),
// TODO: Pass an actual httpStatus argument instead of 0.
&["onLoadComplete".into(), clip.object(), 0.into()],
);
match event_handler {
Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) => {
Avm1::run_stack_frame_for_method(
clip,
broadcaster,
uc,
"broadcastMessage".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()
@ -930,28 +1069,54 @@ impl<'gc> Loader<'gc> {
//error types we can actually inspect.
//This also can get errors from decoding an invalid SWF file,
//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 {
target_clip,
target_broadcaster,
event_handler,
..
}) => (*target_clip, *target_broadcaster),
}) => (*target_clip, *event_handler),
None => return Err(Error::Cancelled),
_ => unreachable!(),
};
if let Some(broadcaster) = broadcaster {
Avm1::run_stack_frame_for_method(
clip,
broadcaster,
uc,
"broadcastMessage".into(),
&[
"onLoadError".into(),
clip.object(),
"LoadNeverCompleted".into(),
],
);
match event_handler {
Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) => {
Avm1::run_stack_frame_for_method(
clip,
broadcaster,
uc,
"broadcastMessage".into(),
&[
"onLoadError".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()
@ -968,13 +1133,13 @@ impl<'gc> Loader<'gc> {
///
/// Used to fire listener events on clips and terminate completed loaders.
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 {
target_clip,
target_broadcaster,
event_handler,
loader_status,
..
} => (*target_clip, *target_broadcaster, *loader_status),
} => (*target_clip, *event_handler, *loader_status),
_ => return false,
};
@ -982,7 +1147,8 @@ impl<'gc> Loader<'gc> {
LoaderStatus::Pending => false,
LoaderStatus::Failed => true,
LoaderStatus::Succeeded => {
if let Some(broadcaster) = broadcaster {
// AVM2 is handled separately
if let Some(MovieLoaderEventHandler::Avm1Broadcast(broadcaster)) = event_handler {
queue.queue_actions(
clip,
ActionType::Method {

View File

@ -4,6 +4,7 @@ use crate::avm1::globals::system::SystemProperties;
use crate::avm1::object::Object;
use crate::avm1::property::Attribute;
use crate::avm1::{Avm1, ScriptObject, TObject, Value};
use crate::avm2::object::LoaderInfoObject;
use crate::avm2::{
Activation as Avm2Activation, Avm2, Domain as Avm2Domain, EventObject as Avm2EventObject,
};
@ -290,15 +291,29 @@ impl Player {
let global_domain = activation.avm2().global_domain();
let domain = Avm2Domain::movie_domain(&mut activation, global_domain);
drop(activation);
context
activation
.context
.library
.library_for_movie_mut(context.swf.clone())
.library_for_movie_mut(activation.context.swf.clone())
.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);
let flashvars = if !context.swf.parameters().is_empty() {
@ -1936,7 +1951,7 @@ impl PlayerBuilder {
let mut player_lock = player.lock().unwrap();
player_lock.mutate_with_update_context(|context| {
// 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);
context.stage.replace_at_depth(context, fake_root.into(), 0);
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_lessequals, "avm2/lessequals", 1),
(as3_lessthan, "avm2/lessthan", 1),
(as3_loader_events, "avm2/loader_events", 3, img = true),
(as3_loaderinfo_events, "avm2/loaderinfo_events", 2),
(as3_loaderinfo_properties, "avm2/loaderinfo_properties", 2),
(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.