Merge pull request #52 from Dinnerbone/feature/movie_clip_object

Movie clip access from actionscript
This commit is contained in:
Mike Welsh 2019-09-04 15:16:00 -05:00 committed by GitHub
commit 9c9b3db7f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 730 additions and 270 deletions

View File

@ -1,17 +1,20 @@
use crate::avm1::builtins::register_builtins;
use crate::avm1::movie_clip::create_movie_clip;
use crate::avm1::globals::create_globals;
use crate::avm1::object::Object;
use crate::prelude::*;
use gc_arena::{GcCell, MutationContext};
use rand::{rngs::SmallRng, Rng, SeedableRng};
use std::collections::HashMap;
use std::fmt;
use std::io::Cursor;
use swf::avm1::read::Reader;
mod builtins;
mod movie_clip;
mod object;
mod globals;
pub mod movie_clip;
pub mod object;
mod value;
pub use value::Value;
pub struct ActionContext<'a, 'gc, 'gc_context> {
pub gc_context: gc_arena::MutationContext<'gc, 'gc_context>,
@ -28,7 +31,7 @@ pub struct Avm1<'gc> {
rng: SmallRng,
constant_pool: Vec<String>,
locals: HashMap<String, Value<'gc>>,
globals: HashMap<String, Value<'gc>>,
globals: GcCell<'gc, Object<'gc>>,
}
unsafe impl<'gc> gc_arena::Collect for Avm1<'gc> {
@ -44,15 +47,13 @@ type Error = Box<dyn std::error::Error>;
impl<'gc> Avm1<'gc> {
pub fn new(gc_context: MutationContext<'gc, '_>, swf_version: u8) -> Self {
let mut globals = HashMap::new();
register_builtins(gc_context, &mut globals);
Self {
swf_version,
stack: vec![],
rng: SmallRng::from_seed([0u8; 16]), // TODO(Herschel): Get a proper seed on all platforms.
constant_pool: vec![],
locals: HashMap::new(),
globals,
globals: GcCell::allocate(gc_context, create_globals(gc_context)),
}
}
@ -269,7 +270,7 @@ impl<'gc> Avm1<'gc> {
b.push_str(&a.into_string());
self.push(Value::String(b));
} else {
self.push(Value::Number(b.into_number() + a.into_number()));
self.push(Value::Number(b.as_number() + a.as_number()));
}
Ok(())
}
@ -366,7 +367,7 @@ impl<'gc> Avm1<'gc> {
fn action_call_method(
&mut self,
_context: &mut ActionContext<'_, 'gc, '_>,
context: &mut ActionContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let method_name = self.pop()?;
let object = self.pop()?;
@ -378,14 +379,19 @@ impl<'gc> Avm1<'gc> {
match method_name {
Value::Undefined | Value::Null => {
self.stack.push(object.call(&args)?);
let this = context.active_clip.read().object().as_object()?.to_owned();
self.stack.push(object.call(context, this, &args)?);
}
Value::String(name) => {
if name.is_empty() {
self.stack.push(object.call(&args)?);
} else {
self.stack
.push(object.as_object()?.read().get(&name).call(&args)?);
.push(object.call(context, object.as_object()?.to_owned(), &args)?);
} else {
self.stack.push(object.as_object()?.read().get(&name).call(
context,
object.as_object()?.to_owned(),
&args,
)?);
}
}
_ => {
@ -410,7 +416,7 @@ impl<'gc> Avm1<'gc> {
}
fn action_decrement(&mut self, _context: &mut ActionContext) -> Result<(), Error> {
let a = self.pop()?.into_number();
let a = self.pop()?.as_number();
self.push(Value::Number(a - 1.0));
Ok(())
}
@ -562,9 +568,11 @@ impl<'gc> Avm1<'gc> {
let path = var_path.as_string()?;
// Special hardcoded variables
if path == "_root" {
// TODO: Attach this movie clip Object to an actual MovieClip. That's our root.
self.push(create_movie_clip(context.gc_context));
if path == "_root" || path == "this" {
self.push(context.start_clip.read().object());
return Ok(());
} else if path == "_global" {
self.push(Value::Object(self.globals));
return Ok(());
}
@ -575,17 +583,16 @@ impl<'gc> Avm1<'gc> {
Self::resolve_slash_path_variable(context.active_clip, context.root, path)
{
if let Some(clip) = node.read().as_movie_clip() {
if clip.has_variable(var_name) {
result = Some(clip.get_variable(var_name));
let object = clip.object().as_object()?;
if object.read().has_property(var_name) {
result = Some(object.read().get(var_name));
}
}
};
}
if result.is_none() {
if let Some(value) = self.globals.get(path) {
result = Some(value.clone());
}
if result.is_none() && self.globals.read().has_property(path) {
result = Some(self.globals.read().get(path));
}
self.push(result.unwrap_or(Value::Undefined));
Ok(())
@ -684,7 +691,7 @@ impl<'gc> Avm1<'gc> {
}
fn action_increment(&mut self, _context: &mut ActionContext) -> Result<(), Error> {
let a = self.pop()?.into_number();
let a = self.pop()?.as_number();
self.push(Value::Number(a + 1.0));
Ok(())
}
@ -740,7 +747,7 @@ impl<'gc> Avm1<'gc> {
let result = match (a, b) {
(Value::String(a), Value::String(b)) => b.to_string().bytes().lt(a.to_string().bytes()),
(a, b) => b.into_number() < a.into_number(),
(a, b) => b.as_number() < a.as_number(),
};
self.push(Value::Bool(result));
@ -977,7 +984,10 @@ impl<'gc> Avm1<'gc> {
Self::resolve_slash_path_variable(context.active_clip, context.root, var_path)
{
if let Some(clip) = node.write(context.gc_context).as_movie_clip_mut() {
clip.set_variable(var_name, value);
clip.object()
.as_object()?
.write(context.gc_context)
.set(var_name, value);
}
}
Ok(())
@ -1133,7 +1143,7 @@ impl<'gc> Avm1<'gc> {
fn action_to_number(&mut self, _context: &mut ActionContext) -> Result<(), Error> {
let val = self.pop()?;
self.push(Value::Number(val.into_number()));
self.push(Value::Number(val.as_number()));
Ok(())
}
@ -1150,16 +1160,8 @@ impl<'gc> Avm1<'gc> {
}
fn action_type_of(&mut self, _context: &mut ActionContext) -> Result<(), Error> {
let _type_str = match self.pop()? {
Value::Undefined => "undefined",
Value::Null => "null",
Value::Number(_) => "number",
Value::Bool(_) => "boolean",
Value::String(_) => "string",
Value::Object(_) => "object",
Value::NativeFunction(_) => "function",
};
// TODO(Herschel): movieclip
let type_of = self.pop()?.type_of();
self.push(type_of);
Ok(())
}
@ -1206,143 +1208,3 @@ impl<'gc> Avm1<'gc> {
Err("Unimplemented action: With".into())
}
}
#[derive(Clone)]
#[allow(dead_code)]
pub enum Value<'gc> {
Undefined,
Null,
Bool(bool),
Number(f64),
String(String),
Object(GcCell<'gc, Object<'gc>>),
NativeFunction(fn(&[Value<'gc>]) -> Value<'gc>),
}
unsafe impl<'gc> gc_arena::Collect for Value<'gc> {
fn trace(&self, cc: gc_arena::CollectionContext) {
if let Value::Object(object) = self {
object.trace(cc);
}
}
}
impl fmt::Debug for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Value::Undefined => f.write_str("Undefined"),
Value::Null => f.write_str("Null"),
Value::Bool(value) => f.debug_tuple("Bool").field(value).finish(),
Value::Number(value) => f.debug_tuple("Number").field(value).finish(),
Value::String(value) => f.debug_tuple("String").field(value).finish(),
Value::Object(value) => f.debug_tuple("Object").field(value).finish(),
Value::NativeFunction(_) => f.write_str("NativeFunction"),
}
}
}
impl<'gc> Value<'gc> {
fn into_number_v1(self) -> f64 {
match self {
Value::Bool(true) => 1.0,
Value::Number(v) => v,
Value::String(v) => v.parse().unwrap_or(0.0),
_ => 0.0,
}
}
fn into_number(self) -> f64 {
// ECMA-262 2nd edtion s. 9.3 ToNumber
use std::f64::NAN;
match self {
Value::Undefined => NAN,
Value::Null => NAN,
Value::Bool(false) => 0.0,
Value::Bool(true) => 1.0,
Value::Number(v) => v,
Value::String(v) => v.parse().unwrap_or(NAN), // TODO(Herschel): Handle Infinity/etc.?
Value::Object(_object) => {
log::error!("Unimplemented: Object ToNumber");
0.0
}
Value::NativeFunction(_fn) => {
log::error!("Unimplemented: NativeFunction ToNumber");
0.0
}
}
}
fn from_bool_v1(value: bool, swf_version: u8) -> Value<'gc> {
// SWF version 4 did not have true bools and will push bools as 0 or 1.
// e.g. SWF19 p. 72:
// "If the numbers are equal, true is pushed to the stack for SWF 5 and later. For SWF 4, 1 is pushed to the stack."
if swf_version >= 5 {
Value::Bool(value)
} else {
Value::Number(if value { 1.0 } else { 0.0 })
}
}
fn into_string(self) -> String {
match self {
Value::Undefined => "undefined".to_string(),
Value::Null => "null".to_string(),
Value::Bool(v) => v.to_string(),
Value::Number(v) => v.to_string(), // TODO(Herschel): Rounding for int?
Value::String(v) => v,
Value::Object(_) => "[Object object]".to_string(), // TODO(Herschel):
Value::NativeFunction(_) => "[type Function]".to_string(),
}
}
fn as_bool(&self) -> bool {
match *self {
Value::Bool(v) => v,
Value::Number(v) => v != 0.0,
// TODO(Herschel): Value::String(v) => ??
_ => false,
}
}
fn as_i32(&self) -> Result<i32, Error> {
self.as_f64().map(|n| n as i32)
}
fn as_u32(&self) -> Result<u32, Error> {
self.as_f64().map(|n| n as u32)
}
fn as_i64(&self) -> Result<i64, Error> {
self.as_f64().map(|n| n as i64)
}
pub fn as_f64(&self) -> Result<f64, Error> {
match *self {
Value::Number(v) => Ok(v),
_ => Err(format!("Expected Number, found {:?}", self).into()),
}
}
fn as_string(&self) -> Result<&String, Error> {
match self {
Value::String(s) => Ok(s),
_ => Err(format!("Expected String, found {:?}", self).into()),
}
}
fn as_object(&self) -> Result<&GcCell<'gc, Object<'gc>>, Error> {
if let Value::Object(object) = self {
Ok(object)
} else {
Err(format!("Expected Object, found {:?}", self).into())
}
}
fn call(&self, args: &[Value<'gc>]) -> Result<Value<'gc>, Error> {
if let Value::NativeFunction(function) = self {
Ok(function(args))
} else {
Err(format!("Expected function, found {:?}", self).into())
}
}
}

View File

@ -1,12 +0,0 @@
use crate::avm1::Value;
use gc_arena::MutationContext;
use std::collections::HashMap;
mod math;
pub fn register_builtins<'gc>(
gc_context: MutationContext<'gc, '_>,
globals: &mut HashMap<String, Value<'gc>>,
) {
globals.insert("Math".to_string(), math::create(gc_context));
}

View File

@ -1,21 +0,0 @@
use crate::avm1::{Object, Value};
use gc_arena::{GcCell, MutationContext};
fn abs<'gc>(args: &[Value<'gc>]) -> Value<'gc> {
let input = args.get(0).unwrap().as_f64().unwrap();
Value::Number(input.abs())
}
fn round<'gc>(args: &[Value<'gc>]) -> Value<'gc> {
let input = args.get(0).unwrap().as_f64().unwrap();
Value::Number(input.round())
}
pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> Value<'gc> {
let mut math = Object::new();
math.set("abs", Value::NativeFunction(abs));
math.set("round", Value::NativeFunction(round));
Value::Object(GcCell::allocate(gc_context, math))
}

12
core/src/avm1/globals.rs Normal file
View File

@ -0,0 +1,12 @@
use crate::avm1::object::Object;
use gc_arena::MutationContext;
mod math;
pub fn create_globals<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> {
let mut globals = Object::object(gc_context);
globals.set_object("Math", math::create(gc_context));
globals
}

View File

@ -0,0 +1,249 @@
use crate::avm1::{ActionContext, Object, Value};
use gc_arena::{GcCell, MutationContext};
use std::f64::NAN;
macro_rules! wrap_std {
( $object: ident, $gc_context: ident, $($name:expr => $std:path),* ) => {{
$(
$object.set_function(
$name,
|_context, _this, args| -> Value<'gc> {
if let Some(input) = args.get(0) {
Value::Number($std(input.as_number()))
} else {
Value::Number(NAN)
}
},
$gc_context,
);
)*
}};
}
fn atan2<'gc>(
_context: &mut ActionContext<'_, 'gc, '_>,
_this: GcCell<'gc, Object<'gc>>,
args: &[Value<'gc>],
) -> Value<'gc> {
if let Some(y) = args.get(0) {
if let Some(x) = args.get(1) {
return Value::Number(y.as_number().atan2(x.as_number()));
} else {
return Value::Number(y.as_number().atan2(0.0));
}
}
Value::Number(NAN)
}
pub fn create<'gc>(gc_context: MutationContext<'gc, '_>) -> GcCell<'gc, Object<'gc>> {
let mut math = Object::object(gc_context);
math.set("E", Value::Number(std::f64::consts::E));
math.set("LN10", Value::Number(std::f64::consts::LN_10));
math.set("LN2", Value::Number(std::f64::consts::LN_2));
math.set("LOG10E", Value::Number(std::f64::consts::LOG10_E));
math.set("LOG2E", Value::Number(std::f64::consts::LOG2_E));
math.set("PI", Value::Number(std::f64::consts::PI));
math.set("SQRT1_2", Value::Number(std::f64::consts::FRAC_1_SQRT_2));
math.set("SQRT2", Value::Number(std::f64::consts::SQRT_2));
wrap_std!(math, gc_context,
"abs" => f64::abs,
"acos" => f64::acos,
"asin" => f64::asin,
"atan" => f64::atan,
"ceil" => f64::ceil,
"cos" => f64::cos,
"exp" => f64::exp,
"floor" => f64::floor,
"round" => f64::round,
"sin" => f64::sin,
"sqrt" => f64::sqrt,
"tan" => f64::tan
);
math.set_function("atan2", atan2, gc_context);
GcCell::allocate(gc_context, math)
}
#[cfg(test)]
#[allow(clippy::unreadable_literal)]
#[allow(clippy::approx_constant)]
mod tests {
use super::*;
use crate::avm1::Error;
use crate::backend::audio::NullAudioBackend;
use crate::display_object::DisplayObject;
use crate::movie_clip::MovieClip;
use gc_arena::rootless_arena;
macro_rules! test_std {
( $test: ident, $name: expr, $($args: expr => $out: expr),* ) => {
#[test]
fn $test() -> Result<(), Error> {
with_avm(|context| {
let math = create(context.gc_context);
let function = math.read().get($name);
$(
assert_eq!(function.call(context, math, $args)?, $out);
)*
Ok(())
})
}
};
}
fn with_avm<F, R>(test: F) -> R
where
F: FnOnce(&mut ActionContext) -> R,
{
rootless_arena(|gc_context| {
let movie_clip: Box<dyn DisplayObject> = Box::new(MovieClip::new(gc_context));
let root = GcCell::allocate(gc_context, movie_clip);
let mut context = ActionContext {
gc_context,
global_time: 0,
root,
start_clip: root,
active_clip: root,
audio: &mut NullAudioBackend::new(),
};
test(&mut context)
})
}
test_std!(test_abs, "abs",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(-50.0)] => Value::Number(50.0),
&[Value::Number(25.0)] => Value::Number(25.0)
);
test_std!(test_acos, "acos",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(-1.0)] => Value::Number(3.141592653589793),
&[Value::Number(0.0)] => Value::Number(1.5707963267948966),
&[Value::Number(1.0)] => Value::Number(0.0)
);
test_std!(test_asin, "asin",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(-1.0)] => Value::Number(-1.5707963267948966),
&[Value::Number(0.0)] => Value::Number(0.0),
&[Value::Number(1.0)] => Value::Number(1.5707963267948966)
);
test_std!(test_atan, "atan",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(-1.0)] => Value::Number(-0.7853981633974483),
&[Value::Number(0.0)] => Value::Number(0.0),
&[Value::Number(1.0)] => Value::Number(0.7853981633974483)
);
test_std!(test_ceil, "ceil",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(12.5)] => Value::Number(13.0)
);
test_std!(test_cos, "cos",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(0.0)] => Value::Number(1.0),
&[Value::Number(std::f64::consts::PI)] => Value::Number(-1.0)
);
test_std!(test_exp, "exp",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(1.0)] => Value::Number(2.718281828459045),
&[Value::Number(2.0)] => Value::Number(7.38905609893065)
);
test_std!(test_floor, "floor",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(12.5)] => Value::Number(12.0)
);
test_std!(test_round, "round",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(12.5)] => Value::Number(13.0),
&[Value::Number(23.2)] => Value::Number(23.0)
);
test_std!(test_sin, "sin",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(0.0)] => Value::Number(0.0),
&[Value::Number(std::f64::consts::PI / 2.0)] => Value::Number(1.0)
);
test_std!(test_sqrt, "sqrt",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(0.0)] => Value::Number(0.0),
&[Value::Number(5.0)] => Value::Number(2.23606797749979)
);
test_std!(test_tan, "tan",
&[] => Value::Number(NAN),
&[Value::Null] => Value::Number(NAN),
&[Value::Number(0.0)] => Value::Number(0.0),
&[Value::Number(1.0)] => Value::Number(1.5574077246549023)
);
#[test]
fn test_atan2_nan() {
with_avm(|context| {
let math = GcCell::allocate(context.gc_context, create(context.gc_context));
assert_eq!(atan2(context, *math.read(), &[]), Value::Number(NAN));
assert_eq!(
atan2(context, *math.read(), &[Value::Number(1.0), Value::Null]),
Value::Number(NAN)
);
assert_eq!(
atan2(
context,
*math.read(),
&[Value::Number(1.0), Value::Undefined]
),
Value::Number(NAN)
);
assert_eq!(
atan2(
context,
*math.read(),
&[Value::Undefined, Value::Number(1.0)]
),
Value::Number(NAN)
);
});
}
#[test]
fn test_atan2_valid() {
with_avm(|context| {
let math = GcCell::allocate(context.gc_context, create(context.gc_context));
assert_eq!(
atan2(context, *math.read(), &[Value::Number(10.0)]),
Value::Number(std::f64::consts::FRAC_PI_2)
);
assert_eq!(
atan2(
context,
*math.read(),
&[Value::Number(1.0), Value::Number(2.0)]
),
Value::Number(0.4636476090008061)
);
});
}
}

View File

@ -1,18 +1,82 @@
use crate::avm1::{Object, Value};
use gc_arena::{GcCell, MutationContext};
use crate::avm1::object::Object;
use crate::avm1::Value;
use crate::movie_clip::MovieClip;
use gc_arena::MutationContext;
pub fn create_movie_clip<'gc>(gc_context: MutationContext<'gc, '_>) -> Value<'gc> {
let mut class = Object::new();
class.set(
"getBytesTotal",
Value::NativeFunction(|_args: &[Value<'gc>]| Value::Number(1.0)),
);
class.set(
"getBytesLoaded",
Value::NativeFunction(|_args: &[Value<'gc>]| Value::Number(1.0)),
);
Value::Object(GcCell::allocate(gc_context, class))
macro_rules! with_movie_clip {
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
$(
$object.set_function(
$name,
|_context, this, args| -> Value<'gc> {
if let Some(display_object) = this.read().display_node() {
if let Some(movie_clip) = display_object.read().as_movie_clip() {
return $fn(movie_clip, args);
}
}
Value::Undefined
},
$gc_context,
);
)*
}};
}
macro_rules! with_movie_clip_mut {
( $gc_context: ident, $object:ident, $($name:expr => $fn:expr),* ) => {{
$(
$object.set_function(
$name,
|context, this, args| -> Value<'gc> {
if let Some(display_object) = this.read().display_node() {
if let Some(movie_clip) = display_object.write(context.gc_context).as_movie_clip_mut() {
return $fn(movie_clip, args);
}
}
Value::Undefined
},
$gc_context,
);
)*
}};
}
pub fn create_movie_object<'gc>(gc_context: MutationContext<'gc, '_>) -> Object<'gc> {
let mut object = Object::object(gc_context);
with_movie_clip_mut!(
gc_context,
object,
"nextFrame" => |movie_clip: &mut MovieClip, _args| {
movie_clip.next_frame();
Value::Undefined
},
"prevFrame" => |movie_clip: &mut MovieClip, _args| {
movie_clip.prev_frame();
Value::Undefined
},
"play" => |movie_clip: &mut MovieClip, _args| {
movie_clip.play();
Value::Undefined
},
"stop" => |movie_clip: &mut MovieClip, _args| {
movie_clip.stop();
Value::Undefined
}
);
with_movie_clip!(
gc_context,
object,
"getBytesLoaded" => |_movie_clip: &MovieClip, _args| {
// TODO find a correct value
Value::Number(0.0)
},
"getBytesTotal" => |_movie_clip: &MovieClip, _args| {
// TODO find a correct value
Value::Number(0.0)
}
);
object
}

View File

@ -1,27 +1,141 @@
use crate::avm1::Value;
use gc_arena::Collect;
use crate::avm1::{ActionContext, Value};
use crate::display_object::DisplayNode;
use core::fmt;
use gc_arena::{GcCell, MutationContext};
use std::collections::HashMap;
#[derive(Clone, Debug, Collect, Default)]
#[collect(empty_drop)]
pub type NativeFunction<'gc> =
fn(&mut ActionContext<'_, 'gc, '_>, GcCell<'gc, Object<'gc>>, &[Value<'gc>]) -> Value<'gc>;
pub const TYPE_OF_OBJECT: &str = "object";
pub const TYPE_OF_FUNCTION: &str = "function";
pub const TYPE_OF_MOVIE_CLIP: &str = "movieclip";
fn default_to_string<'gc>(
_: &mut ActionContext<'_, 'gc, '_>,
_: GcCell<'gc, Object<'gc>>,
_: &[Value<'gc>],
) -> Value<'gc> {
Value::String("[Object object]".to_string())
}
#[derive(Clone)]
pub struct Object<'gc> {
display_node: Option<DisplayNode<'gc>>,
values: HashMap<String, Value<'gc>>,
function: Option<NativeFunction<'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 new() -> Self {
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.set_function("toString", default_to_string, gc_context);
result
}
pub fn function(function: NativeFunction<'gc>) -> Self {
Self {
type_of: TYPE_OF_FUNCTION,
function: Some(function),
display_node: None,
values: HashMap::new(),
}
}
pub fn get(&self, name: &str) -> Value<'gc> {
self.values
.get(name)
.map_or(Value::Undefined, |v| v.to_owned())
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: Value<'gc>) {
self.values.insert(name.to_owned(), value);
}
pub fn set_object(&mut self, name: &str, object: GcCell<'gc, Object<'gc>>) {
self.values.insert(name.to_owned(), Value::Object(object));
}
pub fn set_function(
&mut self,
name: &str,
function: NativeFunction<'gc>,
gc_context: MutationContext<'gc, '_>,
) {
self.set(
name,
Value::Object(GcCell::allocate(gc_context, Object::function(function))),
)
}
pub fn get(&self, name: &str) -> Value<'gc> {
if let Some(value) = self.values.get(name) {
return value.to_owned();
}
Value::Undefined
}
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 call(
&self,
context: &mut ActionContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
args: &[Value<'gc>],
) -> Value<'gc> {
if let Some(function) = self.function {
function(context, this, args)
} else {
Value::Undefined
}
}
pub fn as_string(&self) -> String {
if self.function.is_some() {
"[type Function]".to_string()
} else {
"[object Object]".to_string()
}
}
pub fn set_type_of(&mut self, type_of: &'static str) {
self.type_of = type_of;
}
pub fn type_of(&self) -> &'static str {
self.type_of
}
}

175
core/src/avm1/value.rs Normal file
View File

@ -0,0 +1,175 @@
use crate::avm1::object::Object;
use crate::avm1::{ActionContext, Error};
use gc_arena::GcCell;
#[derive(Clone, Debug)]
#[allow(dead_code)]
pub enum Value<'gc> {
Undefined,
Null,
Bool(bool),
Number(f64),
String(String),
Object(GcCell<'gc, Object<'gc>>),
}
unsafe impl<'gc> gc_arena::Collect for Value<'gc> {
fn trace(&self, cc: gc_arena::CollectionContext) {
if let Value::Object(object) = self {
object.trace(cc);
}
}
}
impl PartialEq for Value<'_> {
fn eq(&self, other: &Self) -> bool {
match self {
Value::Undefined => match other {
Value::Undefined => true,
_ => false,
},
Value::Null => match other {
Value::Null => true,
_ => false,
},
Value::Bool(value) => match other {
Value::Bool(other_value) => value == other_value,
_ => false,
},
Value::Number(value) => match other {
Value::Number(other_value) => {
(value == other_value) || (value.is_nan() && other_value.is_nan())
}
_ => false,
},
Value::String(value) => match other {
Value::String(other_value) => value == other_value,
_ => false,
},
Value::Object(value) => match other {
Value::Object(other_value) => value.as_ptr() == other_value.as_ptr(),
_ => false,
},
}
}
}
impl<'gc> Value<'gc> {
pub fn into_number_v1(self) -> f64 {
match self {
Value::Bool(true) => 1.0,
Value::Number(v) => v,
Value::String(v) => v.parse().unwrap_or(0.0),
_ => 0.0,
}
}
pub fn as_number(&self) -> f64 {
// ECMA-262 2nd edtion s. 9.3 ToNumber
use std::f64::NAN;
match self {
Value::Undefined => NAN,
Value::Null => NAN,
Value::Bool(false) => 0.0,
Value::Bool(true) => 1.0,
Value::Number(v) => *v,
Value::String(v) => v.parse().unwrap_or(NAN), // TODO(Herschel): Handle Infinity/etc.?
Value::Object(_object) => {
log::error!("Unimplemented: Object ToNumber");
0.0
}
}
}
pub fn from_bool_v1(value: bool, swf_version: u8) -> Value<'gc> {
// SWF version 4 did not have true bools and will push bools as 0 or 1.
// e.g. SWF19 p. 72:
// "If the numbers are equal, true is pushed to the stack for SWF 5 and later. For SWF 4, 1 is pushed to the stack."
if swf_version >= 5 {
Value::Bool(value)
} else {
Value::Number(if value { 1.0 } else { 0.0 })
}
}
pub fn into_string(self) -> String {
match self {
Value::Undefined => "undefined".to_string(),
Value::Null => "null".to_string(),
Value::Bool(v) => v.to_string(),
Value::Number(v) => v.to_string(), // TODO(Herschel): Rounding for int?
Value::String(v) => v,
Value::Object(object) => object.read().as_string(),
}
}
pub fn as_bool(&self) -> bool {
match *self {
Value::Bool(v) => v,
Value::Number(v) => v != 0.0,
// TODO(Herschel): Value::String(v) => ??
_ => false,
}
}
pub fn type_of(&self) -> Value<'gc> {
Value::String(
match self {
Value::Undefined => "undefined",
Value::Null => "null",
Value::Number(_) => "number",
Value::Bool(_) => "boolean",
Value::String(_) => "string",
Value::Object(object) => object.read().type_of(),
}
.to_string(),
)
}
pub fn as_i32(&self) -> Result<i32, Error> {
self.as_f64().map(|n| n as i32)
}
pub fn as_u32(&self) -> Result<u32, Error> {
self.as_f64().map(|n| n as u32)
}
pub fn as_i64(&self) -> Result<i64, Error> {
self.as_f64().map(|n| n as i64)
}
pub fn as_f64(&self) -> Result<f64, Error> {
match *self {
Value::Number(v) => Ok(v),
_ => Err(format!("Expected Number, found {:?}", self).into()),
}
}
pub fn as_string(&self) -> Result<&String, Error> {
match self {
Value::String(s) => Ok(s),
_ => Err(format!("Expected String, found {:?}", self).into()),
}
}
pub fn as_object(&self) -> Result<GcCell<'gc, Object<'gc>>, Error> {
if let Value::Object(object) = self {
Ok(object.to_owned())
} else {
Err(format!("Expected Object, found {:?}", self).into())
}
}
pub fn call(
&self,
context: &mut ActionContext<'_, 'gc, '_>,
this: GcCell<'gc, Object<'gc>>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error> {
if let Value::Object(object) = self {
Ok(object.read().call(context, this, args))
} else {
Err(format!("Expected function, found {:?}", self).into())
}
}
}

View File

@ -4,7 +4,7 @@ use crate::player::{RenderContext, UpdateContext};
use crate::prelude::*;
use std::collections::BTreeMap;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Button<'gc> {
base: DisplayObjectBase<'gc>,
static_data: gc_arena::Gc<'gc, ButtonStatic>,

View File

@ -1,9 +1,11 @@
use crate::avm1::Value;
use crate::player::{RenderContext, UpdateContext};
use crate::prelude::*;
use crate::transform::Transform;
use gc_arena::{Collect, GcCell};
use gc_arena::{Collect, GcCell, MutationContext};
use std::fmt::Debug;
#[derive(Clone, Collect)]
#[derive(Clone, Collect, Debug)]
#[collect(empty_drop)]
pub struct DisplayObjectBase<'gc> {
parent: Option<DisplayNode<'gc>>,
@ -68,7 +70,7 @@ impl<'gc> DisplayObject<'gc> for DisplayObjectBase<'gc> {
}
}
pub trait DisplayObject<'gc>: 'gc + Collect {
pub trait DisplayObject<'gc>: 'gc + Collect + Debug {
fn local_bounds(&self) -> BoundingBox {
BoundingBox::default()
}
@ -115,6 +117,10 @@ pub trait DisplayObject<'gc>: 'gc + Collect {
}
fn box_clone(&self) -> Box<dyn DisplayObject<'gc>>;
fn object(&self) -> Value<'gc> {
Value::Undefined // todo: impl for every type and delete this fallback
}
fn hit_test(&self, _: (Twips, Twips)) -> bool {
false
}
@ -126,6 +132,13 @@ pub trait DisplayObject<'gc>: 'gc + Collect {
) -> Option<DisplayNode<'gc>> {
None
}
fn post_instantiation(
&mut self,
_gc_context: MutationContext<'gc, '_>,
_display_object: DisplayNode<'gc>,
) {
}
}
impl<'gc> Clone for Box<dyn DisplayObject<'gc>> {

View File

@ -3,7 +3,7 @@ use crate::display_object::{DisplayObject, DisplayObjectBase};
use crate::player::{RenderContext, UpdateContext};
use crate::prelude::*;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Graphic<'gc> {
base: DisplayObjectBase<'gc>,
static_data: gc_arena::Gc<'gc, GraphicStatic>,

View File

@ -58,7 +58,11 @@ impl<'gc> Library<'gc> {
return Err("Character id doesn't exist".into());
}
};
Ok(GcCell::allocate(gc_context, obj))
let result = GcCell::allocate(gc_context, obj);
result
.write(gc_context)
.post_instantiation(gc_context, result);
Ok(result)
}
pub fn get_font(&self, id: CharacterId) -> Option<&Font> {

View File

@ -5,7 +5,7 @@ use crate::prelude::*;
use std::collections::HashMap;
use swf::Twips;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct MorphShape<'gc> {
base: DisplayObjectBase<'gc>,

View File

@ -1,4 +1,6 @@
use crate::avm1;
use crate::avm1::movie_clip::create_movie_object;
use crate::avm1::object::{Object, TYPE_OF_MOVIE_CLIP};
use crate::avm1::Value;
use crate::backend::audio::AudioStreamHandle;
use crate::character::Character;
use crate::color_transform::ColorTransform;
@ -11,14 +13,14 @@ use crate::player::{RenderContext, UpdateContext};
use crate::prelude::*;
use crate::tag_utils::{self, DecodeResult, SwfStream};
use crate::text::Text;
use gc_arena::{Gc, MutationContext};
use gc_arena::{Gc, GcCell, MutationContext};
use std::collections::{BTreeMap, HashMap};
use swf::read::SwfRead;
type Depth = i16;
type FrameNumber = u16;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct MovieClip<'gc> {
base: DisplayObjectBase<'gc>,
static_data: Gc<'gc, MovieClipStatic>,
@ -28,7 +30,7 @@ pub struct MovieClip<'gc> {
current_frame: FrameNumber,
audio_stream: Option<AudioStreamHandle>,
children: BTreeMap<Depth, DisplayNode<'gc>>,
variables: HashMap<String, avm1::Value<'gc>>,
object: GcCell<'gc, Object<'gc>>,
}
impl<'gc> MovieClip<'gc> {
@ -42,7 +44,7 @@ impl<'gc> MovieClip<'gc> {
current_frame: 0,
audio_stream: None,
children: BTreeMap::new(),
variables: HashMap::new(),
object: GcCell::allocate(gc_context, create_movie_object(gc_context)),
}
}
@ -72,7 +74,7 @@ impl<'gc> MovieClip<'gc> {
current_frame: 0,
audio_stream: None,
children: BTreeMap::new(),
variables: HashMap::new(),
object: GcCell::allocate(gc_context, create_movie_object(gc_context)),
}
}
@ -225,23 +227,6 @@ impl<'gc> MovieClip<'gc> {
self.goto_queue.clear();
}
pub fn has_variable(&self, var_name: &str) -> bool {
self.variables.contains_key(var_name)
}
pub fn get_variable(&self, var_name: &str) -> avm1::Value<'gc> {
// TODO: Value should be Copy (and contain a Cow/GcCell for big objects)
self.variables
.get(var_name)
.unwrap_or(&avm1::Value::Undefined)
.clone()
}
pub fn set_variable(&mut self, var_name: &str, value: avm1::Value<'gc>) {
// TODO: Cow for String values.
self.variables.insert(var_name.to_owned(), value);
}
pub fn id(&self) -> CharacterId {
self.static_data.id
}
@ -442,6 +427,20 @@ impl<'gc> DisplayObject<'gc> for MovieClip<'gc> {
fn as_movie_clip_mut(&mut self) -> Option<&mut crate::movie_clip::MovieClip<'gc>> {
Some(self)
}
fn post_instantiation(
&mut self,
gc_context: MutationContext<'gc, '_>,
display_object: DisplayNode<'gc>,
) {
let mut object = self.object.write(gc_context);
object.set_display_node(display_object);
object.set_type_of(TYPE_OF_MOVIE_CLIP);
}
fn object(&self) -> Value<'gc> {
Value::Object(self.object)
}
}
unsafe impl<'gc> gc_arena::Collect for MovieClip<'gc> {
@ -452,6 +451,7 @@ unsafe impl<'gc> gc_arena::Collect for MovieClip<'gc> {
}
self.base.trace(cc);
self.static_data.trace(cc);
self.object.trace(cc);
}
}

View File

@ -3,7 +3,7 @@ use crate::player::{RenderContext, UpdateContext};
use crate::prelude::*;
use crate::transform::Transform;
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Text<'gc> {
base: DisplayObjectBase<'gc>,
text_transform: Matrix,

View File

@ -4,7 +4,7 @@ use gc_arena::Collect;
/// Represents the transform for a DisplayObject.
/// This includes both the transformation matrix and the color transform.
///
#[derive(Clone, Collect)]
#[derive(Clone, Collect, Debug)]
#[collect(require_static)]
pub struct Transform {
pub matrix: Matrix,