From 8286900d2f393b67c1b056ca737c9afcdb4d7903 Mon Sep 17 00:00:00 2001 From: CUB3D Date: Sun, 21 Sep 2025 17:21:52 +0100 Subject: [PATCH] avm1: Fix 21703 If `.object` on a `DisplayObject` returns `Undefined` then `MovieClipReference::try_from_stage_object` will fail, create a pure string-path reference in this case. Fixes #21703 --- core/src/avm1/activation.rs | 10 +++++----- core/src/avm1/object_reference.rs | 29 ++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index 7e58c3a06156..b418c33dbd3f 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -1,6 +1,7 @@ use crate::avm1::callable_value::CallableValue; use crate::avm1::error::Error; use crate::avm1::function::{Avm1Function, ExecutionReason, FunctionObject}; +use crate::avm1::object_reference::MovieClipReference; use crate::avm1::property::Attribute; use crate::avm1::runtime::skip_actions; use crate::avm1::scope::{Scope, ScopeClass}; @@ -30,8 +31,6 @@ use swf::avm1::types::*; use url::form_urlencoded; use web_time::Instant; -use super::object_reference::MovieClipReference; - macro_rules! avm_debug { ($avm: expr, $($arg:tt)*) => ( if $avm.show_debug_output() { @@ -870,7 +869,9 @@ impl<'a, 'gc> Activation<'a, 'gc> { let swf_version = self.swf_version(); let func_data = parent_data.to_unbounded_subslice(action.actions); let constant_pool = self.constant_pool(); - let bc = self.base_clip.object().coerce_to_object(self); + + let mcr = MovieClipReference::from_display_object(self, self.base_clip); + let func = Avm1Function::from_swf_function( self.gc(), swf_version, @@ -878,8 +879,7 @@ impl<'a, 'gc> Activation<'a, 'gc> { action, self.scope(), constant_pool, - // `base_clip` should always be a living `MovieClip` so this can't fail - MovieClipReference::try_from_stage_object(self, bc).unwrap(), + mcr, ); let name = func.name(); let prototype = Object::new( diff --git a/core/src/avm1/object_reference.rs b/core/src/avm1/object_reference.rs index d176a4a13fcf..042414255533 100644 --- a/core/src/avm1/object_reference.rs +++ b/core/src/avm1/object_reference.rs @@ -67,6 +67,25 @@ struct MovieClipReferenceData<'gc> { } impl<'gc> MovieClipReference<'gc> { + pub fn from_display_object( + activation: &mut Activation<'_, 'gc>, + display_object: DisplayObject<'gc>, + ) -> Self { + let bc = display_object.object().coerce_to_object(activation); + + // This can fail if `display_object.object() == Undefined`, so create a reference with only a path + match MovieClipReference::try_from_stage_object(activation, bc) { + Some(mcr) => mcr, + None => { + let mc_ref = MovieClipReferenceData { + path: MovieClipPath::new_from_path(activation.gc(), display_object.path()), + cached_object: None.into(), + }; + MovieClipReference(Gc::new(activation.gc(), mc_ref)) + } + } + } + pub fn try_from_stage_object( activation: &mut Activation<'_, 'gc>, object: Object<'gc>, @@ -94,12 +113,12 @@ impl<'gc> MovieClipReference<'gc> { Some(Self(Gc::new(activation.gc(), mc_ref))) } - /// Handle the logic of swfv5 DisplayObjects + /// Handle the logic of SWFv5 DisplayObjects fn process_swf5_references( activation: &mut Activation<'_, 'gc>, mut display_object: DisplayObject<'gc>, ) -> Option> { - // In swfv5 paths resolve to the first MovieClip parent if the target isn't a movieclip + // In SWFv5 paths resolve to the first MovieClip parent if the target isn't a MovieClip if activation.swf_version() <= 5 { while display_object.as_movie_clip().is_none() { if let Some(p) = display_object.avm1_parent() { @@ -128,12 +147,12 @@ impl<'gc> MovieClipReference<'gc> { .and_then(|c| c.upgrade(activation.gc())) { if let Some(display_object) = cache.as_display_object_no_super() { - // We have to fallback to manual path-walking if the object is removed + // We have to fall back to manual path-walking if the object is removed if !display_object.avm1_removed() { let display_object = Self::process_swf5_references(activation, display_object)?; // Note that there is a bug here but this *is* how it works in Flash: - // If we are using the cached DisplayObject, we return it's path, which can be changed by modifying `_name` + // If we are using the cached DisplayObject, we return its path, which can be changed by modifying `_name` // However, if we remove and re-create the clip, the stored path (the original path) will differ from the path of the cached object (the modified path) // Essentially, changes to `_name` are reverted after the clip is re-created @@ -192,7 +211,7 @@ impl<'gc> MovieClipReference<'gc> { None => istr!(""), // Found the reference, cached, we can't re-use `self.path` sadly, it would be quicker if we could // But if the clip has been re-named, since being created then `mc.path() != path` - Some((true, _, dobj)) => AvmString::new(activation.gc(), dobj.path()), + Some((true, _, disp_obj)) => AvmString::new(activation.gc(), disp_obj.path()), // Found the reference, un-cached, so our cached path must be correct Some((false, _, _)) => self.0.path.full_path, }