ruffle/core/src/avm1.rs

562 lines
17 KiB
Rust

use crate::avm1::globals::create_globals;
use crate::avm1::object::{search_prototype, stage_object};
use crate::context::UpdateContext;
use crate::prelude::*;
use gc_arena::{GcCell, MutationContext};
use swf::avm1::read::Reader;
use crate::display_object::DisplayObject;
use crate::tag_utils::SwfSlice;
#[cfg(test)]
#[macro_use]
mod test_utils;
pub mod activation;
mod callable_value;
pub mod debug;
pub mod error;
mod fscommand;
pub mod function;
pub mod globals;
pub mod object;
mod property;
mod scope;
mod string;
mod timer;
mod value;
#[cfg(test)]
mod tests;
use crate::avm1::activation::{Activation, ActivationIdentifier};
pub use crate::avm1::error::Error;
use crate::avm1::globals::as_broadcaster;
use crate::avm1::globals::as_broadcaster::BroadcasterFunctions;
pub use globals::SystemPrototypes;
pub use object::script_object::ScriptObject;
pub use object::sound_object::SoundObject;
pub use object::stage_object::StageObject;
pub use object::{Object, ObjectPtr, TObject};
use scope::Scope;
use smallvec::alloc::borrow::Cow;
pub use string::AvmString;
pub use timer::Timers;
pub use value::Value;
macro_rules! avm_debug {
($avm: expr, $($arg:tt)*) => (
if $avm.show_debug_output() {
log::debug!($($arg)*)
}
)
}
#[macro_export]
macro_rules! avm_warn {
($activation: ident, $($arg:tt)*) => (
if cfg!(feature = "avm_debug") {
log::warn!("{} -- in {}", format!($($arg)*), $activation.id)
} else {
log::warn!($($arg)*)
}
)
}
#[macro_export]
macro_rules! avm_error {
($activation: ident, $($arg:tt)*) => (
if cfg!(feature = "avm_debug") {
log::error!("{} -- in {}", format!($($arg)*), $activation.id)
} else {
log::error!($($arg)*)
}
)
}
pub struct Avm1<'gc> {
/// The Flash Player version we're emulating.
player_version: u8,
/// 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>,
/// System builtins that we use internally to construct new objects.
prototypes: globals::SystemPrototypes<'gc>,
/// Cached functions for the AsBroadcaster
broadcaster_functions: BroadcasterFunctions<'gc>,
/// DisplayObject property map.
display_properties: GcCell<'gc, stage_object::DisplayPropertyMap<'gc>>,
/// The operand stack (shared across functions).
stack: Vec<Value<'gc>>,
/// The register slots (also shared across functions).
/// `ActionDefineFunction2` defined functions do not use these slots.
registers: [Value<'gc>; 4],
/// If a serious error has occurred, or a user has requested it, the AVM may be halted.
/// This will completely prevent any further actions from being executed.
halted: bool,
/// The maximum amount of functions that can be called before a `Error::FunctionRecursionLimit`
/// is raised. This defaults to 256 but can be changed per movie.
max_recursion_depth: u16,
/// Whether a Mouse listener has been registered.
/// Used to prevent scrolling on web.
has_mouse_listener: bool,
#[cfg(feature = "avm_debug")]
pub debug_output: bool,
}
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.system_listeners.trace(cc);
self.prototypes.trace(cc);
self.display_properties.trace(cc);
self.stack.trace(cc);
for register in &self.registers {
register.trace(cc);
}
}
}
impl<'gc> Avm1<'gc> {
pub fn new(gc_context: MutationContext<'gc, '_>, player_version: u8) -> Self {
let (prototypes, globals, broadcaster_functions) = create_globals(gc_context);
Self {
player_version,
constant_pool: GcCell::allocate(gc_context, vec![]),
globals,
prototypes,
broadcaster_functions,
display_properties: stage_object::DisplayPropertyMap::new(gc_context),
stack: vec![],
registers: [
Value::Undefined,
Value::Undefined,
Value::Undefined,
Value::Undefined,
],
halted: false,
max_recursion_depth: 255,
has_mouse_listener: false,
#[cfg(feature = "avm_debug")]
debug_output: false,
}
}
/// Add a stack frame that executes code in timeline scope
///
/// This creates a new frame stack.
pub fn run_stack_frame_for_action<S: Into<Cow<'static, str>>>(
active_clip: DisplayObject<'gc>,
name: S,
swf_version: u8,
code: SwfSlice,
context: &mut UpdateContext<'_, 'gc, '_>,
) {
if context.avm1.halted {
// We've been told to ignore all future execution.
return;
}
let globals = context.avm1.global_object_cell();
let mut parent_activation = Activation::from_nothing(
context.reborrow(),
ActivationIdentifier::root("[Actions Parent]"),
swf_version,
globals,
active_clip,
);
let clip_obj = active_clip
.object()
.coerce_to_object(&mut parent_activation);
let child_scope = GcCell::allocate(
parent_activation.context.gc_context,
Scope::new(
parent_activation.scope_cell(),
scope::ScopeClass::Target,
clip_obj,
),
);
let constant_pool = parent_activation.context.avm1.constant_pool;
let child_name = parent_activation.id.child(name);
let mut child_activation = Activation::from_action(
parent_activation.context.reborrow(),
child_name,
swf_version,
child_scope,
constant_pool,
active_clip,
clip_obj,
None,
);
if let Err(e) = child_activation.run_actions(code) {
root_error_handler(&mut child_activation, e);
}
}
/// Add a stack frame that executes code in initializer scope.
///
/// This creates a new frame stack.
pub fn run_with_stack_frame_for_display_object<'a, F, R>(
active_clip: DisplayObject<'gc>,
swf_version: u8,
action_context: &mut UpdateContext<'_, 'gc, '_>,
function: F,
) -> R
where
for<'b> F: FnOnce(&mut Activation<'b, 'gc, '_>) -> R,
{
let clip_obj = match active_clip.object() {
Value::Object(o) => o,
_ => panic!("No script object for display object"),
};
let global_scope = GcCell::allocate(
action_context.gc_context,
Scope::from_global_object(action_context.avm1.globals),
);
let child_scope = GcCell::allocate(
action_context.gc_context,
Scope::new(global_scope, scope::ScopeClass::Target, clip_obj),
);
let constant_pool = action_context.avm1.constant_pool;
let mut activation = Activation::from_action(
action_context.reborrow(),
ActivationIdentifier::root("[Display Object]"),
swf_version,
child_scope,
constant_pool,
active_clip,
clip_obj,
None,
);
function(&mut activation)
}
/// Add a stack frame that executes code in initializer scope.
///
/// This creates a new frame stack.
pub fn run_stack_frame_for_init_action(
active_clip: DisplayObject<'gc>,
swf_version: u8,
code: SwfSlice,
context: &mut UpdateContext<'_, 'gc, '_>,
) {
if context.avm1.halted {
// We've been told to ignore all future execution.
return;
}
let globals = context.avm1.global_object_cell();
let mut parent_activation = Activation::from_nothing(
context.reborrow(),
ActivationIdentifier::root("[Init Parent]"),
swf_version,
globals,
active_clip,
);
let clip_obj = active_clip
.object()
.coerce_to_object(&mut parent_activation);
let child_scope = GcCell::allocate(
parent_activation.context.gc_context,
Scope::new(
parent_activation.scope_cell(),
scope::ScopeClass::Target,
clip_obj,
),
);
parent_activation.context.avm1.push(Value::Undefined);
let constant_pool = parent_activation.context.avm1.constant_pool;
let child_name = parent_activation.id.child("[Init]");
let mut child_activation = Activation::from_action(
parent_activation.context.reborrow(),
child_name,
swf_version,
child_scope,
constant_pool,
active_clip,
clip_obj,
None,
);
if let Err(e) = child_activation.run_actions(code) {
root_error_handler(&mut child_activation, e);
}
}
/// Add a stack frame that executes code in timeline scope for an object
/// method, such as an event handler.
///
/// This creates a new frame stack.
pub fn run_stack_frame_for_method<'a, 'b>(
active_clip: DisplayObject<'gc>,
obj: Object<'gc>,
swf_version: u8,
context: &'a mut UpdateContext<'b, 'gc, '_>,
name: &str,
args: &[Value<'gc>],
) {
if context.avm1.halted {
// We've been told to ignore all future execution.
return;
}
let globals = context.avm1.global_object_cell();
let mut activation = Activation::from_nothing(
context.reborrow(),
ActivationIdentifier::root(name.to_owned()),
swf_version,
globals,
active_clip,
);
let search_result =
search_prototype(Some(obj), name, &mut activation, obj).map(|r| (r.0, r.1));
if let Ok((callback, base_proto)) = search_result {
let _ = callback.call(name, &mut activation, obj, base_proto, args);
}
}
pub fn notify_system_listeners(
active_clip: DisplayObject<'gc>,
swf_version: u8,
context: &mut UpdateContext<'_, 'gc, '_>,
broadcaster_name: &str,
method: &str,
args: &[Value<'gc>],
) {
let global = context.avm1.global_object_cell();
let mut activation = Activation::from_nothing(
context.reborrow(),
ActivationIdentifier::root("[System Listeners]"),
swf_version,
global,
active_clip,
);
let broadcaster = global
.get(broadcaster_name, &mut activation)
.unwrap()
.coerce_to_object(&mut activation);
let has_listener =
as_broadcaster::broadcast_internal(&mut activation, broadcaster, args, method)
.unwrap_or(false);
drop(activation);
if broadcaster_name == "Mouse" {
context.avm1.has_mouse_listener = has_listener;
}
}
/// Returns true if the `Mouse` object has a listener registered.
/// Used to prevent mouse wheel scrolling on web.
pub fn has_mouse_listener(&self) -> bool {
self.has_mouse_listener
}
/// Halts the AVM, preventing execution of any further actions.
///
/// If the AVM is currently evaluating an action, it will continue until it realizes that it has
/// been halted. If an immediate stop is required, an Error must be raised inside of the execution.
///
/// This is most often used when serious errors or infinite loops are encountered.
pub fn halt(&mut self) {
if !self.halted {
self.halted = true;
log::error!("No more actions will be executed in this movie.")
}
}
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);
}
#[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
}
/// Obtain the value of `_global`.
pub fn global_object(&self) -> Value<'gc> {
Value::Object(self.globals)
}
/// Obtain a reference to `_global`.
pub fn global_object_cell(&self) -> Object<'gc> {
self.globals
}
/// Obtain system built-in prototypes for this instance.
pub fn prototypes(&self) -> &globals::SystemPrototypes<'gc> {
&self.prototypes
}
pub fn max_recursion_depth(&self) -> u16 {
self.max_recursion_depth
}
pub fn set_max_recursion_depth(&mut self, max_recursion_depth: u16) {
self.max_recursion_depth = max_recursion_depth
}
#[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) {}
}
pub fn root_error_handler<'gc>(activation: &mut Activation<'_, 'gc, '_>, error: Error<'gc>) {
if let Error::ThrownValue(error) = &error {
let message = error
.coerce_to_string(activation)
.unwrap_or_else(|_| "undefined".into());
activation.context.log.avm_trace(&message);
} else {
log::error!("{}", error);
}
if error.is_halting() {
activation.context.avm1.halt();
}
}
/// Utility function used by `Avm1::action_wait_for_frame` and
/// `Avm1::action_wait_for_frame_2`.
fn skip_actions(reader: &mut Reader<'_>, num_actions_to_skip: u8) {
for _ in 0..num_actions_to_skip {
if let Err(e) = reader.read_action() {
log::warn!("Couldn't skip action: {}", e);
}
}
}
/// Starts draggining this display object, making it follow the cursor.
/// Runs via the `startDrag` method or `StartDrag` AVM1 action.
pub fn start_drag<'gc>(
display_object: DisplayObject<'gc>,
activation: &mut Activation<'_, 'gc, '_>,
args: &[Value<'gc>],
) {
let lock_center = args
.get(0)
.map(|o| o.as_bool(activation.context.swf.version()))
.unwrap_or(false);
let offset = if lock_center {
// The object's origin point is locked to the mouse.
Default::default()
} else {
// The object moves relative to current mouse position.
// Calculate the offset from the mouse to the object in world space.
let obj_pos = display_object.local_to_global(Default::default());
(
obj_pos.0 - activation.context.mouse_position.0,
obj_pos.1 - activation.context.mouse_position.1,
)
};
let constraint = if args.len() > 1 {
// Invalid values turn into 0.
let mut x_min = args
.get(1)
.unwrap_or(&Value::Undefined)
.coerce_to_f64(activation)
.map(|n| if n.is_finite() { n } else { 0.0 })
.map(Twips::from_pixels)
.unwrap_or_default();
let mut y_min = args
.get(2)
.unwrap_or(&Value::Undefined)
.coerce_to_f64(activation)
.map(|n| if n.is_finite() { n } else { 0.0 })
.map(Twips::from_pixels)
.unwrap_or_default();
let mut x_max = args
.get(3)
.unwrap_or(&Value::Undefined)
.coerce_to_f64(activation)
.map(|n| if n.is_finite() { n } else { 0.0 })
.map(Twips::from_pixels)
.unwrap_or_default();
let mut y_max = args
.get(4)
.unwrap_or(&Value::Undefined)
.coerce_to_f64(activation)
.map(|n| if n.is_finite() { n } else { 0.0 })
.map(Twips::from_pixels)
.unwrap_or_default();
// Normalize the bounds.
if x_max.get() < x_min.get() {
std::mem::swap(&mut x_min, &mut x_max);
}
if y_max.get() < y_min.get() {
std::mem::swap(&mut y_min, &mut y_max);
}
BoundingBox {
valid: true,
x_min,
y_min,
x_max,
y_max,
}
} else {
// No constraints.
Default::default()
};
let drag_object = crate::player::DragObject {
display_object,
offset,
constraint,
};
*activation.context.drag_object = Some(drag_object);
}