core: Merge #103, improve execution order of ActionScript and gotos

core: Improve execution order of ActionScript and gotos
This commit is contained in:
Mike Welsh 2019-10-26 13:43:04 -07:00 committed by GitHub
commit c4426bf377
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 591 additions and 278 deletions

View File

@ -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<Vec<u8>>,
}
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");
}

View File

@ -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)
}

View File

@ -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<F, R>(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();

View File

@ -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<F, R>(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();

View File

@ -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 {

View File

@ -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());

View File

@ -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<DisplayNode<'gc>>,
/// The previous sibling of this display object in order of execution.
prev_sibling: Option<DisplayNode<'gc>>,
/// The next sibling of this display object in order of execution.
next_sibling: Option<DisplayNode<'gc>>,
/// 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<DisplayNode<'gc>>) {
self.parent = parent;
}
fn first_child(&self) -> Option<DisplayNode<'gc>> {
self.first_child
}
fn set_first_child(&mut self, node: Option<DisplayNode<'gc>>) {
self.first_child = node;
}
fn prev_sibling(&self) -> Option<DisplayNode<'gc>> {
self.prev_sibling
}
fn set_prev_sibling(&mut self, node: Option<DisplayNode<'gc>>) {
self.prev_sibling = node;
}
fn next_sibling(&self) -> Option<DisplayNode<'gc>> {
self.next_sibling
}
fn set_next_sibling(&mut self, node: Option<DisplayNode<'gc>>) {
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<dyn DisplayObject<'gc>> {
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<DisplayNode<'gc>>;
fn set_parent(&mut self, parent: Option<DisplayNode<'gc>>);
fn first_child(&self) -> Option<DisplayNode<'gc>>;
fn set_first_child(&mut self, node: Option<DisplayNode<'gc>>);
fn prev_sibling(&self) -> Option<DisplayNode<'gc>>;
fn set_prev_sibling(&mut self, node: Option<DisplayNode<'gc>>);
fn next_sibling(&self) -> Option<DisplayNode<'gc>>;
fn set_next_sibling(&mut self, node: Option<DisplayNode<'gc>>);
/// 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<crate::display_object::DisplayNode<'gc>>) {
self.$field.set_parent(parent)
}
fn first_child(&self) -> Option<DisplayNode<'gc>> {
self.$field.first_child()
}
fn set_first_child(&mut self, node: Option<DisplayNode<'gc>>) {
self.$field.set_first_child(node);
}
fn prev_sibling(&self) -> Option<DisplayNode<'gc>> {
self.$field.prev_sibling()
}
fn set_prev_sibling(&mut self, node: Option<DisplayNode<'gc>>) {
self.$field.set_prev_sibling(node);
}
fn next_sibling(&self) -> Option<DisplayNode<'gc>> {
self.$field.next_sibling()
}
fn set_next_sibling(&mut self, node: Option<DisplayNode<'gc>>) {
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<dyn crate::display_object::DisplayObject<'gc>> {
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<dyn DisplayObject<'gc>>>;
pub struct ChildIter<'gc> {
cur_child: Option<DisplayNode<'gc>>,
}
impl<'gc> Iterator for ChildIter<'gc> {
type Item = DisplayNode<'gc>;
fn next(&mut self) -> Option<Self::Item> {
let cur = self.cur_child;
self.cur_child = self
.cur_child
.and_then(|display_cell| display_cell.read().next_sibling());
cur
}
}

View File

@ -65,16 +65,16 @@ 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);
.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()
@ -113,6 +113,7 @@ impl<'gc> DisplayObject<'gc> for EditText<'gc> {
transform.matrix.tx += advance * scale;
}
}
}
context.transform_stack.pop();
}
}

View File

@ -10,15 +10,15 @@ use swf::CharacterId;
pub struct Library<'gc> {
characters: HashMap<CharacterId, Character<'gc>>,
jpeg_tables: Option<Vec<u8>>,
device_font: Box<Font>,
device_font: Option<Box<Font>>,
}
impl<'gc> Library<'gc> {
pub fn new(device_font: Box<Font>) -> 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<Box<Font>>) {
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()
}
}

View File

@ -26,7 +26,6 @@ pub struct MovieClip<'gc> {
static_data: Gc<'gc, MovieClipStatic>,
tag_stream_pos: u64,
is_playing: bool,
goto_queue: Vec<FrameNumber>,
current_frame: FrameNumber,
audio_stream: Option<AudioStreamHandle>,
children: BTreeMap<Depth, DisplayNode<'gc>>,
@ -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<DisplayNode<'gc>> {
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(&params.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<Depth, GotoPlaceObject>,
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(())
}

View File

@ -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<DisplayNode<'gc>>>, // TODO: Remove GcCell wrapped inside GcCell.
avm: GcCell<'gc, Avm1<'gc>>,
action_queue: GcCell<'gc, ActionQueue<'gc>>,
}
type Error = Box<dyn std::error::Error>;
@ -117,8 +119,14 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
// 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,8 +151,11 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
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)),
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,
Box::new(MovieClip::new_with_data(
@ -158,6 +169,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
),
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<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
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<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
}
}
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<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
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<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
*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<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
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<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
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<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
.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<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
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,14 +594,20 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
&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() {
{
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,
@ -590,29 +617,25 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
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,
};
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(
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,
action,
actions.actions,
&mut action_context,
);
let _ = update_context.avm.run_stack_till_empty(&mut action_context);
}
}
// 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![]);
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<Vec<u8>>,
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<Depth>,
}
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<QueuedActions<'gc>>,
}
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<QueuedActions<'gc>> {
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));
}
}

View File

@ -13,9 +13,10 @@ type Error = Box<dyn std::error::Error>;
// 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"),
@ -24,7 +25,7 @@ macro_rules! swf_tests {
)
}
)*
}
};
}
// 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(())
}

View File

@ -0,0 +1,5 @@
root 1
child 1
child 2
root 2
root 3

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
root frame 1
gotoAndPlay(2)
root 2
childA frame 1
childB frame 1
childB frame 2
childA frame 2

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,4 @@
root frame 1
gotoAndPlay(3)
childA frame 1
root frame 3

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,2 @@
1
2

Binary file not shown.

Binary file not shown.