commit
75fd2b6a52
185
core/src/avm1.rs
185
core/src/avm1.rs
|
@ -1,6 +1,5 @@
|
|||
use crate::avm1::function::Avm1Function;
|
||||
use crate::avm1::globals::create_globals;
|
||||
use crate::avm1::object::Object;
|
||||
use crate::backend::navigator::NavigationMethod;
|
||||
use crate::context::UpdateContext;
|
||||
use crate::prelude::*;
|
||||
|
@ -17,11 +16,12 @@ use crate::tag_utils::SwfSlice;
|
|||
mod activation;
|
||||
mod fscommand;
|
||||
mod function;
|
||||
mod globals;
|
||||
pub mod movie_clip;
|
||||
pub mod globals;
|
||||
pub mod object;
|
||||
mod property;
|
||||
mod return_value;
|
||||
mod scope;
|
||||
pub mod script_object;
|
||||
mod value;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -31,7 +31,10 @@ mod test_utils;
|
|||
mod tests;
|
||||
|
||||
use activation::Activation;
|
||||
pub use globals::SystemPrototypes;
|
||||
pub use object::{Object, ObjectCell};
|
||||
use scope::Scope;
|
||||
pub use script_object::ScriptObject;
|
||||
pub use value::Value;
|
||||
|
||||
pub struct Avm1<'gc> {
|
||||
|
@ -42,7 +45,10 @@ pub struct Avm1<'gc> {
|
|||
constant_pool: Vec<String>,
|
||||
|
||||
/// The global object.
|
||||
globals: GcCell<'gc, Object<'gc>>,
|
||||
globals: ObjectCell<'gc>,
|
||||
|
||||
/// System builtins that we use internally to construct new objects.
|
||||
prototypes: globals::SystemPrototypes<'gc>,
|
||||
|
||||
/// All activation records for the current execution context.
|
||||
stack_frames: Vec<GcCell<'gc, Activation<'gc>>>,
|
||||
|
@ -59,8 +65,13 @@ unsafe impl<'gc> gc_arena::Collect for Avm1<'gc> {
|
|||
#[inline]
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
self.globals.trace(cc);
|
||||
self.prototypes.trace(cc);
|
||||
self.stack_frames.trace(cc);
|
||||
self.stack.trace(cc);
|
||||
|
||||
for register in &self.registers {
|
||||
register.trace(cc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,10 +79,13 @@ type Error = Box<dyn std::error::Error>;
|
|||
|
||||
impl<'gc> Avm1<'gc> {
|
||||
pub fn new(gc_context: MutationContext<'gc, '_>, player_version: u8) -> Self {
|
||||
let (prototypes, globals) = create_globals(gc_context);
|
||||
|
||||
Self {
|
||||
player_version,
|
||||
constant_pool: vec![],
|
||||
globals: GcCell::allocate(gc_context, create_globals(gc_context)),
|
||||
globals: GcCell::allocate(gc_context, globals),
|
||||
prototypes,
|
||||
stack_frames: vec![],
|
||||
stack: vec![],
|
||||
registers: [
|
||||
|
@ -390,6 +404,7 @@ impl<'gc> Avm1<'gc> {
|
|||
Action::Increment => self.action_increment(context),
|
||||
Action::InitArray => self.action_init_array(context),
|
||||
Action::InitObject => self.action_init_object(context),
|
||||
Action::InstanceOf => self.action_instance_of(context),
|
||||
Action::Jump { offset } => self.action_jump(context, offset, reader),
|
||||
Action::Less => self.action_less(context),
|
||||
Action::Less2 => self.action_less_2(context),
|
||||
|
@ -799,7 +814,19 @@ impl<'gc> Avm1<'gc> {
|
|||
context.gc_context,
|
||||
);
|
||||
let func = Avm1Function::from_df1(swf_version, func_data, name, params, scope);
|
||||
let func_obj = GcCell::allocate(context.gc_context, Object::action_function(func));
|
||||
let prototype = GcCell::allocate(
|
||||
context.gc_context,
|
||||
Box::new(ScriptObject::object(
|
||||
context.gc_context,
|
||||
Some(self.prototypes.object),
|
||||
)) as Box<dyn Object<'gc>>,
|
||||
);
|
||||
let func_obj = ScriptObject::function(
|
||||
context.gc_context,
|
||||
func,
|
||||
Some(self.prototypes.function),
|
||||
Some(prototype),
|
||||
);
|
||||
if name == "" {
|
||||
self.push(func_obj);
|
||||
} else {
|
||||
|
@ -830,7 +857,19 @@ impl<'gc> Avm1<'gc> {
|
|||
context.gc_context,
|
||||
);
|
||||
let func = Avm1Function::from_df2(swf_version, func_data, action_func, scope);
|
||||
let func_obj = GcCell::allocate(context.gc_context, Object::action_function(func));
|
||||
let prototype = GcCell::allocate(
|
||||
context.gc_context,
|
||||
Box::new(ScriptObject::object(
|
||||
context.gc_context,
|
||||
Some(self.prototypes.object),
|
||||
)) as Box<dyn Object<'gc>>,
|
||||
);
|
||||
let func_obj = ScriptObject::function(
|
||||
context.gc_context,
|
||||
func,
|
||||
Some(self.prototypes.function),
|
||||
Some(prototype),
|
||||
);
|
||||
if action_func.name == "" {
|
||||
self.push(func_obj);
|
||||
} else {
|
||||
|
@ -1062,10 +1101,15 @@ impl<'gc> Avm1<'gc> {
|
|||
}
|
||||
|
||||
/// Obtain a reference to `_global`.
|
||||
pub fn global_object_cell(&self) -> GcCell<'gc, Object<'gc>> {
|
||||
pub fn global_object_cell(&self) -> ObjectCell<'gc> {
|
||||
self.globals
|
||||
}
|
||||
|
||||
/// Obtain system built-in prototypes for this instance.
|
||||
pub fn prototypes(&self) -> &globals::SystemPrototypes<'gc> {
|
||||
&self.prototypes
|
||||
}
|
||||
|
||||
fn action_get_variable(
|
||||
&mut self,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
|
@ -1275,15 +1319,54 @@ impl<'gc> Avm1<'gc> {
|
|||
Err("Unimplemented action: InitArray".into())
|
||||
}
|
||||
|
||||
fn action_init_object(&mut self, _context: &mut UpdateContext) -> Result<(), Error> {
|
||||
fn action_init_object(
|
||||
&mut self,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
) -> Result<(), Error> {
|
||||
let num_props = self.pop()?.as_i64()?;
|
||||
let mut object = ScriptObject::object(context.gc_context, Some(self.prototypes.object));
|
||||
for _ in 0..num_props {
|
||||
let _value = self.pop()?;
|
||||
let _name = self.pop()?;
|
||||
let value = self.pop()?;
|
||||
let name = self.pop()?.into_string();
|
||||
let this = self.current_stack_frame().unwrap().read().this_cell();
|
||||
|
||||
object.set(&name, value, self, context, this)?;
|
||||
}
|
||||
|
||||
// TODO(Herschel)
|
||||
Err("Unimplemented action: InitObject".into())
|
||||
self.push(Value::Object(GcCell::allocate(
|
||||
context.gc_context,
|
||||
Box::new(object),
|
||||
)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_instance_of(
|
||||
&mut self,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
) -> Result<(), Error> {
|
||||
let constr = self.pop()?.as_object()?;
|
||||
let obj = self.pop()?.as_object()?;
|
||||
|
||||
//TODO: Interface detection on SWF7
|
||||
let prototype = constr
|
||||
.read()
|
||||
.get("prototype", self, context, constr)?
|
||||
.resolve(self, context)?
|
||||
.as_object()?;
|
||||
let mut proto = obj.read().proto();
|
||||
|
||||
while let Some(this_proto) = proto {
|
||||
if GcCell::ptr_eq(this_proto, prototype) {
|
||||
self.push(true);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
proto = this_proto.read().proto();
|
||||
}
|
||||
|
||||
self.push(false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_jump(
|
||||
|
@ -1400,24 +1483,72 @@ impl<'gc> Avm1<'gc> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn action_new_method(&mut self, _context: &mut UpdateContext) -> Result<(), Error> {
|
||||
let _name = self.pop()?.as_string()?;
|
||||
let _object = self.pop()?.as_object()?;
|
||||
let _num_args = self.pop()?.as_i64()?;
|
||||
self.push(Value::Undefined);
|
||||
// TODO(Herschel)
|
||||
Err("Unimplemented action: NewMethod".into())
|
||||
fn action_new_method(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
let method_name = self.pop()?;
|
||||
let object = self.pop()?.as_object()?;
|
||||
let num_args = self.pop()?.as_i64()?;
|
||||
let mut args = Vec::new();
|
||||
for _ in 0..num_args {
|
||||
args.push(self.pop()?);
|
||||
}
|
||||
|
||||
let constructor = object
|
||||
.read()
|
||||
.get(&method_name.as_string()?, self, context, object.to_owned())?
|
||||
.resolve(self, context)?
|
||||
.as_object()?;
|
||||
let prototype = constructor
|
||||
.read()
|
||||
.get("prototype", self, context, constructor)?
|
||||
.resolve(self, context)?
|
||||
.as_object()?;
|
||||
|
||||
let this = prototype.read().new(self, context, prototype, &args)?;
|
||||
|
||||
//TODO: What happens if you `ActionNewMethod` without a method name?
|
||||
constructor
|
||||
.read()
|
||||
.call(self, context, this, &args)?
|
||||
.resolve(self, context)?;
|
||||
|
||||
self.push(this);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_new_object(&mut self, _context: &mut UpdateContext) -> Result<(), Error> {
|
||||
let _object = self.pop()?.as_string()?;
|
||||
fn action_new_object(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
let fn_name = self.pop()?;
|
||||
let num_args = self.pop()?.as_i64()?;
|
||||
let mut args = Vec::new();
|
||||
for _ in 0..num_args {
|
||||
let _arg = self.pop()?;
|
||||
args.push(self.pop()?);
|
||||
}
|
||||
self.push(Value::Undefined);
|
||||
// TODO(Herschel)
|
||||
Err("Unimplemented action: NewObject".into())
|
||||
|
||||
let constructor = self
|
||||
.stack_frames
|
||||
.last()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.read()
|
||||
.resolve(fn_name.as_string()?, self, context)?
|
||||
.resolve(self, context)?
|
||||
.as_object()?;
|
||||
let prototype = constructor
|
||||
.read()
|
||||
.get("prototype", self, context, constructor)?
|
||||
.resolve(self, context)?
|
||||
.as_object()?;
|
||||
|
||||
let this = prototype.read().new(self, context, prototype, &args)?;
|
||||
|
||||
constructor
|
||||
.read()
|
||||
.call(self, context, this, &args)?
|
||||
.resolve(self, context)?;
|
||||
|
||||
self.push(this);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_or(&mut self, _context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
|
@ -1528,7 +1659,7 @@ impl<'gc> Avm1<'gc> {
|
|||
fn action_set_member(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
let value = self.pop()?;
|
||||
let name_val = self.pop()?;
|
||||
let name = name_val.as_string()?;
|
||||
let name = name_val.into_string();
|
||||
let object = self.pop()?.as_object()?;
|
||||
let this = self.current_stack_frame().unwrap().read().this_cell();
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
//! Activation records
|
||||
|
||||
use crate::avm1::object::Object;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::scope::Scope;
|
||||
use crate::avm1::{Avm1, Error, Value};
|
||||
use crate::avm1::{Avm1, Error, ObjectCell, Value};
|
||||
use crate::context::UpdateContext;
|
||||
use crate::tag_utils::SwfSlice;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
|
@ -68,10 +67,10 @@ pub struct Activation<'gc> {
|
|||
scope: GcCell<'gc, Scope<'gc>>,
|
||||
|
||||
/// The immutable value of `this`.
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
this: ObjectCell<'gc>,
|
||||
|
||||
/// The arguments this function was called by.
|
||||
arguments: Option<GcCell<'gc, Object<'gc>>>,
|
||||
arguments: Option<ObjectCell<'gc>>,
|
||||
|
||||
/// The return value of the activation.
|
||||
return_value: Option<Value<'gc>>,
|
||||
|
@ -115,8 +114,8 @@ impl<'gc> Activation<'gc> {
|
|||
swf_version: u8,
|
||||
code: SwfSlice,
|
||||
scope: GcCell<'gc, Scope<'gc>>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
arguments: Option<GcCell<'gc, Object<'gc>>>,
|
||||
this: ObjectCell<'gc>,
|
||||
arguments: Option<ObjectCell<'gc>>,
|
||||
) -> Activation<'gc> {
|
||||
Activation {
|
||||
swf_version,
|
||||
|
@ -136,8 +135,8 @@ impl<'gc> Activation<'gc> {
|
|||
swf_version: u8,
|
||||
code: SwfSlice,
|
||||
scope: GcCell<'gc, Scope<'gc>>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
arguments: Option<GcCell<'gc, Object<'gc>>>,
|
||||
this: ObjectCell<'gc>,
|
||||
arguments: Option<ObjectCell<'gc>>,
|
||||
) -> Activation<'gc> {
|
||||
Activation {
|
||||
swf_version,
|
||||
|
@ -161,7 +160,7 @@ impl<'gc> Activation<'gc> {
|
|||
/// it.
|
||||
pub fn from_nothing(
|
||||
swf_version: u8,
|
||||
globals: GcCell<'gc, Object<'gc>>,
|
||||
globals: ObjectCell<'gc>,
|
||||
mc: MutationContext<'gc, '_>,
|
||||
) -> Activation<'gc> {
|
||||
let global_scope = GcCell::allocate(mc, Scope::from_global_object(globals));
|
||||
|
@ -297,7 +296,7 @@ impl<'gc> Activation<'gc> {
|
|||
}
|
||||
|
||||
/// Returns value of `this` as a reference.
|
||||
pub fn this_cell(&self) -> GcCell<'gc, Object<'gc>> {
|
||||
pub fn this_cell(&self) -> ObjectCell<'gc> {
|
||||
self.this
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
//! Code relating to executable functions + calling conventions.
|
||||
|
||||
use crate::avm1::activation::Activation;
|
||||
use crate::avm1::object::{Attribute::*, Object};
|
||||
use crate::avm1::property::Attribute::*;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::scope::Scope;
|
||||
use crate::avm1::value::Value;
|
||||
use crate::avm1::{Avm1, Error, UpdateContext};
|
||||
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext};
|
||||
use crate::tag_utils::SwfSlice;
|
||||
use gc_arena::GcCell;
|
||||
use gc_arena::{Collect, CollectionContext, GcCell};
|
||||
use swf::avm1::types::FunctionParam;
|
||||
|
||||
/// Represents a function defined in Ruffle's code.
|
||||
|
@ -27,13 +27,14 @@ use swf::avm1::types::FunctionParam;
|
|||
pub type NativeFunction<'gc> = fn(
|
||||
&mut Avm1<'gc>,
|
||||
&mut UpdateContext<'_, 'gc, '_>,
|
||||
GcCell<'gc, Object<'gc>>,
|
||||
ObjectCell<'gc>,
|
||||
&[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error>;
|
||||
|
||||
/// Represents a function defined in the AVM1 runtime, either through
|
||||
/// `DefineFunction` or `DefineFunction2`.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Collect)]
|
||||
#[collect(no_drop)]
|
||||
pub struct Avm1Function<'gc> {
|
||||
/// The file format version of the SWF that generated this function.
|
||||
swf_version: u8,
|
||||
|
@ -171,6 +172,15 @@ pub enum Executable<'gc> {
|
|||
Action(Avm1Function<'gc>),
|
||||
}
|
||||
|
||||
unsafe impl<'gc> Collect for Executable<'gc> {
|
||||
fn trace(&self, cc: CollectionContext) {
|
||||
match self {
|
||||
Self::Native(_) => {}
|
||||
Self::Action(af) => af.trace(cc),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> Executable<'gc> {
|
||||
/// Execute the given code.
|
||||
///
|
||||
|
@ -182,7 +192,7 @@ impl<'gc> Executable<'gc> {
|
|||
&self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
ac: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
match self {
|
||||
|
@ -192,29 +202,29 @@ impl<'gc> Executable<'gc> {
|
|||
ac.gc_context,
|
||||
Scope::new_local_scope(af.scope(), ac.gc_context),
|
||||
);
|
||||
let mut arguments = Object::object(ac.gc_context);
|
||||
let mut arguments =
|
||||
ScriptObject::object(ac.gc_context, Some(avm.prototypes().object));
|
||||
if !af.suppress_arguments {
|
||||
for i in 0..args.len() {
|
||||
arguments.force_set(
|
||||
arguments.define_value(
|
||||
&format!("{}", i),
|
||||
args.get(i).unwrap().clone(),
|
||||
DontDelete,
|
||||
DontDelete.into(),
|
||||
)
|
||||
}
|
||||
|
||||
arguments.force_set(
|
||||
"length",
|
||||
Value::Number(args.len() as f64),
|
||||
DontDelete | DontEnum,
|
||||
);
|
||||
arguments.define_value("length", args.len().into(), DontDelete | DontEnum);
|
||||
}
|
||||
|
||||
let argcell = GcCell::allocate(ac.gc_context, arguments);
|
||||
let argcell = GcCell::allocate(
|
||||
ac.gc_context,
|
||||
Box::new(arguments) as Box<dyn Object<'gc> + 'gc>,
|
||||
);
|
||||
let effective_ver = if avm.current_swf_version() > 5 {
|
||||
af.swf_version()
|
||||
} else {
|
||||
this.read()
|
||||
.display_node()
|
||||
.as_display_node()
|
||||
.map(|dn| dn.read().swf_version())
|
||||
.unwrap_or(ac.player_version)
|
||||
};
|
||||
|
@ -301,3 +311,15 @@ impl<'gc> Executable<'gc> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> From<NativeFunction<'gc>> for Executable<'gc> {
|
||||
fn from(nf: NativeFunction<'gc>) -> Self {
|
||||
Executable::Native(nf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> From<Avm1Function<'gc>> for Executable<'gc> {
|
||||
fn from(af: Avm1Function<'gc>) -> Self {
|
||||
Executable::Action(af)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
use crate::avm1::fscommand;
|
||||
use crate::avm1::function::Executable;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, Object, UpdateContext, Value};
|
||||
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value};
|
||||
use crate::backend::navigator::NavigationMethod;
|
||||
use enumset::EnumSet;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
use gc_arena::MutationContext;
|
||||
use rand::Rng;
|
||||
|
||||
mod function;
|
||||
mod math;
|
||||
mod movie_clip;
|
||||
mod object;
|
||||
|
||||
#[allow(non_snake_case, unused_must_use)] //can't use errors yet
|
||||
pub fn getURL<'a, 'gc>(
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'a, 'gc, '_>,
|
||||
_this: GcCell<'gc, Object<'gc>>,
|
||||
_this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
//TODO: Error behavior if no arguments are present
|
||||
|
@ -40,7 +44,7 @@ pub fn getURL<'a, 'gc>(
|
|||
pub fn random<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: GcCell<'gc, Object<'gc>>,
|
||||
_this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
match args.get(0) {
|
||||
|
@ -52,7 +56,7 @@ pub fn random<'gc>(
|
|||
pub fn boolean<'gc>(
|
||||
avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: GcCell<'gc, Object<'gc>>,
|
||||
_this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(val) = args.get(0) {
|
||||
|
@ -65,7 +69,7 @@ pub fn boolean<'gc>(
|
|||
pub fn number<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: GcCell<'gc, Object<'gc>>,
|
||||
_this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(val) = args.get(0) {
|
||||
|
@ -78,7 +82,7 @@ pub fn number<'gc>(
|
|||
pub fn is_nan<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: GcCell<'gc, Object<'gc>>,
|
||||
_this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(val) = args.get(0) {
|
||||
|
@ -88,24 +92,121 @@ pub fn is_nan<'gc>(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn create_globals<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> {
|
||||
let mut globals = Object::object(gc_context);
|
||||
/// This structure represents all system builtins that are used regardless of
|
||||
/// whatever the hell happens to `_global`. These are, of course,
|
||||
/// user-modifiable.
|
||||
#[derive(Clone)]
|
||||
pub struct SystemPrototypes<'gc> {
|
||||
pub object: ObjectCell<'gc>,
|
||||
pub function: ObjectCell<'gc>,
|
||||
pub movie_clip: ObjectCell<'gc>,
|
||||
}
|
||||
|
||||
globals.force_set_function("isNaN", is_nan, gc_context, EnumSet::empty());
|
||||
globals.force_set_function("Boolean", boolean, gc_context, EnumSet::empty());
|
||||
globals.force_set("Math", math::create(gc_context), EnumSet::empty());
|
||||
globals.force_set_function("getURL", getURL, gc_context, EnumSet::empty());
|
||||
globals.force_set_function("Number", number, gc_context, EnumSet::empty());
|
||||
globals.force_set_function("random", random, gc_context, EnumSet::empty());
|
||||
unsafe impl<'gc> gc_arena::Collect for SystemPrototypes<'gc> {
|
||||
#[inline]
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
self.object.trace(cc);
|
||||
self.function.trace(cc);
|
||||
self.movie_clip.trace(cc);
|
||||
}
|
||||
}
|
||||
|
||||
globals.force_set("NaN", Value::Number(std::f64::NAN), EnumSet::empty());
|
||||
globals.force_set(
|
||||
/// Initialize default global scope and builtins for an AVM1 instance.
|
||||
pub fn create_globals<'gc>(
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
) -> (SystemPrototypes<'gc>, Box<dyn Object<'gc> + 'gc>) {
|
||||
let object_proto = ScriptObject::object_cell(gc_context, None);
|
||||
let function_proto = function::create_proto(gc_context, object_proto);
|
||||
|
||||
object::fill_proto(gc_context, object_proto, function_proto);
|
||||
|
||||
let movie_clip_proto: ObjectCell<'gc> =
|
||||
movie_clip::create_proto(gc_context, object_proto, function_proto);
|
||||
|
||||
//TODO: These need to be constructors and should also set `.prototype` on each one
|
||||
let object = ScriptObject::function(
|
||||
gc_context,
|
||||
Executable::Native(object::constructor),
|
||||
Some(function_proto),
|
||||
Some(object_proto),
|
||||
);
|
||||
|
||||
let function = ScriptObject::function(
|
||||
gc_context,
|
||||
Executable::Native(function::constructor),
|
||||
Some(function_proto),
|
||||
Some(function_proto),
|
||||
);
|
||||
let movie_clip = ScriptObject::function(
|
||||
gc_context,
|
||||
Executable::Native(movie_clip::constructor),
|
||||
Some(function_proto),
|
||||
Some(movie_clip_proto),
|
||||
);
|
||||
|
||||
let mut globals = ScriptObject::object(gc_context, Some(object_proto));
|
||||
globals.define_value("Object", object.into(), EnumSet::empty());
|
||||
globals.define_value("Function", function.into(), EnumSet::empty());
|
||||
globals.define_value("MovieClip", movie_clip.into(), EnumSet::empty());
|
||||
globals.force_set_function(
|
||||
"Number",
|
||||
number,
|
||||
gc_context,
|
||||
EnumSet::empty(),
|
||||
Some(function_proto),
|
||||
);
|
||||
globals.force_set_function(
|
||||
"Boolean",
|
||||
boolean,
|
||||
gc_context,
|
||||
EnumSet::empty(),
|
||||
Some(function_proto),
|
||||
);
|
||||
globals.define_value(
|
||||
"Math",
|
||||
Value::Object(math::create(
|
||||
gc_context,
|
||||
Some(object_proto),
|
||||
Some(function_proto),
|
||||
)),
|
||||
EnumSet::empty(),
|
||||
);
|
||||
globals.force_set_function(
|
||||
"isNaN",
|
||||
is_nan,
|
||||
gc_context,
|
||||
EnumSet::empty(),
|
||||
Some(function_proto),
|
||||
);
|
||||
globals.force_set_function(
|
||||
"getURL",
|
||||
getURL,
|
||||
gc_context,
|
||||
EnumSet::empty(),
|
||||
Some(function_proto),
|
||||
);
|
||||
globals.force_set_function(
|
||||
"random",
|
||||
random,
|
||||
gc_context,
|
||||
EnumSet::empty(),
|
||||
Some(function_proto),
|
||||
);
|
||||
globals.define_value("NaN", Value::Number(std::f64::NAN), EnumSet::empty());
|
||||
globals.define_value(
|
||||
"Infinity",
|
||||
Value::Number(std::f64::INFINITY),
|
||||
EnumSet::empty(),
|
||||
);
|
||||
|
||||
globals
|
||||
(
|
||||
SystemPrototypes {
|
||||
object: object_proto,
|
||||
function: function_proto,
|
||||
movie_clip: movie_clip_proto,
|
||||
},
|
||||
Box::new(globals),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
//! Function prototype
|
||||
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, ObjectCell, ScriptObject, UpdateContext, Value};
|
||||
use enumset::EnumSet;
|
||||
use gc_arena::MutationContext;
|
||||
|
||||
/// Implements `Function`
|
||||
pub fn constructor<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: ObjectCell<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
pub fn call<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: ObjectCell<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
pub fn apply<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: ObjectCell<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
/// Partially construct `Function.prototype`.
|
||||
///
|
||||
/// `__proto__` and other cross-linked properties of this object will *not*
|
||||
/// be defined here. The caller of this function is responsible for linking
|
||||
/// them in order to obtain a valid ECMAScript `Function` prototype. The
|
||||
/// returned object is also a bare object, which will need to be linked into
|
||||
/// the prototype of `Object`.
|
||||
pub fn create_proto<'gc>(
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
proto: ObjectCell<'gc>,
|
||||
) -> ObjectCell<'gc> {
|
||||
let function_proto = ScriptObject::object_cell(gc_context, Some(proto));
|
||||
|
||||
function_proto
|
||||
.write(gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.force_set_function(
|
||||
"call",
|
||||
call,
|
||||
gc_context,
|
||||
EnumSet::empty(),
|
||||
Some(function_proto),
|
||||
);
|
||||
function_proto
|
||||
.write(gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.force_set_function(
|
||||
"apply",
|
||||
apply,
|
||||
gc_context,
|
||||
EnumSet::empty(),
|
||||
Some(function_proto),
|
||||
);
|
||||
|
||||
function_proto
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
use crate::avm1::object::Attribute::*;
|
||||
use crate::avm1::property::Attribute::*;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, Object, UpdateContext, Value};
|
||||
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value};
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
use rand::Rng;
|
||||
use std::f64::NAN;
|
||||
|
||||
macro_rules! wrap_std {
|
||||
( $object: ident, $gc_context: ident, $($name:expr => $std:path),* ) => {{
|
||||
( $object: ident, $gc_context: ident, $proto: ident, $($name:expr => $std:path),* ) => {{
|
||||
$(
|
||||
$object.force_set_function(
|
||||
$name,
|
||||
|
@ -19,6 +19,7 @@ macro_rules! wrap_std {
|
|||
},
|
||||
$gc_context,
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
$proto
|
||||
);
|
||||
)*
|
||||
}};
|
||||
|
@ -27,7 +28,7 @@ macro_rules! wrap_std {
|
|||
fn atan2<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: GcCell<'gc, Object<'gc>>,
|
||||
_this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(y) = args.get(0) {
|
||||
|
@ -43,57 +44,61 @@ fn atan2<'gc>(
|
|||
pub fn random<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: GcCell<'gc, Object<'gc>>,
|
||||
_this: ObjectCell<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(action_context.rng.gen_range(0.0f64, 1.0f64).into())
|
||||
}
|
||||
|
||||
pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<'gc>> {
|
||||
let mut math = Object::object(gc_context);
|
||||
pub fn create<'gc>(
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
proto: Option<ObjectCell<'gc>>,
|
||||
fn_proto: Option<ObjectCell<'gc>>,
|
||||
) -> ObjectCell<'gc> {
|
||||
let mut math = ScriptObject::object(gc_context, proto);
|
||||
|
||||
math.force_set(
|
||||
math.define_value(
|
||||
"E",
|
||||
Value::Number(std::f64::consts::E),
|
||||
std::f64::consts::E.into(),
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
);
|
||||
math.force_set(
|
||||
math.define_value(
|
||||
"LN10",
|
||||
Value::Number(std::f64::consts::LN_10),
|
||||
std::f64::consts::LN_10.into(),
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
);
|
||||
math.force_set(
|
||||
math.define_value(
|
||||
"LN2",
|
||||
Value::Number(std::f64::consts::LN_2),
|
||||
std::f64::consts::LN_2.into(),
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
);
|
||||
math.force_set(
|
||||
math.define_value(
|
||||
"LOG10E",
|
||||
Value::Number(std::f64::consts::LOG10_E),
|
||||
std::f64::consts::LOG10_E.into(),
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
);
|
||||
math.force_set(
|
||||
math.define_value(
|
||||
"LOG2E",
|
||||
Value::Number(std::f64::consts::LOG2_E),
|
||||
std::f64::consts::LOG2_E.into(),
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
);
|
||||
math.force_set(
|
||||
math.define_value(
|
||||
"PI",
|
||||
Value::Number(std::f64::consts::PI),
|
||||
std::f64::consts::PI.into(),
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
);
|
||||
math.force_set(
|
||||
math.define_value(
|
||||
"SQRT1_2",
|
||||
Value::Number(std::f64::consts::FRAC_1_SQRT_2),
|
||||
std::f64::consts::FRAC_1_SQRT_2.into(),
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
);
|
||||
math.force_set(
|
||||
math.define_value(
|
||||
"SQRT2",
|
||||
Value::Number(std::f64::consts::SQRT_2),
|
||||
std::f64::consts::SQRT_2.into(),
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
);
|
||||
|
||||
wrap_std!(math, gc_context,
|
||||
wrap_std!(math, gc_context, fn_proto,
|
||||
"abs" => f64::abs,
|
||||
"acos" => f64::acos,
|
||||
"asin" => f64::asin,
|
||||
|
@ -108,15 +113,22 @@ pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<'
|
|||
"tan" => f64::tan
|
||||
);
|
||||
|
||||
math.force_set_function("atan2", atan2, gc_context, DontDelete | ReadOnly | DontEnum);
|
||||
math.force_set_function(
|
||||
"atan2",
|
||||
atan2,
|
||||
gc_context,
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
fn_proto,
|
||||
);
|
||||
math.force_set_function(
|
||||
"random",
|
||||
random,
|
||||
gc_context,
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
fn_proto,
|
||||
);
|
||||
|
||||
GcCell::allocate(gc_context, math)
|
||||
GcCell::allocate(gc_context, Box::new(math))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -130,7 +142,7 @@ mod tests {
|
|||
#[test]
|
||||
fn $test() -> Result<(), Error> {
|
||||
with_avm(19, |avm, context, _root| {
|
||||
let math = create(context.gc_context);
|
||||
let math = create(context.gc_context, Some(avm.prototypes().object), Some(avm.prototypes().function));
|
||||
let function = math.read().get($name, avm, context, math)?.unwrap_immediate();
|
||||
|
||||
$(
|
||||
|
@ -236,7 +248,15 @@ mod tests {
|
|||
#[test]
|
||||
fn test_atan2_nan() {
|
||||
with_avm(19, |avm, context, _root| {
|
||||
let math = GcCell::allocate(context.gc_context, create(context.gc_context));
|
||||
let math = GcCell::allocate(
|
||||
context.gc_context,
|
||||
create(
|
||||
context.gc_context,
|
||||
Some(avm.prototypes().object),
|
||||
Some(avm.prototypes().function),
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(atan2(avm, context, *math.read(), &[]).unwrap(), NAN.into());
|
||||
assert_eq!(
|
||||
atan2(avm, context, *math.read(), &[1.0.into(), Value::Null]).unwrap(),
|
||||
|
@ -256,7 +276,15 @@ mod tests {
|
|||
#[test]
|
||||
fn test_atan2_valid() {
|
||||
with_avm(19, |avm, context, _root| {
|
||||
let math = GcCell::allocate(context.gc_context, create(context.gc_context));
|
||||
let math = GcCell::allocate(
|
||||
context.gc_context,
|
||||
create(
|
||||
context.gc_context,
|
||||
Some(avm.prototypes().object),
|
||||
Some(avm.prototypes().function),
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
atan2(avm, context, *math.read(), &[10.0.into()]).unwrap(),
|
||||
std::f64::consts::FRAC_PI_2.into()
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
//! MovieClip prototype
|
||||
|
||||
use crate::avm1::function::Executable;
|
||||
use crate::avm1::object::{Attribute::*, Object};
|
||||
use crate::avm1::property::Attribute::*;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, UpdateContext, Value};
|
||||
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value};
|
||||
use crate::display_object::{DisplayNode, DisplayObject, MovieClip};
|
||||
use enumset::EnumSet;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
|
||||
/// Implements `MovieClip`
|
||||
pub fn constructor<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: ObjectCell<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
macro_rules! with_movie_clip {
|
||||
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
|
||||
( $gc_context: ident, $object:ident, $fn_proto: expr, $($name:expr => $fn:expr),* ) => {{
|
||||
$(
|
||||
$object.force_set_function(
|
||||
$name,
|
||||
|_avm, _context, this, args| -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(display_object) = this.read().display_node() {
|
||||
if let Some(display_object) = this.read().as_display_node() {
|
||||
if let Some(movie_clip) = display_object.read().as_movie_clip() {
|
||||
return Ok($fn(movie_clip, args));
|
||||
}
|
||||
|
@ -21,18 +33,19 @@ macro_rules! with_movie_clip {
|
|||
},
|
||||
$gc_context,
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
$fn_proto
|
||||
);
|
||||
)*
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! with_movie_clip_mut {
|
||||
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
|
||||
( $gc_context: ident, $object:ident, $fn_proto: expr, $($name:expr => $fn:expr),* ) => {{
|
||||
$(
|
||||
$object.force_set_function(
|
||||
$name,
|
||||
|_avm, context: &mut UpdateContext<'_, 'gc, '_>, this, args| -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(display_object) = this.read().display_node() {
|
||||
if let Some(display_object) = this.read().as_display_node() {
|
||||
if let Some(movie_clip) = display_object.write(context.gc_context).as_movie_clip_mut() {
|
||||
return Ok($fn(movie_clip, context, display_object, args).into());
|
||||
}
|
||||
|
@ -41,6 +54,7 @@ macro_rules! with_movie_clip_mut {
|
|||
} as crate::avm1::function::NativeFunction<'gc>,
|
||||
$gc_context,
|
||||
DontDelete | ReadOnly | DontEnum,
|
||||
$fn_proto
|
||||
);
|
||||
)*
|
||||
}};
|
||||
|
@ -49,7 +63,7 @@ macro_rules! with_movie_clip_mut {
|
|||
pub fn overwrite_root<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
ac: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
let new_val = args
|
||||
|
@ -57,7 +71,7 @@ pub fn overwrite_root<'gc>(
|
|||
.map(|v| v.to_owned())
|
||||
.unwrap_or(Value::Undefined);
|
||||
this.write(ac.gc_context)
|
||||
.force_set("_root", new_val, EnumSet::new());
|
||||
.define_value("_root", new_val, EnumSet::new());
|
||||
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
@ -65,7 +79,7 @@ pub fn overwrite_root<'gc>(
|
|||
pub fn overwrite_global<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
ac: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
let new_val = args
|
||||
|
@ -73,17 +87,22 @@ pub fn overwrite_global<'gc>(
|
|||
.map(|v| v.to_owned())
|
||||
.unwrap_or(Value::Undefined);
|
||||
this.write(ac.gc_context)
|
||||
.force_set("_global", new_val, EnumSet::new());
|
||||
.define_value("_global", new_val, EnumSet::new());
|
||||
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> {
|
||||
let mut object = Object::object(gc_context);
|
||||
pub fn create_proto<'gc>(
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
proto: ObjectCell<'gc>,
|
||||
fn_proto: ObjectCell<'gc>,
|
||||
) -> ObjectCell<'gc> {
|
||||
let mut object = ScriptObject::object(gc_context, Some(proto));
|
||||
|
||||
with_movie_clip_mut!(
|
||||
gc_context,
|
||||
object,
|
||||
Some(fn_proto),
|
||||
"nextFrame" => |movie_clip: &mut MovieClip<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _cell: DisplayNode<'gc>, _args| {
|
||||
movie_clip.next_frame(context);
|
||||
Value::Undefined
|
||||
|
@ -105,6 +124,7 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<
|
|||
with_movie_clip!(
|
||||
gc_context,
|
||||
object,
|
||||
Some(fn_proto),
|
||||
"getBytesLoaded" => |_movie_clip: &MovieClip<'gc>, _args| {
|
||||
// TODO find a correct value
|
||||
1.0.into()
|
||||
|
@ -118,26 +138,26 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<
|
|||
}
|
||||
);
|
||||
|
||||
object.force_set_virtual(
|
||||
object.add_property(
|
||||
"_global",
|
||||
Executable::Native(|avm, context, _this, _args| Ok(avm.global_object(context).into())),
|
||||
Some(Executable::Native(overwrite_global)),
|
||||
EnumSet::new(),
|
||||
);
|
||||
|
||||
object.force_set_virtual(
|
||||
object.add_property(
|
||||
"_root",
|
||||
Executable::Native(|avm, context, _this, _args| Ok(avm.root_object(context).into())),
|
||||
Some(Executable::Native(overwrite_root)),
|
||||
EnumSet::new(),
|
||||
);
|
||||
|
||||
object.force_set_virtual(
|
||||
object.add_property(
|
||||
"_parent",
|
||||
Executable::Native(|_avm, _context, this, _args| {
|
||||
Ok(this
|
||||
.read()
|
||||
.display_node()
|
||||
.as_display_node()
|
||||
.and_then(|mc| mc.read().parent())
|
||||
.and_then(|dn| dn.read().object().as_object().ok())
|
||||
.map(|o| Value::Object(o.to_owned()))
|
||||
|
@ -148,5 +168,5 @@ pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<
|
|||
EnumSet::new(),
|
||||
);
|
||||
|
||||
object
|
||||
GcCell::allocate(gc_context, Box::new(object))
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
//! Object prototype
|
||||
use crate::avm1::property::Attribute::*;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, ObjectCell, UpdateContext, Value};
|
||||
use enumset::EnumSet;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
|
||||
/// Implements `Object`
|
||||
pub fn constructor<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: ObjectCell<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
/// Implements `Object.prototype.addProperty`
|
||||
pub fn add_property<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
let name = args.get(0).unwrap_or(&Value::Undefined);
|
||||
let getter = args.get(1).unwrap_or(&Value::Undefined);
|
||||
let setter = args.get(2).unwrap_or(&Value::Undefined);
|
||||
|
||||
match (name, getter) {
|
||||
(Value::String(name), Value::Object(get)) if !name.is_empty() => {
|
||||
if let Some(get_func) = get.read().as_executable() {
|
||||
if let Value::Object(set) = setter {
|
||||
if let Some(set_func) = set.read().as_executable() {
|
||||
this.write(context.gc_context).add_property(
|
||||
name,
|
||||
get_func,
|
||||
Some(set_func),
|
||||
EnumSet::empty(),
|
||||
);
|
||||
} else {
|
||||
return Ok(false.into());
|
||||
}
|
||||
} else if let Value::Null = setter {
|
||||
this.write(context.gc_context).add_property(
|
||||
name,
|
||||
get_func,
|
||||
None,
|
||||
ReadOnly.into(),
|
||||
);
|
||||
} else {
|
||||
return Ok(false.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false.into())
|
||||
}
|
||||
_ => Ok(false.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements `Object.prototype.hasOwnProperty`
|
||||
pub fn has_own_property<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
match args.get(0) {
|
||||
Some(Value::String(name)) => Ok(Value::Bool(this.read().has_own_property(name)).into()),
|
||||
_ => Ok(Value::Bool(false).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements `Object.prototype.toString`
|
||||
fn to_string<'gc>(
|
||||
_: &mut Avm1<'gc>,
|
||||
_: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_: ObjectCell<'gc>,
|
||||
_: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(ReturnValue::Immediate("[object Object]".into()))
|
||||
}
|
||||
|
||||
/// Implements `Object.prototype.isPropertyEnumerable`
|
||||
fn is_property_enumerable<'gc>(
|
||||
_: &mut Avm1<'gc>,
|
||||
_: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
match args.get(0) {
|
||||
Some(Value::String(name)) => {
|
||||
Ok(Value::Bool(this.read().is_property_enumerable(name)).into())
|
||||
}
|
||||
_ => Ok(Value::Bool(false).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements `Object.prototype.isPrototypeOf`
|
||||
fn is_prototype_of<'gc>(
|
||||
_: &mut Avm1<'gc>,
|
||||
_: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
match args.get(0) {
|
||||
Some(val) => {
|
||||
let ob = match val.as_object() {
|
||||
Ok(ob) => ob,
|
||||
Err(_) => return Ok(Value::Bool(false).into()),
|
||||
};
|
||||
let mut proto = ob.read().proto();
|
||||
|
||||
while let Some(proto_ob) = proto {
|
||||
if GcCell::ptr_eq(this, proto_ob) {
|
||||
return Ok(Value::Bool(true).into());
|
||||
}
|
||||
|
||||
proto = proto_ob.read().proto();
|
||||
}
|
||||
|
||||
Ok(Value::Bool(false).into())
|
||||
}
|
||||
_ => Ok(Value::Bool(false).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements `Object.prototype.valueOf`
|
||||
fn value_of<'gc>(
|
||||
_: &mut Avm1<'gc>,
|
||||
_: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
_: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(ReturnValue::Immediate(this.into()))
|
||||
}
|
||||
|
||||
/// Partially construct `Object.prototype`.
|
||||
///
|
||||
/// `__proto__` and other cross-linked properties of this object will *not*
|
||||
/// be defined here. The caller of this function is responsible for linking
|
||||
/// them in order to obtain a valid ECMAScript `Object` prototype.
|
||||
///
|
||||
/// Since Object and Function are so heavily intertwined, this function does
|
||||
/// not allocate an object to store either proto. Instead, you must allocate
|
||||
/// bare objects for both and let this function fill Object for you.
|
||||
pub fn fill_proto<'gc>(
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
object_proto: ObjectCell<'gc>,
|
||||
fn_proto: ObjectCell<'gc>,
|
||||
) {
|
||||
let mut ob_proto_write = object_proto.write(gc_context);
|
||||
|
||||
ob_proto_write
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.force_set_function(
|
||||
"addProperty",
|
||||
add_property,
|
||||
gc_context,
|
||||
DontDelete | DontEnum,
|
||||
Some(fn_proto),
|
||||
);
|
||||
ob_proto_write
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.force_set_function(
|
||||
"hasOwnProperty",
|
||||
has_own_property,
|
||||
gc_context,
|
||||
DontDelete | DontEnum,
|
||||
Some(fn_proto),
|
||||
);
|
||||
ob_proto_write
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.force_set_function(
|
||||
"isPropertyEnumerable",
|
||||
is_property_enumerable,
|
||||
gc_context,
|
||||
DontDelete | DontEnum,
|
||||
Some(fn_proto),
|
||||
);
|
||||
ob_proto_write
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.force_set_function(
|
||||
"isPrototypeOf",
|
||||
is_prototype_of,
|
||||
gc_context,
|
||||
DontDelete | DontEnum,
|
||||
Some(fn_proto),
|
||||
);
|
||||
ob_proto_write
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.force_set_function(
|
||||
"toString",
|
||||
to_string,
|
||||
gc_context,
|
||||
DontDelete | DontEnum,
|
||||
Some(fn_proto),
|
||||
);
|
||||
ob_proto_write
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.force_set_function(
|
||||
"valueOf",
|
||||
value_of,
|
||||
gc_context,
|
||||
DontDelete | DontEnum,
|
||||
Some(fn_proto),
|
||||
);
|
||||
}
|
|
@ -1,681 +1,187 @@
|
|||
use self::Attribute::*;
|
||||
use crate::avm1::function::{Avm1Function, Executable, NativeFunction};
|
||||
//! Object trait to expose objects to AVM
|
||||
|
||||
use crate::avm1::function::Executable;
|
||||
use crate::avm1::property::Attribute;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, UpdateContext, Value};
|
||||
use crate::avm1::{Avm1, Error, ScriptObject, UpdateContext, Value};
|
||||
use crate::display_object::DisplayNode;
|
||||
use core::fmt;
|
||||
use enumset::{EnumSet, EnumSetType};
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::HashMap;
|
||||
use std::mem::replace;
|
||||
use enumset::EnumSet;
|
||||
use gc_arena::{Collect, GcCell};
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub const TYPE_OF_OBJECT: &str = "object";
|
||||
pub const TYPE_OF_FUNCTION: &str = "function";
|
||||
pub const TYPE_OF_MOVIE_CLIP: &str = "movieclip";
|
||||
pub type ObjectCell<'gc> = GcCell<'gc, Box<dyn Object<'gc> + 'gc>>;
|
||||
|
||||
fn default_to_string<'gc>(
|
||||
_: &mut Avm1<'gc>,
|
||||
_: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_: GcCell<'gc, Object<'gc>>,
|
||||
_: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(ReturnValue::Immediate("[Object object]".into()))
|
||||
}
|
||||
|
||||
#[derive(EnumSetType, Debug)]
|
||||
pub enum Attribute {
|
||||
DontDelete,
|
||||
DontEnum,
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Property<'gc> {
|
||||
Virtual {
|
||||
get: Executable<'gc>,
|
||||
set: Option<Executable<'gc>>,
|
||||
attributes: EnumSet<Attribute>,
|
||||
},
|
||||
Stored {
|
||||
value: Value<'gc>,
|
||||
attributes: EnumSet<Attribute>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'gc> Property<'gc> {
|
||||
/// Get the value of a property slot.
|
||||
/// Represents an object that can be directly interacted with by the AVM
|
||||
/// runtime.
|
||||
pub trait Object<'gc>: 'gc + Collect + Debug {
|
||||
/// Retrieve a named property from this object exclusively.
|
||||
///
|
||||
/// This function yields `None` if the value is being determined on the AVM
|
||||
/// stack. Otherwise, if the value can be determined on the Rust stack,
|
||||
/// then this function returns the value.
|
||||
pub fn get(
|
||||
&self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
match self {
|
||||
Property::Virtual { get, .. } => get.exec(avm, context, this, &[]),
|
||||
Property::Stored { value, .. } => Ok(value.to_owned().into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a property slot.
|
||||
/// This function takes a redundant `this` parameter which should be
|
||||
/// the object's own `GcCell`, so that it can pass it to user-defined
|
||||
/// overrides that may need to interact with the underlying object.
|
||||
///
|
||||
/// This function returns `true` if the set has completed, or `false` if
|
||||
/// it has not yet occured. If `false`, and you need to run code after the
|
||||
/// set has occured, you must recursively execute the top-most frame via
|
||||
/// `run_current_frame`.
|
||||
pub fn set(
|
||||
&mut self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
new_value: impl Into<Value<'gc>>,
|
||||
) -> Result<bool, Error> {
|
||||
match self {
|
||||
Property::Virtual { set, .. } => {
|
||||
if let Some(function) = set {
|
||||
let return_value = function.exec(avm, context, this, &[new_value.into()])?;
|
||||
Ok(return_value.is_immediate())
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
Property::Stored {
|
||||
value, attributes, ..
|
||||
} => {
|
||||
if !attributes.contains(ReadOnly) {
|
||||
replace::<Value<'gc>>(value, new_value.into());
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_delete(&self) -> bool {
|
||||
match self {
|
||||
Property::Virtual { attributes, .. } => !attributes.contains(DontDelete),
|
||||
Property::Stored { attributes, .. } => !attributes.contains(DontDelete),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enumerable(&self) -> bool {
|
||||
match self {
|
||||
Property::Virtual { attributes, .. } => !attributes.contains(DontEnum),
|
||||
Property::Stored { attributes, .. } => !attributes.contains(DontEnum),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_overwritable(&self) -> bool {
|
||||
match self {
|
||||
Property::Virtual {
|
||||
attributes, set, ..
|
||||
} => !attributes.contains(ReadOnly) && !set.is_none(),
|
||||
Property::Stored { attributes, .. } => !attributes.contains(ReadOnly),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
attributes,
|
||||
} => f
|
||||
.debug_struct("Property::Virtual")
|
||||
.field("get", &true)
|
||||
.field("set", &set.is_some())
|
||||
.field("attributes", &attributes)
|
||||
.finish(),
|
||||
Property::Stored { value, attributes } => f
|
||||
.debug_struct("Property::Stored")
|
||||
.field("value", &value)
|
||||
.field("attributes", &attributes)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Object<'gc> {
|
||||
display_node: Option<DisplayNode<'gc>>,
|
||||
values: HashMap<String, Property<'gc>>,
|
||||
function: Option<Executable<'gc>>,
|
||||
type_of: &'static str,
|
||||
}
|
||||
|
||||
unsafe impl<'gc> gc_arena::Collect for Object<'gc> {
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
self.display_node.trace(cc);
|
||||
self.values.trace(cc);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Object<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("Object")
|
||||
.field("display_node", &self.display_node)
|
||||
.field("values", &self.values)
|
||||
.field("function", &self.function.is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> Object<'gc> {
|
||||
pub fn object(gc_context: MutationContext<'gc, '_>) -> Self {
|
||||
let mut result = Self {
|
||||
type_of: TYPE_OF_OBJECT,
|
||||
display_node: None,
|
||||
values: HashMap::new(),
|
||||
function: None,
|
||||
};
|
||||
|
||||
result.force_set_function(
|
||||
"toString",
|
||||
default_to_string,
|
||||
gc_context,
|
||||
DontDelete | DontEnum,
|
||||
);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Constructs an object with no values, not even builtins.
|
||||
///
|
||||
/// Intended for constructing scope chains, since they exclusively use the
|
||||
/// object values, but can't just have a hashmap because of `with` and
|
||||
/// friends.
|
||||
pub fn bare_object() -> Self {
|
||||
Self {
|
||||
type_of: TYPE_OF_OBJECT,
|
||||
display_node: None,
|
||||
values: HashMap::new(),
|
||||
function: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn native_function(function: NativeFunction<'gc>) -> Self {
|
||||
Self {
|
||||
type_of: TYPE_OF_FUNCTION,
|
||||
function: Some(Executable::Native(function)),
|
||||
display_node: None,
|
||||
values: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_function(func: Avm1Function<'gc>) -> Self {
|
||||
Self {
|
||||
type_of: TYPE_OF_FUNCTION,
|
||||
function: Some(Executable::Action(func)),
|
||||
display_node: None,
|
||||
values: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_display_node(&mut self, display_node: DisplayNode<'gc>) {
|
||||
self.display_node = Some(display_node);
|
||||
}
|
||||
|
||||
pub fn display_node(&self) -> Option<DisplayNode<'gc>> {
|
||||
self.display_node
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
&mut self,
|
||||
name: &str,
|
||||
value: impl Into<Value<'gc>>,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
) -> Result<(), Error> {
|
||||
match self.values.entry(name.to_owned()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
entry.get_mut().set(avm, context, this, value)?;
|
||||
Ok(())
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(Property::Stored {
|
||||
value: value.into(),
|
||||
attributes: Default::default(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn force_set_virtual<A>(
|
||||
&mut self,
|
||||
name: &str,
|
||||
get: Executable<'gc>,
|
||||
set: Option<Executable<'gc>>,
|
||||
attributes: A,
|
||||
) where
|
||||
A: Into<EnumSet<Attribute>>,
|
||||
{
|
||||
self.values.insert(
|
||||
name.to_owned(),
|
||||
Property::Virtual {
|
||||
get,
|
||||
set,
|
||||
attributes: attributes.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn force_set<A>(&mut self, name: &str, value: impl Into<Value<'gc>>, attributes: A)
|
||||
where
|
||||
A: Into<EnumSet<Attribute>>,
|
||||
{
|
||||
self.values.insert(
|
||||
name.to_string(),
|
||||
Property::Stored {
|
||||
value: value.into(),
|
||||
attributes: attributes.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn force_set_function<A>(
|
||||
&mut self,
|
||||
name: &str,
|
||||
function: NativeFunction<'gc>,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
attributes: A,
|
||||
) where
|
||||
A: Into<EnumSet<Attribute>>,
|
||||
{
|
||||
self.force_set(
|
||||
name,
|
||||
GcCell::allocate(gc_context, Object::native_function(function)),
|
||||
attributes,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the value of a particular property on this object.
|
||||
///
|
||||
/// The `avm`, `context`, and `this` parameters exist so that this object
|
||||
/// can call virtual properties. Likewise, this function returns a
|
||||
/// `ReturnValue` which allows pulling data from the return values of user
|
||||
/// functions.
|
||||
pub fn get(
|
||||
/// This function should not inspect prototype chains. Instead, use `get`
|
||||
/// to do ordinary property look-up and resolution.
|
||||
fn get_local(
|
||||
&self,
|
||||
name: &str,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(value) = self.values.get(name) {
|
||||
return value.get(avm, context, this);
|
||||
}
|
||||
this: ObjectCell<'gc>,
|
||||
) -> Result<ReturnValue<'gc>, Error>;
|
||||
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
/// Delete a given value off the object.
|
||||
pub fn delete(&mut self, name: &str) -> bool {
|
||||
if let Some(prop) = self.values.get(name) {
|
||||
if prop.can_delete() {
|
||||
self.values.remove(name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn has_property(&self, name: &str) -> bool {
|
||||
self.values.contains_key(name)
|
||||
}
|
||||
|
||||
pub fn has_own_property(&self, name: &str) -> bool {
|
||||
self.values.contains_key(name)
|
||||
}
|
||||
|
||||
pub fn is_property_overwritable(&self, name: &str) -> bool {
|
||||
self.values
|
||||
.get(name)
|
||||
.map(|p| p.is_overwritable())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn get_keys(&self) -> Vec<String> {
|
||||
self.values
|
||||
.iter()
|
||||
.filter_map(|(k, p)| {
|
||||
if p.is_enumerable() {
|
||||
Some(k.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn call(
|
||||
/// Retrieve a named property from the object, or it's prototype.
|
||||
fn get(
|
||||
&self,
|
||||
name: &str,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
args: &[Value<'gc>],
|
||||
this: ObjectCell<'gc>,
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(function) = &self.function {
|
||||
function.exec(avm, context, this, args)
|
||||
if self.has_own_property(name) {
|
||||
self.get_local(name, avm, context, this)
|
||||
} else {
|
||||
let mut depth = 0;
|
||||
let mut proto = self.proto();
|
||||
|
||||
while proto.is_some() {
|
||||
if depth == 255 {
|
||||
return Err("Encountered an excessively deep prototype chain.".into());
|
||||
}
|
||||
|
||||
if proto.unwrap().read().has_own_property(name) {
|
||||
return proto.unwrap().read().get_local(name, avm, context, this);
|
||||
}
|
||||
|
||||
proto = proto.unwrap().read().proto();
|
||||
depth += 1;
|
||||
}
|
||||
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_string(&self) -> String {
|
||||
if self.function.is_some() {
|
||||
"[type Function]".to_string()
|
||||
} else {
|
||||
"[object Object]".to_string()
|
||||
}
|
||||
}
|
||||
/// Set a named property on this object, or it's prototype.
|
||||
///
|
||||
/// This function takes a redundant `this` parameter which should be
|
||||
/// the object's own `GcCell`, so that it can pass it to user-defined
|
||||
/// overrides that may need to interact with the underlying object.
|
||||
fn set(
|
||||
&mut self,
|
||||
name: &str,
|
||||
value: Value<'gc>,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
) -> Result<(), Error>;
|
||||
|
||||
pub fn set_type_of(&mut self, type_of: &'static str) {
|
||||
self.type_of = type_of;
|
||||
}
|
||||
/// Call the underlying object.
|
||||
///
|
||||
/// This function takes a redundant `this` parameter which should be
|
||||
/// the object's own `GcCell`, so that it can pass it to user-defined
|
||||
/// overrides that may need to interact with the underlying object.
|
||||
fn call(
|
||||
&self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error>;
|
||||
|
||||
pub fn type_of(&self) -> &'static str {
|
||||
self.type_of
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::avm1::activation::Activation;
|
||||
use crate::backend::audio::NullAudioBackend;
|
||||
use crate::backend::navigator::NullNavigatorBackend;
|
||||
use crate::backend::render::NullRenderer;
|
||||
use crate::display_object::{DisplayObject, MovieClip};
|
||||
use crate::library::Library;
|
||||
use crate::prelude::*;
|
||||
use gc_arena::rootless_arena;
|
||||
use rand::{rngs::SmallRng, SeedableRng};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn with_object<F, R>(swf_version: u8, test: F) -> R
|
||||
where
|
||||
F: for<'a, 'gc> FnOnce(
|
||||
&mut Avm1<'gc>,
|
||||
&mut UpdateContext<'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(swf_version, gc_context));
|
||||
let root = GcCell::allocate(gc_context, movie_clip);
|
||||
let mut context = UpdateContext {
|
||||
gc_context,
|
||||
global_time: 0,
|
||||
player_version: 32,
|
||||
swf_version,
|
||||
root,
|
||||
start_clip: root,
|
||||
active_clip: root,
|
||||
target_clip: Some(root),
|
||||
target_path: Value::Undefined,
|
||||
rng: &mut SmallRng::from_seed([0u8; 16]),
|
||||
action_queue: &mut crate::context::ActionQueue::new(),
|
||||
audio: &mut NullAudioBackend::new(),
|
||||
background_color: &mut Color {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 0,
|
||||
},
|
||||
library: &mut Library::new(),
|
||||
navigator: &mut NullNavigatorBackend::new(),
|
||||
renderer: &mut NullRenderer::new(),
|
||||
swf_data: &mut Arc::new(vec![]),
|
||||
};
|
||||
|
||||
let object = GcCell::allocate(gc_context, Object::object(gc_context));
|
||||
|
||||
let globals = avm.global_object_cell();
|
||||
avm.insert_stack_frame(GcCell::allocate(
|
||||
gc_context,
|
||||
Activation::from_nothing(swf_version, globals, 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)
|
||||
.unwrap(),
|
||||
Value::Undefined.into()
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_get() {
|
||||
with_object(0, |avm, context, object| {
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.force_set("forced", "forced", EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.set("natural", "natural", avm, context, object)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
object.read().get("forced", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("forced".into())
|
||||
);
|
||||
assert_eq!(
|
||||
object.read().get("natural", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("natural".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_readonly() {
|
||||
with_object(0, |avm, context, object| {
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.force_set("normal", "initial", EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.force_set("readonly", "initial", ReadOnly);
|
||||
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.set("normal", "replaced", avm, context, object)
|
||||
.unwrap();
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.set("readonly", "replaced", avm, context, object)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
object.read().get("normal", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("replaced".into())
|
||||
);
|
||||
assert_eq!(
|
||||
object.read().get("readonly", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("initial".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deletable_not_readonly() {
|
||||
with_object(0, |avm, context, object| {
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.force_set("test", "initial", DontDelete);
|
||||
|
||||
assert_eq!(object.write(context.gc_context).delete("test"), false);
|
||||
assert_eq!(
|
||||
object.read().get("test", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("initial".into())
|
||||
);
|
||||
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.set("test", "replaced", avm, context, object)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(object.write(context.gc_context).delete("test"), false);
|
||||
assert_eq!(
|
||||
object.read().get("test", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("replaced".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtual_get() {
|
||||
with_object(0, |avm, context, object| {
|
||||
let getter = Executable::Native(|_avm, _context, _this, _args| {
|
||||
Ok(ReturnValue::Immediate("Virtual!".into()))
|
||||
});
|
||||
|
||||
object.write(context.gc_context).force_set_virtual(
|
||||
"test",
|
||||
getter,
|
||||
None,
|
||||
EnumSet::empty(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
object.read().get("test", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("Virtual!".into())
|
||||
);
|
||||
|
||||
// This set should do nothing
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.set("test", "Ignored!", avm, context, object)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
object.read().get("test", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("Virtual!".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete() {
|
||||
with_object(0, |avm, context, object| {
|
||||
let getter = Executable::Native(|_avm, _context, _this, _args| {
|
||||
Ok(ReturnValue::Immediate("Virtual!".into()))
|
||||
});
|
||||
|
||||
object.write(context.gc_context).force_set_virtual(
|
||||
"virtual",
|
||||
getter.clone(),
|
||||
None,
|
||||
EnumSet::empty(),
|
||||
);
|
||||
object.write(context.gc_context).force_set_virtual(
|
||||
"virtual_un",
|
||||
getter,
|
||||
None,
|
||||
DontDelete,
|
||||
);
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.force_set("stored", "Stored!", EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.force_set("stored_un", "Stored!", DontDelete);
|
||||
|
||||
assert_eq!(object.write(context.gc_context).delete("virtual"), true);
|
||||
assert_eq!(object.write(context.gc_context).delete("virtual_un"), false);
|
||||
assert_eq!(object.write(context.gc_context).delete("stored"), true);
|
||||
assert_eq!(object.write(context.gc_context).delete("stored_un"), false);
|
||||
assert_eq!(
|
||||
object.write(context.gc_context).delete("non_existent"),
|
||||
false
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
object.read().get("virtual", avm, context, object).unwrap(),
|
||||
Value::Undefined.into()
|
||||
);
|
||||
assert_eq!(
|
||||
object
|
||||
.read()
|
||||
.get("virtual_un", avm, context, object)
|
||||
.unwrap(),
|
||||
ReturnValue::Immediate("Virtual!".into())
|
||||
);
|
||||
assert_eq!(
|
||||
object.read().get("stored", avm, context, object).unwrap(),
|
||||
Value::Undefined.into()
|
||||
);
|
||||
assert_eq!(
|
||||
object
|
||||
.read()
|
||||
.get("stored_un", avm, context, object)
|
||||
.unwrap(),
|
||||
ReturnValue::Immediate("Stored!".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_values() {
|
||||
with_object(0, |_avm, context, object| {
|
||||
let getter = Executable::Native(|_avm, _context, _this, _args| Ok(Value::Null.into()));
|
||||
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.force_set("stored", Value::Null, EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.force_set("stored_hidden", Value::Null, DontEnum);
|
||||
object.write(context.gc_context).force_set_virtual(
|
||||
"virtual",
|
||||
getter.clone(),
|
||||
None,
|
||||
EnumSet::empty(),
|
||||
);
|
||||
object.write(context.gc_context).force_set_virtual(
|
||||
"virtual_hidden",
|
||||
getter,
|
||||
None,
|
||||
DontEnum,
|
||||
);
|
||||
|
||||
let keys = object.read().get_keys();
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert_eq!(keys.contains(&"stored".to_string()), true);
|
||||
assert_eq!(keys.contains(&"stored_hidden".to_string()), false);
|
||||
assert_eq!(keys.contains(&"virtual".to_string()), true);
|
||||
assert_eq!(keys.contains(&"virtual_hidden".to_string()), false);
|
||||
})
|
||||
}
|
||||
/// Construct a host object of some kind and return it's cell.
|
||||
///
|
||||
/// As the first step in object construction, the `new` method is called on
|
||||
/// the prototype to initialize an object. The prototype may construct any
|
||||
/// object implementation it wants, with itself as the new object's proto.
|
||||
/// Then, the constructor is `call`ed with the new object as `this` to
|
||||
/// initialize the object.
|
||||
///
|
||||
/// The arguments passed to the constructor are provided here; however, all
|
||||
/// object construction should happen in `call`, not `new`. `new` exists
|
||||
/// purely so that host objects can be constructed by the VM.
|
||||
fn new(
|
||||
&self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ObjectCell<'gc>, Error>;
|
||||
|
||||
/// Delete a named property from the object.
|
||||
///
|
||||
/// Returns false if the property cannot be deleted.
|
||||
fn delete(&mut self, name: &str) -> bool;
|
||||
|
||||
/// Retrieve the `__proto__` of a given object.
|
||||
///
|
||||
/// The proto is another object used to resolve methods across a class of
|
||||
/// multiple objects. It should also be accessible as `__proto__` from
|
||||
/// `get`.
|
||||
fn proto(&self) -> Option<ObjectCell<'gc>>;
|
||||
|
||||
/// Define a value on an object.
|
||||
///
|
||||
/// Unlike setting a value, this function is intended to replace any
|
||||
/// existing virtual or built-in properties already installed on a given
|
||||
/// object. As such, this should not run any setters; the resulting name
|
||||
/// slot should either be completely replaced with the value or completely
|
||||
/// untouched.
|
||||
///
|
||||
/// It is not guaranteed that all objects accept value definitions,
|
||||
/// especially if a property name conflicts with a built-in property, such
|
||||
/// as `__proto__`.
|
||||
fn define_value(&mut self, name: &str, value: Value<'gc>, attributes: EnumSet<Attribute>);
|
||||
|
||||
/// Define a virtual property onto a given object.
|
||||
///
|
||||
/// A virtual property is a set of get/set functions that are called when a
|
||||
/// given named property is retrieved or stored on an object. These
|
||||
/// functions are then responsible for providing or accepting the value
|
||||
/// that is given to or taken from the AVM.
|
||||
///
|
||||
/// It is not guaranteed that all objects accept virtual properties,
|
||||
/// especially if a property name conflicts with a built-in property, such
|
||||
/// as `__proto__`.
|
||||
fn add_property(
|
||||
&mut self,
|
||||
name: &str,
|
||||
get: Executable<'gc>,
|
||||
set: Option<Executable<'gc>>,
|
||||
attributes: EnumSet<Attribute>,
|
||||
);
|
||||
|
||||
/// Checks if the object has a given named property.
|
||||
fn has_property(&self, name: &str) -> bool;
|
||||
|
||||
/// Checks if the object has a given named property on itself (and not,
|
||||
/// say, the object's prototype or superclass)
|
||||
fn has_own_property(&self, name: &str) -> bool;
|
||||
|
||||
/// Checks if a named property can be overwritten.
|
||||
fn is_property_overwritable(&self, name: &str) -> bool;
|
||||
|
||||
/// Checks if a named property appears when enumerating the object.
|
||||
fn is_property_enumerable(&self, name: &str) -> bool;
|
||||
|
||||
/// Enumerate the object.
|
||||
fn get_keys(&self) -> HashSet<String>;
|
||||
|
||||
/// Coerce the object into a string.
|
||||
fn as_string(&self) -> String;
|
||||
|
||||
/// Get the object's type string.
|
||||
fn type_of(&self) -> &'static str;
|
||||
|
||||
/// Get the underlying script object, if it exists.
|
||||
fn as_script_object(&self) -> Option<&ScriptObject<'gc>>;
|
||||
|
||||
/// Get the underlying script object, if it exists.
|
||||
fn as_script_object_mut(&mut self) -> Option<&mut ScriptObject<'gc>>;
|
||||
|
||||
/// Get the underlying display node for this object, if it exists.
|
||||
fn as_display_node(&self) -> Option<DisplayNode<'gc>>;
|
||||
|
||||
/// Get the underlying executable for this object, if it exists.
|
||||
fn as_executable(&self) -> Option<Executable<'gc>>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
//! User-defined properties
|
||||
|
||||
use self::Attribute::*;
|
||||
use crate::avm1::function::Executable;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, ObjectCell, UpdateContext, Value};
|
||||
use core::fmt;
|
||||
use enumset::{EnumSet, EnumSetType};
|
||||
use std::mem::replace;
|
||||
|
||||
#[derive(EnumSetType, Debug)]
|
||||
pub enum Attribute {
|
||||
DontDelete,
|
||||
DontEnum,
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Property<'gc> {
|
||||
Virtual {
|
||||
get: Executable<'gc>,
|
||||
set: Option<Executable<'gc>>,
|
||||
attributes: EnumSet<Attribute>,
|
||||
},
|
||||
Stored {
|
||||
value: Value<'gc>,
|
||||
attributes: EnumSet<Attribute>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'gc> Property<'gc> {
|
||||
/// Get the value of a property slot.
|
||||
///
|
||||
/// This function yields `ReturnValue` because some properties may be
|
||||
/// user-defined.
|
||||
pub fn get(
|
||||
&self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
match self {
|
||||
Property::Virtual { get, .. } => get.exec(avm, context, this, &[]),
|
||||
Property::Stored { value, .. } => Ok(value.to_owned().into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a property slot.
|
||||
///
|
||||
/// This function returns `true` if the set has completed, or `false` if
|
||||
/// it has not yet occured. If `false`, and you need to run code after the
|
||||
/// set has occured, you must recursively execute the top-most frame via
|
||||
/// `run_current_frame`.
|
||||
pub fn set(
|
||||
&mut self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
new_value: impl Into<Value<'gc>>,
|
||||
) -> Result<bool, Error> {
|
||||
match self {
|
||||
Property::Virtual { set, .. } => {
|
||||
if let Some(function) = set {
|
||||
let return_value = function.exec(avm, context, this, &[new_value.into()])?;
|
||||
Ok(return_value.is_immediate())
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
Property::Stored {
|
||||
value, attributes, ..
|
||||
} => {
|
||||
if !attributes.contains(ReadOnly) {
|
||||
replace::<Value<'gc>>(value, new_value.into());
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_delete(&self) -> bool {
|
||||
match self {
|
||||
Property::Virtual { attributes, .. } => !attributes.contains(DontDelete),
|
||||
Property::Stored { attributes, .. } => !attributes.contains(DontDelete),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_enumerable(&self) -> bool {
|
||||
match self {
|
||||
Property::Virtual { attributes, .. } => !attributes.contains(DontEnum),
|
||||
Property::Stored { attributes, .. } => !attributes.contains(DontEnum),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_overwritable(&self) -> bool {
|
||||
match self {
|
||||
Property::Virtual {
|
||||
attributes, set, ..
|
||||
} => !attributes.contains(ReadOnly) && !set.is_none(),
|
||||
Property::Stored { attributes, .. } => !attributes.contains(ReadOnly),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
attributes,
|
||||
} => f
|
||||
.debug_struct("Property::Virtual")
|
||||
.field("get", &true)
|
||||
.field("set", &set.is_some())
|
||||
.field("attributes", &attributes)
|
||||
.finish(),
|
||||
Property::Stored { value, attributes } => f
|
||||
.debug_struct("Property::Stored")
|
||||
.field("value", &value)
|
||||
.field("attributes", &attributes)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
//! Return value enum
|
||||
|
||||
use crate::avm1::activation::Activation;
|
||||
use crate::avm1::{Avm1, Error, Object, Value};
|
||||
use crate::avm1::{Avm1, Error, ObjectCell, Value};
|
||||
use crate::context::UpdateContext;
|
||||
use gc_arena::{Collect, GcCell};
|
||||
use std::fmt;
|
||||
|
@ -99,12 +99,6 @@ impl<'gc> ReturnValue<'gc> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Consumes the given return value.
|
||||
///
|
||||
/// This exists primarily so that users of return values can indicate that
|
||||
/// they do not plan to use them.
|
||||
pub fn ignore(self) {}
|
||||
|
||||
pub fn is_immediate(&self) -> bool {
|
||||
use ReturnValue::*;
|
||||
|
||||
|
@ -152,8 +146,8 @@ impl<'gc> From<bool> for ReturnValue<'gc> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'gc> From<GcCell<'gc, Object<'gc>>> for ReturnValue<'gc> {
|
||||
fn from(object: GcCell<'gc, Object<'gc>>) -> Self {
|
||||
impl<'gc> From<ObjectCell<'gc>> for ReturnValue<'gc> {
|
||||
fn from(object: ObjectCell<'gc>) -> Self {
|
||||
ReturnValue::Immediate(Value::Object(object))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Represents AVM1 scope chain resolution.
|
||||
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, Object, UpdateContext, Value};
|
||||
use crate::avm1::{Avm1, Error, Object, ObjectCell, ScriptObject, UpdateContext, Value};
|
||||
use enumset::EnumSet;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
use std::cell::{Ref, RefMut};
|
||||
|
@ -30,7 +30,7 @@ pub enum ScopeClass {
|
|||
pub struct Scope<'gc> {
|
||||
parent: Option<GcCell<'gc, Scope<'gc>>>,
|
||||
class: ScopeClass,
|
||||
values: GcCell<'gc, Object<'gc>>,
|
||||
values: ObjectCell<'gc>,
|
||||
}
|
||||
|
||||
unsafe impl<'gc> gc_arena::Collect for Scope<'gc> {
|
||||
|
@ -43,7 +43,7 @@ unsafe impl<'gc> gc_arena::Collect for Scope<'gc> {
|
|||
|
||||
impl<'gc> Scope<'gc> {
|
||||
/// Construct a global scope (one without a parent).
|
||||
pub fn from_global_object(globals: GcCell<'gc, Object<'gc>>) -> Scope<'gc> {
|
||||
pub fn from_global_object(globals: ObjectCell<'gc>) -> Scope<'gc> {
|
||||
Scope {
|
||||
parent: None,
|
||||
class: ScopeClass::Global,
|
||||
|
@ -56,7 +56,7 @@ impl<'gc> Scope<'gc> {
|
|||
Scope {
|
||||
parent: Some(parent),
|
||||
class: ScopeClass::Local,
|
||||
values: GcCell::allocate(mc, Object::bare_object()),
|
||||
values: ScriptObject::object_cell(mc, None),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,7 @@ impl<'gc> Scope<'gc> {
|
|||
Self {
|
||||
parent: None,
|
||||
class: ScopeClass::Global,
|
||||
values: GcCell::allocate(mc, Object::bare_object()),
|
||||
values: ScriptObject::object_cell(mc, None),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
@ -119,7 +119,7 @@ impl<'gc> Scope<'gc> {
|
|||
/// scope has been replaced with another given object.
|
||||
pub fn new_target_scope(
|
||||
mut parent: GcCell<'gc, Self>,
|
||||
clip: GcCell<'gc, Object<'gc>>,
|
||||
clip: ObjectCell<'gc>,
|
||||
mc: MutationContext<'gc, '_>,
|
||||
) -> GcCell<'gc, Self> {
|
||||
let mut bottom_scope = None;
|
||||
|
@ -163,7 +163,7 @@ impl<'gc> Scope<'gc> {
|
|||
Self {
|
||||
parent: None,
|
||||
class: ScopeClass::Global,
|
||||
values: GcCell::allocate(mc, Object::bare_object()),
|
||||
values: ScriptObject::object_cell(mc, None),
|
||||
},
|
||||
)
|
||||
})
|
||||
|
@ -176,7 +176,7 @@ impl<'gc> Scope<'gc> {
|
|||
/// scope. This requires some scope chain juggling.
|
||||
pub fn new_with_scope(
|
||||
locals: GcCell<'gc, Self>,
|
||||
with_object: GcCell<'gc, Object<'gc>>,
|
||||
with_object: ObjectCell<'gc>,
|
||||
mc: MutationContext<'gc, '_>,
|
||||
) -> GcCell<'gc, Self> {
|
||||
let parent_scope = locals.read().parent;
|
||||
|
@ -204,7 +204,7 @@ impl<'gc> Scope<'gc> {
|
|||
pub fn new(
|
||||
parent: GcCell<'gc, Self>,
|
||||
class: ScopeClass,
|
||||
with_object: GcCell<'gc, Object<'gc>>,
|
||||
with_object: ObjectCell<'gc>,
|
||||
) -> Scope<'gc> {
|
||||
Scope {
|
||||
parent: Some(parent),
|
||||
|
@ -214,17 +214,17 @@ impl<'gc> Scope<'gc> {
|
|||
}
|
||||
|
||||
/// Returns a reference to the current local scope object.
|
||||
pub fn locals(&self) -> Ref<Object<'gc>> {
|
||||
pub fn locals(&self) -> Ref<Box<dyn Object<'gc>>> {
|
||||
self.values.read()
|
||||
}
|
||||
|
||||
/// Returns a gc cell of the current local scope object.
|
||||
pub fn locals_cell(&self) -> GcCell<'gc, Object<'gc>> {
|
||||
pub fn locals_cell(&self) -> ObjectCell<'gc> {
|
||||
self.values.to_owned()
|
||||
}
|
||||
|
||||
/// Returns a reference to the current local scope object for mutation.
|
||||
pub fn locals_mut(&self, mc: MutationContext<'gc, '_>) -> RefMut<Object<'gc>> {
|
||||
pub fn locals_mut(&self, mc: MutationContext<'gc, '_>) -> RefMut<Box<dyn Object<'gc>>> {
|
||||
self.values.write(mc)
|
||||
}
|
||||
|
||||
|
@ -246,7 +246,7 @@ impl<'gc> Scope<'gc> {
|
|||
name: &str,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
this: ObjectCell<'gc>,
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if self.locals().has_property(name) {
|
||||
return self.locals().get(name, avm, context, this);
|
||||
|
@ -286,7 +286,7 @@ impl<'gc> Scope<'gc> {
|
|||
value: Value<'gc>,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
this: ObjectCell<'gc>,
|
||||
) -> Result<Option<Value<'gc>>, Error> {
|
||||
if self.locals().has_property(name) && self.locals().is_property_overwritable(name) {
|
||||
self.locals_mut(context.gc_context)
|
||||
|
@ -308,7 +308,8 @@ impl<'gc> Scope<'gc> {
|
|||
/// chain. As a result, this function always force sets a property on the
|
||||
/// local object and does not traverse the scope chain.
|
||||
pub fn define(&self, name: &str, value: impl Into<Value<'gc>>, mc: MutationContext<'gc, '_>) {
|
||||
self.locals_mut(mc).force_set(name, value, EnumSet::empty());
|
||||
self.locals_mut(mc)
|
||||
.define_value(name, value.into(), EnumSet::empty());
|
||||
}
|
||||
|
||||
/// Delete a value from scope
|
||||
|
|
|
@ -0,0 +1,707 @@
|
|||
use crate::avm1::function::{Executable, NativeFunction};
|
||||
use crate::avm1::property::{Attribute, Property};
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, Object, ObjectCell, UpdateContext, Value};
|
||||
use crate::display_object::DisplayNode;
|
||||
use core::fmt;
|
||||
use enumset::EnumSet;
|
||||
use gc_arena::{GcCell, MutationContext};
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub const TYPE_OF_OBJECT: &str = "object";
|
||||
pub const TYPE_OF_FUNCTION: &str = "function";
|
||||
pub const TYPE_OF_MOVIE_CLIP: &str = "movieclip";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ScriptObject<'gc> {
|
||||
prototype: Option<ObjectCell<'gc>>,
|
||||
display_node: Option<DisplayNode<'gc>>,
|
||||
values: HashMap<String, Property<'gc>>,
|
||||
function: Option<Executable<'gc>>,
|
||||
type_of: &'static str,
|
||||
}
|
||||
|
||||
unsafe impl<'gc> gc_arena::Collect for ScriptObject<'gc> {
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
self.prototype.trace(cc);
|
||||
self.display_node.trace(cc);
|
||||
self.values.trace(cc);
|
||||
self.function.trace(cc);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ScriptObject<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("Object")
|
||||
.field("prototype", &self.prototype)
|
||||
.field("display_node", &self.display_node)
|
||||
.field("values", &self.values)
|
||||
.field("function", &self.function.is_some())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> ScriptObject<'gc> {
|
||||
pub fn object(
|
||||
_gc_context: MutationContext<'gc, '_>,
|
||||
proto: Option<ObjectCell<'gc>>,
|
||||
) -> ScriptObject<'gc> {
|
||||
ScriptObject {
|
||||
prototype: proto,
|
||||
type_of: TYPE_OF_OBJECT,
|
||||
display_node: None,
|
||||
values: HashMap::new(),
|
||||
function: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs and allocates an empty but normal object in one go.
|
||||
pub fn object_cell(
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
proto: Option<ObjectCell<'gc>>,
|
||||
) -> ObjectCell<'gc> {
|
||||
GcCell::allocate(
|
||||
gc_context,
|
||||
Box::new(ScriptObject {
|
||||
prototype: proto,
|
||||
type_of: TYPE_OF_OBJECT,
|
||||
display_node: None,
|
||||
values: HashMap::new(),
|
||||
function: None,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Constructs an object with no values, not even builtins.
|
||||
///
|
||||
/// Intended for constructing scope chains, since they exclusively use the
|
||||
/// object values, but can't just have a hashmap because of `with` and
|
||||
/// friends.
|
||||
pub fn bare_object() -> Self {
|
||||
ScriptObject {
|
||||
prototype: None,
|
||||
type_of: TYPE_OF_OBJECT,
|
||||
display_node: None,
|
||||
values: HashMap::new(),
|
||||
function: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a function sans prototype.
|
||||
pub fn bare_function(
|
||||
function: impl Into<Executable<'gc>>,
|
||||
fn_proto: Option<ObjectCell<'gc>>,
|
||||
) -> Self {
|
||||
ScriptObject {
|
||||
prototype: fn_proto,
|
||||
type_of: TYPE_OF_FUNCTION,
|
||||
function: Some(function.into()),
|
||||
display_node: None,
|
||||
values: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a function from an executable and associated protos.
|
||||
///
|
||||
/// Since prototypes need to link back to themselves, this function builds
|
||||
/// both objects itself and returns the function to you, fully allocated.
|
||||
///
|
||||
/// `fn_proto` refers to the implicit proto of the function object, and the
|
||||
/// `prototype` refers to the explicit prototype of the function. If
|
||||
/// provided, the function and it's prototype will be linked to each other.
|
||||
pub fn function(
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
function: impl Into<Executable<'gc>>,
|
||||
fn_proto: Option<ObjectCell<'gc>>,
|
||||
prototype: Option<ObjectCell<'gc>>,
|
||||
) -> ObjectCell<'gc> {
|
||||
let function = GcCell::allocate(
|
||||
gc_context,
|
||||
Box::new(Self::bare_function(function, fn_proto)) as Box<dyn Object<'gc> + 'gc>,
|
||||
);
|
||||
|
||||
//TODO: Can we make these proper sets or no?
|
||||
if let Some(p) = prototype {
|
||||
p.write(gc_context).define_value(
|
||||
"constructor",
|
||||
Value::Object(function),
|
||||
Attribute::DontEnum.into(),
|
||||
);
|
||||
function
|
||||
.write(gc_context)
|
||||
.define_value("prototype", p.into(), EnumSet::empty());
|
||||
}
|
||||
|
||||
function
|
||||
}
|
||||
|
||||
pub fn set_display_node(&mut self, display_node: DisplayNode<'gc>) {
|
||||
self.display_node = Some(display_node);
|
||||
}
|
||||
|
||||
pub fn display_node(&self) -> Option<DisplayNode<'gc>> {
|
||||
self.display_node
|
||||
}
|
||||
|
||||
/// Declare a native function on the current object.
|
||||
///
|
||||
/// This is intended for use with defining host object prototypes. Notably,
|
||||
/// this creates a function object without an explicit `prototype`, which
|
||||
/// is only possible when defining host functions. User-defined functions
|
||||
/// always get a fresh explicit prototype, so you should never force set a
|
||||
/// user-defined function.
|
||||
pub fn force_set_function<A>(
|
||||
&mut self,
|
||||
name: &str,
|
||||
function: NativeFunction<'gc>,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
attributes: A,
|
||||
fn_proto: Option<ObjectCell<'gc>>,
|
||||
) where
|
||||
A: Into<EnumSet<Attribute>>,
|
||||
{
|
||||
self.define_value(
|
||||
name,
|
||||
Value::Object(ScriptObject::function(gc_context, function, fn_proto, None)),
|
||||
attributes.into(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_prototype(&mut self, prototype: ObjectCell<'gc>) {
|
||||
self.prototype = Some(prototype);
|
||||
}
|
||||
|
||||
pub fn set_type_of(&mut self, type_of: &'static str) {
|
||||
self.type_of = type_of;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> Object<'gc> for ScriptObject<'gc> {
|
||||
/// Get the value of a particular property on this object.
|
||||
///
|
||||
/// The `avm`, `context`, and `this` parameters exist so that this object
|
||||
/// can call virtual properties. Furthermore, since some virtual properties
|
||||
/// may resolve on the AVM stack, this function may return `None` instead
|
||||
/// of a `Value`. *This is not equivalent to `undefined`.* Instead, it is a
|
||||
/// signal that your value will be returned on the ActionScript stack, and
|
||||
/// that you should register a stack continuation in order to get it.
|
||||
fn get_local(
|
||||
&self,
|
||||
name: &str,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if name == "__proto__" {
|
||||
return Ok(self
|
||||
.prototype
|
||||
.map_or(Value::Undefined, Value::Object)
|
||||
.into());
|
||||
}
|
||||
|
||||
if let Some(value) = self.values.get(name) {
|
||||
return value.get(avm, context, this);
|
||||
}
|
||||
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
/// Set a named property on the object.
|
||||
///
|
||||
/// This function takes a redundant `this` parameter which should be
|
||||
/// the object's own `GcCell`, so that it can pass it to user-defined
|
||||
/// overrides that may need to interact with the underlying object.
|
||||
fn set(
|
||||
&mut self,
|
||||
name: &str,
|
||||
value: Value<'gc>,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
) -> Result<(), Error> {
|
||||
if name == "__proto__" {
|
||||
self.prototype = value.as_object().ok().to_owned();
|
||||
} else {
|
||||
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,
|
||||
attributes: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Call the underlying object.
|
||||
///
|
||||
/// This function takes a redundant `this` parameter which should be
|
||||
/// the object's own `GcCell`, so that it can pass it to user-defined
|
||||
/// overrides that may need to interact with the underlying object.
|
||||
fn call(
|
||||
&self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(function) = &self.function {
|
||||
function.exec(avm, context, this, args)
|
||||
} else {
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
fn new(
|
||||
&self,
|
||||
_avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: ObjectCell<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<ObjectCell<'gc>, Error> {
|
||||
Ok(GcCell::allocate(
|
||||
context.gc_context,
|
||||
Box::new(ScriptObject::object(context.gc_context, Some(this))) as Box<dyn Object<'gc>>,
|
||||
))
|
||||
}
|
||||
|
||||
/// Delete a named property from the object.
|
||||
///
|
||||
/// Returns false if the property cannot be deleted.
|
||||
fn delete(&mut self, name: &str) -> bool {
|
||||
if let Some(prop) = self.values.get(name) {
|
||||
if prop.can_delete() {
|
||||
self.values.remove(name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn add_property(
|
||||
&mut self,
|
||||
name: &str,
|
||||
get: Executable<'gc>,
|
||||
set: Option<Executable<'gc>>,
|
||||
attributes: EnumSet<Attribute>,
|
||||
) {
|
||||
self.values.insert(
|
||||
name.to_owned(),
|
||||
Property::Virtual {
|
||||
get,
|
||||
set,
|
||||
attributes,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn define_value(&mut self, name: &str, value: Value<'gc>, attributes: EnumSet<Attribute>) {
|
||||
self.values
|
||||
.insert(name.to_string(), Property::Stored { value, attributes });
|
||||
}
|
||||
|
||||
fn proto(&self) -> Option<ObjectCell<'gc>> {
|
||||
self.prototype
|
||||
}
|
||||
|
||||
/// Checks if the object has a given named property.
|
||||
fn has_property(&self, name: &str) -> bool {
|
||||
self.has_own_property(name)
|
||||
|| self
|
||||
.prototype
|
||||
.as_ref()
|
||||
.map_or(false, |p| p.read().has_property(name))
|
||||
}
|
||||
|
||||
/// Checks if the object has a given named property on itself (and not,
|
||||
/// say, the object's prototype or superclass)
|
||||
fn has_own_property(&self, name: &str) -> bool {
|
||||
if name == "__proto__" {
|
||||
return true;
|
||||
}
|
||||
self.values.contains_key(name)
|
||||
}
|
||||
|
||||
fn is_property_overwritable(&self, name: &str) -> bool {
|
||||
self.values
|
||||
.get(name)
|
||||
.map(|p| p.is_overwritable())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Checks if a named property appears when enumerating the object.
|
||||
fn is_property_enumerable(&self, name: &str) -> bool {
|
||||
if let Some(prop) = self.values.get(name) {
|
||||
prop.is_enumerable()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumerate the object.
|
||||
fn get_keys(&self) -> HashSet<String> {
|
||||
let mut result = self
|
||||
.prototype
|
||||
.map_or_else(HashSet::new, |p| p.read().get_keys());
|
||||
|
||||
self.values
|
||||
.iter()
|
||||
.filter_map(|(k, p)| {
|
||||
if p.is_enumerable() {
|
||||
Some(k.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.for_each(|k| {
|
||||
result.insert(k);
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn as_string(&self) -> String {
|
||||
if self.function.is_some() {
|
||||
"[type Function]".to_string()
|
||||
} else {
|
||||
"[object Object]".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn type_of(&self) -> &'static str {
|
||||
self.type_of
|
||||
}
|
||||
|
||||
fn as_script_object(&self) -> Option<&ScriptObject<'gc>> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn as_script_object_mut(&mut self) -> Option<&mut ScriptObject<'gc>> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
/// Get the underlying display node for this object, if it exists.
|
||||
fn as_display_node(&self) -> Option<DisplayNode<'gc>> {
|
||||
self.display_node
|
||||
}
|
||||
|
||||
/// Returns a copy of a given function.
|
||||
///
|
||||
/// TODO: We have to clone here because of how executables are stored on
|
||||
/// objects directly. This might not be a good idea for performance.
|
||||
fn as_executable(&self) -> Option<Executable<'gc>> {
|
||||
self.function.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::avm1::activation::Activation;
|
||||
use crate::avm1::property::Attribute::*;
|
||||
use crate::backend::audio::NullAudioBackend;
|
||||
use crate::backend::navigator::NullNavigatorBackend;
|
||||
use crate::backend::render::NullRenderer;
|
||||
use crate::display_object::{DisplayObject, MovieClip};
|
||||
use crate::library::Library;
|
||||
use crate::prelude::*;
|
||||
use gc_arena::rootless_arena;
|
||||
use rand::{rngs::SmallRng, SeedableRng};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn with_object<F, R>(swf_version: u8, test: F) -> R
|
||||
where
|
||||
F: for<'a, 'gc> FnOnce(
|
||||
&mut Avm1<'gc>,
|
||||
&mut UpdateContext<'a, 'gc, '_>,
|
||||
ObjectCell<'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(swf_version, gc_context));
|
||||
let root = GcCell::allocate(gc_context, movie_clip);
|
||||
let mut context = UpdateContext {
|
||||
gc_context,
|
||||
global_time: 0,
|
||||
player_version: 32,
|
||||
swf_version,
|
||||
root,
|
||||
start_clip: root,
|
||||
active_clip: root,
|
||||
target_clip: Some(root),
|
||||
target_path: Value::Undefined,
|
||||
rng: &mut SmallRng::from_seed([0u8; 16]),
|
||||
action_queue: &mut crate::context::ActionQueue::new(),
|
||||
audio: &mut NullAudioBackend::new(),
|
||||
background_color: &mut Color {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 0,
|
||||
},
|
||||
library: &mut Library::new(),
|
||||
navigator: &mut NullNavigatorBackend::new(),
|
||||
renderer: &mut NullRenderer::new(),
|
||||
swf_data: &mut Arc::new(vec![]),
|
||||
system_prototypes: avm.prototypes().clone(),
|
||||
};
|
||||
|
||||
let object = GcCell::allocate(
|
||||
gc_context,
|
||||
Box::new(ScriptObject::object(
|
||||
gc_context,
|
||||
Some(avm.prototypes().object),
|
||||
)) as Box<dyn Object<'_>>,
|
||||
);
|
||||
|
||||
let globals = avm.global_object_cell();
|
||||
avm.insert_stack_frame(GcCell::allocate(
|
||||
gc_context,
|
||||
Activation::from_nothing(swf_version, globals, 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)
|
||||
.unwrap(),
|
||||
ReturnValue::Immediate(Value::Undefined)
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_get() {
|
||||
with_object(0, |avm, context, object| {
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.define_value("forced", "forced".into(), EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.set("natural", "natural".into(), avm, context, object)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
object.read().get("forced", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("forced".into())
|
||||
);
|
||||
assert_eq!(
|
||||
object.read().get("natural", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("natural".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_readonly() {
|
||||
with_object(0, |avm, context, object| {
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.define_value("normal", "initial".into(), EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.define_value("readonly", "initial".into(), ReadOnly.into());
|
||||
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.set("normal", "replaced".into(), avm, context, object)
|
||||
.unwrap();
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.set("readonly", "replaced".into(), avm, context, object)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
object.read().get("normal", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("replaced".into())
|
||||
);
|
||||
assert_eq!(
|
||||
object.read().get("readonly", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("initial".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deletable_not_readonly() {
|
||||
with_object(0, |avm, context, object| {
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.define_value("test", "initial".into(), DontDelete.into());
|
||||
|
||||
assert_eq!(object.write(context.gc_context).delete("test"), false);
|
||||
assert_eq!(
|
||||
object.read().get("test", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("initial".into())
|
||||
);
|
||||
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.set("test", "replaced".into(), avm, context, object)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(object.write(context.gc_context).delete("test"), false);
|
||||
assert_eq!(
|
||||
object.read().get("test", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("replaced".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_virtual_get() {
|
||||
with_object(0, |avm, context, object| {
|
||||
let getter = Executable::Native(|_avm, _context, _this, _args| {
|
||||
Ok(ReturnValue::Immediate("Virtual!".into()))
|
||||
});
|
||||
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.add_property("test", getter, None, EnumSet::empty());
|
||||
|
||||
assert_eq!(
|
||||
object.read().get("test", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("Virtual!".into())
|
||||
);
|
||||
|
||||
// This set should do nothing
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.set("test", "Ignored!".into(), avm, context, object)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
object.read().get("test", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate("Virtual!".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete() {
|
||||
with_object(0, |avm, context, object| {
|
||||
let getter = Executable::Native(|_avm, _context, _this, _args| {
|
||||
Ok(ReturnValue::Immediate("Virtual!".into()))
|
||||
});
|
||||
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.add_property("virtual", getter.clone(), None, EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.add_property("virtual_un", getter, None, DontDelete.into());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.define_value("stored", "Stored!".into(), EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.define_value("stored_un", "Stored!".into(), DontDelete.into());
|
||||
|
||||
assert_eq!(object.write(context.gc_context).delete("virtual"), true);
|
||||
assert_eq!(object.write(context.gc_context).delete("virtual_un"), false);
|
||||
assert_eq!(object.write(context.gc_context).delete("stored"), true);
|
||||
assert_eq!(object.write(context.gc_context).delete("stored_un"), false);
|
||||
assert_eq!(
|
||||
object.write(context.gc_context).delete("non_existent"),
|
||||
false
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
object.read().get("virtual", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate(Value::Undefined)
|
||||
);
|
||||
assert_eq!(
|
||||
object
|
||||
.read()
|
||||
.get("virtual_un", avm, context, object)
|
||||
.unwrap(),
|
||||
ReturnValue::Immediate("Virtual!".into())
|
||||
);
|
||||
assert_eq!(
|
||||
object.read().get("stored", avm, context, object).unwrap(),
|
||||
ReturnValue::Immediate(Value::Undefined)
|
||||
);
|
||||
assert_eq!(
|
||||
object
|
||||
.read()
|
||||
.get("stored_un", avm, context, object)
|
||||
.unwrap(),
|
||||
ReturnValue::Immediate("Stored!".into())
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_values() {
|
||||
with_object(0, |_avm, context, object| {
|
||||
let getter = Executable::Native(|_avm, _context, _this, _args| {
|
||||
Ok(ReturnValue::Immediate(Value::Null))
|
||||
});
|
||||
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.define_value("stored", Value::Null, EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.define_value("stored_hidden", Value::Null, DontEnum.into());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.add_property("virtual", getter.clone(), None, EnumSet::empty());
|
||||
object
|
||||
.write(context.gc_context)
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.add_property("virtual_hidden", getter, None, DontEnum.into());
|
||||
|
||||
let keys = object.read().get_keys();
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert_eq!(keys.contains(&"stored".to_string()), true);
|
||||
assert_eq!(keys.contains(&"stored_hidden".to_string()), false);
|
||||
assert_eq!(keys.contains(&"virtual".to_string()), true);
|
||||
assert_eq!(keys.contains(&"virtual_hidden".to_string()), false);
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
use crate::avm1::activation::Activation;
|
||||
use crate::avm1::{Avm1, Object, UpdateContext, Value};
|
||||
use crate::avm1::{Avm1, ObjectCell, UpdateContext, Value};
|
||||
use crate::backend::audio::NullAudioBackend;
|
||||
use crate::backend::navigator::NullNavigatorBackend;
|
||||
use crate::backend::render::NullRenderer;
|
||||
|
@ -7,19 +7,18 @@ use crate::context::ActionQueue;
|
|||
use crate::display_object::{DisplayObject, MovieClip};
|
||||
use crate::library::Library;
|
||||
use crate::prelude::*;
|
||||
use gc_arena::{rootless_arena, GcCell};
|
||||
use gc_arena::{rootless_arena, GcCell, MutationContext};
|
||||
use rand::{rngs::SmallRng, SeedableRng};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn with_avm<F, R>(swf_version: u8, test: F) -> R
|
||||
where
|
||||
F: for<'a, 'gc> FnOnce(
|
||||
&mut Avm1<'gc>,
|
||||
&mut UpdateContext<'a, 'gc, '_>,
|
||||
GcCell<'gc, Object<'gc>>,
|
||||
) -> R,
|
||||
F: for<'a, 'gc> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>, ObjectCell<'gc>) -> R,
|
||||
{
|
||||
rootless_arena(|gc_context| {
|
||||
fn in_the_arena<'gc, F, R>(swf_version: u8, test: F, gc_context: MutationContext<'gc, '_>) -> R
|
||||
where
|
||||
F: for<'a> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>, ObjectCell<'gc>) -> R,
|
||||
{
|
||||
let mut avm = Avm1::new(gc_context, swf_version);
|
||||
let movie_clip: Box<dyn DisplayObject> = Box::new(MovieClip::new(swf_version, gc_context));
|
||||
let root = GcCell::allocate(gc_context, movie_clip);
|
||||
|
@ -46,6 +45,7 @@ where
|
|||
navigator: &mut NullNavigatorBackend::new(),
|
||||
renderer: &mut NullRenderer::new(),
|
||||
swf_data: &mut Arc::new(vec![]),
|
||||
system_prototypes: avm.prototypes().clone(),
|
||||
};
|
||||
|
||||
let globals = avm.global_object_cell();
|
||||
|
@ -57,5 +57,7 @@ where
|
|||
let this = root.read().object().as_object().unwrap().to_owned();
|
||||
|
||||
test(&mut avm, &mut context, this)
|
||||
})
|
||||
}
|
||||
|
||||
rootless_arena(|gc_context| in_the_arena(swf_version, test, gc_context))
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@ fn locals_into_form_values() {
|
|||
|
||||
my_locals
|
||||
.write(context.gc_context)
|
||||
.set("value1", "string", avm, context, my_locals)
|
||||
.set("value1", "string".into(), avm, context, my_locals)
|
||||
.unwrap();
|
||||
my_locals
|
||||
.write(context.gc_context)
|
||||
.set("value2", 2.0, avm, context, my_locals)
|
||||
.set("value2", 2.0.into(), avm, context, my_locals)
|
||||
.unwrap();
|
||||
|
||||
avm.insert_stack_frame(GcCell::allocate(context.gc_context, my_activation));
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::avm1::object::Object;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, UpdateContext};
|
||||
use gc_arena::GcCell;
|
||||
use crate::avm1::{Avm1, Error, ObjectCell, UpdateContext};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
|
@ -11,7 +9,7 @@ pub enum Value<'gc> {
|
|||
Bool(bool),
|
||||
Number(f64),
|
||||
String(String),
|
||||
Object(GcCell<'gc, Object<'gc>>),
|
||||
Object(ObjectCell<'gc>),
|
||||
}
|
||||
|
||||
impl<'gc> From<String> for Value<'gc> {
|
||||
|
@ -32,8 +30,8 @@ impl<'gc> From<bool> for Value<'gc> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'gc> From<GcCell<'gc, Object<'gc>>> for Value<'gc> {
|
||||
fn from(object: GcCell<'gc, Object<'gc>>) -> Self {
|
||||
impl<'gc> From<ObjectCell<'gc>> for Value<'gc> {
|
||||
fn from(object: ObjectCell<'gc>) -> Self {
|
||||
Value::Object(object)
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +66,12 @@ impl<'gc> From<u32> for Value<'gc> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'gc> From<usize> for Value<'gc> {
|
||||
fn from(value: usize) -> Self {
|
||||
Value::Number(value as f64)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'gc> gc_arena::Collect for Value<'gc> {
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
if let Value::Object(object) = self {
|
||||
|
@ -230,6 +234,10 @@ impl<'gc> Value<'gc> {
|
|||
self.as_f64().map(|n| n as i64)
|
||||
}
|
||||
|
||||
pub fn as_usize(&self) -> Result<usize, Error> {
|
||||
self.as_f64().map(|n| n as usize)
|
||||
}
|
||||
|
||||
pub fn as_f64(&self) -> Result<f64, Error> {
|
||||
match *self {
|
||||
Value::Number(v) => Ok(v),
|
||||
|
@ -244,7 +252,7 @@ impl<'gc> Value<'gc> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn as_object(&self) -> Result<GcCell<'gc, Object<'gc>>, Error> {
|
||||
pub fn as_object(&self) -> Result<ObjectCell<'gc>, Error> {
|
||||
if let Value::Object(object) = self {
|
||||
Ok(object.to_owned())
|
||||
} else {
|
||||
|
@ -256,7 +264,7 @@ impl<'gc> Value<'gc> {
|
|||
&self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: GcCell<'gc, Object<'gc>>,
|
||||
this: ObjectCell<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Value::Object(object) = self {
|
||||
|
|
|
@ -81,6 +81,10 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> {
|
|||
/// request, but this requires us to implement auto-generated
|
||||
/// _names ("instanceN" etc. for unnamed clips).
|
||||
pub target_path: avm1::Value<'gc>,
|
||||
|
||||
/// The current set of system-specified prototypes to use when constructing
|
||||
/// new built-in objects.
|
||||
pub system_prototypes: avm1::SystemPrototypes<'gc>,
|
||||
}
|
||||
|
||||
/// A queued ActionScript call.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::avm1::Value;
|
||||
use crate::avm1::{ObjectCell, Value};
|
||||
use crate::context::{RenderContext, UpdateContext};
|
||||
use crate::player::NEWEST_PLAYER_VERSION;
|
||||
use crate::prelude::*;
|
||||
|
@ -259,6 +259,7 @@ pub trait DisplayObject<'gc>: 'gc + Collect + Debug {
|
|||
&mut self,
|
||||
_gc_context: MutationContext<'gc, '_>,
|
||||
_display_object: DisplayNode<'gc>,
|
||||
_proto: ObjectCell<'gc>,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
|
@ -73,10 +73,11 @@ impl<'gc> Button<'gc> {
|
|||
self.children.clear();
|
||||
for record in &self.static_data.records {
|
||||
if record.states.contains(&swf_state) {
|
||||
if let Ok(child) = context
|
||||
.library
|
||||
.instantiate_display_object(record.id, context.gc_context)
|
||||
{
|
||||
if let Ok(child) = context.library.instantiate_display_object(
|
||||
record.id,
|
||||
context.gc_context,
|
||||
&context.system_prototypes,
|
||||
) {
|
||||
child
|
||||
.write(context.gc_context)
|
||||
.set_parent(Some(context.active_clip));
|
||||
|
@ -161,10 +162,11 @@ impl<'gc> DisplayObject<'gc> for Button<'gc> {
|
|||
|
||||
for record in &self.static_data.records {
|
||||
if record.states.contains(&swf::ButtonState::HitTest) {
|
||||
match context
|
||||
.library
|
||||
.instantiate_display_object(record.id, context.gc_context)
|
||||
{
|
||||
match context.library.instantiate_display_object(
|
||||
record.id,
|
||||
context.gc_context,
|
||||
&context.system_prototypes,
|
||||
) {
|
||||
Ok(child) => {
|
||||
{
|
||||
let mut child = child.write(context.gc_context);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
//! `MovieClip` display object and support code.
|
||||
use crate::avm1::movie_clip::create_movie_object;
|
||||
use crate::avm1::object::{Object, TYPE_OF_MOVIE_CLIP};
|
||||
use crate::avm1::Value;
|
||||
use crate::avm1::script_object::TYPE_OF_MOVIE_CLIP;
|
||||
use crate::avm1::{ObjectCell, ScriptObject, Value};
|
||||
use crate::backend::audio::AudioStreamHandle;
|
||||
use crate::character::Character;
|
||||
use crate::context::{RenderContext, UpdateContext};
|
||||
|
@ -34,7 +33,7 @@ pub struct MovieClip<'gc> {
|
|||
current_frame: FrameNumber,
|
||||
audio_stream: Option<AudioStreamHandle>,
|
||||
children: BTreeMap<Depth, DisplayNode<'gc>>,
|
||||
object: GcCell<'gc, Object<'gc>>,
|
||||
object: ObjectCell<'gc>,
|
||||
}
|
||||
|
||||
impl<'gc> MovieClip<'gc> {
|
||||
|
@ -48,7 +47,7 @@ impl<'gc> MovieClip<'gc> {
|
|||
current_frame: 0,
|
||||
audio_stream: None,
|
||||
children: BTreeMap::new(),
|
||||
object: GcCell::allocate(gc_context, create_movie_object(gc_context)),
|
||||
object: GcCell::allocate(gc_context, Box::new(ScriptObject::bare_object())),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +78,7 @@ impl<'gc> MovieClip<'gc> {
|
|||
current_frame: 0,
|
||||
audio_stream: None,
|
||||
children: BTreeMap::new(),
|
||||
object: GcCell::allocate(gc_context, create_movie_object(gc_context)),
|
||||
object: GcCell::allocate(gc_context, Box::new(ScriptObject::bare_object())),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,10 +302,11 @@ impl<'gc> MovieClip<'gc> {
|
|||
depth: Depth,
|
||||
copy_previous_properties: bool,
|
||||
) -> Option<DisplayNode<'gc>> {
|
||||
if let Ok(child_cell) = context
|
||||
.library
|
||||
.instantiate_display_object(id, context.gc_context)
|
||||
{
|
||||
if let Ok(child_cell) = context.library.instantiate_display_object(
|
||||
id,
|
||||
context.gc_context,
|
||||
&context.system_prototypes,
|
||||
) {
|
||||
// Remove previous child from children list,
|
||||
// and add new childonto front of the list.
|
||||
let prev_child = self.children.insert(depth, child_cell);
|
||||
|
@ -626,10 +626,18 @@ impl<'gc> DisplayObject<'gc> for MovieClip<'gc> {
|
|||
&mut self,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
display_object: DisplayNode<'gc>,
|
||||
proto: ObjectCell<'gc>,
|
||||
) {
|
||||
let mut object = self.object.write(gc_context);
|
||||
object.set_display_node(display_object);
|
||||
object.set_type_of(TYPE_OF_MOVIE_CLIP);
|
||||
object
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.set_display_node(display_object);
|
||||
object
|
||||
.as_script_object_mut()
|
||||
.unwrap()
|
||||
.set_type_of(TYPE_OF_MOVIE_CLIP);
|
||||
object.as_script_object_mut().unwrap().set_prototype(proto);
|
||||
}
|
||||
|
||||
fn object(&self) -> Value<'gc> {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use crate::avm1::globals::SystemPrototypes;
|
||||
use crate::avm1::ObjectCell;
|
||||
use crate::backend::audio::SoundHandle;
|
||||
use crate::character::Character;
|
||||
use crate::display_object::DisplayObject;
|
||||
|
@ -47,15 +49,19 @@ impl<'gc> Library<'gc> {
|
|||
&self,
|
||||
id: CharacterId,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
prototypes: &SystemPrototypes<'gc>,
|
||||
) -> Result<DisplayNode<'gc>, Box<dyn std::error::Error>> {
|
||||
let obj: Box<dyn DisplayObject<'gc>> = match self.characters.get(&id) {
|
||||
Some(Character::Bitmap(bitmap)) => bitmap.clone(),
|
||||
Some(Character::EditText(edit_text)) => edit_text.clone(),
|
||||
Some(Character::Graphic(graphic)) => graphic.clone(),
|
||||
Some(Character::MorphShape(morph_shape)) => morph_shape.clone(),
|
||||
Some(Character::MovieClip(movie_clip)) => movie_clip.clone(),
|
||||
Some(Character::Button(button)) => button.clone(),
|
||||
Some(Character::Text(text)) => text.clone(),
|
||||
let (obj, proto): (Box<dyn DisplayObject<'gc>>, ObjectCell<'gc>) = match self
|
||||
.characters
|
||||
.get(&id)
|
||||
{
|
||||
Some(Character::Bitmap(bitmap)) => (bitmap.clone(), prototypes.object),
|
||||
Some(Character::EditText(edit_text)) => (edit_text.clone(), prototypes.object),
|
||||
Some(Character::Graphic(graphic)) => (graphic.clone(), prototypes.object),
|
||||
Some(Character::MorphShape(morph_shape)) => (morph_shape.clone(), prototypes.object),
|
||||
Some(Character::MovieClip(movie_clip)) => (movie_clip.clone(), prototypes.movie_clip),
|
||||
Some(Character::Button(button)) => (button.clone(), prototypes.object),
|
||||
Some(Character::Text(text)) => (text.clone(), prototypes.object),
|
||||
Some(_) => return Err("Not a DisplayObject".into()),
|
||||
None => {
|
||||
log::error!("Tried to instantiate non-registered character ID {}", id);
|
||||
|
@ -65,7 +71,7 @@ impl<'gc> Library<'gc> {
|
|||
let result = GcCell::allocate(gc_context, obj);
|
||||
result
|
||||
.write(gc_context)
|
||||
.post_instantiation(gc_context, result);
|
||||
.post_instantiation(gc_context, result, proto);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::avm1::{self, Avm1};
|
||||
use crate::avm1::{Avm1, Value};
|
||||
use crate::backend::{
|
||||
audio::AudioBackend, navigator::NavigatorBackend, render::Letterbox, render::RenderBackend,
|
||||
};
|
||||
|
@ -188,10 +188,11 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
|
|||
};
|
||||
|
||||
player.gc_arena.mutate(|gc_context, gc_root| {
|
||||
gc_root
|
||||
.root
|
||||
.write(gc_context)
|
||||
.post_instantiation(gc_context, gc_root.root)
|
||||
gc_root.root.write(gc_context).post_instantiation(
|
||||
gc_context,
|
||||
gc_root.root,
|
||||
gc_root.avm.read().prototypes().movie_clip,
|
||||
)
|
||||
});
|
||||
|
||||
player.build_matrices();
|
||||
|
@ -336,7 +337,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
|
|||
start_clip: gc_root.root,
|
||||
target_clip: Some(gc_root.root),
|
||||
root: gc_root.root,
|
||||
target_path: avm1::Value::Undefined,
|
||||
target_path: Value::Undefined,
|
||||
system_prototypes: gc_root.avm.read().prototypes().clone(),
|
||||
};
|
||||
|
||||
if let Some(node) = &*gc_root.mouse_hover_node.read() {
|
||||
|
@ -414,7 +416,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
|
|||
start_clip: gc_root.root,
|
||||
target_clip: Some(gc_root.root),
|
||||
root: gc_root.root,
|
||||
target_path: avm1::Value::Undefined,
|
||||
target_path: Value::Undefined,
|
||||
system_prototypes: gc_root.avm.read().prototypes().clone(),
|
||||
};
|
||||
|
||||
// RollOut of previous node.
|
||||
|
@ -484,7 +487,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
|
|||
start_clip: gc_root.root,
|
||||
target_clip: Some(gc_root.root),
|
||||
root: gc_root.root,
|
||||
target_path: avm1::Value::Undefined,
|
||||
target_path: Value::Undefined,
|
||||
system_prototypes: gc_root.avm.read().prototypes().clone(),
|
||||
};
|
||||
|
||||
let mut morph_shapes = fnv::FnvHashMap::default();
|
||||
|
@ -547,7 +551,8 @@ impl<Audio: AudioBackend, Renderer: RenderBackend, Navigator: NavigatorBackend>
|
|||
start_clip: gc_root.root,
|
||||
target_clip: Some(gc_root.root),
|
||||
root: gc_root.root,
|
||||
target_path: avm1::Value::Undefined,
|
||||
target_path: Value::Undefined,
|
||||
system_prototypes: gc_root.avm.read().prototypes().clone(),
|
||||
};
|
||||
|
||||
gc_root
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use gc_arena::Collect;
|
||||
use std::sync::Arc;
|
||||
use swf::TagCode;
|
||||
|
||||
|
@ -5,7 +6,8 @@ pub type DecodeResult = Result<(), Box<dyn std::error::Error>>;
|
|||
pub type SwfStream<R> = swf::read::Reader<std::io::Cursor<R>>;
|
||||
|
||||
/// A shared-ownership reference to some portion of an immutable datastream.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Collect)]
|
||||
#[collect(no_drop)]
|
||||
pub struct SwfSlice {
|
||||
pub data: Arc<Vec<u8>>,
|
||||
pub start: usize,
|
||||
|
|
|
@ -60,14 +60,49 @@ swf_tests! {
|
|||
(timeline_function_def, "avm1/timeline_function_def", 3),
|
||||
(root_global_parent, "avm1/root_global_parent", 3),
|
||||
(register_underflow, "avm1/register_underflow", 1),
|
||||
(object_prototypes, "avm1/object_prototypes", 1),
|
||||
(movieclip_prototype_extension, "avm1/movieclip_prototype_extension", 1),
|
||||
(recursive_prototypes, "avm1/recursive_prototypes", 1),
|
||||
(has_own_property, "avm1/has_own_property", 1),
|
||||
#[ignore] (extends_chain, "avm1/extends_chain", 1),
|
||||
(is_prototype_of, "avm1/is_prototype_of", 1),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prototype_enumerate() -> Result<(), Error> {
|
||||
let trace_log = run_swf("tests/swfs/avm1/prototype_enumerate/test.swf", 1)?;
|
||||
let mut actual: Vec<String> = trace_log.lines().map(|s| s.to_string()).collect();
|
||||
let mut expected = vec!["a", "b", "c", "d", "e"];
|
||||
|
||||
actual.sort();
|
||||
expected.sort();
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads an SWF and runs it through the Ruffle core for a number of frames.
|
||||
/// Tests that the trace output matches the given expected output.
|
||||
fn test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Result<(), Error> {
|
||||
let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info));
|
||||
let expected_output = std::fs::read_to_string(expected_output_path)?.replace("\r\n", "\n");
|
||||
|
||||
let trace_log = run_swf(swf_path, num_frames)?;
|
||||
if trace_log != expected_output {
|
||||
println!(
|
||||
"Ruffle output:\n{}\nExpected output:\n{}",
|
||||
trace_log, expected_output
|
||||
);
|
||||
panic!("Ruffle output did not match expected output.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads an SWF and runs it through the Ruffle core for a number of frames.
|
||||
/// Tests that the trace output matches the given expected output.
|
||||
fn run_swf(swf_path: &str, num_frames: u32) -> Result<String, Error> {
|
||||
let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info));
|
||||
|
||||
let swf_data = std::fs::read(swf_path)?;
|
||||
let mut player = Player::new(
|
||||
NullRenderer,
|
||||
|
@ -80,16 +115,7 @@ fn test_swf(swf_path: &str, num_frames: u32, expected_output_path: &str) -> Resu
|
|||
player.run_frame();
|
||||
}
|
||||
|
||||
let trace_log = trace_log();
|
||||
if trace_log != expected_output {
|
||||
println!(
|
||||
"Ruffle output:\n{}\nExpected output:\n{}",
|
||||
trace_log, expected_output
|
||||
);
|
||||
panic!("Ruffle output did not match expected output.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(trace_log())
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
interface Blue {
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
class ChildA extends Super {
|
||||
function ChildA() {
|
||||
super();
|
||||
trace("ChildA constructor");
|
||||
}
|
||||
|
||||
function work() {
|
||||
super.work();
|
||||
trace("ChildA work");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
class ChildB extends Super implements Blue {
|
||||
function ChildB() {
|
||||
super();
|
||||
trace("ChildA constructor");
|
||||
}
|
||||
|
||||
function work() {
|
||||
super.work();
|
||||
trace("ChildA work");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
class GrandchildBA extends ChildB {
|
||||
function GrandchildBA() {
|
||||
super();
|
||||
trace("GrandchildBA constructor");
|
||||
}
|
||||
|
||||
function work() {
|
||||
super.work();
|
||||
trace("GrandchildBA work");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
class GrandchildBB extends ChildB implements Pink {
|
||||
function GrandchildBB() {
|
||||
super();
|
||||
trace("GrandchildBB constructor");
|
||||
}
|
||||
|
||||
function work() {
|
||||
super.work();
|
||||
trace("GrandchildBB work");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
interface Pink extends Red {
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
interface Red {
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
class Super {
|
||||
function Super() {
|
||||
trace("Super constructor");
|
||||
}
|
||||
|
||||
function work() {
|
||||
trace("Super work");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
// ChildA constructor
|
||||
Super constructor
|
||||
ChildA constructor
|
||||
|
||||
// ChildA work
|
||||
Super work
|
||||
ChildA work
|
||||
|
||||
// ChildA instanceof Super
|
||||
true
|
||||
|
||||
// ChildA instanceof ChildA
|
||||
true
|
||||
|
||||
// ChildA instanceof ChildB
|
||||
false
|
||||
|
||||
// ChildA instanceof GrandchildBA
|
||||
false
|
||||
|
||||
// ChildA instanceof GrandchildBB
|
||||
false
|
||||
|
||||
// ChildA instanceof Red
|
||||
false
|
||||
|
||||
// ChildA instanceof Blue
|
||||
false
|
||||
|
||||
// ChildA instanceof Pink
|
||||
false
|
||||
|
||||
|
||||
// ChildB constructor
|
||||
Super constructor
|
||||
ChildA constructor
|
||||
|
||||
// ChildB work
|
||||
Super work
|
||||
ChildA work
|
||||
|
||||
// ChildB instanceof Super
|
||||
true
|
||||
|
||||
// ChildB instanceof ChildA
|
||||
false
|
||||
|
||||
// ChildB instanceof ChildB
|
||||
true
|
||||
|
||||
// ChildB instanceof GrandchildBA
|
||||
false
|
||||
|
||||
// ChildB instanceof GrandchildBB
|
||||
false
|
||||
|
||||
// ChildB instanceof Red
|
||||
false
|
||||
|
||||
// ChildB instanceof Blue
|
||||
true
|
||||
|
||||
// ChildB instanceof Pink
|
||||
false
|
||||
|
||||
|
||||
// GrandchildBA constructor
|
||||
Super constructor
|
||||
ChildA constructor
|
||||
GrandchildBA constructor
|
||||
|
||||
// GrandchildBA work
|
||||
Super work
|
||||
ChildA work
|
||||
GrandchildBA work
|
||||
|
||||
// GrandchildBA instanceof Super
|
||||
true
|
||||
|
||||
// GrandchildBA instanceof ChildA
|
||||
false
|
||||
|
||||
// GrandchildBA instanceof ChildB
|
||||
true
|
||||
|
||||
// GrandchildBA instanceof GrandchildBA
|
||||
true
|
||||
|
||||
// GrandchildBA instanceof GrandchildBB
|
||||
false
|
||||
|
||||
// GrandchildBA instanceof Red
|
||||
false
|
||||
|
||||
// GrandchildBA instanceof Blue
|
||||
true
|
||||
|
||||
// GrandchildBA instanceof Pink
|
||||
false
|
||||
|
||||
|
||||
// GrandchildBB constructor
|
||||
Super constructor
|
||||
ChildA constructor
|
||||
GrandchildBB constructor
|
||||
|
||||
// GrandchildBB work
|
||||
Super work
|
||||
ChildA work
|
||||
GrandchildBB work
|
||||
|
||||
// GrandchildBB instanceof Super
|
||||
true
|
||||
|
||||
// GrandchildBB instanceof ChildA
|
||||
false
|
||||
|
||||
// GrandchildBB instanceof ChildB
|
||||
true
|
||||
|
||||
// GrandchildBB instanceof GrandchildBA
|
||||
false
|
||||
|
||||
// GrandchildBB instanceof GrandchildBB
|
||||
true
|
||||
|
||||
// GrandchildBB instanceof Red
|
||||
true
|
||||
|
||||
// GrandchildBB instanceof Blue
|
||||
true
|
||||
|
||||
// GrandchildBB instanceof Pink
|
||||
true
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,18 @@
|
|||
// base.hasOwnProperty("name")
|
||||
true
|
||||
|
||||
// child.hasOwnProperty("name")
|
||||
false
|
||||
|
||||
// base.hasOwnProperty("__proto__")
|
||||
true
|
||||
|
||||
// child.hasOwnProperty("__proto__")
|
||||
true
|
||||
|
||||
// base.hasOwnProperty("prototype")
|
||||
false
|
||||
|
||||
// child.hasOwnProperty("prototype")
|
||||
false
|
||||
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,90 @@
|
|||
// a.isPrototypeOf(a)
|
||||
false
|
||||
|
||||
// a.isPrototypeOf(b)
|
||||
true
|
||||
|
||||
// a.isPrototypeOf(c)
|
||||
false
|
||||
|
||||
// a.isPrototypeOf(d)
|
||||
false
|
||||
|
||||
// a.isPrototypeOf(Fun)
|
||||
false
|
||||
|
||||
// b.isPrototypeOf(a)
|
||||
false
|
||||
|
||||
// b.isPrototypeOf(b)
|
||||
false
|
||||
|
||||
// b.isPrototypeOf(c)
|
||||
false
|
||||
|
||||
// b.isPrototypeOf(d)
|
||||
false
|
||||
|
||||
// b.isPrototypeOf(Fun)
|
||||
false
|
||||
|
||||
// c.isPrototypeOf(a)
|
||||
false
|
||||
|
||||
// c.isPrototypeOf(b)
|
||||
false
|
||||
|
||||
// c.isPrototypeOf(c)
|
||||
false
|
||||
|
||||
// c.isPrototypeOf(d)
|
||||
true
|
||||
|
||||
// c.isPrototypeOf(Fun)
|
||||
false
|
||||
|
||||
// d.isPrototypeOf(a)
|
||||
false
|
||||
|
||||
// d.isPrototypeOf(b)
|
||||
false
|
||||
|
||||
// d.isPrototypeOf(c)
|
||||
false
|
||||
|
||||
// d.isPrototypeOf(d)
|
||||
false
|
||||
|
||||
// d.isPrototypeOf(Fun)
|
||||
false
|
||||
|
||||
// Fun.prototype.isPrototypeOf(a)
|
||||
false
|
||||
|
||||
// Fun.prototype.isPrototypeOf(b)
|
||||
false
|
||||
|
||||
// Fun.prototype.isPrototypeOf(c)
|
||||
true
|
||||
|
||||
// Fun.prototype.isPrototypeOf(d)
|
||||
true
|
||||
|
||||
// Fun.prototype.isPrototypeOf(Fun)
|
||||
false
|
||||
|
||||
// Object.prototype.isPrototypeOf(a)
|
||||
true
|
||||
|
||||
// Object.prototype.isPrototypeOf(b)
|
||||
true
|
||||
|
||||
// Object.prototype.isPrototypeOf(c)
|
||||
true
|
||||
|
||||
// Object.prototype.isPrototypeOf(d)
|
||||
true
|
||||
|
||||
// Object.prototype.isPrototypeOf(Fun)
|
||||
true
|
||||
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
// this.three
|
||||
undefined
|
||||
|
||||
// this.three
|
||||
3
|
||||
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,23 @@
|
|||
Base constructed!
|
||||
Base constructed!
|
||||
// a.name
|
||||
foo
|
||||
|
||||
// b.name
|
||||
foo
|
||||
|
||||
// c.name
|
||||
foo
|
||||
|
||||
|
||||
// a.name = "bar"
|
||||
|
||||
// a.name
|
||||
bar
|
||||
|
||||
// b.name
|
||||
bar
|
||||
|
||||
// c.name
|
||||
foo
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue