ruffle/core/src/avm2.rs

321 lines
9.4 KiB
Rust

//! ActionScript Virtual Machine 2 (AS3) support
use crate::avm2::globals::SystemPrototypes;
use crate::avm2::method::Method;
use crate::avm2::object::EventObject;
use crate::avm2::script::{Script, TranslationUnit};
use crate::avm2::string::AvmString;
use crate::context::UpdateContext;
use crate::tag_utils::SwfSlice;
use gc_arena::{Collect, MutationContext};
use std::collections::HashMap;
use std::rc::Rc;
use swf::avm2::read::Reader;
#[macro_export]
macro_rules! avm_debug {
($avm: expr, $($arg:tt)*) => (
if $avm.show_debug_output() {
log::debug!($($arg)*)
}
)
}
mod activation;
mod array;
mod bytearray;
mod class;
mod domain;
mod events;
mod function;
mod globals;
mod method;
mod names;
mod object;
mod property;
mod property_map;
mod regexp;
mod return_value;
mod scope;
mod script;
mod slot;
mod string;
mod traits;
mod value;
pub use crate::avm2::activation::Activation;
pub use crate::avm2::array::ArrayStorage;
pub use crate::avm2::domain::Domain;
pub use crate::avm2::events::Event;
pub use crate::avm2::names::{Namespace, QName};
pub use crate::avm2::object::{ArrayObject, Object, ScriptObject, StageObject, TObject};
pub use crate::avm2::value::Value;
const BROADCAST_WHITELIST: [&str; 3] = ["enterFrame", "exitFrame", "frameConstructed"];
/// Boxed error alias.
///
/// As AVM2 is a far stricter VM than AVM1, this may eventually be replaced
/// with a proper Avm2Error enum.
pub type Error = Box<dyn std::error::Error>;
/// The state of an AVM2 interpreter.
#[derive(Collect)]
#[collect(no_drop)]
pub struct Avm2<'gc> {
/// Values currently present on the operand stack.
stack: Vec<Value<'gc>>,
/// Global scope object.
globals: Domain<'gc>,
/// System prototypes.
system_prototypes: Option<SystemPrototypes<'gc>>,
/// A list of objects which are capable of recieving broadcasts.
///
/// Certain types of events are "broadcast events" that are emitted on all
/// constructed objects in order of their creation, whether or not they are
/// currently present on the display list. This list keeps track of that.
///
/// TODO: These should be weak object pointers, but our current garbage
/// collector does not support weak references.
broadcast_list: HashMap<AvmString<'gc>, Vec<Object<'gc>>>,
#[cfg(feature = "avm_debug")]
pub debug_output: bool,
}
impl<'gc> Avm2<'gc> {
/// Construct a new AVM interpreter.
pub fn new(mc: MutationContext<'gc, '_>) -> Self {
let globals = Domain::global_domain(mc);
Self {
stack: Vec::new(),
globals,
system_prototypes: None,
broadcast_list: HashMap::new(),
#[cfg(feature = "avm_debug")]
debug_output: false,
}
}
pub fn load_player_globals(context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> {
let globals = context.avm2.globals;
let mut activation = Activation::from_nothing(context.reborrow());
globals::load_player_globals(&mut activation, globals)
}
/// Return the current set of system prototypes.
///
/// This function panics if the interpreter has not yet been initialized.
pub fn prototypes(&self) -> &SystemPrototypes<'gc> {
self.system_prototypes.as_ref().unwrap()
}
/// Run a script's initializer method.
pub fn run_script_initializer(
script: Script<'gc>,
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let mut init_activation = Activation::from_script(context.reborrow(), script)?;
let (method, scope) = script.init();
match method {
Method::Native(nf) => {
nf(&mut init_activation, Some(scope), &[])?;
}
Method::Entry(_) => {
init_activation.run_stack_frame_for_script(script)?;
}
};
Ok(())
}
/// Dispatch an event on an object.
///
/// The `bool` parameter reads true if the event was cancelled.
pub fn dispatch_event(
context: &mut UpdateContext<'_, 'gc, '_>,
event: Event<'gc>,
target: Object<'gc>,
) -> Result<bool, Error> {
use crate::avm2::events::dispatch_event;
let event_proto = context.avm2.system_prototypes.as_ref().unwrap().event;
let event_object = EventObject::from_event(context.gc_context, Some(event_proto), event);
let mut activation = Activation::from_nothing(context.reborrow());
dispatch_event(&mut activation, target, event_object)
}
/// Add an object to the broadcast list.
///
/// Each broadcastable event contains it's own broadcast list. You must
/// register all objects that have event handlers with that event's
/// broadcast list by calling this function. Attempting to register a
/// broadcast listener for a non-broadcast event will do nothing.
///
/// Attempts to register the same listener for the same event will also do
/// nothing.
pub fn register_broadcast_listener(
context: &mut UpdateContext<'_, 'gc, '_>,
object: Object<'gc>,
event_name: AvmString<'gc>,
) {
if !BROADCAST_WHITELIST.iter().any(|x| *x == event_name) {
return;
}
let bucket = context.avm2.broadcast_list.entry(event_name).or_default();
if bucket.iter().any(|x| Object::ptr_eq(*x, object)) {
return;
}
bucket.push(object);
}
/// Dispatch an event on all objects in the current execution list.
///
/// `on_class_proto` specifies a class or interface prototype whose
/// instances, implementers, and/or subclasses define the set of objects
/// that will recieve the event. You can broadcast to just display objects,
/// or specific interfaces, and so on.
///
/// Attempts to broadcast a non-broadcast event will do nothing.
pub fn broadcast_event(
context: &mut UpdateContext<'_, 'gc, '_>,
event: Event<'gc>,
on_class_proto: Object<'gc>,
) -> Result<(), Error> {
let event_name = event.event_type();
if !BROADCAST_WHITELIST.iter().any(|x| *x == event_name) {
return Ok(());
}
let el_length = context
.avm2
.broadcast_list
.entry(event_name)
.or_default()
.len();
for i in 0..el_length {
let object = context
.avm2
.broadcast_list
.get(&event_name)
.unwrap()
.get(i)
.copied();
if let Some(object) = object {
if object.has_prototype_in_chain(on_class_proto, true)? {
Avm2::dispatch_event(context, event.clone(), object)?;
}
}
}
Ok(())
}
pub fn run_stack_frame_for_callable(
callable: Object<'gc>,
reciever: Option<Object<'gc>>,
args: &[Value<'gc>],
context: &mut UpdateContext<'_, 'gc, '_>,
) -> Result<(), Error> {
let mut evt_activation = Activation::from_nothing(context.reborrow());
callable.call(
reciever,
args,
&mut evt_activation,
reciever.and_then(|r| r.proto()),
)?;
Ok(())
}
/// Load an ABC file embedded in a `SwfSlice`.
///
/// The `SwfSlice` must resolve to the contents of an ABC file.
pub fn load_abc(
abc: SwfSlice,
_abc_name: &str,
lazy_init: bool,
context: &mut UpdateContext<'_, 'gc, '_>,
domain: Domain<'gc>,
) -> Result<(), Error> {
let mut read = Reader::new(abc.as_ref());
let abc_file = Rc::new(read.read()?);
let tunit = TranslationUnit::from_abc(abc_file.clone(), domain, context.gc_context);
for i in (0..abc_file.scripts.len()).rev() {
let mut script = tunit.load_script(i as u32, context.avm2, context.gc_context)?;
if !lazy_init {
script.globals(context)?;
}
}
Ok(())
}
pub fn global_domain(&self) -> Domain<'gc> {
self.globals
}
/// Push a value onto the operand stack.
fn push(&mut self, value: impl Into<Value<'gc>>) {
let value = value.into();
avm_debug!(self, "Stack push {}: {:?}", self.stack.len(), value);
self.stack.push(value);
}
/// Retrieve the top-most value on the operand stack.
#[allow(clippy::let_and_return)]
fn pop(&mut self) -> Value<'gc> {
let value = self.stack.pop().unwrap_or_else(|| {
log::warn!("Avm1::pop: Stack underflow");
Value::Undefined
});
avm_debug!(self, "Stack pop {}: {:?}", self.stack.len(), value);
value
}
fn pop_args(&mut self, arg_count: u32) -> Vec<Value<'gc>> {
let mut args = Vec::with_capacity(arg_count as usize);
args.resize(arg_count as usize, Value::Undefined);
for arg in args.iter_mut().rev() {
*arg = self.pop();
}
args
}
#[cfg(feature = "avm_debug")]
#[inline]
pub fn show_debug_output(&self) -> bool {
self.debug_output
}
#[cfg(not(feature = "avm_debug"))]
pub const fn show_debug_output(&self) -> bool {
false
}
#[cfg(feature = "avm_debug")]
pub fn set_show_debug_output(&mut self, visible: bool) {
self.debug_output = visible;
}
#[cfg(not(feature = "avm_debug"))]
pub const fn set_show_debug_output(&self, _visible: bool) {}
}