Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion core/src/avm2/globals/flash/display/display_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub fn initialize_for_allocator<'gc>(
) -> Object<'gc> {
let obj = StageObject::for_display_object(context.gc(), dobj, class);
dobj.set_placed_by_avm2_script(true);
dobj.set_object2(context, obj);
dobj.set_object2(context.gc(), obj);

// [NA] Should these run for everything?
dobj.post_instantiation(context, None, Instantiator::Avm2, false);
Expand All @@ -51,6 +51,14 @@ pub fn initialize_for_allocator<'gc>(
dobj.base().set_skip_next_enter_frame(true);
dobj.on_construction_complete(context);

// All MovieClips constructed from ActionScript start out as orphans. If,
// at the end of a frame, this MovieClip is no longer an orphan, it will
// automatically be removed from the orphan list by
// `OrphanManager::cleanup_dead_orphans`.
if let Some(movie_clip) = dobj.as_movie_clip() {
context.orphan_manager.add_orphan_obj(movie_clip.into());
}

obj.into()
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/avm2/globals/flash/display/simple_button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub fn simple_button_allocator<'gc>(
button.post_instantiation(activation.context, None, Instantiator::Avm2, false);
let display_object = button.into();
let obj = StageObject::for_display_object(activation.gc(), display_object, orig_class);
display_object.set_object2(activation.context, obj);
display_object.set_object2(activation.gc(), obj);
return Ok(obj.into());
}

Expand Down
26 changes: 21 additions & 5 deletions core/src/display_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1852,14 +1852,14 @@
/// Retrieve the parent of this display object.
///
/// This version of the function implements the concept of parenthood as
/// seen in AVM1. Notably, it disallows access to the `Stage` and to
/// non-AVM1 DisplayObjects; for an unfiltered concept of parent,
/// use the `parent` method.
/// seen in AVM1. Notably, it disallows access to the `Stage` and to a
/// `LoaderDisplay`; for an unfiltered concept of parent, use the `parent`
/// method.
#[no_dynamic]
fn avm1_parent(self) -> Option<DisplayObject<'gc>> {
self.parent()
.filter(|p| p.as_stage().is_none())
.filter(|p| !p.movie().is_action_script_3())
.filter(|p| p.as_loader_display().is_none())
}

/// Retrieve the parent of this display object.
Expand Down Expand Up @@ -2229,6 +2229,7 @@
fn set_has_explicit_name(self, value: bool) {
self.base().set_has_explicit_name(value);
}

fn state(&self) -> Option<ButtonState> {
None
}
Expand All @@ -2251,6 +2252,20 @@
/// as properties on the class
fn construct_frame(self, _context: &mut UpdateContext<'gc>) {}

/// Whether this DisplayObject is an AVM2 orphan object. Objects that are
/// no longer AVM2 orphans (e.g. they have been adopted) will be
/// automatically removed from the global orphan list by
/// `OrphanManager::cleanup_dead_orphans`.
///
/// There are two ways a DisplayObject can become an AVM2 orphan:
/// 1 - The clip has no parent
/// 2 - The clip has a parent, but the parent is from an AVM1 movie
#[no_dynamic]
fn is_avm2_orphan(self) -> bool {
self.parent()
.is_none_or(|p| !p.movie().is_action_script_3())
}

/// To be called when an AVM2 display object has finished being constructed.
///
/// This function must be called once and ONLY once, after the object's
Expand Down Expand Up @@ -2532,7 +2547,7 @@

fn object2(self) -> Option<Avm2StageObject<'gc>>;

fn set_object2(self, _context: &mut UpdateContext<'gc>, _to: Avm2StageObject<'gc>) {}
fn set_object2(self, _mc: &Mutation<'gc>, _to: Avm2StageObject<'gc>) {}

Check warning on line 2550 in core/src/display_object.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (2550)

#[no_dynamic]
fn object2_or_null(self) -> Avm2Value<'gc> {
Expand Down Expand Up @@ -2850,6 +2865,7 @@
pub fn as_morph_shape for MorphShape;
pub fn as_video for Video;
pub fn as_bitmap for Bitmap;
pub fn as_loader_display for LoaderDisplay;
}

pub fn as_interactive(self) -> Option<InteractiveObject<'gc>> {
Expand Down
4 changes: 2 additions & 2 deletions core/src/display_object/avm2_button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -658,8 +658,8 @@ impl<'gc> TDisplayObject<'gc> for Avm2Button<'gc> {
self.0.object.get()
}

fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) {
let write = Gc::write(context.gc(), self.0);
fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) {
let write = Gc::write(mc, self.0);
unlock!(write, Avm2ButtonData, object).set(Some(to));
}

Expand Down
4 changes: 2 additions & 2 deletions core/src/display_object/bitmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,8 @@ impl<'gc> TDisplayObject<'gc> for Bitmap<'gc> {
self.0.avm2_object.get()
}

fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) {
self.set_avm2_object(context.gc(), Some(to));
fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) {
self.set_avm2_object(mc, Some(to));
}

fn movie(self) -> Arc<SwfMovie> {
Expand Down
4 changes: 2 additions & 2 deletions core/src/display_object/edit_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2574,8 +2574,8 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> {
self.0.object.get().and_then(|o| o.as_avm2_object())
}

fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) {
self.set_object(Some(to.into()), context.gc());
fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) {
self.set_object(Some(to.into()), mc);
}

fn self_bounds(self) -> Rectangle<Twips> {
Expand Down
5 changes: 2 additions & 3 deletions core/src/display_object/graphic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ impl<'gc> TDisplayObject<'gc> for Graphic<'gc> {
self.into(),
class_object,
) {
Ok(object) => self.set_object2(activation.context, object),
Ok(object) => self.set_object2(activation.gc(), object),
Err(e) => {
tracing::error!("Got error when constructing AVM2 side of shape: {}", e)
}
Expand Down Expand Up @@ -249,8 +249,7 @@ impl<'gc> TDisplayObject<'gc> for Graphic<'gc> {
self.0.avm2_object.get()
}

fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) {
let mc = context.gc();
fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) {
unlock!(Gc::write(mc, self.0), GraphicData, avm2_object).set(Some(to));
}

Expand Down
3 changes: 1 addition & 2 deletions core/src/display_object/loader_display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,7 @@ impl<'gc> TDisplayObject<'gc> for LoaderDisplay<'gc> {
self.0.avm2_object.get()
}

fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) {
let mc = context.gc();
fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) {
unlock!(Gc::write(mc, self.0), LoaderDisplayData, avm2_object).set(Some(to))
}

Expand Down
5 changes: 2 additions & 3 deletions core/src/display_object/morph_shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,7 @@ impl<'gc> TDisplayObject<'gc> for MorphShape<'gc> {
self.0.object.get()
}

fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) {
let mc = context.gc();
fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) {
unlock!(Gc::write(mc, self.0), MorphShapeData, object).set(Some(to))
}

Expand All @@ -103,7 +102,7 @@ impl<'gc> TDisplayObject<'gc> for MorphShape<'gc> {
// We don't need to call the initializer method, as AVM2 can't link
// a custom class to a MorphShape, and the initializer method for
// MorphShape itself is a no-op
self.set_object2(context, object);
self.set_object2(context.gc(), object);

self.on_construction_complete(context);
}
Expand Down
26 changes: 14 additions & 12 deletions core/src/display_object/movie_clip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,6 @@ pub struct MovieClipData<'gc> {
attached_audio: Lock<Option<NetStream<'gc>>>,

/// The next MovieClip in the AVM1 execution list.
///
/// `None` in an AVM2 movie.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate here when it's None vs Some? IIUC it's Some when either this movie clip or the root MC is from AVM1. Because without a comment I'd assume it's for AVM1 only.

next_avm1_clip: Lock<Option<MovieClip<'gc>>>,

audio_stream: Cell<Option<SoundInstanceHandle>>,
Expand Down Expand Up @@ -421,8 +419,8 @@ impl<'gc> MovieClip<'gc> {

/// Execute all other timeline actions on this object.
pub fn run_frame_avm1(self, context: &mut UpdateContext<'gc>) {
if !self.movie().is_action_script_3() {
// Run my load/enterFrame clip event.
// Run my load/enterFrame clip event.
if !self.movie().is_action_script_3() || !context.root_swf.is_action_script_3() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add a comment here explaining this situation? I.e. why it is being run for an AVM2 movie

let is_load_frame = !self.0.contains_flag(MovieClipFlags::INITIALIZED);
if is_load_frame {
self.event_dispatch(context, ClipEvent::Load);
Expand Down Expand Up @@ -1995,7 +1993,11 @@ impl<'gc> MovieClip<'gc> {
let object =
Avm2StageObject::for_display_object(context.gc(), display_object, class_object);

self.set_object2(context, object);
self.set_object2(context.gc(), object);

if self.is_avm2_orphan() {
context.orphan_manager.add_orphan_obj(self.into());
}
}

/// Construct the AVM2 side of this object.
Expand Down Expand Up @@ -2032,7 +2034,10 @@ impl<'gc> MovieClip<'gc> {
let class_object = context.avm2.classes().avm1movie;
let object = Avm2StageObject::for_display_object(context.gc(), self.into(), class_object);

self.set_object2(context, object);
self.set_object2(context.gc(), object);

// No need to mark `self` as an orphan, as it's about to be adopted by
// the AVM2 `Loader` object
}

pub fn register_frame_script(
Expand Down Expand Up @@ -2602,7 +2607,7 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {

self.set_default_instance_name(context);

if !self.movie().is_action_script_3() {
if !self.movie().is_action_script_3() || !context.root_swf.is_action_script_3() {
context.avm1.add_to_exec_list(context.gc(), self);

self.construct_as_avm1_object(context, init_object, instantiated_by, run_frame);
Expand All @@ -2617,12 +2622,9 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
self.0.object2.get()
}

fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) {
let write = Gc::write(context.gc(), self.0);
fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) {
let write = Gc::write(mc, self.0);
unlock!(write, MovieClipData, object2).set(Some(to));
if self.parent().is_none() {
context.orphan_manager.add_orphan_obj(self.into());
}
}

fn set_perspective_projection(self, mut perspective_projection: Option<PerspectiveProjection>) {
Expand Down
5 changes: 2 additions & 3 deletions core/src/display_object/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ impl<'gc> TDisplayObject<'gc> for Text<'gc> {
// We don't need to call the initializer method, as AVM2 can't link
// a custom class to a StaticText, and the initializer method for
// StaticText itself is a no-op
self.set_object2(context, object);
self.set_object2(context.gc(), object);

self.on_construction_complete(context);
}
Expand All @@ -281,8 +281,7 @@ impl<'gc> TDisplayObject<'gc> for Text<'gc> {
self.0.avm2_object.get()
}

fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) {
let mc = context.gc();
fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) {
unlock!(Gc::write(mc, self.0), TextData, avm2_object).set(Some(to));
}
}
Expand Down
11 changes: 5 additions & 6 deletions core/src/display_object/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,7 @@
))
}

fn set_object(&self, context: &mut UpdateContext<'gc>, to: AvmObject<'gc>) {
let mc = context.gc();
fn set_object(&self, mc: &Mutation<'gc>, to: AvmObject<'gc>) {
unlock!(Gc::write(mc, self.0), VideoData, object).set(Some(to));
}

Expand Down Expand Up @@ -432,7 +431,7 @@
Some(context.avm1.prototypes(self.swf_version()).video),
Avm1NativeObject::Video(self),
);
self.set_object(context, object.into());
self.set_object(context.gc(), object.into());
}

self.seek(context, starting_seek);
Expand All @@ -448,7 +447,7 @@
// itself only sets the size of the Video- the Video already has the
// correct size at this point.

self.set_object2(context, object);
self.set_object2(context.gc(), object);

Check warning on line 450 in core/src/display_object/video.rs

View workflow job for this annotation

GitHub Actions / Coverage Report

Coverage

Uncovered line (450)

self.on_construction_complete(context);
}
Expand Down Expand Up @@ -552,8 +551,8 @@
self.0.object.get().and_then(|o| o.as_avm2_object())
}

fn set_object2(self, context: &mut UpdateContext<'gc>, to: Avm2StageObject<'gc>) {
self.set_object(context, to.into());
fn set_object2(self, mc: &Mutation<'gc>, to: Avm2StageObject<'gc>) {
self.set_object(mc, to.into());
}

fn avm1_text_field_bindings(&self) -> Option<Ref<'_, [Avm1TextFieldBinding<'gc>]>> {
Expand Down
25 changes: 17 additions & 8 deletions core/src/frame_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,32 +72,41 @@ pub enum FramePhase {
pub fn run_all_phases_avm2(context: &mut UpdateContext<'_>) {
let stage = context.stage;

if !stage.movie().is_action_script_3() {
return;
}
// We may still have AVM2 orphans that we need to run frames for even though
// the root movie is AVM1. However, we don't run any stage phases if the
// root movie is AVM1.
let is_root_movie_as3 = stage.movie().is_action_script_3();

*context.frame_phase = FramePhase::Enter;
OrphanManager::each_orphan_obj(context, |orphan, context| {
orphan.enter_frame(context);
});
stage.enter_frame(context);
if is_root_movie_as3 {
stage.enter_frame(context);
}

*context.frame_phase = FramePhase::Construct;
OrphanManager::each_orphan_obj(context, |orphan, context| {
orphan.construct_frame(context);
});
stage.construct_frame(context);
stage.frame_constructed(context);
if is_root_movie_as3 {
stage.construct_frame(context);
stage.frame_constructed(context);
}

*context.frame_phase = FramePhase::FrameScripts;
OrphanManager::each_orphan_obj(context, |orphan, context| {
orphan.run_frame_scripts(context);
});
stage.run_frame_scripts(context);
if is_root_movie_as3 {
stage.run_frame_scripts(context);
}
MovieClip::run_frame_script_cleanup(context);

*context.frame_phase = FramePhase::Exit;
stage.exit_frame(context);
if is_root_movie_as3 {
stage.exit_frame(context);
}

// We cannot easily remove dead `GcWeak` instances from the orphan list
// inside `each_orphan_movie`, since the callback may modify the orphan list.
Expand Down
16 changes: 12 additions & 4 deletions core/src/orphan_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ impl<'gc> OrphanManager<'gc> {
pub fn cleanup_dead_orphans(&mut self, mc: &Mutation<'gc>) {
self.orphans_mut().retain(|d| {
if let Some(dobj) = valid_orphan(*d, mc) {
let has_avm1_parent = dobj
.parent()
.is_some_and(|p| !p.movie().is_action_script_3());

// All clips that become orphaned (have their parent removed, or start out with no parent)
// get added to the orphan list. However, there's a distinction between clips
// that are removed from a RemoveObject tag, and clips that are removed from ActionScript.
Expand All @@ -78,14 +82,18 @@ impl<'gc> OrphanManager<'gc> {
// indefinitely (if there are no remaining strong references, they will eventually
// be garbage collected).
//
// To detect this, we check 'placed_by_avm2_script'. This flag get set to 'true'
// To detect this, we check 'placed_by_script'. This flag get set to 'true'
Comment on lines -81 to +85
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 2 changes should be reverted

// for objects constructed from ActionScript, and for objects moved around
// in the timeline (add/remove child, swap depths) by ActionScript. A
// RemoveObject tag will only affect objects instantiated by the timeline,
// which have not been moved in the displaylist by ActionScript. Therefore,
// any orphan we see that has 'placed_by_avm2_script()' should stay on the orphan
// any orphan we see that has 'placed_by_script()' should stay on the orphan
// list, because it was not removed by a RemoveObject tag.
dobj.placed_by_avm2_script()
//
// Also, we consider AVM2 MovieClips loaded into an AVM1 parent to always
// be orphans, since we know they were placed by AVM1 code and not by
// the timeline.
dobj.placed_by_avm2_script() || has_avm1_parent
} else {
false
}
Expand All @@ -109,7 +117,7 @@ fn valid_orphan<'gc>(
mc: &Mutation<'gc>,
) -> Option<DisplayObject<'gc>> {
if let Some(dobj) = dobj.upgrade(mc) {
if dobj.parent().is_none() {
if dobj.is_avm2_orphan() {
return Some(dobj);
}
}
Expand Down
Binary file added tests/tests/swfs/avm1/mixed_avm/avm2.swf
Binary file not shown.
Loading
Loading