diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 45d00b58f..39e69e08b 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -440,7 +440,12 @@ impl<'gc> Avm1<'gc> { object.call(self, context, object.as_object()?.to_owned(), &args)?; self.stack.push(return_value); } else { - let callable = object.as_object()?.read().get(&name); + let callable = object.as_object()?.read().get( + &name, + self, + context, + object.as_object()?.to_owned(), + ); if let Value::Undefined = callable { return Err(format!("Object method {} is not defined", name).into()); @@ -659,14 +664,19 @@ impl<'gc> Avm1<'gc> { if let Some(clip) = node.read().as_movie_clip() { let object = clip.object().as_object()?; if object.read().has_property(var_name) { - result = Some(object.read().get(var_name)); + result = Some(object.read().get(var_name, self, context, object)); } } }; } if result.is_none() && self.globals.read().has_property(path) { - result = Some(self.globals.read().get(path)); + result = Some( + self.globals + .clone() + .read() + .get(path, self, context, self.globals), + ); } self.push(result.unwrap_or(Value::Undefined)); Ok(()) @@ -1133,10 +1143,10 @@ impl<'gc> Avm1<'gc> { Self::resolve_slash_path_variable(context.target_clip, context.root, var_path) { if let Some(clip) = node.write(context.gc_context).as_movie_clip_mut() { - clip.object() - .as_object()? + let object = clip.object().as_object()?; + object .write(context.gc_context) - .set(var_name, value); + .set(var_name, value, self, context, object); } } Ok(()) diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index 99c2cde9f..fe8eec7fa 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -52,9 +52,9 @@ pub fn random<'gc>( pub fn create_globals<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> { let mut globals = Object::object(gc_context); - globals.set_object("Math", math::create(gc_context)); - globals.set_function("getURL", getURL, gc_context); - globals.set_function("random", random, gc_context); + globals.force_set("Math", Value::Object(math::create(gc_context))); + globals.force_set_function("getURL", getURL, gc_context); + globals.force_set_function("random", random, gc_context); globals } diff --git a/core/src/avm1/globals/math.rs b/core/src/avm1/globals/math.rs index 78703d700..ecceddc02 100644 --- a/core/src/avm1/globals/math.rs +++ b/core/src/avm1/globals/math.rs @@ -6,7 +6,7 @@ use std::f64::NAN; macro_rules! wrap_std { ( $object: ident, $gc_context: ident, $($name:expr => $std:path),* ) => {{ $( - $object.set_function( + $object.force_set_function( $name, |_avm, _context, _this, args| -> Value<'gc> { if let Some(input) = args.get(0) { @@ -49,14 +49,14 @@ pub fn random<'gc>( pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<'gc>> { let mut math = Object::object(gc_context); - math.set("E", Value::Number(std::f64::consts::E)); - math.set("LN10", Value::Number(std::f64::consts::LN_10)); - math.set("LN2", Value::Number(std::f64::consts::LN_2)); - math.set("LOG10E", Value::Number(std::f64::consts::LOG10_E)); - math.set("LOG2E", Value::Number(std::f64::consts::LOG2_E)); - math.set("PI", Value::Number(std::f64::consts::PI)); - math.set("SQRT1_2", Value::Number(std::f64::consts::FRAC_1_SQRT_2)); - math.set("SQRT2", Value::Number(std::f64::consts::SQRT_2)); + math.force_set("E", Value::Number(std::f64::consts::E)); + math.force_set("LN10", Value::Number(std::f64::consts::LN_10)); + math.force_set("LN2", Value::Number(std::f64::consts::LN_2)); + math.force_set("LOG10E", Value::Number(std::f64::consts::LOG10_E)); + math.force_set("LOG2E", Value::Number(std::f64::consts::LOG2_E)); + math.force_set("PI", Value::Number(std::f64::consts::PI)); + math.force_set("SQRT1_2", Value::Number(std::f64::consts::FRAC_1_SQRT_2)); + math.force_set("SQRT2", Value::Number(std::f64::consts::SQRT_2)); wrap_std!(math, gc_context, "abs" => f64::abs, @@ -73,8 +73,8 @@ pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<' "tan" => f64::tan ); - math.set_function("atan2", atan2, gc_context); - math.set_function("random", random, gc_context); + math.force_set_function("atan2", atan2, gc_context); + math.force_set_function("random", random, gc_context); GcCell::allocate(gc_context, math) } @@ -96,7 +96,7 @@ mod tests { fn $test() -> Result<(), Error> { with_avm(19, |avm, context| { let math = create(context.gc_context); - let function = math.read().get($name); + let function = math.read().get($name, avm, context, math); $( assert_eq!(function.call(avm, context, math, $args)?, $out); diff --git a/core/src/avm1/movie_clip.rs b/core/src/avm1/movie_clip.rs index 10966792f..42db33e3d 100644 --- a/core/src/avm1/movie_clip.rs +++ b/core/src/avm1/movie_clip.rs @@ -6,7 +6,7 @@ use gc_arena::MutationContext; macro_rules! with_movie_clip { ( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{ $( - $object.set_function( + $object.force_set_function( $name, |_avm, _context, this, args| -> Value<'gc> { if let Some(display_object) = this.read().display_node() { @@ -25,7 +25,7 @@ macro_rules! with_movie_clip { macro_rules! with_movie_clip_mut { ( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{ $( - $object.set_function( + $object.force_set_function( $name, |_avm, context, this, args| -> Value<'gc> { if let Some(display_object) = this.read().display_node() { diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index 0a1a3e10d..97cd092d7 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -2,7 +2,9 @@ use crate::avm1::{ActionContext, Avm1, Value}; use crate::display_object::DisplayNode; use core::fmt; use gc_arena::{GcCell, MutationContext}; +use std::collections::hash_map::Entry; use std::collections::HashMap; +use std::mem::replace; pub type NativeFunction<'gc> = fn( &mut Avm1<'gc>, @@ -24,10 +26,83 @@ fn default_to_string<'gc>( Value::String("[Object object]".to_string()) } +#[derive(Clone)] +pub enum Property<'gc> { + Virtual { + get: NativeFunction<'gc>, + set: Option>, + }, + Stored { + value: Value<'gc>, + // TODO: attributes + }, +} + +impl<'gc> Property<'gc> { + pub fn get( + &self, + avm: &mut Avm1<'gc>, + context: &mut ActionContext<'_, 'gc, '_>, + this: GcCell<'gc, Object<'gc>>, + ) -> Value<'gc> { + match self { + Property::Virtual { get, .. } => get(avm, context, this, &[]), + Property::Stored { value, .. } => value.to_owned(), + } + } + + pub fn set( + &mut self, + avm: &mut Avm1<'gc>, + context: &mut ActionContext<'_, 'gc, '_>, + this: GcCell<'gc, Object<'gc>>, + new_value: Value<'gc>, + ) { + match self { + Property::Virtual { set, .. } => { + if let Some(function) = set { + function(avm, context, this, &[new_value]); + } + } + Property::Stored { value, .. } => { + replace::>(value, new_value); + } + } + } +} + +unsafe impl<'gc> gc_arena::Collect for Property<'gc> { + fn trace(&self, cc: gc_arena::CollectionContext) { + match self { + Property::Virtual { get, set } => { + get.trace(cc); + set.trace(cc); + } + Property::Stored { value, .. } => value.trace(cc), + } + } +} + +impl fmt::Debug for Property<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Property::Virtual { get: _, set } => f + .debug_struct("Property::Virtual") + .field("get", &true) + .field("set", &set.is_some()) + .finish(), + Property::Stored { value } => f + .debug_struct("Property::Stored") + .field("value", &value) + .finish(), + } + } +} + #[derive(Clone)] pub struct Object<'gc> { display_node: Option>, - values: HashMap>, + values: HashMap>, function: Option>, type_of: &'static str, } @@ -58,7 +133,7 @@ impl<'gc> Object<'gc> { function: None, }; - result.set_function("toString", default_to_string, gc_context); + result.force_set_function("toString", default_to_string, gc_context); result } @@ -80,29 +155,80 @@ impl<'gc> Object<'gc> { self.display_node } - pub fn set(&mut self, name: &str, value: Value<'gc>) { - self.values.insert(name.to_owned(), value); + pub fn set( + &mut self, + name: &str, + value: Value<'gc>, + avm: &mut Avm1<'gc>, + context: &mut ActionContext<'_, 'gc, '_>, + this: GcCell<'gc, Object<'gc>>, + ) { + match self.values.entry(name.to_owned()) { + Entry::Occupied(mut entry) => { + entry.get_mut().set(avm, context, this, value); + } + Entry::Vacant(entry) => { + entry.insert(Property::Stored { value }); + } + } } - pub fn set_object(&mut self, name: &str, object: GcCell<'gc, Object<'gc>>) { - self.values.insert(name.to_owned(), Value::Object(object)); + pub fn force_set_virtual( + &mut self, + name: &str, + get: NativeFunction<'gc>, + set: Option>, + ) { + self.values + .insert(name.to_owned(), Property::Virtual { get, set }); + } + + pub fn force_set(&mut self, name: &str, value: Value<'gc>) { + self.values + .insert(name.to_string(), Property::Stored { value }); } pub fn set_function( &mut self, name: &str, function: NativeFunction<'gc>, - gc_context: MutationContext<'gc, '_>, + avm: &mut Avm1<'gc>, + context: &mut ActionContext<'_, 'gc, '_>, + this: GcCell<'gc, Object<'gc>>, ) { self.set( + name, + Value::Object(GcCell::allocate( + context.gc_context, + Object::function(function), + )), + avm, + context, + this, + ) + } + + pub fn force_set_function( + &mut self, + name: &str, + function: NativeFunction<'gc>, + gc_context: MutationContext<'gc, '_>, + ) { + self.force_set( name, Value::Object(GcCell::allocate(gc_context, Object::function(function))), ) } - pub fn get(&self, name: &str) -> Value<'gc> { + pub fn get( + &self, + name: &str, + avm: &mut Avm1<'gc>, + context: &mut ActionContext<'_, 'gc, '_>, + this: GcCell<'gc, Object<'gc>>, + ) -> Value<'gc> { if let Some(value) = self.values.get(name) { - return value.to_owned(); + return value.get(avm, context, this); } Value::Undefined } @@ -145,3 +271,109 @@ impl<'gc> Object<'gc> { self.type_of } } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::backend::audio::NullAudioBackend; + use crate::backend::navigator::NullNavigatorBackend; + use crate::display_object::DisplayObject; + use crate::movie_clip::MovieClip; + use gc_arena::rootless_arena; + use rand::{rngs::SmallRng, SeedableRng}; + + fn with_object(swf_version: u8, test: F) -> R + where + F: for<'a, 'gc> FnOnce( + &mut Avm1<'gc>, + &mut ActionContext<'a, 'gc, '_>, + GcCell<'gc, Object<'gc>>, + ) -> R, + { + rootless_arena(|gc_context| { + let mut avm = Avm1::new(gc_context, swf_version); + let movie_clip: Box = Box::new(MovieClip::new(gc_context)); + let root = GcCell::allocate(gc_context, movie_clip); + let mut context = ActionContext { + gc_context, + global_time: 0, + root, + start_clip: root, + active_clip: root, + target_clip: Some(root), + target_path: Value::Undefined, + rng: &mut SmallRng::from_seed([0u8; 16]), + audio: &mut NullAudioBackend::new(), + navigator: &mut NullNavigatorBackend::new(), + }; + let object = GcCell::allocate(gc_context, Object::object(gc_context)); + + test(&mut avm, &mut context, object) + }) + } + + #[test] + fn test_get_undefined() { + with_object(0, |avm, context, object| { + assert_eq!( + object.read().get("not_defined", avm, context, object), + Value::Undefined + ); + }) + } + + #[test] + fn test_set_get() { + with_object(0, |avm, context, object| { + object + .write(context.gc_context) + .force_set("forced", Value::String("forced".to_string())); + object.write(context.gc_context).set( + "natural", + Value::String("natural".to_string()), + avm, + context, + object, + ); + + assert_eq!( + object.read().get("forced", avm, context, object), + Value::String("forced".to_string()) + ); + assert_eq!( + object.read().get("natural", avm, context, object), + Value::String("natural".to_string()) + ); + }) + } + + #[test] + fn test_virtual_get() { + with_object(0, |avm, context, object| { + let getter: NativeFunction = + |_avm, _context, _this, _args| Value::String("Virtual!".to_string()); + object + .write(context.gc_context) + .force_set_virtual("test", getter, None); + + assert_eq!( + object.read().get("test", avm, context, object), + Value::String("Virtual!".to_string()) + ); + + // This set should do nothing + object.write(context.gc_context).set( + "test", + Value::String("Ignored!".to_string()), + avm, + context, + object, + ); + assert_eq!( + object.read().get("test", avm, context, object), + Value::String("Virtual!".to_string()) + ); + }) + } +}