diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index 491e63ce2..93a30c0b2 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -17,6 +17,7 @@ mod key; mod math; pub(crate) mod mouse; pub(crate) mod movie_clip; +mod movie_clip_loader; pub(crate) mod number; mod object; mod sound; @@ -154,6 +155,9 @@ pub fn create_globals<'gc>( let movie_clip_proto: Object<'gc> = movie_clip::create_proto(gc_context, object_proto, function_proto); + let movie_clip_loader_proto: Object<'gc> = + movie_clip_loader::create_proto(gc_context, object_proto, function_proto); + let sound_proto: Object<'gc> = sound::create_proto(gc_context, object_proto, function_proto); let text_field_proto: Object<'gc> = @@ -200,6 +204,12 @@ pub fn create_globals<'gc>( Some(function_proto), Some(movie_clip_proto), ); + let movie_clip_loader = FunctionObject::function( + gc_context, + Executable::Native(movie_clip_loader::constructor), + Some(function_proto), + Some(movie_clip_loader_proto), + ); let sound = FunctionObject::function( gc_context, Executable::Native(sound::constructor), @@ -249,6 +259,12 @@ pub fn create_globals<'gc>( globals.define_value(gc_context, "Object", object.into(), EnumSet::empty()); globals.define_value(gc_context, "Function", function.into(), EnumSet::empty()); globals.define_value(gc_context, "MovieClip", movie_clip.into(), EnumSet::empty()); + globals.define_value( + gc_context, + "MovieClipLoader", + movie_clip_loader.into(), + EnumSet::empty(), + ); globals.define_value(gc_context, "Sound", sound.into(), EnumSet::empty()); globals.define_value(gc_context, "TextField", text_field.into(), EnumSet::empty()); globals.define_value( diff --git a/core/src/avm1/globals/movie_clip_loader.rs b/core/src/avm1/globals/movie_clip_loader.rs new file mode 100644 index 000000000..34ddff673 --- /dev/null +++ b/core/src/avm1/globals/movie_clip_loader.rs @@ -0,0 +1,162 @@ +//! `MovieClipLoader` impl + +use crate::avm1::object::TObject; +use crate::avm1::property::Attribute; +use crate::avm1::return_value::ReturnValue; +use crate::avm1::script_object::ScriptObject; +use crate::avm1::{Avm1, Error, Object, UpdateContext, Value}; +use enumset::EnumSet; +use gc_arena::MutationContext; + +pub fn constructor<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error> { + let listeners = ScriptObject::array(context.gc_context, Some(avm.prototypes().array)); + this.define_value( + context.gc_context, + "_listeners", + Value::Object(listeners.into()), + Attribute::DontEnum.into(), + ); + listeners.set("0", Value::Object(this), avm, context)?; + + Ok(Value::Undefined.into()) +} + +pub fn add_listener<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let new_listener = args.get(0).cloned().unwrap_or(Value::Undefined); + let listeners = this + .get("_listeners", avm, context)? + .resolve(avm, context)?; + + if let Value::Object(listeners) = listeners { + let length = listeners.length(); + listeners.set_length(context.gc_context, length + 1); + listeners.set_array_element(length, new_listener, context.gc_context); + } + + Ok(true.into()) +} + +pub fn remove_listener<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let old_listener = args.get(0).cloned().unwrap_or(Value::Undefined); + let listeners = this + .get("_listeners", avm, context)? + .resolve(avm, context)?; + + if let Value::Object(listeners) = listeners { + let length = listeners.length(); + let mut position = None; + + for i in 0..length { + let other_listener = listeners + .get(&format!("{}", i), avm, context)? + .resolve(avm, context)?; + if old_listener == other_listener { + position = Some(i); + break; + } + } + + if let Some(position) = position { + if length > 0 { + let new_length = length - 1; + for i in position..new_length { + listeners.set_array_element( + i, + listeners.array_element(i + 1), + context.gc_context, + ); + } + + listeners.delete_array_element(new_length, context.gc_context); + listeners.delete(context.gc_context, &new_length.to_string()); + + listeners.set_length(context.gc_context, new_length); + } + } + } + + Ok(true.into()) +} + +pub fn broadcast_message<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let event_name = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let call_args = &args[0..]; + + let listeners = this + .get("_listeners", avm, context)? + .resolve(avm, context)?; + if let Value::Object(listeners) = listeners { + for i in 0..listeners.length() { + let listener = listeners + .get(&format!("{}", i), avm, context)? + .resolve(avm, context)?; + + if let Value::Object(listener) = listener { + let handler = listener + .get(&event_name, avm, context)? + .resolve(avm, context)?; + handler + .call(avm, context, listener, call_args)? + .resolve(avm, context)?; + } + } + } + + Ok(Value::Undefined.into()) +} + +pub fn create_proto<'gc>( + gc_context: MutationContext<'gc, '_>, + proto: Object<'gc>, + fn_proto: Object<'gc>, +) -> Object<'gc> { + let mcl_proto = ScriptObject::object(gc_context, Some(proto)); + + mcl_proto.as_script_object().unwrap().force_set_function( + "addListener", + add_listener, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); + mcl_proto.as_script_object().unwrap().force_set_function( + "removeListener", + remove_listener, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); + mcl_proto.as_script_object().unwrap().force_set_function( + "broadcastMessage", + broadcast_message, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); + + mcl_proto.into() +} diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index c3ebd4336..098bde10c 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -158,6 +158,7 @@ swf_tests! { (undefined_to_string_swf6, "avm1/undefined_to_string_swf6", 1), (define_function2_preload, "avm1/define_function2_preload", 1), (define_function2_preload_order, "avm1/define_function2_preload_order", 1), + (mcl_as_broadcaster, "avm1/mcl_as_broadcaster", 1), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/mcl_as_broadcaster/output.txt b/core/tests/swfs/avm1/mcl_as_broadcaster/output.txt new file mode 100644 index 000000000..c8bf05af1 --- /dev/null +++ b/core/tests/swfs/avm1/mcl_as_broadcaster/output.txt @@ -0,0 +1,12 @@ +Called from MovieClipLoader +[object Object] +true +false +Called from New Listener +[object Object] +false +true +Called from New Listener +[object Object] +false +true diff --git a/core/tests/swfs/avm1/mcl_as_broadcaster/test.fla b/core/tests/swfs/avm1/mcl_as_broadcaster/test.fla new file mode 100644 index 000000000..324f8c0ba Binary files /dev/null and b/core/tests/swfs/avm1/mcl_as_broadcaster/test.fla differ diff --git a/core/tests/swfs/avm1/mcl_as_broadcaster/test.swf b/core/tests/swfs/avm1/mcl_as_broadcaster/test.swf new file mode 100644 index 000000000..7c1b21100 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_as_broadcaster/test.swf differ