diff --git a/core/src/avm1.rs b/core/src/avm1.rs index c4ef8fd9c..be6c2e849 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -61,9 +61,14 @@ pub struct ActionContext<'a, 'gc, 'gc_context> { /// _names ("instanceN" etc. for unnamed clips). pub target_path: Value<'gc>, + pub background_color: &'a mut Color, pub rng: &'a mut SmallRng, + pub action_queue: &'a mut crate::player::ActionQueue<'gc>, pub audio: &'a mut dyn crate::backend::audio::AudioBackend, + pub library: &'a mut crate::library::Library<'gc>, pub navigator: &'a mut dyn crate::backend::navigator::NavigatorBackend, + pub renderer: &'a mut dyn crate::backend::render::RenderBackend, + pub swf_data: &'a std::sync::Arc>, } pub struct Avm1<'gc> { @@ -120,6 +125,29 @@ impl<'gc> Avm1<'gc> { } } + /// Makes an `UpdateContext` from an `ActionContext`. + /// TODO: The Contexts should probably be merged. + fn update_context<'a, 'gc_context, 'b>( + &self, + context: &'b mut ActionContext<'a, 'gc, 'gc_context>, + ) -> crate::player::UpdateContext<'b, 'gc, 'gc_context> { + crate::player::UpdateContext { + player_version: context.player_version, + global_time: context.global_time, + swf_data: context.swf_data, + swf_version: self.current_swf_version(), + library: context.library, + background_color: context.background_color, + rng: context.rng, + renderer: context.renderer, + audio: context.audio, + navigator: context.navigator, + action_queue: context.action_queue, + gc_context: context.gc_context, + active_clip: context.active_clip, + } + } + /// Convert the current locals pool into a set of form values. /// /// This is necessary to support form submission from Flash via a couple of @@ -1125,12 +1153,17 @@ impl<'gc> Avm1<'gc> { Ok(()) } - fn action_goto_frame(&mut self, context: &mut ActionContext, frame: u16) -> Result<(), Error> { + fn action_goto_frame( + &mut self, + context: &mut ActionContext<'_, 'gc, '_>, + frame: u16, + ) -> Result<(), Error> { if let Some(clip) = context.target_clip { let mut display_object = clip.write(context.gc_context); if let Some(clip) = display_object.as_movie_clip_mut() { + let mut update_context = self.update_context(context); // The frame on the stack is 0-based, not 1-based. - clip.goto_frame(frame + 1, true); + clip.goto_frame(&mut update_context, frame + 1, true); } else { log::error!("GotoFrame failed: Target is not a MovieClip"); } @@ -1142,7 +1175,7 @@ impl<'gc> Avm1<'gc> { fn action_goto_frame_2( &mut self, - context: &mut ActionContext, + context: &mut ActionContext<'_, 'gc, '_>, set_playing: bool, scene_offset: u16, ) -> Result<(), Error> { @@ -1151,14 +1184,19 @@ impl<'gc> Avm1<'gc> { if let Some(clip) = context.target_clip { let mut display_object = clip.write(context.gc_context); if let Some(clip) = display_object.as_movie_clip_mut() { + let mut update_context = self.update_context(context); match self.pop()? { Value::Number(frame) => { // The frame on the stack is 1-based, not 0-based. - clip.goto_frame(scene_offset + (frame as u16), !set_playing) + clip.goto_frame( + &mut update_context, + scene_offset + (frame as u16), + !set_playing, + ) } Value::String(frame_label) => { if let Some(frame) = clip.frame_label_to_number(&frame_label) { - clip.goto_frame(scene_offset + frame, !set_playing) + clip.goto_frame(&mut update_context, scene_offset + frame, !set_playing) } else { log::warn!( "GotoFrame2: MovieClip {} does not contain frame label '{}'", @@ -1178,12 +1216,17 @@ impl<'gc> Avm1<'gc> { Ok(()) } - fn action_goto_label(&mut self, context: &mut ActionContext, label: &str) -> Result<(), Error> { + fn action_goto_label( + &mut self, + context: &mut ActionContext<'_, 'gc, '_>, + label: &str, + ) -> Result<(), Error> { if let Some(clip) = context.target_clip { let mut display_object = clip.write(context.gc_context); if let Some(clip) = display_object.as_movie_clip_mut() { if let Some(frame) = clip.frame_label_to_number(label) { - clip.goto_frame(frame, true); + let mut update_context = self.update_context(context); + clip.goto_frame(&mut update_context, frame, true); } else { log::warn!("GoToLabel: Frame label '{}' not found", label); } @@ -1336,11 +1379,12 @@ impl<'gc> Avm1<'gc> { Ok(()) } - fn action_next_frame(&mut self, context: &mut ActionContext) -> Result<(), Error> { + fn action_next_frame(&mut self, context: &mut ActionContext<'_, 'gc, '_>) -> Result<(), Error> { if let Some(clip) = context.target_clip { let mut display_object = clip.write(context.gc_context); if let Some(clip) = display_object.as_movie_clip_mut() { - clip.next_frame(); + let mut update_context = self.update_context(context); + clip.next_frame(&mut update_context); } else { log::warn!("NextFrame: Target is not a MovieClip"); } @@ -1393,11 +1437,12 @@ impl<'gc> Avm1<'gc> { Ok(()) } - fn action_prev_frame(&mut self, context: &mut ActionContext) -> Result<(), Error> { + fn action_prev_frame(&mut self, context: &mut ActionContext<'_, 'gc, '_>) -> Result<(), Error> { if let Some(clip) = context.target_clip { let mut display_object = clip.write(context.gc_context); if let Some(clip) = display_object.as_movie_clip_mut() { - clip.prev_frame(); + let mut update_context = self.update_context(context); + clip.prev_frame(&mut update_context); } else { log::warn!("PrevFrame: Target is not a MovieClip"); } diff --git a/core/src/avm1/movie_clip.rs b/core/src/avm1/movie_clip.rs index c6e57b906..8ec21bdfe 100644 --- a/core/src/avm1/movie_clip.rs +++ b/core/src/avm1/movie_clip.rs @@ -1,5 +1,6 @@ use crate::avm1::object::{Attribute::*, Object}; use crate::avm1::{ActionContext, Avm1, Value}; +use crate::display_object::DisplayNode; use crate::movie_clip::MovieClip; use enumset::EnumSet; use gc_arena::{GcCell, MutationContext}; @@ -29,14 +30,14 @@ macro_rules! with_movie_clip_mut { $( $object.force_set_function( $name, - |_avm, context, this, args| -> Value<'gc> { + |avm, context: &mut ActionContext<'_, 'gc, '_>, this, args| -> Value<'gc> { if let Some(display_object) = this.read().display_node() { if let Some(movie_clip) = display_object.write(context.gc_context).as_movie_clip_mut() { - return $fn(movie_clip, args); + return $fn(movie_clip, avm, context, display_object, args); } } Value::Undefined - }, + } as crate::avm1::function::NativeFunction<'gc>, $gc_context, DontDelete | ReadOnly | DontEnum, ); @@ -82,19 +83,21 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object< with_movie_clip_mut!( gc_context, object, - "nextFrame" => |movie_clip: &mut MovieClip, _args| { - movie_clip.next_frame(); + "nextFrame" => |movie_clip: &mut MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut ActionContext<'_, 'gc, '_>, _cell: DisplayNode<'gc>, _args| { + let mut update_context = avm.update_context(context); + movie_clip.next_frame(&mut update_context); Value::Undefined }, - "prevFrame" => |movie_clip: &mut MovieClip, _args| { - movie_clip.prev_frame(); + "prevFrame" => |movie_clip: &mut MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut ActionContext<'_, 'gc, '_>, _cell: DisplayNode<'gc>, _args| { + let mut update_context = avm.update_context(context); + movie_clip.prev_frame(&mut update_context); Value::Undefined }, - "play" => |movie_clip: &mut MovieClip, _args| { + "play" => |movie_clip: &mut MovieClip<'gc>, _avm: &mut Avm1<'gc>, _context: &mut ActionContext<'_, 'gc, '_>, _cell: DisplayNode<'gc>, _args| { movie_clip.play(); Value::Undefined }, - "stop" => |movie_clip: &mut MovieClip, _args| { + "stop" => |movie_clip: &mut MovieClip<'gc>, _avm: &mut Avm1<'gc>, _context: &mut ActionContext<'_, 'gc, '_>, _cell: DisplayNode<'gc>, _args| { movie_clip.stop(); Value::Undefined } @@ -103,11 +106,11 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object< with_movie_clip!( gc_context, object, - "getBytesLoaded" => |_movie_clip: &MovieClip, _args| { + "getBytesLoaded" => |_movie_clip: &MovieClip<'gc>, _args| { // TODO find a correct value Value::Number(1.0) }, - "getBytesTotal" => |_movie_clip: &MovieClip, _args| { + "getBytesTotal" => |_movie_clip: &MovieClip<'gc>, _args| { // TODO find a correct value Value::Number(1.0) } diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index b12f1a0c9..76e069cd2 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -373,10 +373,14 @@ mod tests { use crate::avm1::activation::Activation; use crate::backend::audio::NullAudioBackend; use crate::backend::navigator::NullNavigatorBackend; + use crate::backend::render::NullRenderer; use crate::display_object::DisplayObject; + use crate::library::Library; use crate::movie_clip::MovieClip; + use crate::prelude::*; use gc_arena::rootless_arena; use rand::{rngs::SmallRng, SeedableRng}; + use std::sync::Arc; fn with_object(swf_version: u8, test: F) -> R where @@ -401,9 +405,20 @@ mod tests { target_clip: Some(root), target_path: Value::Undefined, rng: &mut SmallRng::from_seed([0u8; 16]), + action_queue: &mut crate::player::ActionQueue::new(), audio: &mut NullAudioBackend::new(), + background_color: &mut Color { + r: 0, + g: 0, + b: 0, + a: 0, + }, + library: &mut Library::new(), navigator: &mut NullNavigatorBackend::new(), + renderer: &mut NullRenderer::new(), + swf_data: &mut Arc::new(vec![]), }; + let object = GcCell::allocate(gc_context, Object::object(gc_context)); let globals = avm.global_object_cell(); diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index d9950d649..963fc2001 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -2,10 +2,15 @@ use crate::avm1::activation::Activation; use crate::avm1::{ActionContext, Avm1, Object, Value}; use crate::backend::audio::NullAudioBackend; use crate::backend::navigator::NullNavigatorBackend; +use crate::backend::render::NullRenderer; use crate::display_object::DisplayObject; +use crate::library::Library; use crate::movie_clip::MovieClip; +use crate::player::ActionQueue; +use crate::prelude::*; use gc_arena::{rootless_arena, GcCell}; use rand::{rngs::SmallRng, SeedableRng}; +use std::sync::Arc; pub fn with_avm(swf_version: u8, test: F) -> R where @@ -30,7 +35,17 @@ where target_path: Value::Undefined, rng: &mut SmallRng::from_seed([0u8; 16]), audio: &mut NullAudioBackend::new(), + action_queue: &mut ActionQueue::new(), + background_color: &mut Color { + r: 0, + g: 0, + b: 0, + a: 0, + }, + library: &mut Library::new(), navigator: &mut NullNavigatorBackend::new(), + renderer: &mut NullRenderer::new(), + swf_data: &mut Arc::new(vec![]), }; let globals = avm.global_object_cell(); diff --git a/core/src/backend/render.rs b/core/src/backend/render.rs index 45de0df25..7db3d67b7 100644 --- a/core/src/backend/render.rs +++ b/core/src/backend/render.rs @@ -56,6 +56,18 @@ pub enum Letterbox { pub struct NullRenderer; +impl NullRenderer { + pub fn new() -> Self { + Self + } +} + +impl Default for NullRenderer { + fn default() -> Self { + Self::new() + } +} + impl RenderBackend for NullRenderer { fn set_viewport_dimensions(&mut self, _width: u32, _height: u32) {} fn register_shape(&mut self, _shape: &swf::Shape) -> ShapeHandle { diff --git a/core/src/button.rs b/core/src/button.rs index 778a1337a..fbf743fd5 100644 --- a/core/src/button.rs +++ b/core/src/button.rs @@ -137,7 +137,9 @@ impl<'gc> Button<'gc> { for action in &self.static_data.actions { if action.condition == condition && action.key_code == key_code { // Note that AVM1 buttons run actions relative to their parent, not themselves. - context.actions.push((parent, action.action_data.clone())); + context + .action_queue + .queue_actions(parent, action.action_data.clone()); } } } @@ -190,13 +192,6 @@ impl<'gc> DisplayObject<'gc> for Button<'gc> { } } - fn run_post_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { - for child in self.children.values_mut() { - context.active_clip = *child; - child.write(context.gc_context).run_post_frame(context); - } - } - fn render(&self, context: &mut RenderContext<'_, 'gc>) { context.transform_stack.push(self.transform()); diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 9cb6e0974..03bbf6b31 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -14,6 +14,20 @@ pub struct DisplayObjectBase<'gc> { transform: Transform, name: String, clip_depth: Depth, + + /// The first child of this display object in order of execution. + /// This is differen than render order. + first_child: Option>, + + /// The previous sibling of this display object in order of execution. + prev_sibling: Option>, + + /// The next sibling of this display object in order of execution. + next_sibling: Option>, + + /// Whether this child has been removed from the display list. + /// Necessary in AVM1 to throw away queued actions from removed movie clips. + removed: bool, } impl<'gc> Default for DisplayObjectBase<'gc> { @@ -25,6 +39,10 @@ impl<'gc> Default for DisplayObjectBase<'gc> { transform: Default::default(), name: Default::default(), clip_depth: Default::default(), + first_child: None, + prev_sibling: None, + next_sibling: None, + removed: false, } } } @@ -79,6 +97,30 @@ impl<'gc> DisplayObject<'gc> for DisplayObjectBase<'gc> { fn set_parent(&mut self, parent: Option>) { self.parent = parent; } + fn first_child(&self) -> Option> { + self.first_child + } + fn set_first_child(&mut self, node: Option>) { + self.first_child = node; + } + fn prev_sibling(&self) -> Option> { + self.prev_sibling + } + fn set_prev_sibling(&mut self, node: Option>) { + self.prev_sibling = node; + } + fn next_sibling(&self) -> Option> { + self.next_sibling + } + fn set_next_sibling(&mut self, node: Option>) { + self.next_sibling = node; + } + fn removed(&self) -> bool { + self.removed + } + fn set_removed(&mut self, removed: bool) { + self.removed = removed; + } fn box_clone(&self) -> Box> { Box::new(self.clone()) } @@ -109,9 +151,22 @@ pub trait DisplayObject<'gc>: 'gc + Collect + Debug { fn set_clip_depth(&mut self, depth: Depth); fn parent(&self) -> Option>; fn set_parent(&mut self, parent: Option>); - + fn first_child(&self) -> Option>; + fn set_first_child(&mut self, node: Option>); + fn prev_sibling(&self) -> Option>; + fn set_prev_sibling(&mut self, node: Option>); + fn next_sibling(&self) -> Option>; + fn set_next_sibling(&mut self, node: Option>); + /// Iterates over the children of this display object in execution order. + /// This is different than render order. + fn children(&self) -> ChildIter<'gc> { + ChildIter { + cur_child: self.first_child(), + } + } + fn removed(&self) -> bool; + fn set_removed(&mut self, removed: bool); fn run_frame(&mut self, _context: &mut UpdateContext<'_, 'gc, '_>) {} - fn run_post_frame(&mut self, _context: &mut UpdateContext<'_, 'gc, '_>) {} fn render(&self, _context: &mut RenderContext<'_, 'gc>) {} fn as_button(&self) -> Option<&crate::button::Button<'gc>> { @@ -132,15 +187,15 @@ pub trait DisplayObject<'gc>: 'gc + Collect + Debug { fn as_morph_shape_mut(&mut self) -> Option<&mut crate::morph_shape::MorphShape<'gc>> { None } - fn apply_place_object(&mut self, place_object: swf::PlaceObject) { - if let Some(matrix) = place_object.matrix { - self.set_matrix(&matrix.into()); + fn apply_place_object(&mut self, place_object: &swf::PlaceObject) { + if let Some(matrix) = &place_object.matrix { + self.set_matrix(&matrix.clone().into()); } - if let Some(color_transform) = place_object.color_transform { - self.set_color_transform(&color_transform.into()); + if let Some(color_transform) = &place_object.color_transform { + self.set_color_transform(&color_transform.clone().into()); } - if let Some(name) = place_object.name { - self.set_name(&name); + if let Some(name) = &place_object.name { + self.set_name(name); } if let Some(clip_depth) = place_object.clip_depth { self.set_clip_depth(clip_depth); @@ -251,6 +306,30 @@ macro_rules! impl_display_object { fn set_parent(&mut self, parent: Option>) { self.$field.set_parent(parent) } + fn first_child(&self) -> Option> { + self.$field.first_child() + } + fn set_first_child(&mut self, node: Option>) { + self.$field.set_first_child(node); + } + fn prev_sibling(&self) -> Option> { + self.$field.prev_sibling() + } + fn set_prev_sibling(&mut self, node: Option>) { + self.$field.set_prev_sibling(node); + } + fn next_sibling(&self) -> Option> { + self.$field.next_sibling() + } + fn set_next_sibling(&mut self, node: Option>) { + self.$field.set_next_sibling(node); + } + fn removed(&self) -> bool { + self.$field.removed() + } + fn set_removed(&mut self, value: bool) { + self.$field.set_removed(value) + } fn box_clone(&self) -> Box> { Box::new(self.clone()) } @@ -301,3 +380,18 @@ pub fn render_children<'gc>( /// TODO(Herschel): The extra Box here is necessary to hold the trait object inside a GC pointer, /// but this is an extra allocation... Can we avoid this, maybe with a DST? pub type DisplayNode<'gc> = GcCell<'gc, Box>>; + +pub struct ChildIter<'gc> { + cur_child: Option>, +} + +impl<'gc> Iterator for ChildIter<'gc> { + type Item = DisplayNode<'gc>; + fn next(&mut self) -> Option { + let cur = self.cur_child; + self.cur_child = self + .cur_child + .and_then(|display_cell| display_cell.read().next_sibling()); + cur + } +} diff --git a/core/src/edit_text.rs b/core/src/edit_text.rs index 7478d807f..db807da7b 100644 --- a/core/src/edit_text.rs +++ b/core/src/edit_text.rs @@ -65,52 +65,53 @@ impl<'gc> DisplayObject<'gc> for EditText<'gc> { transform.color_transform.g_mult = f32::from(color.g) / 255.0; transform.color_transform.b_mult = f32::from(color.b) / 255.0; transform.color_transform.a_mult = f32::from(color.a) / 255.0; - let device_font = context.library.device_font(); // If the font can't be found or has no glyph information, use the "device font" instead. // We're cheating a bit and not actually rendering text using the OS/web. // Instead, we embed an SWF version of Noto Sans to use as the "device font", and render // it the same as any other SWF outline text. - let font = context + if let Some(font) = context .library .get_font(font_id) .filter(|font| font.has_glyphs()) - .unwrap_or(device_font); - let scale = if let Some(height) = static_data.height { - transform.matrix.ty += f32::from(height); - f32::from(height) / font.scale() - } else { - 1.0 - }; - if let Some(layout) = &static_data.layout { - transform.matrix.ty -= layout.leading.get() as f32; - } - transform.matrix.a = scale; - transform.matrix.d = scale; - let mut chars = self.text.chars().peekable(); - let has_kerning_info = font.has_kerning_info(); - while let Some(c) = chars.next() { - // TODO: SWF text fields can contain a limited subset of HTML (and often do in SWF versions >6). - // This is a quicky-and-dirty way to skip the HTML tags. This is obviously not correct - // and we will need to properly parse and handle the HTML at some point. - // See SWF19 pp. 173-174 for supported HTML tags. - if self.static_data.0.is_html && c == '<' { - // Skip characters until we see a close bracket. - chars.by_ref().skip_while(|&x| x != '>').next(); - } else if let Some(glyph) = font.get_glyph_for_char(c) { - // Render glyph. - context.transform_stack.push(&transform); - context - .renderer - .render_shape(glyph.shape, context.transform_stack.transform()); - context.transform_stack.pop(); - // Step horizontally. - let mut advance = f32::from(glyph.advance); - if has_kerning_info { - advance += font - .get_kerning_offset(c, chars.peek().cloned().unwrap_or('\0')) - .get() as f32; + .or_else(|| context.library.device_font()) + { + let scale = if let Some(height) = static_data.height { + transform.matrix.ty += f32::from(height); + f32::from(height) / font.scale() + } else { + 1.0 + }; + if let Some(layout) = &static_data.layout { + transform.matrix.ty -= layout.leading.get() as f32; + } + transform.matrix.a = scale; + transform.matrix.d = scale; + let mut chars = self.text.chars().peekable(); + let has_kerning_info = font.has_kerning_info(); + while let Some(c) = chars.next() { + // TODO: SWF text fields can contain a limited subset of HTML (and often do in SWF versions >6). + // This is a quicky-and-dirty way to skip the HTML tags. This is obviously not correct + // and we will need to properly parse and handle the HTML at some point. + // See SWF19 pp. 173-174 for supported HTML tags. + if self.static_data.0.is_html && c == '<' { + // Skip characters until we see a close bracket. + chars.by_ref().skip_while(|&x| x != '>').next(); + } else if let Some(glyph) = font.get_glyph_for_char(c) { + // Render glyph. + context.transform_stack.push(&transform); + context + .renderer + .render_shape(glyph.shape, context.transform_stack.transform()); + context.transform_stack.pop(); + // Step horizontally. + let mut advance = f32::from(glyph.advance); + if has_kerning_info { + advance += font + .get_kerning_offset(c, chars.peek().cloned().unwrap_or('\0')) + .get() as f32; + } + transform.matrix.tx += advance * scale; } - transform.matrix.tx += advance * scale; } } context.transform_stack.pop(); diff --git a/core/src/library.rs b/core/src/library.rs index c8c06b799..dc1ef920c 100644 --- a/core/src/library.rs +++ b/core/src/library.rs @@ -10,15 +10,15 @@ use swf::CharacterId; pub struct Library<'gc> { characters: HashMap>, jpeg_tables: Option>, - device_font: Box, + device_font: Option>, } impl<'gc> Library<'gc> { - pub fn new(device_font: Box) -> Self { + pub fn new() -> Self { Library { characters: HashMap::new(), jpeg_tables: None, - device_font, + device_font: None, } } @@ -95,8 +95,13 @@ impl<'gc> Library<'gc> { } /// Returns the device font for use when a font is unavailable. - pub fn device_font(&self) -> &Font { - &*self.device_font + pub fn device_font(&self) -> Option<&Font> { + self.device_font.as_ref().map(AsRef::as_ref) + } + + /// Sets the device font. + pub fn set_device_font(&mut self, font: Option>) { + self.device_font = font; } } @@ -108,3 +113,9 @@ unsafe impl<'gc> gc_arena::Collect for Library<'gc> { } } } + +impl Default for Library<'_> { + fn default() -> Self { + Self::new() + } +} diff --git a/core/src/movie_clip.rs b/core/src/movie_clip.rs index 7b6394756..83969824a 100644 --- a/core/src/movie_clip.rs +++ b/core/src/movie_clip.rs @@ -26,7 +26,6 @@ pub struct MovieClip<'gc> { static_data: Gc<'gc, MovieClipStatic>, tag_stream_pos: u64, is_playing: bool, - goto_queue: Vec, current_frame: FrameNumber, audio_stream: Option, children: BTreeMap>, @@ -41,7 +40,6 @@ impl<'gc> MovieClip<'gc> { static_data: Gc::allocate(gc_context, MovieClipStatic::default()), tag_stream_pos: 0, is_playing: false, - goto_queue: Vec::new(), current_frame: 0, audio_stream: None, children: BTreeMap::new(), @@ -73,7 +71,6 @@ impl<'gc> MovieClip<'gc> { ), tag_stream_pos: 0, is_playing: true, - goto_queue: Vec::new(), current_frame: 0, audio_stream: None, children: BTreeMap::new(), @@ -85,9 +82,9 @@ impl<'gc> MovieClip<'gc> { self.is_playing } - pub fn next_frame(&mut self) { + pub fn next_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { if self.current_frame() < self.total_frames() { - self.goto_frame(self.current_frame + 1, true); + self.goto_frame(context, self.current_frame + 1, true); } } @@ -98,9 +95,9 @@ impl<'gc> MovieClip<'gc> { } } - pub fn prev_frame(&mut self) { + pub fn prev_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { if self.current_frame > 1 { - self.goto_frame(self.current_frame - 1, true); + self.goto_frame(context, self.current_frame - 1, true); } } @@ -110,9 +107,14 @@ impl<'gc> MovieClip<'gc> { /// Queues up a goto to the specified frame. /// `frame` should be 1-based. - pub fn goto_frame(&mut self, frame: FrameNumber, stop: bool) { + pub fn goto_frame( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + frame: FrameNumber, + stop: bool, + ) { if frame != self.current_frame { - self.goto_queue.push(frame); + self.run_goto(context, frame); } if stop { @@ -203,19 +205,6 @@ impl<'gc> MovieClip<'gc> { self.static_data.frame_labels.get(frame_label).copied() } - pub fn run_goto_queue(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { - let mut i = 0; - while i < self.goto_queue.len() { - let frame = self.goto_queue[i]; - if self.current_frame != frame { - self.run_goto(context, frame); - } - i += 1; - } - - self.goto_queue.clear(); - } - fn tag_stream_start(&self) -> u64 { self.static_data.tag_stream_start } @@ -235,6 +224,7 @@ impl<'gc> MovieClip<'gc> { cursor.set_position(self.tag_stream_pos); swf::read::Reader::new(cursor, context.swf_version) } + fn run_frame_internal( &mut self, context: &mut UpdateContext<'_, 'gc, '_>, @@ -292,13 +282,20 @@ impl<'gc> MovieClip<'gc> { depth: Depth, copy_previous_properties: bool, ) -> Option> { - if let Ok(child) = context + if let Ok(child_cell) = context .library .instantiate_display_object(id, context.gc_context) { - let prev_child = self.children.insert(depth, child); + // Remove previous child from children list, + // and add new childonto front of the list. + let prev_child = self.children.insert(depth, child_cell); + if let Some(prev_child) = prev_child { + self.remove_child_from_exec_list(context.gc_context, prev_child); + } + self.add_child_to_exec_list(context.gc_context, child_cell); { - let mut child = child.write(context.gc_context); + let mut child = child_cell.write(context.gc_context); + // Set initial properties for child. child.set_parent(Some(context.active_clip)); child.set_place_frame(self.current_frame); if copy_previous_properties { @@ -306,28 +303,72 @@ impl<'gc> MovieClip<'gc> { child.copy_display_properties_from(prev_child); } } + let prev_clip = context.active_clip; + // Run first frame. + context.active_clip = child_cell; + child.run_frame(context); + context.active_clip = prev_clip; } - Some(child) + Some(child_cell) } else { log::error!("Unable to instantiate display node id {}", id); None } } - - fn run_goto(&mut self, context: &mut UpdateContext<'_, 'gc, '_>, frame: FrameNumber) { + /// Adds a child to the front of the execution list. + /// This does not affect the render list. + fn add_child_to_exec_list( + &mut self, + gc_context: MutationContext<'gc, '_>, + child_cell: DisplayNode<'gc>, + ) { + if let Some(head) = self.first_child() { + head.write(gc_context).set_prev_sibling(Some(child_cell)); + child_cell.write(gc_context).set_next_sibling(Some(head)); + } + self.set_first_child(Some(child_cell)); + } + /// Removes a child from the execution list. + /// This does not affect the render list. + fn remove_child_from_exec_list( + &mut self, + gc_context: MutationContext<'gc, '_>, + child_cell: DisplayNode<'gc>, + ) { + let mut child = child_cell.write(gc_context); + // Remove from children linked list. + let prev = child.prev_sibling(); + let next = child.next_sibling(); + if let Some(prev) = prev { + prev.write(gc_context).set_next_sibling(next); + } + if let Some(next) = next { + next.write(gc_context).set_prev_sibling(prev); + } + if let Some(head) = self.first_child() { + if GcCell::ptr_eq(head, child_cell) { + self.set_first_child(next); + } + } + // Flag child as removed. + child.set_removed(true); + } + pub fn run_goto(&mut self, context: &mut UpdateContext<'_, 'gc, '_>, frame: FrameNumber) { // Flash gotos are tricky: - // 1) MovieClip timelines are stored as deltas from frame to frame, - // so we have to step through the intermediate frames to goto a target frame. - // For rewinds, this means starting from frame 1. - // 2) Objects that would persist over the goto should not be recreated and destroyed, - // they should keep their properties. - // Particularly for rewinds, the object should persist if it was create + // 1) Conceptually, a goto should act like the playhead is advancing forward or + // backward to a frame. + // 2) However, MovieClip timelines are stored as deltas from frame to frame, + // so for rewinds, we must restart to frame 1 and play forward. + // 3) Objects that would persist over the goto conceptually should not be + // destroyed and recreated; they should keep their properties. + // Particularly for rewinds, the object should persist if it was created // *before* the frame we are going to. (DisplayNode::place_frame). - // 3) We want to avoid creating objects just to destroy them if they aren't on - // the goto frame, so we should instead aggregate the deltas into a list - // of commands at the end of the goto, and THEN create the needed objects. + // 4) We want to avoid creating objects just to destroy them if they aren't on + // the goto frame, so we should instead aggregate the deltas into a final list + // of commands, and THEN modify the children as necessary. // This map will maintain a map of depth -> placement commands. + // TODO: Move this to UpdateContext to avoid allocations. let mut goto_commands = fnv::FnvHashMap::default(); let is_rewind = if frame < self.current_frame() { @@ -335,6 +376,25 @@ impl<'gc> MovieClip<'gc> { // when rewinding. self.tag_stream_pos = 0; self.current_frame = 0; + + // Remove all display objects that were created after the desination frame. + // TODO: We want to do something like self.children.retain here, + // but BTreeMap::retain does not exist. + let children: smallvec::SmallVec<[_; 16]> = self + .children + .iter() + .filter_map(|(depth, clip)| { + if clip.read().place_frame() > frame { + Some((*depth, *clip)) + } else { + None + } + }) + .collect(); + for (depth, child) in children { + self.children.remove(&depth); + self.remove_child_from_exec_list(context.gc_context, child); + } true } else { false @@ -343,6 +403,7 @@ impl<'gc> MovieClip<'gc> { // Step through the intermediate frames, and aggregate the deltas of each frame. let mut frame_pos = self.tag_stream_pos; let mut reader = self.reader(context); + let gc_context = context.gc_context; while self.current_frame < frame { self.current_frame += 1; frame_pos = reader.get_inner().position(); @@ -362,36 +423,33 @@ impl<'gc> MovieClip<'gc> { self.goto_place_object(reader, tag_len, 4, &mut goto_commands) } TagCode::RemoveObject => { - self.goto_remove_object(reader, 1, &mut goto_commands, is_rewind) + self.goto_remove_object(reader, 1, gc_context, &mut goto_commands, is_rewind) } TagCode::RemoveObject2 => { - self.goto_remove_object(reader, 2, &mut goto_commands, is_rewind) + self.goto_remove_object(reader, 2, gc_context, &mut goto_commands, is_rewind) } _ => Ok(()), }; let _ = tag_utils::decode_tags(&mut reader, tag_callback, TagCode::ShowFrame); } - let prev_active_clip = context.active_clip; - - // Run the final list of commands. - if is_rewind { - // TODO: We want to do something like self.children.retain here, - // but BTreeMap::retain does not exist. - let mut children = std::mem::replace(&mut self.children, BTreeMap::new()); - goto_commands.into_iter().for_each(|(depth, params)| { - let (was_instantiated, child) = match children.get_mut(&depth).copied() { + // Run the list of goto commands to actually create and update the display objects. + let run_goto_command = + |clip: &mut MovieClip<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + (&depth, params): (&Depth, &GotoPlaceObject)| { + let (was_instantiated, child) = match clip.children.get_mut(&depth).copied() { // For rewinds, if an object was created before the final frame, // it will exist on the final frame as well. Re-use this object // instead of recreating. - Some(prev_child) if prev_child.read().place_frame() <= frame => { - self.children.insert(depth, prev_child); - (false, prev_child) - } - _ => { - if let Some(child) = - self.instantiate_child(context, params.id(), depth, false) - { + Some(prev_child) => (false, prev_child), + None => { + if let Some(child) = clip.instantiate_child( + context, + params.id(), + depth, + params.modifies_original_item(), + ) { (true, child) } else { return; @@ -400,61 +458,34 @@ impl<'gc> MovieClip<'gc> { }; // Apply final delta to display pamareters. - let child_node = child; let mut child = child.write(context.gc_context); - child.apply_place_object(params.place_object); + child.apply_place_object(¶ms.place_object); if was_instantiated { // Set the placement frame for the new object to the frame // it is actually created on. child.set_place_frame(params.frame); - // We must run newly created objects for one frame - // to ensure they place any children objects. - // TODO: This will probably move as our order-of-execution - // becomes more accurate. - context.active_clip = child_node; - child.run_frame(context); - context.active_clip = prev_active_clip; } - }); - } else { - goto_commands.into_iter().for_each(|(depth, params)| { - let id = params.id(); - let child = if id != 0 { - if let Some(child) = - self.instantiate_child(context, id, depth, params.modifies_original_item()) - { - child - } else { - return; - } - } else if let Some(child) = self.children.get_mut(&depth) { - *child - } else { - return; - }; + }; - // Apply final delta to display pamareters. - let child_node = child; - let mut child = child.write(context.gc_context); - child.apply_place_object(params.place_object); - if id != 0 { - // Set the placement frame for the new object to the frame - // it is actually created on. - child.set_place_frame(params.frame); - // We must run newly created objects for one frame - // to ensure they place any children objects. - // TODO: This will probably move as our order-of-execution - // becomes more accurate. - context.active_clip = child_node; - child.run_frame(context); - context.active_clip = prev_active_clip; - } - }); - } - // Re-run the final frame to run all other tags (DoAction, StartSound, etc.) + // We have to be sure that queued actions are generated in the same order + // as if the playhead had reached this frame normally. + // First, run frames for children that were created before this frame. + goto_commands + .iter() + .filter(|(_, params)| params.frame < frame) + .for_each(|goto| run_goto_command(self, context, goto)); + + // Next, run the final frame for the parent clip. + // Re-run the final frame without display tags (DoAction, StartSound, etc.) self.current_frame = frame - 1; self.tag_stream_pos = frame_pos; self.run_frame_internal(context, false); + + // Finally, run frames for children that are placed on this frame. + goto_commands + .iter() + .filter(|(_, params)| params.frame >= frame) + .for_each(|goto| run_goto_command(self, context, goto)); } /// Handles a PlaceObject tag when running a goto action. @@ -492,6 +523,7 @@ impl<'gc> MovieClip<'gc> { &mut self, reader: &mut SwfStream<&'a [u8]>, version: u8, + gc_context: MutationContext<'gc, '_>, goto_commands: &mut fnv::FnvHashMap, is_rewind: bool, ) -> DecodeResult { @@ -507,7 +539,9 @@ impl<'gc> MovieClip<'gc> { // Don't do this for rewinds, because they conceptually // start from an empty display list, and we also want to examine // the old children to decide if they persist (place_frame <= goto_frame). - self.children.remove(&remove_object.depth); + if let Some(child) = self.children.remove(&remove_object.depth) { + self.remove_child_from_exec_list(gc_context, child); + } } Ok(()) } @@ -521,24 +555,17 @@ impl<'gc> DisplayObject<'gc> for MovieClip<'gc> { } fn run_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { - if self.is_playing { - self.run_frame_internal(context, true); - } - - // TODO(Herschel): Verify order of execution for parent/children. - // Parent first? Children first? Sorted by depth? - for child in self.children.values_mut() { - context.active_clip = *child; + // Children must run first. + let prev_clip = context.active_clip; + for child in self.children() { + context.active_clip = child; child.write(context.gc_context).run_frame(context); } - } + context.active_clip = prev_clip; - fn run_post_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { - self.run_goto_queue(context); - - for child in self.children.values() { - context.active_clip = *child; - child.write(context.gc_context).run_post_frame(context); + // Run myself. + if self.is_playing { + self.run_frame_internal(context, true); } } @@ -1207,7 +1234,9 @@ impl<'gc, 'a> MovieClip<'gc> { start, end, }; - context.actions.push((context.active_clip, slice)); + context + .action_queue + .queue_actions(context.active_clip, slice); Ok(()) } @@ -1238,7 +1267,7 @@ impl<'gc, 'a> MovieClip<'gc> { ) { child .write(context.gc_context) - .apply_place_object(place_object); + .apply_place_object(&place_object); child } else { return Ok(()); @@ -1248,7 +1277,7 @@ impl<'gc, 'a> MovieClip<'gc> { if let Some(child) = self.children.get_mut(&place_object.depth) { child .write(context.gc_context) - .apply_place_object(place_object); + .apply_place_object(&place_object); *child } else { return Ok(()); @@ -1272,7 +1301,7 @@ impl<'gc, 'a> MovieClip<'gc> { reader.read_remove_object_2() }?; if let Some(child) = self.children.remove(&remove_object.depth) { - child.write(context.gc_context).set_parent(None); + self.remove_child_from_exec_list(context.gc_context, child); } Ok(()) } diff --git a/core/src/player.rs b/core/src/player.rs index fe51bc405..a21465b99 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -6,6 +6,7 @@ use crate::events::{ButtonEvent, PlayerEvent}; use crate::library::Library; use crate::movie_clip::MovieClip; use crate::prelude::*; +use crate::tag_utils::SwfSlice; use crate::transform::TransformStack; use gc_arena::{make_arena, ArenaParameters, Collect, GcCell, MutationContext}; use log::info; @@ -25,6 +26,7 @@ struct GcRoot<'gc> { root: DisplayNode<'gc>, mouse_hover_node: GcCell<'gc, Option>>, // TODO: Remove GcCell wrapped inside GcCell. avm: GcCell<'gc, Avm1<'gc>>, + action_queue: GcCell<'gc, ActionQueue<'gc>>, } type Error = Box; @@ -117,8 +119,14 @@ impl // Load and parse the device font. // TODO: We could use lazy_static here. - let device_font = Self::load_device_font(DEVICE_FONT_TAG, &mut renderer) - .expect("Unable to load device font"); + let device_font = match Self::load_device_font(DEVICE_FONT_TAG, &mut renderer) { + Ok(font) => Some(font), + Err(e) => { + log::error!("Unable to load device font: {}", e); + None + } + }; + let mut player = Player { player_version: NEWEST_PLAYER_VERSION, @@ -143,21 +151,26 @@ impl rng: SmallRng::from_seed([0u8; 16]), // TODO(Herschel): Get a proper seed on all platforms. - gc_arena: GcArena::new(ArenaParameters::default(), |gc_context| GcRoot { - library: GcCell::allocate(gc_context, Library::new(device_font)), - root: GcCell::allocate( - gc_context, - Box::new(MovieClip::new_with_data( - header.version, + gc_arena: GcArena::new(ArenaParameters::default(), |gc_context| { + let mut library = Library::new(); + library.set_device_font(device_font); + GcRoot { + library: GcCell::allocate(gc_context, library), + root: GcCell::allocate( gc_context, - 0, - 0, - swf_len, - header.num_frames, - )), - ), - mouse_hover_node: GcCell::allocate(gc_context, None), - avm: GcCell::allocate(gc_context, Avm1::new(gc_context, NEWEST_PLAYER_VERSION)), + Box::new(MovieClip::new_with_data( + header.version, + gc_context, + 0, + 0, + swf_len, + header.num_frames, + )), + ), + mouse_hover_node: GcCell::allocate(gc_context, None), + avm: GcCell::allocate(gc_context, Avm1::new(gc_context, NEWEST_PLAYER_VERSION)), + action_queue: GcCell::allocate(gc_context, ActionQueue::new()), + } }), frame_rate: header.frame_rate.into(), @@ -297,14 +310,13 @@ impl global_time, swf_data, swf_version, - library: gc_root.library.write(gc_context), + library: &mut *gc_root.library.write(gc_context), background_color, - avm: gc_root.avm.write(gc_context), rng, renderer, audio, navigator, - actions: vec![], + action_queue: &mut *gc_root.action_queue.write(gc_context), gc_context, active_clip: gc_root.root, }; @@ -331,7 +343,11 @@ impl } } - Self::run_actions(&mut update_context, gc_root.root); + Self::run_actions( + &mut *gc_root.avm.write(gc_context), + &mut update_context, + gc_root.root, + ); }); if needs_render { @@ -372,14 +388,13 @@ impl global_time, swf_data, swf_version, - library: gc_root.library.write(gc_context), + library: &mut *gc_root.library.write(gc_context), background_color, - avm: gc_root.avm.write(gc_context), rng, renderer, audio, navigator, - actions: vec![], + action_queue: &mut *gc_root.action_queue.write(gc_context), gc_context, active_clip: gc_root.root, }; @@ -402,7 +417,11 @@ impl *cur_hover_node = new_hover_node; - Self::run_actions(&mut update_context, gc_root.root); + Self::run_actions( + &mut *gc_root.avm.write(gc_context), + &mut update_context, + gc_root.root, + ); true } else { false @@ -439,14 +458,13 @@ impl global_time, swf_data, swf_version, - library: gc_root.library.write(gc_context), + library: &mut *gc_root.library.write(gc_context), background_color, - avm: gc_root.avm.write(gc_context), rng, renderer, audio, navigator, - actions: vec![], + action_queue: &mut *gc_root.action_queue.write(gc_context), gc_context, active_clip: gc_root.root, }; @@ -499,14 +517,13 @@ impl global_time, swf_data, swf_version, - library: gc_root.library.write(gc_context), + library: &mut *gc_root.library.write(gc_context), background_color, - avm: gc_root.avm.write(gc_context), rng, renderer, audio, navigator, - actions: vec![], + action_queue: &mut *gc_root.action_queue.write(gc_context), gc_context, active_clip: gc_root.root, }; @@ -516,7 +533,11 @@ impl .write(gc_context) .run_frame(&mut update_context); - Self::run_actions(&mut update_context, gc_root.root); + Self::run_actions( + &mut *gc_root.avm.write(gc_context), + &mut update_context, + gc_root.root, + ); }); // Update mouse state (check for new hovered button, etc.) @@ -548,7 +569,7 @@ impl self.gc_arena.mutate(|_gc_context, gc_root| { let mut render_context = RenderContext { renderer, - library: gc_root.library.read(), + library: &*gc_root.library.read(), transform_stack, view_bounds, clip_depth_stack: vec![], @@ -573,46 +594,48 @@ impl &mut self.renderer } - fn run_actions<'gc>(update_context: &mut UpdateContext<'_, 'gc, '_>, root: DisplayNode<'gc>) { + fn run_actions<'gc>( + avm: &mut Avm1<'gc>, + update_context: &mut UpdateContext<'_, 'gc, '_>, + root: DisplayNode<'gc>, + ) { // TODO: Loop here because goto-ing a frame can queue up for actions. // I think this will eventually be cleaned up; // Need to figure out the proper order of operations between ticking a clip // and running the actions. - let mut actions = std::mem::replace(&mut update_context.actions, vec![]); - while !actions.is_empty() { - { - let mut action_context = crate::avm1::ActionContext { - gc_context: update_context.gc_context, - global_time: update_context.global_time, - root, - player_version: update_context.player_version, - start_clip: root, - active_clip: root, - target_clip: Some(root), - target_path: crate::avm1::Value::Undefined, - rng: update_context.rng, - audio: update_context.audio, - navigator: update_context.navigator, - }; - for (active_clip, action) in actions { - action_context.start_clip = active_clip; - action_context.active_clip = active_clip; - action_context.target_clip = Some(active_clip); - update_context.avm.insert_stack_frame_for_action( - update_context.swf_version, - action, - &mut action_context, - ); - let _ = update_context.avm.run_stack_till_empty(&mut action_context); - } + while let Some(actions) = update_context.action_queue.pop() { + // We don't run the action f the clip was removed after it queued the action. + if actions.clip.read().removed() { + continue; } + let mut action_context = crate::avm1::ActionContext { + gc_context: update_context.gc_context, + global_time: update_context.global_time, + root, + player_version: update_context.player_version, + start_clip: root, + active_clip: root, + target_clip: Some(root), + target_path: crate::avm1::Value::Undefined, + action_queue: &mut update_context.action_queue, + rng: update_context.rng, + audio: update_context.audio, + background_color: update_context.background_color, + library: update_context.library, + navigator: update_context.navigator, + renderer: update_context.renderer, + swf_data: update_context.swf_data, + }; - // Run goto queues. - update_context.active_clip = root; - root.write(update_context.gc_context) - .run_post_frame(update_context); - - actions = std::mem::replace(&mut update_context.actions, vec![]); + action_context.start_clip = actions.clip; + action_context.active_clip = actions.clip; + action_context.target_clip = Some(actions.clip); + avm.insert_stack_frame_for_action( + update_context.swf_version, + actions.actions, + &mut action_context, + ); + let _ = avm.run_stack_till_empty(&mut action_context); } } @@ -674,22 +697,62 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { pub swf_version: u8, pub swf_data: &'a Arc>, pub global_time: u64, - pub library: std::cell::RefMut<'a, Library<'gc>>, + pub library: &'a mut Library<'gc>, pub gc_context: MutationContext<'gc, 'gc_context>, pub background_color: &'a mut Color, - pub avm: std::cell::RefMut<'a, Avm1<'gc>>, pub renderer: &'a mut dyn RenderBackend, pub audio: &'a mut dyn AudioBackend, pub navigator: &'a mut dyn NavigatorBackend, pub rng: &'a mut SmallRng, - pub actions: Vec<(DisplayNode<'gc>, crate::tag_utils::SwfSlice)>, + pub action_queue: &'a mut ActionQueue<'gc>, pub active_clip: DisplayNode<'gc>, } pub struct RenderContext<'a, 'gc> { pub renderer: &'a mut dyn RenderBackend, - pub library: std::cell::Ref<'a, Library<'gc>>, + pub library: &'a Library<'gc>, pub transform_stack: &'a mut TransformStack, pub view_bounds: BoundingBox, pub clip_depth_stack: Vec, } + +pub struct QueuedActions<'gc> { + clip: DisplayNode<'gc>, + actions: SwfSlice, +} + +/// Action and gotos need to be queued up to execute at the end of the frame. +pub struct ActionQueue<'gc> { + queue: std::collections::VecDeque>, +} + +impl<'gc> ActionQueue<'gc> { + const DEFAULT_CAPACITY: usize = 32; + + pub fn new() -> Self { + Self { + queue: std::collections::VecDeque::with_capacity(Self::DEFAULT_CAPACITY), + } + } + + pub fn queue_actions(&mut self, clip: DisplayNode<'gc>, actions: SwfSlice) { + self.queue.push_back(QueuedActions { clip, actions }) + } + + pub fn pop(&mut self) -> Option> { + self.queue.pop_front() + } +} + +impl<'gc> Default for ActionQueue<'gc> { + fn default() -> Self { + Self::new() + } +} + +unsafe impl<'gc> Collect for ActionQueue<'gc> { + #[inline] + fn trace(&self, cc: gc_arena::CollectionContext) { + self.queue.iter().for_each(|o| o.trace(cc)); + } +} diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 097eaa56f..a7cb59df5 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -13,9 +13,10 @@ type Error = Box; // This macro generates test cases for a given list of SWFs. macro_rules! swf_tests { - ($(($name:ident, $path:expr, $num_frames:literal),)*) => { - $( + ($($(#[$attr:meta])* ($name:ident, $path:expr, $num_frames:literal),)*) => { + $( #[test] + $(#[$attr])* fn $name() -> Result<(), Error> { test_swf( concat!("tests/swfs/", $path, "/test.swf"), @@ -23,8 +24,8 @@ macro_rules! swf_tests { concat!("tests/swfs/", $path, "/output.txt"), ) } - )* - } + )* + }; } // List of SWFs to test. @@ -32,6 +33,9 @@ macro_rules! swf_tests { // The test folder is a relative to core/tests/swfs // Inside the folder is expected to be "test.swf" and "output.txt" with the correct output. swf_tests! { + (execution_order1, "avm1/execution_order1", 3), + (execution_order2, "avm1/execution_order2", 15), + (execution_order3, "avm1/execution_order3", 5), (single_frame, "avm1/single_frame", 2), (looping, "avm1/looping", 6), (goto_advance1, "avm1/goto_advance1", 10), @@ -41,6 +45,7 @@ swf_tests! { (goto_rewind1, "avm1/goto_rewind1", 10), (goto_rewind2, "avm1/goto_rewind2", 10), (goto_rewind3, "avm1/goto_rewind3", 10), + (goto_execution_order, "avm1/goto_execution_order", 3), (greaterthan_swf5, "avm1/greaterthan_swf5", 1), (greaterthan_swf8, "avm1/greaterthan_swf8", 1), (strictly_equals, "avm1/strictly_equals", 1), @@ -74,7 +79,14 @@ fn test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Resu player.run_frame(); } - assert_eq!(trace_log(), expected_output); + let trace_log = trace_log(); + if trace_log != expected_output { + println!( + "Ruffle output:\n{}\nExpected output:\n{}", + trace_log, expected_output + ); + panic!("Ruffle output did not match expected output."); + } Ok(()) } diff --git a/core/tests/swfs/avm1/execution_order1/output.txt b/core/tests/swfs/avm1/execution_order1/output.txt new file mode 100644 index 000000000..6fedceeda --- /dev/null +++ b/core/tests/swfs/avm1/execution_order1/output.txt @@ -0,0 +1,5 @@ +root 1 +child 1 +child 2 +root 2 +root 3 diff --git a/core/tests/swfs/avm1/execution_order1/test.fla b/core/tests/swfs/avm1/execution_order1/test.fla new file mode 100644 index 000000000..cf7e85d5b Binary files /dev/null and b/core/tests/swfs/avm1/execution_order1/test.fla differ diff --git a/core/tests/swfs/avm1/execution_order1/test.swf b/core/tests/swfs/avm1/execution_order1/test.swf new file mode 100644 index 000000000..51e12705f Binary files /dev/null and b/core/tests/swfs/avm1/execution_order1/test.swf differ diff --git a/core/tests/swfs/avm1/execution_order2/output.txt b/core/tests/swfs/avm1/execution_order2/output.txt new file mode 100644 index 000000000..ed49602b5 --- /dev/null +++ b/core/tests/swfs/avm1/execution_order2/output.txt @@ -0,0 +1,7 @@ +root frame 1 +gotoAndPlay(2) +root 2 +childA frame 1 +childB frame 1 +childB frame 2 +childA frame 2 diff --git a/core/tests/swfs/avm1/execution_order2/test.fla b/core/tests/swfs/avm1/execution_order2/test.fla new file mode 100644 index 000000000..c9b79c100 Binary files /dev/null and b/core/tests/swfs/avm1/execution_order2/test.fla differ diff --git a/core/tests/swfs/avm1/execution_order2/test.swf b/core/tests/swfs/avm1/execution_order2/test.swf new file mode 100644 index 000000000..1357187da Binary files /dev/null and b/core/tests/swfs/avm1/execution_order2/test.swf differ diff --git a/core/tests/swfs/avm1/execution_order3/output.txt b/core/tests/swfs/avm1/execution_order3/output.txt new file mode 100644 index 000000000..bd2cd571c --- /dev/null +++ b/core/tests/swfs/avm1/execution_order3/output.txt @@ -0,0 +1,4 @@ +root frame 1 +gotoAndPlay(3) +childA frame 1 +root frame 3 diff --git a/core/tests/swfs/avm1/execution_order3/test.fla b/core/tests/swfs/avm1/execution_order3/test.fla new file mode 100644 index 000000000..fdd6ff4fa Binary files /dev/null and b/core/tests/swfs/avm1/execution_order3/test.fla differ diff --git a/core/tests/swfs/avm1/execution_order3/test.swf b/core/tests/swfs/avm1/execution_order3/test.swf new file mode 100644 index 000000000..ec5c971d9 Binary files /dev/null and b/core/tests/swfs/avm1/execution_order3/test.swf differ diff --git a/core/tests/swfs/avm1/goto_execution_order/output.txt b/core/tests/swfs/avm1/goto_execution_order/output.txt new file mode 100644 index 000000000..1191247b6 --- /dev/null +++ b/core/tests/swfs/avm1/goto_execution_order/output.txt @@ -0,0 +1,2 @@ +1 +2 diff --git a/core/tests/swfs/avm1/goto_execution_order/test.fla b/core/tests/swfs/avm1/goto_execution_order/test.fla new file mode 100644 index 000000000..4a215c1d2 Binary files /dev/null and b/core/tests/swfs/avm1/goto_execution_order/test.fla differ diff --git a/core/tests/swfs/avm1/goto_execution_order/test.swf b/core/tests/swfs/avm1/goto_execution_order/test.swf new file mode 100644 index 000000000..f12e0c923 Binary files /dev/null and b/core/tests/swfs/avm1/goto_execution_order/test.swf differ