Initial work on avm1 method calling, local and global variables.

This contains just enough AS1 support for early trivial loading screens to not crash (ie badgers badgers badgers)
This commit is contained in:
Nathan Adams 2019-08-27 00:53:50 +02:00 committed by Mike Welsh
parent f41470129d
commit a74d1734af
9 changed files with 272 additions and 79 deletions

View File

@ -15,7 +15,7 @@ log = "0.4"
minimp3 = { version = "0.3.3", optional = true }
puremp3 = { version = "0.1", optional = true }
rand = "0.6.5"
swf = { git = "https://github.com/Herschel/swf-rs", rev = "07eb5bb" }
swf = { git = "https://github.com/ruffle-rs/swf-rs", rev = "89fee4c" }
[dependencies.jpeg-decoder]
git = "https://github.com/kaksmet/jpeg-decoder"

View File

@ -1,6 +1,10 @@
use crate::builtins::Object;
use crate::builtins::{create_movie_clip, register_builtins};
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;
@ -13,28 +17,45 @@ pub struct ActionContext<'a, 'gc, 'gc_context> {
pub audio: &'a mut dyn crate::backend::audio::AudioBackend,
}
pub struct Avm1 {
pub struct Avm1<'gc> {
swf_version: u8,
stack: Vec<Value>,
stack: Vec<Value<'gc>>,
rng: SmallRng,
constant_pool: Vec<String>,
locals: HashMap<String, Value>,
locals: HashMap<String, Value<'gc>>,
globals: HashMap<String, Value<'gc>>,
}
unsafe impl<'gc> gc_arena::Collect for Avm1<'gc> {
#[inline]
fn trace(&self, cc: gc_arena::CollectionContext) {
self.stack.trace(cc);
self.locals.trace(cc);
self.globals.trace(cc);
}
}
type Error = Box<dyn std::error::Error>;
impl Avm1 {
pub fn new(swf_version: u8) -> Self {
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,
}
}
pub fn do_action(&mut self, context: &mut ActionContext, code: &[u8]) -> Result<(), Error> {
pub fn do_action(
&mut self,
context: &mut ActionContext<'_, 'gc, '_>,
code: &[u8],
) -> Result<(), Error> {
let mut reader = Reader::new(Cursor::new(code), self.swf_version);
while let Some(action) = reader.read_action()? {
@ -157,11 +178,11 @@ impl Avm1 {
Ok(())
}
pub fn resolve_slash_path<'gc>(
start: DisplayNode<'gc>,
root: DisplayNode<'gc>,
pub fn resolve_slash_path<'dgc>(
start: DisplayNode<'dgc>,
root: DisplayNode<'dgc>,
mut path: &str,
) -> Option<DisplayNode<'gc>> {
) -> Option<DisplayNode<'dgc>> {
let mut cur_clip = if path.bytes().nth(0).unwrap_or(0) == b'/' {
path = &path[1..];
root
@ -185,11 +206,11 @@ impl Avm1 {
Some(cur_clip)
}
pub fn resolve_slash_path_variable<'gc, 's>(
start: DisplayNode<'gc>,
root: DisplayNode<'gc>,
pub fn resolve_slash_path_variable<'dgc, 's>(
start: DisplayNode<'dgc>,
root: DisplayNode<'dgc>,
path: &'s str,
) -> Option<(DisplayNode<'gc>, &'s str)> {
) -> Option<(DisplayNode<'dgc>, &'s str)> {
if !path.is_empty() {
let mut var_iter = path.splitn(2, ':');
match (var_iter.next(), var_iter.next()) {
@ -206,11 +227,11 @@ impl Avm1 {
None
}
fn push(&mut self, value: impl Into<Value>) {
fn push(&mut self, value: impl Into<Value<'gc>>) {
self.stack.push(value.into());
}
fn pop(&mut self) -> Result<Value, Error> {
fn pop(&mut self) -> Result<Value<'gc>, Error> {
self.stack.pop().ok_or_else(|| "Stack underflow".into())
}
@ -338,17 +359,40 @@ impl Avm1 {
Err("Unimplemented action: CallFunction".into())
}
fn action_call_method(&mut self, _context: &mut ActionContext) -> Result<(), Error> {
let _method_name = self.pop()?.as_string()?;
let _object = self.pop()?.as_object()?;
fn action_call_method(
&mut self,
_context: &mut ActionContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let method_name = self.pop()?;
let object = self.pop()?;
let num_args = self.pop()?.as_i64()?; // TODO(Herschel): max arg count?
let mut args = Vec::new();
for _ in 0..num_args {
self.pop()?;
args.push(self.pop()?);
}
self.stack.push(Value::Undefined);
// TODO(Herschel)
Err("Unimplemented action: CallMethod".into())
match method_name {
Value::Undefined | Value::Null => {
self.stack.push(object.call(&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)?);
}
}
_ => {
return Err(format!(
"Invalid method name, expected string but found {:?}",
method_name
)
.into())
}
}
Ok(())
}
fn action_constant_pool(
@ -504,20 +548,41 @@ impl Avm1 {
Ok(())
}
fn action_get_variable(&mut self, context: &mut ActionContext) -> Result<(), Error> {
fn action_get_variable(
&mut self,
context: &mut ActionContext<'_, 'gc, '_>,
) -> Result<(), Error> {
// Flash 4-style variable
let var_path = self.pop()?;
if let Some((node, var_name)) = Self::resolve_slash_path_variable(
context.active_clip,
context.root,
var_path.as_string()?,
) {
if let Some(clip) = node.read().as_movie_clip() {
self.push(clip.get_variable(var_name));
}
} else {
self.push(Value::Undefined);
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));
return Ok(());
}
let mut result = self.locals.get(path).map(|v| v.to_owned());
if result.is_none() {
if let Some((node, var_name)) =
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));
}
}
};
}
if result.is_none() {
if let Some(value) = self.globals.get(path) {
result = Some(value.clone());
}
}
self.push(result.unwrap_or(Value::Undefined));
Ok(())
}
@ -887,15 +952,25 @@ impl Avm1 {
Ok(())
}
fn action_set_variable(&mut self, context: &mut ActionContext) -> Result<(), Error> {
fn action_set_variable(
&mut self,
context: &mut ActionContext<'_, 'gc, '_>,
) -> Result<(), Error> {
// Flash 4-style variable
let value = self.pop()?;
let var_path = self.pop()?;
if let Some((node, var_name)) = Self::resolve_slash_path_variable(
context.active_clip,
context.root,
var_path.as_string()?,
) {
self.set_variable(context, var_path.as_string()?, value)
}
pub fn set_variable(
&mut self,
context: &mut ActionContext<'_, 'gc, '_>,
var_path: &str,
value: Value<'gc>,
) -> Result<(), Error> {
if let Some((node, var_name)) =
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);
}
@ -1030,7 +1105,10 @@ impl Avm1 {
Ok(())
}
fn action_target_path(&mut self, _context: &mut ActionContext) -> Result<(), Error> {
fn action_target_path(
&mut self,
_context: &mut ActionContext<'_, 'gc, '_>,
) -> Result<(), Error> {
// TODO(Herschel)
let _clip = self.pop()?.as_object()?;
self.push(Value::Undefined);
@ -1074,8 +1152,9 @@ impl Avm1 {
Value::Bool(_) => "boolean",
Value::String(_) => "string",
Value::Object(_) => "object",
Value::NativeFunction(_) => "function",
};
// TODO(Herschel): function, movieclip
// TODO(Herschel): movieclip
Ok(())
}
@ -1123,20 +1202,41 @@ impl Avm1 {
}
}
type ObjectPtr = std::marker::PhantomData<()>;
#[derive(Debug, Clone)]
#[derive(Clone)]
#[allow(dead_code)]
pub enum Value {
pub enum Value<'gc> {
Undefined,
Null,
Bool(bool),
Number(f64),
String(String),
Object(ObjectPtr),
Object(GcCell<'gc, Object<'gc>>),
NativeFunction(fn(&[Value<'gc>]) -> Value<'gc>),
}
impl Value {
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,
@ -1160,10 +1260,14 @@ impl Value {
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 {
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."
@ -1182,6 +1286,7 @@ impl Value {
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(),
}
}
@ -1206,7 +1311,7 @@ impl Value {
self.as_f64().map(|n| n as i64)
}
fn as_f64(&self) -> Result<f64, Error> {
pub fn as_f64(&self) -> Result<f64, Error> {
match *self {
Value::Number(v) => Ok(v),
_ => Err(format!("Expected Number, found {:?}", self).into()),
@ -1220,11 +1325,19 @@ impl Value {
}
}
fn as_object(&self) -> Result<&ObjectPtr, Error> {
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())
}
}
}

25
core/src/builtins/math.rs Normal file
View File

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

17
core/src/builtins/mod.rs Normal file
View File

@ -0,0 +1,17 @@
use crate::avm1::Value;
use gc_arena::MutationContext;
use std::collections::HashMap;
mod math;
mod movie_clip;
mod object;
pub use movie_clip::create_movie_clip;
pub use object::Object;
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

@ -0,0 +1,19 @@
use crate::avm1::Value;
use crate::builtins::Object;
use gc_arena::{GcCell, 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))
}

View File

@ -0,0 +1,27 @@
use crate::avm1::Value;
use gc_arena::Collect;
use std::collections::HashMap;
#[derive(Clone, Debug, Collect)]
#[collect(empty_drop)]
pub struct Object<'gc> {
values: HashMap<String, Value<'gc>>,
}
impl<'gc> Object<'gc> {
pub fn new() -> Self {
Self {
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(&mut self, name: &str, value: Value<'gc>) {
self.values.insert(name.to_owned(), value);
}
}

View File

@ -3,6 +3,7 @@ mod display_object;
mod avm1;
mod bounding_box;
mod builtins;
mod button;
mod character;
mod color_transform;

View File

@ -28,7 +28,7 @@ pub struct MovieClip<'gc> {
current_frame: FrameNumber,
audio_stream: Option<AudioStreamHandle>,
children: BTreeMap<Depth, DisplayNode<'gc>>,
variables: HashMap<String, avm1::Value>,
variables: HashMap<String, avm1::Value<'gc>>,
}
impl<'gc> MovieClip<'gc> {
@ -225,7 +225,11 @@ impl<'gc> MovieClip<'gc> {
self.goto_queue.clear();
}
pub fn get_variable(&self, var_name: &str) -> avm1::Value {
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)
@ -233,7 +237,7 @@ impl<'gc> MovieClip<'gc> {
.clone()
}
pub fn set_variable(&mut self, var_name: &str, value: avm1::Value) {
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);
}

View File

@ -15,6 +15,7 @@ struct GcRoot<'gc> {
library: GcCell<'gc, Library<'gc>>,
root: DisplayNode<'gc>,
mouse_hover_node: GcCell<'gc, Option<DisplayNode<'gc>>>, // TODO: Remove GcCell wrapped inside GcCell.
avm: GcCell<'gc, Avm1<'gc>>,
}
make_arena!(GcArena, GcRoot);
@ -25,7 +26,6 @@ pub struct Player<Audio: AudioBackend, Renderer: RenderBackend> {
is_playing: bool,
avm: Avm1,
audio: Audio,
renderer: Renderer,
transform_stack: TransformStack,
@ -72,7 +72,6 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
is_playing: false,
avm: Avm1::new(header.version),
renderer,
audio,
@ -99,6 +98,7 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
)),
),
mouse_hover_node: GcCell::allocate(gc_context, None),
avm: GcCell::allocate(gc_context, Avm1::new(gc_context, header.version)),
}),
frame_rate: header.frame_rate.into(),
@ -202,23 +202,13 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
}
}
let (
global_time,
swf_data,
swf_version,
background_color,
renderer,
audio,
avm,
is_mouse_down,
) = (
let (global_time, swf_data, swf_version, background_color, renderer, audio, is_mouse_down) = (
self.global_time,
&mut self.swf_data,
self.swf_version,
&mut self.background_color,
&mut self.renderer,
&mut self.audio,
&mut self.avm,
&mut self.is_mouse_down,
);
@ -229,7 +219,7 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
swf_version,
library: gc_root.library.write(gc_context),
background_color,
avm,
avm: gc_root.avm.write(gc_context),
renderer,
audio,
actions: vec![],
@ -274,14 +264,13 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
return false;
}
let (global_time, swf_data, swf_version, background_color, renderer, audio, avm) = (
let (global_time, swf_data, swf_version, background_color, renderer, audio) = (
self.global_time,
&mut self.swf_data,
self.swf_version,
&mut self.background_color,
&mut self.renderer,
&mut self.audio,
&mut self.avm,
);
let mouse_pos = &self.mouse_pos;
@ -299,7 +288,7 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
swf_version,
library: gc_root.library.write(gc_context),
background_color,
avm,
avm: gc_root.avm.write(gc_context),
renderer,
audio,
actions: vec![],
@ -334,14 +323,13 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
}
fn preload(&mut self) {
let (global_time, swf_data, swf_version, background_color, renderer, audio, avm) = (
let (global_time, swf_data, swf_version, background_color, renderer, audio) = (
self.global_time,
&mut self.swf_data,
self.swf_version,
&mut self.background_color,
&mut self.renderer,
&mut self.audio,
&mut self.avm,
);
self.gc_arena.mutate(|gc_context, gc_root| {
@ -351,7 +339,7 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
swf_version,
library: gc_root.library.write(gc_context),
background_color,
avm,
avm: gc_root.avm.write(gc_context),
renderer,
audio,
actions: vec![],
@ -364,14 +352,13 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
}
pub fn run_frame(&mut self) {
let (global_time, swf_data, swf_version, background_color, renderer, audio, avm) = (
let (global_time, swf_data, swf_version, background_color, renderer, audio) = (
self.global_time,
&mut self.swf_data,
self.swf_version,
&mut self.background_color,
&mut self.renderer,
&mut self.audio,
&mut self.avm,
);
self.gc_arena.mutate(|gc_context, gc_root| {
@ -381,7 +368,7 @@ impl<Audio: AudioBackend, Renderer: RenderBackend> Player<Audio, Renderer> {
swf_version,
library: gc_root.library.write(gc_context),
background_color,
avm,
avm: gc_root.avm.write(gc_context),
renderer,
audio,
actions: vec![],
@ -529,7 +516,7 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> {
pub library: std::cell::RefMut<'a, Library<'gc>>,
pub gc_context: MutationContext<'gc, 'gc_context>,
pub background_color: &'a mut Color,
pub avm: &'a mut Avm1,
pub avm: std::cell::RefMut<'a, Avm1<'gc>>,
pub renderer: &'a mut dyn RenderBackend,
pub audio: &'a mut dyn AudioBackend,
pub actions: Vec<(DisplayNode<'gc>, crate::tag_utils::SwfSlice)>,