commit
f55e8036bb
143
core/src/avm1.rs
143
core/src/avm1.rs
|
@ -27,6 +27,7 @@ mod return_value;
|
|||
mod scope;
|
||||
pub mod script_object;
|
||||
mod stage_object;
|
||||
mod super_object;
|
||||
mod value;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -51,8 +52,9 @@ pub struct Avm1<'gc> {
|
|||
/// The Flash Player version we're emulating.
|
||||
player_version: u8,
|
||||
|
||||
/// The currently installed constant pool.
|
||||
constant_pool: Vec<String>,
|
||||
/// The constant pool to use for new activations from code sources that
|
||||
/// don't close over the constant pool they were defined with.
|
||||
constant_pool: GcCell<'gc, Vec<String>>,
|
||||
|
||||
/// The global object.
|
||||
globals: Object<'gc>,
|
||||
|
@ -78,6 +80,7 @@ unsafe impl<'gc> gc_arena::Collect for Avm1<'gc> {
|
|||
#[inline]
|
||||
fn trace(&self, cc: gc_arena::CollectionContext) {
|
||||
self.globals.trace(cc);
|
||||
self.constant_pool.trace(cc);
|
||||
self.prototypes.trace(cc);
|
||||
self.display_properties.trace(cc);
|
||||
self.stack_frames.trace(cc);
|
||||
|
@ -97,7 +100,7 @@ impl<'gc> Avm1<'gc> {
|
|||
|
||||
Self {
|
||||
player_version,
|
||||
constant_pool: vec![],
|
||||
constant_pool: GcCell::allocate(gc_context, vec![]),
|
||||
globals,
|
||||
prototypes,
|
||||
display_properties: stage_object::DisplayPropertyMap::new(gc_context),
|
||||
|
@ -170,7 +173,14 @@ impl<'gc> Avm1<'gc> {
|
|||
);
|
||||
self.stack_frames.push(GcCell::allocate(
|
||||
action_context.gc_context,
|
||||
Activation::from_action(swf_version, code, child_scope, clip_obj, None),
|
||||
Activation::from_action(
|
||||
swf_version,
|
||||
code,
|
||||
child_scope,
|
||||
self.constant_pool,
|
||||
clip_obj,
|
||||
None,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -197,7 +207,14 @@ impl<'gc> Avm1<'gc> {
|
|||
self.push(Value::Undefined);
|
||||
self.stack_frames.push(GcCell::allocate(
|
||||
action_context.gc_context,
|
||||
Activation::from_action(swf_version, code, child_scope, clip_obj, None),
|
||||
Activation::from_action(
|
||||
swf_version,
|
||||
code,
|
||||
child_scope,
|
||||
self.constant_pool,
|
||||
clip_obj,
|
||||
None,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -370,6 +387,7 @@ impl<'gc> Avm1<'gc> {
|
|||
Action::Call => self.action_call(context),
|
||||
Action::CallFunction => self.action_call_function(context),
|
||||
Action::CallMethod => self.action_call_method(context),
|
||||
Action::CastOp => self.action_cast_op(context),
|
||||
Action::CharToAscii => self.action_char_to_ascii(context),
|
||||
Action::CloneSprite => self.action_clone_sprite(context),
|
||||
Action::ConstantPool(constant_pool) => {
|
||||
|
@ -392,6 +410,7 @@ impl<'gc> Avm1<'gc> {
|
|||
Action::Enumerate2 => self.action_enumerate_2(context),
|
||||
Action::Equals => self.action_equals(context),
|
||||
Action::Equals2 => self.action_equals_2(context),
|
||||
Action::Extends => self.action_extends(context),
|
||||
Action::GetMember => self.action_get_member(context),
|
||||
Action::GetProperty => self.action_get_property(context),
|
||||
Action::GetTime => self.action_get_time(context),
|
||||
|
@ -415,6 +434,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::ImplementsOp => self.action_implements_op(context),
|
||||
Action::InstanceOf => self.action_instance_of(context),
|
||||
Action::Jump { offset } => self.action_jump(context, offset, reader),
|
||||
Action::Less => self.action_less(context),
|
||||
|
@ -808,12 +828,38 @@ impl<'gc> Avm1<'gc> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn action_cast_op(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
let obj = self.pop()?.as_object()?;
|
||||
let constr = self.pop()?.as_object()?;
|
||||
|
||||
let prototype = constr
|
||||
.get("prototype", self, context)?
|
||||
.resolve(self, context)?
|
||||
.as_object()?;
|
||||
|
||||
if obj.is_instance_of(self, context, constr, prototype)? {
|
||||
self.push(obj);
|
||||
} else {
|
||||
self.push(Value::Null);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_constant_pool(
|
||||
&mut self,
|
||||
_context: &mut UpdateContext,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
constant_pool: &[&str],
|
||||
) -> Result<(), Error> {
|
||||
self.constant_pool = constant_pool.iter().map(|s| s.to_string()).collect();
|
||||
self.constant_pool = GcCell::allocate(
|
||||
context.gc_context,
|
||||
constant_pool.iter().map(|s| s.to_string()).collect(),
|
||||
);
|
||||
self.current_stack_frame()
|
||||
.unwrap()
|
||||
.write(context.gc_context)
|
||||
.set_constant_pool(self.constant_pool);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -842,7 +888,9 @@ impl<'gc> Avm1<'gc> {
|
|||
self.current_stack_frame().unwrap().read().scope_cell(),
|
||||
context.gc_context,
|
||||
);
|
||||
let func = Avm1Function::from_df1(swf_version, func_data, name, params, scope);
|
||||
let constant_pool = self.current_stack_frame().unwrap().read().constant_pool();
|
||||
let func =
|
||||
Avm1Function::from_df1(swf_version, func_data, name, params, scope, constant_pool);
|
||||
let prototype =
|
||||
ScriptObject::object(context.gc_context, Some(self.prototypes.object)).into();
|
||||
let func_obj = ScriptObject::function(
|
||||
|
@ -880,7 +928,9 @@ impl<'gc> Avm1<'gc> {
|
|||
self.current_stack_frame().unwrap().read().scope_cell(),
|
||||
context.gc_context,
|
||||
);
|
||||
let func = Avm1Function::from_df2(swf_version, func_data, action_func, scope);
|
||||
let constant_pool = self.current_stack_frame().unwrap().read().constant_pool();
|
||||
let func =
|
||||
Avm1Function::from_df2(swf_version, func_data, action_func, scope, constant_pool);
|
||||
let prototype =
|
||||
ScriptObject::object(context.gc_context, Some(self.prototypes.object)).into();
|
||||
let func_obj = ScriptObject::function(
|
||||
|
@ -1034,6 +1084,27 @@ impl<'gc> Avm1<'gc> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn action_extends(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
let superclass = self.pop()?.as_object()?;
|
||||
let subclass = self.pop()?.as_object()?;
|
||||
|
||||
//TODO: What happens if we try to extend an object which has no `prototype`?
|
||||
//e.g. `class Whatever extends Object.prototype` or `class Whatever extends 5`
|
||||
let super_proto = superclass
|
||||
.get("prototype", self, context)?
|
||||
.resolve(self, context)
|
||||
.and_then(|val| val.as_object())
|
||||
.unwrap_or(self.prototypes.object);
|
||||
|
||||
let sub_prototype: Object<'gc> =
|
||||
ScriptObject::object(context.gc_context, Some(super_proto)).into();
|
||||
|
||||
sub_prototype.set("constructor", superclass.into(), self, context)?;
|
||||
subclass.set("prototype", sub_prototype.into(), self, context)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_get_member(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
|
||||
let name_val = self.pop()?;
|
||||
let name = name_val.coerce_to_string(self, context)?;
|
||||
|
@ -1324,6 +1395,30 @@ impl<'gc> Avm1<'gc> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn action_implements_op(
|
||||
&mut self,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
) -> Result<(), Error> {
|
||||
let constr = self.pop()?.as_object()?;
|
||||
let count = self.pop()?.as_i64()?; //TODO: Is this coercion actually performed by Flash?
|
||||
let mut interfaces = vec![];
|
||||
|
||||
//TODO: If one of the interfaces is not an object, do we leave the
|
||||
//whole stack dirty, or...?
|
||||
for _ in 0..count {
|
||||
interfaces.push(self.pop()?.as_object()?);
|
||||
}
|
||||
|
||||
let mut prototype = constr
|
||||
.get("prototype", self, context)?
|
||||
.resolve(self, context)?
|
||||
.as_object()?;
|
||||
|
||||
prototype.set_interfaces(context.gc_context, interfaces);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_instance_of(
|
||||
&mut self,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
|
@ -1331,23 +1426,13 @@ impl<'gc> Avm1<'gc> {
|
|||
let constr = self.pop()?.as_object()?;
|
||||
let obj = self.pop()?.as_object()?;
|
||||
|
||||
//TODO: Interface detection on SWF7
|
||||
let prototype = constr
|
||||
.get("prototype", self, context)?
|
||||
.resolve(self, context)?
|
||||
.as_object()?;
|
||||
let mut proto = obj.proto();
|
||||
let is_instance_of = obj.is_instance_of(self, context, constr, prototype)?;
|
||||
|
||||
while let Some(this_proto) = proto {
|
||||
if Object::ptr_eq(this_proto, prototype) {
|
||||
self.push(true);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
proto = this_proto.proto();
|
||||
}
|
||||
|
||||
self.push(false);
|
||||
self.push(is_instance_of);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -1583,13 +1668,25 @@ impl<'gc> Avm1<'gc> {
|
|||
SwfValue::Str(v) => v.to_string().into(),
|
||||
SwfValue::Register(v) => self.current_register(*v),
|
||||
SwfValue::ConstantPool(i) => {
|
||||
if let Some(value) = self.constant_pool.get(*i as usize) {
|
||||
if let Some(value) = self
|
||||
.current_stack_frame()
|
||||
.unwrap()
|
||||
.read()
|
||||
.constant_pool()
|
||||
.read()
|
||||
.get(*i as usize)
|
||||
{
|
||||
value.to_string().into()
|
||||
} else {
|
||||
log::warn!(
|
||||
"ActionPush: Constant pool index {} out of range (len = {})",
|
||||
i,
|
||||
self.constant_pool.len()
|
||||
self.current_stack_frame()
|
||||
.unwrap()
|
||||
.read()
|
||||
.constant_pool()
|
||||
.read()
|
||||
.len()
|
||||
);
|
||||
Value::Undefined
|
||||
}
|
||||
|
|
|
@ -66,6 +66,9 @@ pub struct Activation<'gc> {
|
|||
/// All defined local variables in this stack frame.
|
||||
scope: GcCell<'gc, Scope<'gc>>,
|
||||
|
||||
/// The currently in use constant pool.
|
||||
constant_pool: GcCell<'gc, Vec<String>>,
|
||||
|
||||
/// The immutable value of `this`.
|
||||
this: Object<'gc>,
|
||||
|
||||
|
@ -114,6 +117,7 @@ impl<'gc> Activation<'gc> {
|
|||
swf_version: u8,
|
||||
code: SwfSlice,
|
||||
scope: GcCell<'gc, Scope<'gc>>,
|
||||
constant_pool: GcCell<'gc, Vec<String>>,
|
||||
this: Object<'gc>,
|
||||
arguments: Option<Object<'gc>>,
|
||||
) -> Activation<'gc> {
|
||||
|
@ -122,6 +126,7 @@ impl<'gc> Activation<'gc> {
|
|||
data: code,
|
||||
pc: 0,
|
||||
scope,
|
||||
constant_pool,
|
||||
this,
|
||||
arguments,
|
||||
return_value: None,
|
||||
|
@ -135,6 +140,7 @@ impl<'gc> Activation<'gc> {
|
|||
swf_version: u8,
|
||||
code: SwfSlice,
|
||||
scope: GcCell<'gc, Scope<'gc>>,
|
||||
constant_pool: GcCell<'gc, Vec<String>>,
|
||||
this: Object<'gc>,
|
||||
arguments: Option<Object<'gc>>,
|
||||
) -> Activation<'gc> {
|
||||
|
@ -143,6 +149,7 @@ impl<'gc> Activation<'gc> {
|
|||
data: code,
|
||||
pc: 0,
|
||||
scope,
|
||||
constant_pool,
|
||||
this,
|
||||
arguments,
|
||||
return_value: None,
|
||||
|
@ -166,6 +173,7 @@ impl<'gc> Activation<'gc> {
|
|||
) -> Activation<'gc> {
|
||||
let global_scope = GcCell::allocate(mc, Scope::from_global_object(globals));
|
||||
let child_scope = GcCell::allocate(mc, Scope::new_local_scope(global_scope, mc));
|
||||
let empty_constant_pool = GcCell::allocate(mc, Vec::new());
|
||||
|
||||
Activation {
|
||||
swf_version,
|
||||
|
@ -176,6 +184,7 @@ impl<'gc> Activation<'gc> {
|
|||
},
|
||||
pc: 0,
|
||||
scope: child_scope,
|
||||
constant_pool: empty_constant_pool,
|
||||
this: globals,
|
||||
arguments: None,
|
||||
return_value: None,
|
||||
|
@ -192,6 +201,7 @@ impl<'gc> Activation<'gc> {
|
|||
data: code,
|
||||
pc: 0,
|
||||
scope,
|
||||
constant_pool: self.constant_pool,
|
||||
this: self.this,
|
||||
arguments: self.arguments,
|
||||
return_value: None,
|
||||
|
@ -342,6 +352,14 @@ impl<'gc> Activation<'gc> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn constant_pool(&self) -> GcCell<'gc, Vec<String>> {
|
||||
self.constant_pool
|
||||
}
|
||||
|
||||
pub fn set_constant_pool(&mut self, constant_pool: GcCell<'gc, Vec<String>>) {
|
||||
self.constant_pool = constant_pool;
|
||||
}
|
||||
|
||||
/// Attempts to lock the activation frame for execution.
|
||||
///
|
||||
/// If this frame is already executing, that is an error condition.
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::avm1::activation::Activation;
|
|||
use crate::avm1::property::Attribute::*;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::scope::Scope;
|
||||
use crate::avm1::super_object::SuperObject;
|
||||
use crate::avm1::value::Value;
|
||||
use crate::avm1::{Avm1, Error, Object, ScriptObject, TObject, UpdateContext};
|
||||
use crate::display_object::TDisplayObject;
|
||||
|
@ -66,6 +67,9 @@ pub struct Avm1Function<'gc> {
|
|||
|
||||
/// The scope the function was born into.
|
||||
scope: GcCell<'gc, Scope<'gc>>,
|
||||
|
||||
/// The constant pool the function executes with.
|
||||
constant_pool: GcCell<'gc, Vec<String>>,
|
||||
}
|
||||
|
||||
impl<'gc> Avm1Function<'gc> {
|
||||
|
@ -79,6 +83,7 @@ impl<'gc> Avm1Function<'gc> {
|
|||
name: &str,
|
||||
params: &[&str],
|
||||
scope: GcCell<'gc, Scope<'gc>>,
|
||||
constant_pool: GcCell<'gc, Vec<String>>,
|
||||
) -> Self {
|
||||
let name = match name {
|
||||
"" => None,
|
||||
|
@ -101,6 +106,7 @@ impl<'gc> Avm1Function<'gc> {
|
|||
preload_global: false,
|
||||
params: params.iter().map(|s| (None, s.to_string())).collect(),
|
||||
scope,
|
||||
constant_pool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,6 +116,7 @@ impl<'gc> Avm1Function<'gc> {
|
|||
actions: SwfSlice,
|
||||
swf_function: &swf::avm1::types::Function,
|
||||
scope: GcCell<'gc, Scope<'gc>>,
|
||||
constant_pool: GcCell<'gc, Vec<String>>,
|
||||
) -> Self {
|
||||
let name = match swf_function.name {
|
||||
"" => None,
|
||||
|
@ -141,6 +148,7 @@ impl<'gc> Avm1Function<'gc> {
|
|||
preload_global: swf_function.preload_global,
|
||||
params: owned_params,
|
||||
scope,
|
||||
constant_pool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,6 +231,19 @@ impl<'gc> Executable<'gc> {
|
|||
}
|
||||
|
||||
let argcell = arguments.into();
|
||||
let super_object: Option<Object<'gc>> = if !af.suppress_super {
|
||||
Some(SuperObject::from_child_object(this, avm, ac)?.into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(super_object) = super_object {
|
||||
super_object
|
||||
.as_super_object()
|
||||
.unwrap()
|
||||
.bind_this(ac.gc_context, super_object);
|
||||
}
|
||||
|
||||
let effective_ver = if avm.current_swf_version() > 5 {
|
||||
af.swf_version()
|
||||
} else {
|
||||
|
@ -237,6 +258,7 @@ impl<'gc> Executable<'gc> {
|
|||
effective_ver,
|
||||
af.data(),
|
||||
child_scope,
|
||||
af.constant_pool,
|
||||
this,
|
||||
Some(argcell),
|
||||
),
|
||||
|
@ -261,12 +283,15 @@ impl<'gc> Executable<'gc> {
|
|||
preload_r += 1;
|
||||
}
|
||||
|
||||
if let Some(super_object) = super_object {
|
||||
if af.preload_super {
|
||||
//TODO: super not implemented
|
||||
log::warn!("Cannot preload super into register because it's not implemented");
|
||||
frame.set_local_register(preload_r, super_object, ac.gc_context);
|
||||
//TODO: What happens if you specify both suppress and
|
||||
//preload for super?
|
||||
preload_r += 1;
|
||||
} else {
|
||||
frame.define("super", super_object, ac.gc_context);
|
||||
}
|
||||
}
|
||||
|
||||
if af.preload_root {
|
||||
|
|
|
@ -232,6 +232,13 @@ pub fn create_globals<'gc>(
|
|||
EnumSet::empty(),
|
||||
Some(function_proto),
|
||||
);
|
||||
globals.force_set_function(
|
||||
"ASSetPropFlags",
|
||||
object::as_set_prop_flags,
|
||||
gc_context,
|
||||
EnumSet::empty(),
|
||||
Some(function_proto),
|
||||
);
|
||||
globals.add_property(
|
||||
gc_context,
|
||||
"NaN",
|
||||
|
|
|
@ -15,6 +15,7 @@ pub fn constructor<'gc>(
|
|||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
/// Implements `Function.prototype.call`
|
||||
pub fn call<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
|
@ -24,6 +25,7 @@ pub fn call<'gc>(
|
|||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
/// Implements `Function.prototype.apply`
|
||||
pub fn apply<'gc>(
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_action_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
|
@ -33,6 +35,16 @@ pub fn apply<'gc>(
|
|||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
/// Implements `Function.prototype.toString`
|
||||
fn to_string<'gc>(
|
||||
_: &mut Avm1<'gc>,
|
||||
_: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_: Object<'gc>,
|
||||
_: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(ReturnValue::Immediate("[type Function]".into()))
|
||||
}
|
||||
|
||||
/// Partially construct `Function.prototype`.
|
||||
///
|
||||
/// `__proto__` and other cross-linked properties of this object will *not*
|
||||
|
@ -51,6 +63,10 @@ pub fn create_proto<'gc>(gc_context: MutationContext<'gc, '_>, proto: Object<'gc
|
|||
.as_script_object()
|
||||
.unwrap()
|
||||
.force_set_function("apply", apply, gc_context, EnumSet::empty(), this);
|
||||
function_proto
|
||||
.as_script_object()
|
||||
.unwrap()
|
||||
.force_set_function("toString", to_string, gc_context, EnumSet::empty(), this);
|
||||
|
||||
function_proto
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//! Object prototype
|
||||
use crate::avm1::property::Attribute::*;
|
||||
use crate::avm1::property::Attribute::{self, *};
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::{Avm1, Error, Object, TObject, UpdateContext, Value};
|
||||
use enumset::EnumSet;
|
||||
|
@ -180,3 +180,76 @@ pub fn fill_proto<'gc>(
|
|||
Some(fn_proto),
|
||||
);
|
||||
}
|
||||
|
||||
/// Implements `ASSetPropFlags`.
|
||||
///
|
||||
/// This is an undocumented function that allows ActionScript 2.0 classes to
|
||||
/// declare the property flags of a given property. It's not part of
|
||||
/// `Object.prototype`, and I suspect that's a deliberate omission.
|
||||
pub fn as_set_prop_flags<'gc>(
|
||||
avm: &mut Avm1<'gc>,
|
||||
ac: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_: Object<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
//This exists because `.into` won't work inline
|
||||
let my_error: Result<ReturnValue<'gc>, Error> =
|
||||
Err("ASSetPropFlags called without object to apply to!".into());
|
||||
let my_error_2: Result<ReturnValue<'gc>, Error> =
|
||||
Err("ASSetPropFlags called without object list!".into());
|
||||
|
||||
let mut object = args
|
||||
.get(0)
|
||||
.ok_or_else(|| my_error.unwrap_err())?
|
||||
.as_object()?;
|
||||
let properties = match args.get(1).ok_or_else(|| my_error_2.unwrap_err())? {
|
||||
Value::Object(ob) => {
|
||||
//Convert to native array.
|
||||
//TODO: Can we make this an iterator?
|
||||
let mut array = vec![];
|
||||
let length = ob
|
||||
.get("length", avm, ac)?
|
||||
.resolve(avm, ac)?
|
||||
.as_number(avm, ac)? as usize;
|
||||
for i in 0..length {
|
||||
array.push(
|
||||
ob.get(&format!("{}", i), avm, ac)?
|
||||
.resolve(avm, ac)?
|
||||
.coerce_to_string(avm, ac)?,
|
||||
)
|
||||
}
|
||||
|
||||
Some(array)
|
||||
}
|
||||
Value::String(s) => Some(s.split(',').map(String::from).collect()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let set_attributes = EnumSet::<Attribute>::from_bits(
|
||||
args.get(2)
|
||||
.unwrap_or(&Value::Number(0.0))
|
||||
.as_number(avm, ac)? as u128,
|
||||
);
|
||||
|
||||
let clear_attributes = EnumSet::<Attribute>::from_bits(
|
||||
args.get(3)
|
||||
.unwrap_or(&Value::Number(0.0))
|
||||
.as_number(avm, ac)? as u128,
|
||||
);
|
||||
|
||||
match properties {
|
||||
Some(properties) => {
|
||||
for prop_name in properties {
|
||||
object.set_attributes(
|
||||
ac.gc_context,
|
||||
Some(&prop_name),
|
||||
set_attributes,
|
||||
clear_attributes,
|
||||
)
|
||||
}
|
||||
}
|
||||
None => object.set_attributes(ac.gc_context, None, set_attributes, clear_attributes),
|
||||
}
|
||||
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use crate::avm1::function::Executable;
|
||||
use crate::avm1::property::Attribute;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::super_object::SuperObject;
|
||||
use crate::avm1::{Avm1, Error, ScriptObject, StageObject, UpdateContext, Value};
|
||||
use crate::display_object::DisplayObject;
|
||||
use enumset::EnumSet;
|
||||
|
@ -19,6 +20,7 @@ use std::fmt::Debug;
|
|||
pub enum Object<'gc> {
|
||||
ScriptObject(ScriptObject<'gc>),
|
||||
StageObject(StageObject<'gc>),
|
||||
SuperObject(SuperObject<'gc>),
|
||||
}
|
||||
)]
|
||||
pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy {
|
||||
|
@ -124,6 +126,21 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
|
|||
attributes: EnumSet<Attribute>,
|
||||
);
|
||||
|
||||
/// Set the attributes of a given property.
|
||||
///
|
||||
/// Leaving `name` unspecified allows setting all properties on a given
|
||||
/// object to the same set of properties.
|
||||
///
|
||||
/// Attributes can be set, cleared, or left as-is using the pairs of `set_`
|
||||
/// and `clear_attributes` parameters.
|
||||
fn set_attributes(
|
||||
&mut self,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
name: Option<&str>,
|
||||
set_attributes: EnumSet<Attribute>,
|
||||
clear_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
|
||||
|
@ -165,9 +182,75 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
|
|||
/// Get the object's type string.
|
||||
fn type_of(&self) -> &'static str;
|
||||
|
||||
/// Enumerate all interfaces implemented by this object.
|
||||
fn interfaces(&self) -> Vec<Object<'gc>>;
|
||||
|
||||
/// Set the interface list for this object. (Only useful for prototypes.)
|
||||
fn set_interfaces(
|
||||
&mut self,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
iface_list: Vec<Object<'gc>>,
|
||||
);
|
||||
|
||||
/// Determine if this object is an instance of a class.
|
||||
///
|
||||
/// The class is provided in the form of it's constructor function and the
|
||||
/// explicit prototype of that constructor function. It is assumed that
|
||||
/// they are already linked.
|
||||
///
|
||||
/// Because ActionScript 2.0 added interfaces, this function cannot simply
|
||||
/// check the prototype chain and call it a day. Each interface represents
|
||||
/// a new, parallel prototype chain which also needs to be checked. You
|
||||
/// can't implement interfaces within interfaces (fortunately), but if you
|
||||
/// somehow could this would support that, too.
|
||||
fn is_instance_of(
|
||||
&self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
constructor: Object<'gc>,
|
||||
prototype: Object<'gc>,
|
||||
) -> Result<bool, Error> {
|
||||
let mut proto_stack = vec![];
|
||||
if let Some(p) = self.proto() {
|
||||
proto_stack.push(p);
|
||||
}
|
||||
|
||||
while let Some(this_proto) = proto_stack.pop() {
|
||||
if Object::ptr_eq(this_proto, prototype) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Some(p) = this_proto.proto() {
|
||||
proto_stack.push(p);
|
||||
}
|
||||
|
||||
if avm.current_swf_version() >= 7 {
|
||||
for interface in this_proto.interfaces() {
|
||||
if Object::ptr_eq(interface, constructor) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Value::Object(o) = interface
|
||||
.get("prototype", avm, context)?
|
||||
.resolve(avm, context)?
|
||||
{
|
||||
proto_stack.push(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Get the underlying script object, if it exists.
|
||||
fn as_script_object(&self) -> Option<ScriptObject<'gc>>;
|
||||
|
||||
/// Get the underlying super object, if it exists.
|
||||
fn as_super_object(&self) -> Option<SuperObject<'gc>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the underlying display node for this object, if it exists.
|
||||
fn as_display_object(&self) -> Option<DisplayObject<'gc>>;
|
||||
|
||||
|
|
|
@ -8,10 +8,12 @@ use core::fmt;
|
|||
use enumset::{EnumSet, EnumSetType};
|
||||
use std::mem::replace;
|
||||
|
||||
/// Attributes of properties in the AVM runtime.
|
||||
/// The order is significant and should match the order used by `object::as_set_prop_flags`.
|
||||
#[derive(EnumSetType, Debug)]
|
||||
pub enum Attribute {
|
||||
DontDelete,
|
||||
DontEnum,
|
||||
DontDelete,
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
|
@ -79,6 +81,26 @@ impl<'gc> Property<'gc> {
|
|||
}
|
||||
}
|
||||
|
||||
/// List this property's attributes.
|
||||
pub fn attributes(&self) -> EnumSet<Attribute> {
|
||||
match self {
|
||||
Property::Virtual { attributes, .. } => *attributes,
|
||||
Property::Stored { attributes, .. } => *attributes,
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-define this property's attributes.
|
||||
pub fn set_attributes(&mut self, new_attributes: EnumSet<Attribute>) {
|
||||
match self {
|
||||
Property::Virtual {
|
||||
ref mut attributes, ..
|
||||
} => *attributes = new_attributes,
|
||||
Property::Stored {
|
||||
ref mut attributes, ..
|
||||
} => *attributes = new_attributes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_delete(&self) -> bool {
|
||||
match self {
|
||||
Property::Virtual { attributes, .. } => !attributes.contains(DontDelete),
|
||||
|
|
|
@ -27,6 +27,7 @@ pub struct ScriptObjectData<'gc> {
|
|||
prototype: Option<Object<'gc>>,
|
||||
values: HashMap<String, Property<'gc>>,
|
||||
function: Option<Executable<'gc>>,
|
||||
interfaces: Vec<Object<'gc>>,
|
||||
type_of: &'static str,
|
||||
array: ArrayStorage<'gc>,
|
||||
}
|
||||
|
@ -37,6 +38,7 @@ unsafe impl<'gc> Collect for ScriptObjectData<'gc> {
|
|||
self.values.trace(cc);
|
||||
self.function.trace(cc);
|
||||
self.array.trace(cc);
|
||||
self.interfaces.trace(cc);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +66,7 @@ impl<'gc> ScriptObject<'gc> {
|
|||
values: HashMap::new(),
|
||||
function: None,
|
||||
array: ArrayStorage::Properties { length: 0 },
|
||||
interfaces: vec![],
|
||||
},
|
||||
))
|
||||
}
|
||||
|
@ -80,6 +83,7 @@ impl<'gc> ScriptObject<'gc> {
|
|||
values: HashMap::new(),
|
||||
function: None,
|
||||
array: ArrayStorage::Vector(Vec::new()),
|
||||
interfaces: vec![],
|
||||
},
|
||||
));
|
||||
object.sync_native_property("length", gc_context, Some(0.into()));
|
||||
|
@ -99,6 +103,7 @@ impl<'gc> ScriptObject<'gc> {
|
|||
values: HashMap::new(),
|
||||
function: None,
|
||||
array: ArrayStorage::Properties { length: 0 },
|
||||
interfaces: vec![],
|
||||
},
|
||||
))
|
||||
.into()
|
||||
|
@ -118,6 +123,7 @@ impl<'gc> ScriptObject<'gc> {
|
|||
values: HashMap::new(),
|
||||
function: None,
|
||||
array: ArrayStorage::Properties { length: 0 },
|
||||
interfaces: vec![],
|
||||
},
|
||||
))
|
||||
}
|
||||
|
@ -136,6 +142,7 @@ impl<'gc> ScriptObject<'gc> {
|
|||
function: Some(function.into()),
|
||||
values: HashMap::new(),
|
||||
array: ArrayStorage::Properties { length: 0 },
|
||||
interfaces: vec![],
|
||||
},
|
||||
))
|
||||
}
|
||||
|
@ -391,6 +398,30 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> {
|
|||
.insert(name.to_string(), Property::Stored { value, attributes });
|
||||
}
|
||||
|
||||
fn set_attributes(
|
||||
&mut self,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
name: Option<&str>,
|
||||
set_attributes: EnumSet<Attribute>,
|
||||
clear_attributes: EnumSet<Attribute>,
|
||||
) {
|
||||
match name {
|
||||
None => {
|
||||
// Change *all* attributes.
|
||||
for (_name, prop) in self.0.write(gc_context).values.iter_mut() {
|
||||
let new_atts = (prop.attributes() - clear_attributes) | set_attributes;
|
||||
prop.set_attributes(new_atts);
|
||||
}
|
||||
}
|
||||
Some(name) => {
|
||||
if let Some(prop) = self.0.write(gc_context).values.get_mut(name) {
|
||||
let new_atts = (prop.attributes() - clear_attributes) | set_attributes;
|
||||
prop.set_attributes(new_atts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn proto(&self) -> Option<Object<'gc>> {
|
||||
self.0.read().prototype
|
||||
}
|
||||
|
@ -465,6 +496,14 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> {
|
|||
self.0.read().type_of
|
||||
}
|
||||
|
||||
fn interfaces(&self) -> Vec<Object<'gc>> {
|
||||
self.0.read().interfaces.clone()
|
||||
}
|
||||
|
||||
fn set_interfaces(&mut self, context: MutationContext<'gc, '_>, iface_list: Vec<Object<'gc>>) {
|
||||
self.0.write(context).interfaces = iface_list;
|
||||
}
|
||||
|
||||
fn as_script_object(&self) -> Option<ScriptObject<'gc>> {
|
||||
Some(*self)
|
||||
}
|
||||
|
|
|
@ -150,6 +150,17 @@ impl<'gc> TObject<'gc> for StageObject<'gc> {
|
|||
self.base.define_value(gc_context, name, value, attributes)
|
||||
}
|
||||
|
||||
fn set_attributes(
|
||||
&mut self,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
name: Option<&str>,
|
||||
set_attributes: EnumSet<Attribute>,
|
||||
clear_attributes: EnumSet<Attribute>,
|
||||
) {
|
||||
self.base
|
||||
.set_attributes(gc_context, name, set_attributes, clear_attributes)
|
||||
}
|
||||
|
||||
fn add_property(
|
||||
&self,
|
||||
gc_context: MutationContext<'gc, '_>,
|
||||
|
@ -228,6 +239,14 @@ impl<'gc> TObject<'gc> for StageObject<'gc> {
|
|||
self.base.delete_array_element(index, gc_context)
|
||||
}
|
||||
|
||||
fn interfaces(&self) -> Vec<Object<'gc>> {
|
||||
self.base.interfaces()
|
||||
}
|
||||
|
||||
fn set_interfaces(&mut self, context: MutationContext<'gc, '_>, iface_list: Vec<Object<'gc>>) {
|
||||
self.base.set_interfaces(context, iface_list)
|
||||
}
|
||||
|
||||
fn as_string(&self) -> String {
|
||||
self.display_object.path()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,252 @@
|
|||
//! Special object that implements `super`
|
||||
|
||||
use crate::avm1::function::Executable;
|
||||
use crate::avm1::property::Attribute;
|
||||
use crate::avm1::return_value::ReturnValue;
|
||||
use crate::avm1::script_object::TYPE_OF_OBJECT;
|
||||
use crate::avm1::{Avm1, Error, Object, ObjectPtr, ScriptObject, TObject, Value};
|
||||
use crate::context::UpdateContext;
|
||||
use crate::display_object::DisplayObject;
|
||||
use enumset::EnumSet;
|
||||
use gc_arena::{Collect, GcCell, MutationContext};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Implementation of the `super` object in AS2.
|
||||
///
|
||||
/// A `SuperObject` references all data from another object, but with one layer
|
||||
/// of prototyping removed. It's as if the given object had been constructed
|
||||
/// with it's parent class.
|
||||
#[collect(no_drop)]
|
||||
#[derive(Copy, Clone, Collect, Debug)]
|
||||
pub struct SuperObject<'gc>(GcCell<'gc, SuperObjectData<'gc>>);
|
||||
|
||||
#[collect(no_drop)]
|
||||
#[derive(Clone, Collect, Debug)]
|
||||
pub struct SuperObjectData<'gc> {
|
||||
child: Object<'gc>,
|
||||
proto: Option<Object<'gc>>,
|
||||
constr: Option<Object<'gc>>,
|
||||
this: Option<Object<'gc>>,
|
||||
}
|
||||
|
||||
impl<'gc> SuperObject<'gc> {
|
||||
pub fn from_child_object(
|
||||
child: Object<'gc>,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
) -> Result<Self, Error> {
|
||||
let child_proto = child.proto();
|
||||
let parent_proto = child_proto.and_then(|pr| pr.proto());
|
||||
let parent_constr = if let Some(child_proto) = child_proto {
|
||||
Some(
|
||||
child_proto
|
||||
.get("constructor", avm, context)?
|
||||
.resolve(avm, context)?
|
||||
.as_object()?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self(GcCell::allocate(
|
||||
context.gc_context,
|
||||
SuperObjectData {
|
||||
child,
|
||||
proto: parent_proto,
|
||||
constr: parent_constr,
|
||||
this: None,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
/// Set `this` to a particular value.
|
||||
///
|
||||
/// This is intended to be called with a self-reference, so that future
|
||||
/// invocations of `super()` can get a `this` value one level up the chain.
|
||||
pub fn bind_this(&mut self, context: MutationContext<'gc, '_>, this: Object<'gc>) {
|
||||
self.0.write(context).this = Some(this);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> TObject<'gc> for SuperObject<'gc> {
|
||||
fn get_local(
|
||||
&self,
|
||||
_name: &str,
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
_this: Object<'gc>,
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
||||
fn set(
|
||||
&self,
|
||||
_name: &str,
|
||||
_value: Value<'gc>,
|
||||
_avm: &mut Avm1<'gc>,
|
||||
_context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
) -> Result<(), Error> {
|
||||
//TODO: What happens if you set `super.__proto__`?
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn call(
|
||||
&self,
|
||||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: Object<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<ReturnValue<'gc>, Error> {
|
||||
if let Some(constr) = self.0.read().constr {
|
||||
constr.call(avm, context, self.0.read().this.unwrap_or(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: Object<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<Object<'gc>, Error> {
|
||||
if let Some(proto) = self.proto() {
|
||||
proto.new(avm, context, this, args)
|
||||
} else {
|
||||
// TODO: What happens when you `new super` but there's no
|
||||
// super? Is this code even reachable?!
|
||||
self.0.read().child.new(avm, context, this, args)
|
||||
}
|
||||
}
|
||||
|
||||
fn delete(&self, _gc_context: MutationContext<'gc, '_>, _name: &str) -> bool {
|
||||
//`super` cannot have properties deleted from it
|
||||
false
|
||||
}
|
||||
|
||||
fn proto(&self) -> Option<Object<'gc>> {
|
||||
self.0.read().proto
|
||||
}
|
||||
|
||||
fn define_value(
|
||||
&self,
|
||||
_gc_context: MutationContext<'gc, '_>,
|
||||
_name: &str,
|
||||
_value: Value<'gc>,
|
||||
_attributes: EnumSet<Attribute>,
|
||||
) {
|
||||
//`super` cannot have values defined on it
|
||||
}
|
||||
|
||||
fn set_attributes(
|
||||
&mut self,
|
||||
_gc_context: MutationContext<'gc, '_>,
|
||||
_name: Option<&str>,
|
||||
_set_attributes: EnumSet<Attribute>,
|
||||
_clear_attributes: EnumSet<Attribute>,
|
||||
) {
|
||||
//TODO: Does ASSetPropFlags work on `super`? What would it even work on?
|
||||
}
|
||||
|
||||
fn add_property(
|
||||
&self,
|
||||
_gc_context: MutationContext<'gc, '_>,
|
||||
_name: &str,
|
||||
_get: Executable<'gc>,
|
||||
_set: Option<Executable<'gc>>,
|
||||
_attributes: EnumSet<Attribute>,
|
||||
) {
|
||||
//`super` cannot have properties defined on it
|
||||
}
|
||||
|
||||
fn has_property(&self, name: &str) -> bool {
|
||||
self.0.read().child.has_property(name)
|
||||
}
|
||||
|
||||
fn has_own_property(&self, name: &str) -> bool {
|
||||
self.0.read().child.has_own_property(name)
|
||||
}
|
||||
|
||||
fn is_property_enumerable(&self, name: &str) -> bool {
|
||||
self.0.read().child.is_property_enumerable(name)
|
||||
}
|
||||
|
||||
fn is_property_overwritable(&self, name: &str) -> bool {
|
||||
self.0.read().child.is_property_overwritable(name)
|
||||
}
|
||||
|
||||
fn get_keys(&self) -> HashSet<String> {
|
||||
//`super` cannot be enumerated
|
||||
HashSet::new()
|
||||
}
|
||||
|
||||
fn as_string(&self) -> String {
|
||||
self.0.read().child.as_string()
|
||||
}
|
||||
|
||||
fn type_of(&self) -> &'static str {
|
||||
TYPE_OF_OBJECT
|
||||
}
|
||||
|
||||
fn length(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn set_length(&self, _gc_context: MutationContext<'gc, '_>, _new_length: usize) {}
|
||||
|
||||
fn array(&self) -> Vec<Value<'gc>> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn array_element(&self, _index: usize) -> Value<'gc> {
|
||||
Value::Undefined
|
||||
}
|
||||
|
||||
fn set_array_element(
|
||||
&self,
|
||||
_index: usize,
|
||||
_value: Value<'gc>,
|
||||
_gc_context: MutationContext<'gc, '_>,
|
||||
) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn delete_array_element(&self, _index: usize, _gc_context: MutationContext<'gc, '_>) {}
|
||||
|
||||
fn interfaces(&self) -> Vec<Object<'gc>> {
|
||||
//`super` does not implement interfaces
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn set_interfaces(
|
||||
&mut self,
|
||||
_gc_context: MutationContext<'gc, '_>,
|
||||
_iface_list: Vec<Object<'gc>>,
|
||||
) {
|
||||
//`super` probably cannot have interfaces set on it
|
||||
}
|
||||
|
||||
fn as_script_object(&self) -> Option<ScriptObject<'gc>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_super_object(&self) -> Option<SuperObject<'gc>> {
|
||||
Some(*self)
|
||||
}
|
||||
|
||||
fn as_display_object(&self) -> Option<DisplayObject<'gc>> {
|
||||
//`super` actually can be used to invoke MovieClip methods
|
||||
self.0.read().child.as_display_object()
|
||||
}
|
||||
|
||||
fn as_executable(&self) -> Option<Executable<'gc>> {
|
||||
//well, `super` *can* be called...
|
||||
self.0.read().constr.and_then(|c| c.as_executable())
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *const ObjectPtr {
|
||||
self.0.as_ptr() as *const ObjectPtr
|
||||
}
|
||||
}
|
|
@ -74,7 +74,7 @@ swf_tests! {
|
|||
(recursive_prototypes, "avm1/recursive_prototypes", 1),
|
||||
(stage_object_children, "avm1/stage_object_children", 2),
|
||||
(has_own_property, "avm1/has_own_property", 1),
|
||||
#[ignore] (extends_chain, "avm1/extends_chain", 1),
|
||||
(extends_chain, "avm1/extends_chain", 1),
|
||||
(is_prototype_of, "avm1/is_prototype_of", 1),
|
||||
#[ignore] (string_coercion, "avm1/string_coercion", 1),
|
||||
(lessthan_swf4, "avm1/lessthan_swf4", 1),
|
||||
|
@ -89,6 +89,7 @@ swf_tests! {
|
|||
(equals2_swf7, "avm1/equals2_swf7", 1),
|
||||
(strictequals_swf6, "avm1/strictequals_swf6", 1),
|
||||
(global_is_bare, "avm1/global_is_bare", 1),
|
||||
(as2_oop, "avm1/as2_oop", 1),
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
interface MyInterface {
|
||||
function a();
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import MyInterface;
|
||||
|
||||
class MyObject implements MyInterface, MyOtherInterface {
|
||||
function a() {
|
||||
trace("MyObject.a called");
|
||||
}
|
||||
|
||||
function b() {
|
||||
trace("clock crew's back baby");
|
||||
}
|
||||
|
||||
function c() {
|
||||
trace("MyObject.c called");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
interface MyOtherInterface {
|
||||
function b();
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
interface NotMyInterface {
|
||||
function c();
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
[type Function]
|
||||
[object Object]
|
||||
[type Function]
|
||||
[object Object]
|
||||
[object Object]
|
||||
true
|
||||
true
|
||||
false
|
||||
MyObject.a called
|
||||
clock crew's back baby
|
||||
MyObject.c called
|
||||
[object Object]
|
||||
null
|
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue