diff --git a/core/src/avm2/globals/flash/display/movieclip.rs b/core/src/avm2/globals/flash/display/movieclip.rs index c3e9dc588..1869a7a6b 100644 --- a/core/src/avm2/globals/flash/display/movieclip.rs +++ b/core/src/avm2/globals/flash/display/movieclip.rs @@ -4,9 +4,11 @@ use crate::avm2::activation::Activation; use crate::avm2::class::Class; use crate::avm2::method::Method; use crate::avm2::names::{Namespace, QName}; -use crate::avm2::object::Object; +use crate::avm2::object::{Object, TObject}; +use crate::avm2::traits::Trait; use crate::avm2::value::Value; use crate::avm2::Error; +use crate::display_object::TDisplayObject; use gc_arena::{GcCell, MutationContext}; /// Implements `flash.display.MovieClip`'s instance constructor. @@ -27,13 +29,44 @@ pub fn class_init<'gc>( Ok(Value::Undefined) } +/// Implements `addFrameScript`, an undocumented method of `MovieClip` used to +/// specify what methods of a clip's class run on which frames. +pub fn add_frame_script<'gc>( + activation: &mut Activation<'_, 'gc, '_>, + this: Option>, + args: &[Value<'gc>], +) -> Result, Error> { + if let Some(mc) = this + .and_then(|o| o.as_display_object()) + .and_then(|dobj| dobj.as_movie_clip()) + { + for (frame_id, callable) in args.chunks_exact(2).map(|s| (s[0].clone(), s[1].clone())) { + let frame_id = frame_id.coerce_to_u32(activation)? as u16; + let callable = callable.coerce_to_object(activation)?; + + mc.register_frame_script(frame_id, callable, &mut activation.context); + } + } else { + log::error!("Attempted to add frame scripts to non-MovieClip this!"); + } + + Ok(Value::Undefined) +} + /// Construct `MovieClip`'s class. pub fn create_class<'gc>(mc: MutationContext<'gc, '_>) -> GcCell<'gc, Class<'gc>> { - Class::new( + let class = Class::new( QName::new(Namespace::package("flash.display"), "MovieClip"), Some(QName::new(Namespace::package("flash.display"), "Sprite").into()), Method::from_builtin(instance_init), Method::from_builtin(class_init), mc, - ) + ); + + class.write(mc).define_instance_trait(Trait::from_method( + QName::new(Namespace::package(""), "addFrameScript"), + Method::from_builtin(add_frame_script), + )); + + class } diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index d3f634a3f..9fe6e2f3f 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -54,12 +54,27 @@ pub struct MovieClipData<'gc> { children: BTreeMap>, object: Option>, clip_actions: Vec, + frame_scripts: Vec>, has_button_clip_event: bool, flags: EnumSet, avm_constructor: Option>, drawing: Drawing, } +unsafe impl<'gc> Collect for MovieClipData<'gc> { + #[inline] + fn trace(&self, cc: gc_arena::CollectionContext) { + for child in self.children.values() { + child.trace(cc); + } + self.base.trace(cc); + self.static_data.trace(cc); + self.object.trace(cc); + self.avm_constructor.trace(cc); + self.frame_scripts.trace(cc); + } +} + impl<'gc> MovieClip<'gc> { #[allow(dead_code)] pub fn new(swf: SwfSlice, gc_context: MutationContext<'gc, '_>) -> Self { @@ -74,6 +89,7 @@ impl<'gc> MovieClip<'gc> { children: BTreeMap::new(), object: None, clip_actions: Vec::new(), + frame_scripts: Vec::new(), has_button_clip_event: false, flags: EnumSet::empty(), avm_constructor: None, @@ -108,6 +124,7 @@ impl<'gc> MovieClip<'gc> { children: BTreeMap::new(), object: None, clip_actions: Vec::new(), + frame_scripts: Vec::new(), has_button_clip_event: false, flags: MovieClipFlags::Playing.into(), avm_constructor: None, @@ -1276,6 +1293,47 @@ impl<'gc> MovieClip<'gc> { } } } + + pub fn register_frame_script( + self, + frame_id: FrameNumber, + callable: Avm2Object<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + ) { + let mut write = self.0.write(context.gc_context); + + write + .frame_scripts + .push(Avm2FrameScript { frame_id, callable }); + } + + fn run_frame_scripts(self, frame_id: FrameNumber, context: &mut UpdateContext<'_, 'gc, '_>) { + let mut index = 0; + let mut read = self.0.read(); + + let avm2_object = read.object.and_then(|o| o.as_avm2_object().ok()); + + if let Some(avm2_object) = avm2_object { + while let Some(fs) = read.frame_scripts.get(index) { + if fs.frame_id == frame_id { + let callable = fs.callable; + + drop(read); + + let mut activation = Avm2Activation::from_nothing(context.reborrow()); + if let Err(e) = callable.call(Some(avm2_object), &[], &mut activation, None) { + log::error!("Error in script on frame {}: {}", frame_id, e); + } + + read = self.0.read(); + } + + index += 1; + } + } else { + log::error!("Attempted to run AVM2 frame scripts on an AVM1 MovieClip."); + } + } } impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { @@ -1322,6 +1380,18 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { ClipEvent::Load, ); } + + if self + .0 + .read() + .object + .map(|o| o.is_avm2_object()) + .unwrap_or(false) + { + let frame_id = self.0.read().current_frame; + + self.run_frame_scripts(frame_id, context); + } } fn render(&self, context: &mut RenderContext<'_, 'gc>) { @@ -1529,19 +1599,6 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { } } -unsafe impl<'gc> Collect for MovieClipData<'gc> { - #[inline] - fn trace(&self, cc: gc_arena::CollectionContext) { - for child in self.children.values() { - child.trace(cc); - } - self.base.trace(cc); - self.static_data.trace(cc); - self.object.trace(cc); - self.avm_constructor.trace(cc); - } -} - impl<'gc> MovieClipData<'gc> { /// Replace the current MovieClipData with a completely new SwfMovie. ///