Merge pull request #69 from Dinnerbone/feature/virtual_properties
Allow for dynamic properties in Object
This commit is contained in:
commit
8a43523c27
|
@ -440,7 +440,12 @@ impl<'gc> Avm1<'gc> {
|
||||||
object.call(self, context, object.as_object()?.to_owned(), &args)?;
|
object.call(self, context, object.as_object()?.to_owned(), &args)?;
|
||||||
self.stack.push(return_value);
|
self.stack.push(return_value);
|
||||||
} else {
|
} 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 {
|
if let Value::Undefined = callable {
|
||||||
return Err(format!("Object method {} is not defined", name).into());
|
return Err(format!("Object method {} is not defined", name).into());
|
||||||
|
@ -640,13 +645,14 @@ impl<'gc> Avm1<'gc> {
|
||||||
// Flash 4-style variable
|
// Flash 4-style variable
|
||||||
let var_path = self.pop()?;
|
let var_path = self.pop()?;
|
||||||
let path = var_path.as_string()?;
|
let path = var_path.as_string()?;
|
||||||
|
let globals = self.globals;
|
||||||
|
|
||||||
// Special hardcoded variables
|
// Special hardcoded variables
|
||||||
if path == "_root" || path == "this" {
|
if path == "_root" || path == "this" {
|
||||||
self.push(context.start_clip.read().object());
|
self.push(context.start_clip.read().object());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if path == "_global" {
|
} else if path == "_global" {
|
||||||
self.push(Value::Object(self.globals));
|
self.push(Value::Object(globals));
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -659,14 +665,14 @@ impl<'gc> Avm1<'gc> {
|
||||||
if let Some(clip) = node.read().as_movie_clip() {
|
if let Some(clip) = node.read().as_movie_clip() {
|
||||||
let object = clip.object().as_object()?;
|
let object = clip.object().as_object()?;
|
||||||
if object.read().has_property(var_name) {
|
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) {
|
if result.is_none() && globals.read().has_property(path) {
|
||||||
result = Some(self.globals.read().get(path));
|
result = Some(globals.read().get(path, self, context, globals));
|
||||||
}
|
}
|
||||||
self.push(result.unwrap_or(Value::Undefined));
|
self.push(result.unwrap_or(Value::Undefined));
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1133,10 +1139,10 @@ impl<'gc> Avm1<'gc> {
|
||||||
Self::resolve_slash_path_variable(context.target_clip, context.root, var_path)
|
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() {
|
if let Some(clip) = node.write(context.gc_context).as_movie_clip_mut() {
|
||||||
clip.object()
|
let object = clip.object().as_object()?;
|
||||||
.as_object()?
|
object
|
||||||
.write(context.gc_context)
|
.write(context.gc_context)
|
||||||
.set(var_name, value);
|
.set(var_name, value, self, context, object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -52,9 +52,9 @@ pub fn random<'gc>(
|
||||||
pub fn create_globals<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> {
|
pub fn create_globals<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> {
|
||||||
let mut globals = Object::object(gc_context);
|
let mut globals = Object::object(gc_context);
|
||||||
|
|
||||||
globals.set_object("Math", math::create(gc_context));
|
globals.force_set("Math", Value::Object(math::create(gc_context)));
|
||||||
globals.set_function("getURL", getURL, gc_context);
|
globals.force_set_function("getURL", getURL, gc_context);
|
||||||
globals.set_function("random", random, gc_context);
|
globals.force_set_function("random", random, gc_context);
|
||||||
|
|
||||||
globals
|
globals
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use std::f64::NAN;
|
||||||
macro_rules! wrap_std {
|
macro_rules! wrap_std {
|
||||||
( $object: ident, $gc_context: ident, $($name:expr => $std:path),* ) => {{
|
( $object: ident, $gc_context: ident, $($name:expr => $std:path),* ) => {{
|
||||||
$(
|
$(
|
||||||
$object.set_function(
|
$object.force_set_function(
|
||||||
$name,
|
$name,
|
||||||
|_avm, _context, _this, args| -> Value<'gc> {
|
|_avm, _context, _this, args| -> Value<'gc> {
|
||||||
if let Some(input) = args.get(0) {
|
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>> {
|
pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<'gc>> {
|
||||||
let mut math = Object::object(gc_context);
|
let mut math = Object::object(gc_context);
|
||||||
|
|
||||||
math.set("E", Value::Number(std::f64::consts::E));
|
math.force_set("E", Value::Number(std::f64::consts::E));
|
||||||
math.set("LN10", Value::Number(std::f64::consts::LN_10));
|
math.force_set("LN10", Value::Number(std::f64::consts::LN_10));
|
||||||
math.set("LN2", Value::Number(std::f64::consts::LN_2));
|
math.force_set("LN2", Value::Number(std::f64::consts::LN_2));
|
||||||
math.set("LOG10E", Value::Number(std::f64::consts::LOG10_E));
|
math.force_set("LOG10E", Value::Number(std::f64::consts::LOG10_E));
|
||||||
math.set("LOG2E", Value::Number(std::f64::consts::LOG2_E));
|
math.force_set("LOG2E", Value::Number(std::f64::consts::LOG2_E));
|
||||||
math.set("PI", Value::Number(std::f64::consts::PI));
|
math.force_set("PI", Value::Number(std::f64::consts::PI));
|
||||||
math.set("SQRT1_2", Value::Number(std::f64::consts::FRAC_1_SQRT_2));
|
math.force_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("SQRT2", Value::Number(std::f64::consts::SQRT_2));
|
||||||
|
|
||||||
wrap_std!(math, gc_context,
|
wrap_std!(math, gc_context,
|
||||||
"abs" => f64::abs,
|
"abs" => f64::abs,
|
||||||
|
@ -73,8 +73,8 @@ pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<'
|
||||||
"tan" => f64::tan
|
"tan" => f64::tan
|
||||||
);
|
);
|
||||||
|
|
||||||
math.set_function("atan2", atan2, gc_context);
|
math.force_set_function("atan2", atan2, gc_context);
|
||||||
math.set_function("random", random, gc_context);
|
math.force_set_function("random", random, gc_context);
|
||||||
|
|
||||||
GcCell::allocate(gc_context, math)
|
GcCell::allocate(gc_context, math)
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ mod tests {
|
||||||
fn $test() -> Result<(), Error> {
|
fn $test() -> Result<(), Error> {
|
||||||
with_avm(19, |avm, context| {
|
with_avm(19, |avm, context| {
|
||||||
let math = create(context.gc_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);
|
assert_eq!(function.call(avm, context, math, $args)?, $out);
|
||||||
|
|
|
@ -6,7 +6,7 @@ use gc_arena::MutationContext;
|
||||||
macro_rules! with_movie_clip {
|
macro_rules! with_movie_clip {
|
||||||
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
|
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
|
||||||
$(
|
$(
|
||||||
$object.set_function(
|
$object.force_set_function(
|
||||||
$name,
|
$name,
|
||||||
|_avm, _context, this, args| -> Value<'gc> {
|
|_avm, _context, this, args| -> Value<'gc> {
|
||||||
if let Some(display_object) = this.read().display_node() {
|
if let Some(display_object) = this.read().display_node() {
|
||||||
|
@ -25,7 +25,7 @@ macro_rules! with_movie_clip {
|
||||||
macro_rules! with_movie_clip_mut {
|
macro_rules! with_movie_clip_mut {
|
||||||
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
|
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
|
||||||
$(
|
$(
|
||||||
$object.set_function(
|
$object.force_set_function(
|
||||||
$name,
|
$name,
|
||||||
|_avm, context, this, args| -> Value<'gc> {
|
|_avm, context, this, args| -> Value<'gc> {
|
||||||
if let Some(display_object) = this.read().display_node() {
|
if let Some(display_object) = this.read().display_node() {
|
||||||
|
|
|
@ -2,7 +2,9 @@ use crate::avm1::{ActionContext, Avm1, Value};
|
||||||
use crate::display_object::DisplayNode;
|
use crate::display_object::DisplayNode;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use gc_arena::{GcCell, MutationContext};
|
use gc_arena::{GcCell, MutationContext};
|
||||||
|
use std::collections::hash_map::Entry;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::mem::replace;
|
||||||
|
|
||||||
pub type NativeFunction<'gc> = fn(
|
pub type NativeFunction<'gc> = fn(
|
||||||
&mut Avm1<'gc>,
|
&mut Avm1<'gc>,
|
||||||
|
@ -24,10 +26,84 @@ fn default_to_string<'gc>(
|
||||||
Value::String("[Object object]".to_string())
|
Value::String("[Object object]".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Property<'gc> {
|
||||||
|
Virtual {
|
||||||
|
get: NativeFunction<'gc>,
|
||||||
|
set: Option<NativeFunction<'gc>>,
|
||||||
|
},
|
||||||
|
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<'gc>>(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<'_> {
|
||||||
|
#[allow(clippy::unneeded_field_pattern)]
|
||||||
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct Object<'gc> {
|
pub struct Object<'gc> {
|
||||||
display_node: Option<DisplayNode<'gc>>,
|
display_node: Option<DisplayNode<'gc>>,
|
||||||
values: HashMap<String, Value<'gc>>,
|
values: HashMap<String, Property<'gc>>,
|
||||||
function: Option<NativeFunction<'gc>>,
|
function: Option<NativeFunction<'gc>>,
|
||||||
type_of: &'static str,
|
type_of: &'static str,
|
||||||
}
|
}
|
||||||
|
@ -58,7 +134,7 @@ impl<'gc> Object<'gc> {
|
||||||
function: None,
|
function: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
result.set_function("toString", default_to_string, gc_context);
|
result.force_set_function("toString", default_to_string, gc_context);
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
@ -80,29 +156,80 @@ impl<'gc> Object<'gc> {
|
||||||
self.display_node
|
self.display_node
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&mut self, name: &str, value: Value<'gc>) {
|
pub fn set(
|
||||||
self.values.insert(name.to_owned(), value);
|
&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>>) {
|
pub fn force_set_virtual(
|
||||||
self.values.insert(name.to_owned(), Value::Object(object));
|
&mut self,
|
||||||
|
name: &str,
|
||||||
|
get: NativeFunction<'gc>,
|
||||||
|
set: Option<NativeFunction<'gc>>,
|
||||||
|
) {
|
||||||
|
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(
|
pub fn set_function(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
function: NativeFunction<'gc>,
|
function: NativeFunction<'gc>,
|
||||||
gc_context: MutationContext<'gc, '_>,
|
avm: &mut Avm1<'gc>,
|
||||||
|
context: &mut ActionContext<'_, 'gc, '_>,
|
||||||
|
this: GcCell<'gc, Object<'gc>>,
|
||||||
) {
|
) {
|
||||||
self.set(
|
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,
|
name,
|
||||||
Value::Object(GcCell::allocate(gc_context, Object::function(function))),
|
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) {
|
if let Some(value) = self.values.get(name) {
|
||||||
return value.to_owned();
|
return value.get(avm, context, this);
|
||||||
}
|
}
|
||||||
Value::Undefined
|
Value::Undefined
|
||||||
}
|
}
|
||||||
|
@ -145,3 +272,109 @@ impl<'gc> Object<'gc> {
|
||||||
self.type_of
|
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<F, R>(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<dyn DisplayObject> = 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())
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue