avm1: Implement Object.watch & Object.unwatch (#268)

This commit is contained in:
Nathan Adams 2020-07-02 20:37:27 +02:00 committed by Mike Welsh
parent ecbab536b5
commit 8a0430d744
21 changed files with 537 additions and 1 deletions

View File

@ -229,6 +229,21 @@ impl<'gc> TObject<'gc> for ColorTransformObject<'gc> {
.add_property_with_case(activation, gc_context, name, get, set, attributes) .add_property_with_case(activation, gc_context, name, get, set, attributes)
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.base()
.set_watcher(gc_context, name, callback, user_data);
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
self.base().remove_watcher(gc_context, name)
}
fn has_property( fn has_property(
&self, &self,
activation: &mut Activation<'_, 'gc>, activation: &mut Activation<'_, 'gc>,

View File

@ -624,6 +624,20 @@ impl<'gc> TObject<'gc> for FunctionObject<'gc> {
.add_property_with_case(activation, gc_context, name, get, set, attributes) .add_property_with_case(activation, gc_context, name, get, set, attributes)
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.base.set_watcher(gc_context, name, callback, user_data);
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
self.base.remove_watcher(gc_context, name)
}
fn has_property( fn has_property(
&self, &self,
activation: &mut Activation<'_, 'gc>, activation: &mut Activation<'_, 'gc>,

View File

@ -162,6 +162,55 @@ pub fn register_class<'gc>(
Ok(Value::Undefined) Ok(Value::Undefined)
} }
/// Implements `Object.prototype.watch`
fn watch<'gc>(
activation: &mut Activation<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let name = if let Some(name) = args.get(0) {
name.coerce_to_string(activation, context)?
} else {
return Ok(false.into());
};
let callback = if let Some(callback) = args.get(1) {
if let Some(callback) = callback
.coerce_to_object(activation, context)
.as_executable()
{
callback
} else {
return Ok(false.into());
}
} else {
return Ok(false.into());
};
let user_data = args.get(2).cloned().unwrap_or(Value::Undefined);
this.set_watcher(context.gc_context, name, callback, user_data);
Ok(true.into())
}
/// Implements `Object.prototype.unmwatch`
fn unwatch<'gc>(
activation: &mut Activation<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let name = if let Some(name) = args.get(0) {
name.coerce_to_string(activation, context)?
} else {
return Ok(false.into());
};
let result = this.remove_watcher(context.gc_context, name);
Ok(result.into())
}
/// Partially construct `Object.prototype`. /// Partially construct `Object.prototype`.
/// ///
/// `__proto__` and other cross-linked properties of this object will *not* /// `__proto__` and other cross-linked properties of this object will *not*
@ -218,6 +267,20 @@ pub fn fill_proto<'gc>(
DontDelete | DontEnum, DontDelete | DontEnum,
Some(fn_proto), Some(fn_proto),
); );
object_proto.as_script_object().unwrap().force_set_function(
"watch",
watch,
gc_context,
DontDelete | DontEnum,
Some(fn_proto),
);
object_proto.as_script_object().unwrap().force_set_function(
"unwatch",
unwatch,
gc_context,
DontDelete | DontEnum,
Some(fn_proto),
);
} }
/// Implements `ASSetPropFlags`. /// Implements `ASSetPropFlags`.

View File

@ -259,6 +259,23 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
attributes: EnumSet<Attribute>, attributes: EnumSet<Attribute>,
); );
/// Set the 'watcher' of a given property.
///
/// The property does not need to exist at the time of this being called.
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
);
/// Removed any assigned 'watcher' from the given property.
///
/// The return value will indicate if there was a watcher present before this method was
/// called.
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool;
/// Checks if the object has a given named property. /// Checks if the object has a given named property.
fn has_property( fn has_property(
&self, &self,

View File

@ -8,6 +8,7 @@ use core::fmt;
use enumset::EnumSet; use enumset::EnumSet;
use gc_arena::{Collect, GcCell, MutationContext}; use gc_arena::{Collect, GcCell, MutationContext};
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap;
pub const TYPE_OF_OBJECT: &str = "object"; pub const TYPE_OF_OBJECT: &str = "object";
@ -18,6 +19,50 @@ pub enum ArrayStorage<'gc> {
Properties { length: usize }, Properties { length: usize },
} }
#[derive(Debug, Clone, Collect)]
#[collect(no_drop)]
pub struct Watcher<'gc> {
callback: Executable<'gc>,
user_data: Value<'gc>,
}
impl<'gc> Watcher<'gc> {
pub fn new(callback: Executable<'gc>, user_data: Value<'gc>) -> Self {
Self {
callback,
user_data,
}
}
#[allow(clippy::too_many_arguments)]
pub fn call(
&self,
activation: &mut Activation<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
name: &str,
old_value: Value<'gc>,
new_value: Value<'gc>,
this: Object<'gc>,
base_proto: Option<Object<'gc>>,
) -> Result<Value<'gc>, crate::avm1::error::Error<'gc>> {
let args = [
Value::String(name.to_string()),
old_value,
new_value,
self.user_data.clone(),
];
self.callback.exec(
name,
activation,
context,
this,
base_proto,
&args,
ExecutionReason::Special,
)
}
}
#[derive(Debug, Copy, Clone, Collect)] #[derive(Debug, Copy, Clone, Collect)]
#[collect(no_drop)] #[collect(no_drop)]
pub struct ScriptObject<'gc>(GcCell<'gc, ScriptObjectData<'gc>>); pub struct ScriptObject<'gc>(GcCell<'gc, ScriptObjectData<'gc>>);
@ -28,6 +73,7 @@ pub struct ScriptObjectData<'gc> {
interfaces: Vec<Object<'gc>>, interfaces: Vec<Object<'gc>>,
type_of: &'static str, type_of: &'static str,
array: ArrayStorage<'gc>, array: ArrayStorage<'gc>,
watchers: HashMap<String, Watcher<'gc>>,
} }
unsafe impl<'gc> Collect for ScriptObjectData<'gc> { unsafe impl<'gc> Collect for ScriptObjectData<'gc> {
@ -36,6 +82,7 @@ unsafe impl<'gc> Collect for ScriptObjectData<'gc> {
self.values.trace(cc); self.values.trace(cc);
self.array.trace(cc); self.array.trace(cc);
self.interfaces.trace(cc); self.interfaces.trace(cc);
self.watchers.trace(cc);
} }
} }
@ -45,6 +92,7 @@ impl fmt::Debug for ScriptObjectData<'_> {
.field("prototype", &self.prototype) .field("prototype", &self.prototype)
.field("values", &self.values) .field("values", &self.values)
.field("array", &self.array) .field("array", &self.array)
.field("watchers", &self.watchers)
.finish() .finish()
} }
} }
@ -62,6 +110,7 @@ impl<'gc> ScriptObject<'gc> {
values: PropertyMap::new(), values: PropertyMap::new(),
array: ArrayStorage::Properties { length: 0 }, array: ArrayStorage::Properties { length: 0 },
interfaces: vec![], interfaces: vec![],
watchers: HashMap::new(),
}, },
)) ))
} }
@ -78,6 +127,7 @@ impl<'gc> ScriptObject<'gc> {
values: PropertyMap::new(), values: PropertyMap::new(),
array: ArrayStorage::Vector(Vec::new()), array: ArrayStorage::Vector(Vec::new()),
interfaces: vec![], interfaces: vec![],
watchers: HashMap::new(),
}, },
)); ));
object.sync_native_property("length", gc_context, Some(0.into()), false); object.sync_native_property("length", gc_context, Some(0.into()), false);
@ -97,6 +147,7 @@ impl<'gc> ScriptObject<'gc> {
values: PropertyMap::new(), values: PropertyMap::new(),
array: ArrayStorage::Properties { length: 0 }, array: ArrayStorage::Properties { length: 0 },
interfaces: vec![], interfaces: vec![],
watchers: HashMap::new(),
}, },
)) ))
.into() .into()
@ -116,6 +167,7 @@ impl<'gc> ScriptObject<'gc> {
values: PropertyMap::new(), values: PropertyMap::new(),
array: ArrayStorage::Properties { length: 0 }, array: ArrayStorage::Properties { length: 0 },
interfaces: vec![], interfaces: vec![],
watchers: HashMap::new(),
}, },
)) ))
} }
@ -191,7 +243,7 @@ impl<'gc> ScriptObject<'gc> {
pub(crate) fn internal_set( pub(crate) fn internal_set(
&self, &self,
name: &str, name: &str,
value: Value<'gc>, mut value: Value<'gc>,
activation: &mut Activation<'_, 'gc>, activation: &mut Activation<'_, 'gc>,
context: &mut UpdateContext<'_, 'gc, '_>, context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>, this: Object<'gc>,
@ -257,6 +309,28 @@ impl<'gc> ScriptObject<'gc> {
//we'd resolve and return up there, but we have borrows that need //we'd resolve and return up there, but we have borrows that need
//to end before we can do so. //to end before we can do so.
if !worked { if !worked {
let watcher = self.0.read().watchers.get(name).cloned();
let mut return_value = Ok(());
if let Some(watcher) = watcher {
let old_value = self.get(name, activation, context)?;
value = match watcher.call(
activation,
context,
name,
old_value,
value.clone(),
this,
base_proto,
) {
Ok(value) => value,
Err(Error::ThrownValue(error)) => {
return_value = Err(Error::ThrownValue(error));
Value::Undefined
}
Err(_) => Value::Undefined,
};
}
let rval = match self let rval = match self
.0 .0
.write(context.gc_context) .write(context.gc_context)
@ -285,6 +359,8 @@ impl<'gc> ScriptObject<'gc> {
ExecutionReason::Special, ExecutionReason::Special,
); );
} }
return return_value;
} }
} }
@ -479,6 +555,24 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> {
); );
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.0
.write(gc_context)
.watchers
.insert(name.to_string(), Watcher::new(callback, user_data));
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
let old = self.0.write(gc_context).watchers.remove(name.as_ref());
old.is_some()
}
fn define_value( fn define_value(
&self, &self,
gc_context: MutationContext<'gc, '_>, gc_context: MutationContext<'gc, '_>,

View File

@ -191,6 +191,21 @@ impl<'gc> TObject<'gc> for SharedObject<'gc> {
.add_property_with_case(activation, gc_context, name, get, set, attributes) .add_property_with_case(activation, gc_context, name, get, set, attributes)
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.base()
.set_watcher(gc_context, name, callback, user_data);
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
self.base().remove_watcher(gc_context, name)
}
fn has_property( fn has_property(
&self, &self,
activation: &mut Activation<'_, 'gc>, activation: &mut Activation<'_, 'gc>,

View File

@ -251,6 +251,21 @@ impl<'gc> TObject<'gc> for SoundObject<'gc> {
.add_property_with_case(activation, gc_context, name, get, set, attributes) .add_property_with_case(activation, gc_context, name, get, set, attributes)
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.base()
.set_watcher(gc_context, name, callback, user_data);
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
self.base().remove_watcher(gc_context, name)
}
fn has_property( fn has_property(
&self, &self,
activation: &mut Activation<'_, 'gc>, activation: &mut Activation<'_, 'gc>,

View File

@ -332,6 +332,23 @@ impl<'gc> TObject<'gc> for StageObject<'gc> {
.add_property_with_case(activation, gc_context, name, get, set, attributes) .add_property_with_case(activation, gc_context, name, get, set, attributes)
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.0
.read()
.base
.set_watcher(gc_context, name, callback, user_data);
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
self.0.read().base.remove_watcher(gc_context, name)
}
fn has_property( fn has_property(
&self, &self,
activation: &mut Activation<'_, 'gc>, activation: &mut Activation<'_, 'gc>,

View File

@ -237,6 +237,21 @@ impl<'gc> TObject<'gc> for SuperObject<'gc> {
//`super` cannot have properties defined on it //`super` cannot have properties defined on it
} }
fn set_watcher(
&self,
_gc_context: MutationContext<'gc, '_>,
_name: Cow<str>,
_callback: Executable<'gc>,
_user_data: Value<'gc>,
) {
//`super` cannot have properties defined on it
}
fn remove_watcher(&self, _gc_context: MutationContext<'gc, '_>, _name: Cow<str>) -> bool {
//`super` cannot have properties defined on it
false
}
fn has_property( fn has_property(
&self, &self,
activation: &mut Activation<'_, 'gc>, activation: &mut Activation<'_, 'gc>,

View File

@ -231,6 +231,26 @@ impl<'gc> TObject<'gc> for ValueObject<'gc> {
.add_property_with_case(activation, gc_context, name, get, set, attributes) .add_property_with_case(activation, gc_context, name, get, set, attributes)
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.0
.read()
.base
.set_watcher(gc_context, name, callback, user_data);
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
self.0
.write(gc_context)
.base
.remove_watcher(gc_context, name)
}
fn define_value( fn define_value(
&self, &self,
gc_context: MutationContext<'gc, '_>, gc_context: MutationContext<'gc, '_>,

View File

@ -157,6 +157,21 @@ impl<'gc> TObject<'gc> for XMLAttributesObject<'gc> {
.add_property_with_case(activation, gc_context, name, get, set, attributes) .add_property_with_case(activation, gc_context, name, get, set, attributes)
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.base()
.set_watcher(gc_context, name, callback, user_data);
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
self.base().remove_watcher(gc_context, name)
}
fn define_value( fn define_value(
&self, &self,
gc_context: MutationContext<'gc, '_>, gc_context: MutationContext<'gc, '_>,

View File

@ -155,6 +155,21 @@ impl<'gc> TObject<'gc> for XMLIDMapObject<'gc> {
.add_property_with_case(activation, gc_context, name, get, set, attributes) .add_property_with_case(activation, gc_context, name, get, set, attributes)
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.base()
.set_watcher(gc_context, name, callback, user_data);
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
self.base().remove_watcher(gc_context, name)
}
fn define_value( fn define_value(
&self, &self,
gc_context: MutationContext<'gc, '_>, gc_context: MutationContext<'gc, '_>,

View File

@ -145,6 +145,21 @@ impl<'gc> TObject<'gc> for XMLObject<'gc> {
.add_property_with_case(activation, gc_context, name, get, set, attributes) .add_property_with_case(activation, gc_context, name, get, set, attributes)
} }
fn set_watcher(
&self,
gc_context: MutationContext<'gc, '_>,
name: Cow<str>,
callback: Executable<'gc>,
user_data: Value<'gc>,
) {
self.base()
.set_watcher(gc_context, name, callback, user_data);
}
fn remove_watcher(&self, gc_context: MutationContext<'gc, '_>, name: Cow<str>) -> bool {
self.base().remove_watcher(gc_context, name)
}
fn define_value( fn define_value(
&self, &self,
gc_context: MutationContext<'gc, '_>, gc_context: MutationContext<'gc, '_>,

View File

@ -193,6 +193,8 @@ swf_tests! {
(loadvariables_method, "avm1/loadvariables_method", 3), (loadvariables_method, "avm1/loadvariables_method", 3),
(xml_load, "avm1/xml_load", 1), (xml_load, "avm1/xml_load", 1),
(with_return, "avm1/with_return", 1), (with_return, "avm1/with_return", 1),
(watch, "avm1/watch", 1),
#[ignore] (watch_virtual_property, "avm1/watch_virtual_property", 1),
(cross_movie_root, "avm1/cross_movie_root", 5), (cross_movie_root, "avm1/cross_movie_root", 5),
(roots_and_levels, "avm1/roots_and_levels", 1), (roots_and_levels, "avm1/roots_and_levels", 1),
(swf6_case_insensitive, "avm1/swf6_case_insensitive", 1), (swf6_case_insensitive, "avm1/swf6_case_insensitive", 1),

View File

@ -0,0 +1,25 @@
class LoggingWatcher {
var count = 0;
var value = true;
function LoggingWatcher() {
trace("// this.watch(\"value\", this.log)");
this.watch("value", this.log);
}
function log(property, oldValue, newValue, bounds) {
var userdata;
if (typeof bounds === "object") {
userdata = "{ ";
for (var key in bounds) {
userdata += key + "=" + bounds[key] + " ";
}
userdata += "}";
} else {
userdata = bounds;
}
this.count++;
trace("LoggingWatcher count " + this.count + ": " + property + " changed from " + oldValue + " to " + newValue + " with userdata " + userdata);
return newValue;
}
}

View File

@ -0,0 +1,118 @@
// watch()
false
// watch("variable")
false
// watch("variable", true)
false
// watch("variable", clamper, {min: 5, max: 10})
true
// variable = 5
Clamper: variable changed from undefined to 5 with userdata { min=5 max=10 }
// variable
5
// variable = 10
Clamper: variable changed from 5 to 10 with userdata { min=5 max=10 }
// variable
10
// variable = 4
Clamper: variable changed from 10 to 4 with userdata { min=5 max=10 }
// variable
5
// variable = 11
Clamper: variable changed from 5 to 11 with userdata { min=5 max=10 }
// variable
10
// variable = 6
Clamper: variable changed from 10 to 6 with userdata { min=5 max=10 }
// variable
6
// delete(variable)
// variable = 3
Clamper: variable changed from undefined to 3 with userdata { min=5 max=10 }
// variable
5
// watch("variable", clamper, {min: 15, max: 20})
true
// variable = 14
Clamper: variable changed from 5 to 14 with userdata { min=15 max=20 }
// variable
15
// variable = 21
Clamper: variable changed from 15 to 21 with userdata { min=15 max=20 }
// variable
20
// variable = 18
Clamper: variable changed from 20 to 18 with userdata { min=15 max=20 }
// variable
18
// watch("variable", exceptionalClamper, {min: 15, max: 20})
true
// variable = 14
exceptionalClamper: variable changed from 18 to 14 with userdata { min=15 max=20 }
ERROR: too low!
// variable
undefined
// variable = 21
exceptionalClamper: variable changed from undefined to 21 with userdata { min=15 max=20 }
ERROR: too high!
// variable
undefined
// variable = 18
exceptionalClamper: variable changed from undefined to 18 with userdata { min=15 max=20 }
// variable
18
// unwatch("variable")
true
// variable = 4
// variable
4
// variable = 11
// variable
11
// unwatch("variable")
false
// unwatch()
false
// variable = 6
// variable
6
// delete(variable)
// var loggingWatcher = new LoggingWatcher()
// this.watch("value", this.log)
// loggingWatcher.value = true
LoggingWatcher count 1: value changed from true to true with userdata undefined
// loggingWatcher.value = false
LoggingWatcher count 2: value changed from true to false with userdata undefined
// loggingWatcher.count
2

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,61 @@
// watch("variable", plusOne)
true
// addProperty("variable", getter, setter)
plusOne: variable changed from undefined to undefined with userdata undefined
true
// variable = 10
plusOne: variable changed from NaN to 10 with userdata undefined
setter: ignoring new value of 11
// variable
getter: returning 5
5
// variable = 4
plusOne: variable changed from 11 to 4 with userdata undefined
setter: ignoring new value of 5
// variable
getter: returning 5
5
// unwatch("variable")
false
// variable = 10
plusOne: variable changed from 5 to 10 with userdata undefined
setter: ignoring new value of 11
// variable
getter: returning 5
5
// variable = 4
plusOne: variable changed from 11 to 4 with userdata undefined
setter: ignoring new value of 5
// variable
getter: returning 5
5
// watch("variable", plusOne)
true
// variable = 10
plusOne: variable changed from 5 to 10 with userdata undefined
setter: ignoring new value of 11
// variable
getter: returning 5
5
// variable = 4
plusOne: variable changed from 11 to 4 with userdata undefined
setter: ignoring new value of 5
// variable
getter: returning 5
5
// delete(variable)
// addProperty("variable", getter, null)
plusOne: variable changed from undefined to undefined with userdata undefined
true

Binary file not shown.

Binary file not shown.