use crate::avm1::Attribute; use crate::avm1::Avm1; use crate::avm1::Object; use crate::avm1::SystemProperties; use crate::avm1::VariableDumper; use crate::avm1::{Activation, ActivationIdentifier}; use crate::avm1::{ScriptObject, TObject, Value}; use crate::avm2::{ object::LoaderInfoObject, object::TObject as _, Activation as Avm2Activation, Avm2, CallStack, Domain as Avm2Domain, EventObject as Avm2EventObject, Object as Avm2Object, }; use crate::backend::{ audio::{AudioBackend, AudioManager}, log::LogBackend, navigator::{NavigatorBackend, Request}, storage::StorageBackend, ui::{InputManager, MouseCursor, UiBackend}, }; use crate::config::Letterbox; use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext}; use crate::context_menu::{ BuiltInItemFlags, ContextMenuCallback, ContextMenuItem, ContextMenuState, }; use crate::display_object::{ EditText, InteractiveObject, MovieClip, Stage, StageAlign, StageDisplayState, StageQuality, StageScaleMode, TInteractiveObject, WindowMode, }; use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, MouseButton, PlayerEvent}; use crate::external::Value as ExternalValue; use crate::external::{ExternalInterface, ExternalInterfaceProvider}; use crate::focus_tracker::FocusTracker; use crate::font::Font; use crate::frame_lifecycle::{run_all_phases_avm2, FramePhase}; use crate::library::Library; use crate::limits::ExecutionLimit; use crate::loader::{LoadBehavior, LoadManager}; use crate::locale::get_current_date_time; use crate::prelude::*; use crate::string::AvmString; use crate::tag_utils::SwfMovie; use crate::timer::Timers; use crate::vminterface::Instantiator; use gc_arena::{ArenaParameters, Collect, GcCell}; use instant::Instant; use rand::{rngs::SmallRng, SeedableRng}; use ruffle_render::backend::{null::NullRenderer, RenderBackend, ViewportDimensions}; use ruffle_render::commands::CommandList; use ruffle_render::transform::TransformStack; use ruffle_video::backend::VideoBackend; use std::cell::RefCell; use std::collections::{HashMap, VecDeque}; use std::ops::DerefMut; use std::rc::{Rc, Weak as RcWeak}; use std::str::FromStr; use std::sync::{Arc, Mutex, Weak}; use std::time::Duration; use tracing::{info, instrument}; /// The newest known Flash Player version, serves as a default to /// `player_version`. pub const NEWEST_PLAYER_VERSION: u8 = 32; #[derive(Collect)] #[collect(no_drop)] struct GcRoot<'gc> { callstack: GcCell<'gc, GcCallstack<'gc>>, data: GcCell<'gc, GcRootData<'gc>>, } #[derive(Collect, Default)] #[collect(no_drop)] struct GcCallstack<'gc> { avm2: Option>>, } #[derive(Clone)] pub struct StaticCallstack { arena: RcWeak>, } impl StaticCallstack { pub fn avm2(&self, f: impl for<'gc> FnOnce(&CallStack<'gc>)) { if let Some(arena) = self.arena.upgrade() { if let Ok(arena) = arena.try_borrow() { arena.mutate(|_, root| { let callstack = root.callstack.read(); if let Some(callstack) = callstack.avm2 { f(&callstack.read()) } }) } } } } #[derive(Collect)] #[collect(no_drop)] struct GcRootData<'gc> { library: Library<'gc>, /// The root of the display object hierarchy. /// /// It's children are the `level`s of AVM1, it may also be directly /// accessed in AVM2. stage: Stage<'gc>, /// The display object that the mouse is currently hovering over. mouse_hovered_object: Option>, /// If the mouse is down, the display object that the mouse is currently pressing. mouse_pressed_object: Option>, /// The object being dragged via a `startDrag` action. drag_object: Option>, /// Interpreter state for AVM1 code. avm1: Avm1<'gc>, /// Interpreter state for AVM2 code. avm2: Avm2<'gc>, action_queue: ActionQueue<'gc>, /// Object which manages asynchronous processes that need to interact with /// data in the GC arena. load_manager: LoadManager<'gc>, avm1_shared_objects: HashMap>, avm2_shared_objects: HashMap>, /// Text fields with unbound variable bindings. unbound_text_fields: Vec>, /// Timed callbacks created with `setInterval`/`setTimeout`. timers: Timers<'gc>, current_context_menu: Option>, /// External interface for (for example) JavaScript <-> ActionScript interaction external_interface: ExternalInterface<'gc>, /// A tracker for the current keyboard focused element focus_tracker: FocusTracker<'gc>, /// Manager of active sound instances. audio_manager: AudioManager<'gc>, } impl<'gc> GcRootData<'gc> { /// Splits out parameters for creating an `UpdateContext` /// (because we can borrow fields of `self` independently) #[allow(clippy::type_complexity)] fn update_context_params( &mut self, ) -> ( Stage<'gc>, &mut Library<'gc>, &mut ActionQueue<'gc>, &mut Avm1<'gc>, &mut Avm2<'gc>, &mut Option>, &mut LoadManager<'gc>, &mut HashMap>, &mut HashMap>, &mut Vec>, &mut Timers<'gc>, &mut Option>, &mut ExternalInterface<'gc>, &mut AudioManager<'gc>, ) { ( self.stage, &mut self.library, &mut self.action_queue, &mut self.avm1, &mut self.avm2, &mut self.drag_object, &mut self.load_manager, &mut self.avm1_shared_objects, &mut self.avm2_shared_objects, &mut self.unbound_text_fields, &mut self.timers, &mut self.current_context_menu, &mut self.external_interface, &mut self.audio_manager, ) } } type GcArena = gc_arena::Arena]>; type Audio = Box; type Navigator = Box; type Renderer = Box; type Storage = Box; type Log = Box; type Ui = Box; type Video = Box; pub struct Player { /// The version of the player we're emulating. /// /// This serves a few purposes, primarily for compatibility: /// /// * ActionScript can query the player version, ostensibly for graceful /// degradation on older platforms. Certain SWF files broke with the /// release of Flash Player 10 because the version string contains two /// digits. This allows the user to play those old files. /// * Player-specific behavior that was not properly versioned in Flash /// Player can be enabled by setting a particular player version. player_version: u8, swf: Arc, warn_on_unsupported_content: bool, is_playing: bool, needs_render: bool, renderer: Renderer, audio: Audio, navigator: Navigator, storage: Storage, log: Log, ui: Ui, video: Video, transform_stack: TransformStack, rng: SmallRng, gc_arena: Rc>, frame_rate: f64, actions_since_timeout_check: u16, frame_phase: FramePhase, /// A time budget for executing frames. /// Gained by passage of time between host frames, spent by executing SWF frames. /// This is how we support custom SWF framerates /// and compensate for small lags by "catching up" (up to MAX_FRAMES_PER_TICK). frame_accumulator: f64, recent_run_frame_timings: VecDeque, /// Faked time passage for fooling hand-written busy-loop FPS limiters. time_offset: u32, input: InputManager, mouse_pos: (Twips, Twips), /// The current mouse cursor icon. mouse_cursor: MouseCursor, mouse_cursor_needs_check: bool, system: SystemProperties, /// The current instance ID. Used to generate default `instanceN` names. instance_counter: i32, /// Time remaining until the next timer will fire. time_til_next_timer: Option, /// The instant at which the SWF was launched. start_time: Instant, /// The maximum amount of time that can be called before a `Error::ExecutionTimeout` /// is raised. This defaults to 15 seconds but can be changed. max_execution_duration: Duration, /// Self-reference to ourselves. /// /// This is a weak reference that is upgraded and handed out in various /// contexts to other parts of the player. It can be used to ensure the /// player lives across `await` calls in async code. self_reference: Weak>, /// The current frame of the main timeline, if available. /// The first frame is frame 1. current_frame: Option, /// How Ruffle should load movies. load_behavior: LoadBehavior, /// The root SWF URL provided to ActionScript. If None, /// the actual loaded url will be used spoofed_url: Option, } impl Player { /// Fetch the root movie. /// /// This should not be called if a root movie fetch has already been kicked /// off. pub fn fetch_root_movie( &mut self, movie_url: String, parameters: Vec<(String, String)>, on_metadata: Box, ) { self.mutate_with_update_context(|context| { let future = context.load_manager.load_root_movie( context.player.clone(), Request::get(movie_url), parameters, on_metadata, ); context.navigator.spawn_future(future); }); } /// Change the root movie. /// /// This should only be called once, as it makes no attempt at removing /// previous stage contents. If you need to load a new root movie, you /// should destroy and recreate the player instance. pub fn set_root_movie(&mut self, movie: SwfMovie) { info!( "Loaded SWF version {}, with a resolution of {}x{}", movie.version(), movie.width(), movie.height() ); self.frame_rate = movie.frame_rate().into(); self.swf = Arc::new(movie); self.instance_counter = 0; self.mutate_with_update_context(|context| { context.stage.set_movie_size( context.gc_context, context.swf.width().to_pixels() as u32, context.swf.height().to_pixels() as u32, ); context .stage .set_movie(context.gc_context, context.swf.clone()); let mut activation = Avm2Activation::from_nothing(context.reborrow()); let global_domain = activation.avm2().global_domain(); let domain = Avm2Domain::movie_domain(&mut activation, global_domain); activation .context .library .library_for_movie_mut(activation.context.swf.clone()) .set_avm2_domain(domain); activation.context.ui.set_mouse_visible(true); let swf = activation.context.swf.clone(); let root: DisplayObject = MovieClip::player_root_movie(&mut activation, swf.clone()).into(); // The Stage `LoaderInfo` is permanently in the 'not yet loaded' state, // and has no associated `Loader` instance. // However, some properties are always accessible, and take their values // from the root SWF. let stage_loader_info = LoaderInfoObject::not_yet_loaded(&mut activation, swf, None, Some(root), true) .expect("Failed to construct Stage LoaderInfo"); activation .context .stage .set_loader_info(activation.context.gc_context, stage_loader_info); drop(activation); root.set_depth(context.gc_context, 0); let flashvars = if !context.swf.parameters().is_empty() { let object = ScriptObject::new(context.gc_context, None); for (key, value) in context.swf.parameters().iter() { object.define_value( context.gc_context, AvmString::new_utf8(context.gc_context, key), AvmString::new_utf8(context.gc_context, value).into(), Attribute::empty(), ); } Some(object.into()) } else { None }; root.post_instantiation(context, flashvars, Instantiator::Movie, false); root.set_default_root_name(context); context.stage.replace_at_depth(context, root, 0); // Load and parse the device font. if context.library.device_font().is_none() { let device_font = Self::load_device_font(context.gc_context, context.renderer); context.library.set_device_font(device_font); } // Set the version parameter on the root. let mut activation = Activation::from_stub( context.reborrow(), ActivationIdentifier::root("[Version Setter]"), ); let object = root.object().coerce_to_object(&mut activation); let version_string = activation .context .system .get_version_string(activation.context.avm1); object.define_value( activation.context.gc_context, "$version", AvmString::new_utf8(activation.context.gc_context, version_string).into(), Attribute::empty(), ); let stage = activation.context.stage; stage.build_matrices(&mut activation.context); }); if self.swf.is_action_script_3() && self.warn_on_unsupported_content { self.ui.display_unsupported_message(); } self.audio.set_frame_rate(self.frame_rate); } /// Get rough estimate of the max # of times we can update the frame. /// /// In some cases, we might want to update several times in a row. /// For example, if the game runs at 60FPS, but the host runs at 30FPS /// Or if for some reason the we miss a couple of frames. /// However, if the code is simply slow, this is the opposite of what we want; /// If run_frame() consistently takes say 100ms, we don't want `tick` to try to "catch up", /// as this will only make it worse. /// /// This rough heuristic manages this job; for example if average run_frame() /// takes more than 1/3 of frame_time, we shouldn't run it more than twice in a row. /// This logic is far from perfect, as it doesn't take into account /// that things like rendering also take time. But for now it's good enough. fn max_frames_per_tick(&self) -> u32 { const MAX_FRAMES_PER_TICK: u32 = 5; if self.recent_run_frame_timings.is_empty() { 5 } else { let frame_time = 1000.0 / self.frame_rate; let average_run_frame_time = self.recent_run_frame_timings.iter().sum::() / self.recent_run_frame_timings.len() as f64; ((frame_time / average_run_frame_time) as u32).clamp(1, MAX_FRAMES_PER_TICK) } } fn add_frame_timing(&mut self, elapsed: f64) { self.recent_run_frame_timings.push_back(elapsed); if self.recent_run_frame_timings.len() >= 10 { self.recent_run_frame_timings.pop_front(); } } pub fn tick(&mut self, dt: f64) { // Don't run until preloading is complete. // TODO: Eventually we want to stream content similar to the Flash player. if !self.audio.is_loading_complete() { return; } if self.is_playing() { self.frame_accumulator += dt; let frame_rate = self.frame_rate; let frame_time = 1000.0 / frame_rate; let max_frames_per_tick = self.max_frames_per_tick(); let mut frame = 0; while frame < max_frames_per_tick && self.frame_accumulator >= frame_time { let timer = Instant::now(); self.run_frame(); let elapsed = timer.elapsed().as_millis() as f64; self.add_frame_timing(elapsed); self.frame_accumulator -= frame_time; frame += 1; // The script probably tried implementing an FPS limiter with a busy loop. // We fooled the busy loop by pretending that more time has passed that actually did. // Then we need to actually pass this time, by decreasing frame_accumulator // to delay the future frame. if self.time_offset > 0 { self.frame_accumulator -= self.time_offset as f64; } } // Now that we're done running code, // we can stop pretending that more time passed than actually did. // Note: update_timers(dt) doesn't need to see this either. // Timers will run at correct times and see correct time. // Also note that in Flash, a blocking busy loop would delay setTimeout // and cancel some setInterval callbacks, but here busy loops don't block // so timer callbacks won't get cancelled/delayed. self.time_offset = 0; // Sanity: If we had too many frames to tick, just reset the accumulator // to prevent running at turbo speed. if self.frame_accumulator >= frame_time { self.frame_accumulator = 0.0; } // Adjust playback speed for next frame to stay in sync with timeline audio tracks ("stream" sounds). let cur_frame_offset = self.frame_accumulator; self.frame_accumulator += self.mutate_with_update_context(|context| { context .audio_manager .audio_skew_time(context.audio, cur_frame_offset) * 1000.0 }); self.update_timers(dt); self.audio.tick(); } } pub fn time_til_next_timer(&self) -> Option { self.time_til_next_timer } /// Returns the approximate duration of time until the next frame is due to run. /// This is only an approximation to be used for sleep durations. pub fn time_til_next_frame(&self) -> std::time::Duration { let frame_time = 1000.0 / self.frame_rate; let mut dt = if self.frame_accumulator <= 0.0 { frame_time } else if self.frame_accumulator >= frame_time { 0.0 } else { frame_time - self.frame_accumulator }; if let Some(time_til_next_timer) = self.time_til_next_timer { dt = dt.min(time_til_next_timer) } dt = dt.max(0.0); std::time::Duration::from_micros(dt as u64 * 1000) } pub fn is_playing(&self) -> bool { self.is_playing } /// Returns the master volume of the player. 1.0 is 100% volume. pub fn volume(&self) -> f32 { self.audio.volume() } /// Sets the master volume of the player. 1.0 is 100% volume. pub fn set_volume(&mut self, volume: f32) { self.audio.set_volume(volume) } pub fn prepare_context_menu(&mut self) -> Vec { self.mutate_with_update_context(|context| { if !context.stage.show_menu() { return vec![]; } // TODO: This should use a pointed display object with `.menu` let root_dobj = context.stage.root_clip(); let menu = if let Value::Object(obj) = root_dobj.object() { let mut activation = Activation::from_stub( context.reborrow(), ActivationIdentifier::root("[ContextMenu]"), ); let menu_object = if let Ok(Value::Object(menu)) = obj.get("menu", &mut activation) { if let Ok(Value::Object(on_select)) = menu.get("onSelect", &mut activation) { Self::run_context_menu_custom_callback( menu, on_select, &mut activation.context, ); } Some(menu) } else { None }; crate::avm1::make_context_menu_state(menu_object, &mut activation) } else if let Avm2Value::Object(_obj) = root_dobj.object2() { // TODO: send "menuSelect" event tracing::warn!("AVM2 Context menu callbacks are not implemented"); let mut activation = Avm2Activation::from_nothing(context.reborrow()); let menu_object = root_dobj .as_interactive() .map(|iobj| iobj.context_menu()) .and_then(|v| v.as_object()); crate::avm2::make_context_menu_state(menu_object, &mut activation) } else { // no AVM1 or AVM2 object - so just prepare the builtin items let mut menu = ContextMenuState::new(); let builtin_items = BuiltInItemFlags::for_stage(context.stage); menu.build_builtin_items(builtin_items, context.stage); menu }; let ret = menu.info().clone(); *context.current_context_menu = Some(menu); ret }) } pub fn clear_custom_menu_items(&mut self) { self.gc_arena.borrow().mutate(|gc_context, gc_root| { let mut root_data = gc_root.data.write(gc_context); root_data.current_context_menu = None; }); } pub fn run_context_menu_callback(&mut self, index: usize) { self.mutate_with_update_context(|context| { let menu = &context.current_context_menu; if let Some(ref menu) = menu { match menu.callback(index) { ContextMenuCallback::Avm1 { item, callback } => { Self::run_context_menu_custom_callback(*item, *callback, context) } ContextMenuCallback::Play => Self::toggle_play_root_movie(context), ContextMenuCallback::Forward => Self::forward_root_movie(context), ContextMenuCallback::Back => Self::back_root_movie(context), ContextMenuCallback::Rewind => Self::rewind_root_movie(context), ContextMenuCallback::Avm2 { .. } => { // TODO: Send menuItemSelect event } _ => {} } Self::run_actions(context); } }); } fn run_context_menu_custom_callback<'gc>( item: Object<'gc>, callback: Object<'gc>, context: &mut UpdateContext<'_, 'gc>, ) { let root_clip = context.stage.root_clip(); let mut activation = Activation::from_nothing( context.reborrow(), ActivationIdentifier::root("[Context Menu Callback]"), root_clip, ); // TODO: Remember to also change the first arg // when we support contextmenu on non-root-movie let params = vec![root_clip.object(), Value::Object(item)]; let _ = callback.call( "[Context Menu Callback]".into(), &mut activation, Value::Undefined, ¶ms, ); } pub fn set_fullscreen(&mut self, is_fullscreen: bool) { self.mutate_with_update_context(|context| { let display_state = if is_fullscreen { StageDisplayState::FullScreen } else { StageDisplayState::Normal }; context.stage.set_display_state(context, display_state); }); } fn toggle_play_root_movie(context: &mut UpdateContext<'_, '_>) { if let Some(mc) = context.stage.root_clip().as_movie_clip() { if mc.playing() { mc.stop(context); } else { mc.play(context); } } } fn rewind_root_movie(context: &mut UpdateContext<'_, '_>) { if let Some(mc) = context.stage.root_clip().as_movie_clip() { mc.goto_frame(context, 1, true) } } fn forward_root_movie(context: &mut UpdateContext<'_, '_>) { if let Some(mc) = context.stage.root_clip().as_movie_clip() { mc.next_frame(context); } } fn back_root_movie(context: &mut UpdateContext<'_, '_>) { if let Some(mc) = context.stage.root_clip().as_movie_clip() { mc.prev_frame(context); } } pub fn set_is_playing(&mut self, v: bool) { if v { // Allow auto-play after user gesture for web backends. self.audio.play(); } else { self.audio.pause(); } self.is_playing = v; } pub fn needs_render(&self) -> bool { self.needs_render } pub fn background_color(&mut self) -> Option { self.mutate_with_update_context(|context| context.stage.background_color()) } pub fn set_background_color(&mut self, color: Option) { self.mutate_with_update_context(|context| { context .stage .set_background_color(context.gc_context, color) }) } pub fn letterbox(&mut self) -> Letterbox { self.mutate_with_update_context(|context| context.stage.letterbox()) } pub fn set_letterbox(&mut self, letterbox: Letterbox) { self.mutate_with_update_context(|context| { context.stage.set_letterbox(context.gc_context, letterbox) }) } pub fn movie_width(&mut self) -> u32 { self.mutate_with_update_context(|context| context.stage.movie_size().0) } pub fn movie_height(&mut self) -> u32 { self.mutate_with_update_context(|context| context.stage.movie_size().1) } pub fn viewport_dimensions(&mut self) -> ViewportDimensions { self.mutate_with_update_context(|context| context.renderer.viewport_dimensions()) } pub fn set_viewport_dimensions(&mut self, dimensions: ViewportDimensions) { self.mutate_with_update_context(|context| { context.renderer.set_viewport_dimensions(dimensions); context.stage.build_matrices(context); }) } pub fn set_show_menu(&mut self, show_menu: bool) { self.mutate_with_update_context(|context| { let stage = context.stage; stage.set_show_menu(context, show_menu); }) } pub fn set_stage_align(&mut self, stage_align: &str) { self.mutate_with_update_context(|context| { let stage = context.stage; if let Ok(stage_align) = StageAlign::from_str(stage_align) { stage.set_align(context, stage_align); } }) } pub fn set_quality(&mut self, quality: &str) { self.mutate_with_update_context(|context| { let stage = context.stage; if let Ok(quality) = StageQuality::from_str(quality) { stage.set_quality(context.gc_context, quality); } }) } pub fn set_scale_mode(&mut self, scale_mode: &str) { self.mutate_with_update_context(|context| { let stage = context.stage; if let Ok(scale_mode) = StageScaleMode::from_str(scale_mode) { stage.set_scale_mode(context, scale_mode); } }) } pub fn set_window_mode(&mut self, window_mode: &str) { self.mutate_with_update_context(|context| { let stage = context.stage; if let Ok(window_mode) = WindowMode::from_str(window_mode) { stage.set_window_mode(context, window_mode); } }) } /// Handle an event sent into the player from the external windowing system /// or an HTML element. /// /// Event handling is a complicated affair, involving several different /// concerns that need to resolve with specific priority. /// /// 1. (In `avm_debug` builds) /// If Ctrl-Alt-V is pressed, dump all AVM1 variables in the player. /// If Ctrl-Alt-D is pressed, toggle debug output for AVM1 and AVM2. /// If Ctrl-Alt-F is pressed, dump the display object tree. /// 2. If the incoming event is text input or key input that could be /// related to text input (e.g. pressing a letter key), we dispatch a /// key press event onto the stage. /// 3. If the event from step 3 was not handled, we check if an `EditText` /// object is in focus and dispatch a text-control event to said object. /// 4. If the incoming event is text input, and neither step 3 nor step 4 /// resulted in an event being handled, we dispatch a text input event /// to the currently focused `EditText` (if present). /// 5. Regardless of all prior event handling, we dispatch the event /// through the stage normally. /// 6. Then, we dispatch the event through AVM1 global listener objects. /// 7. The AVM1 action queue is drained. /// 8. Mouse state is updated. This triggers button rollovers, which are a /// second wave of event processing. pub fn handle_event(&mut self, event: PlayerEvent) { let prev_is_mouse_down = self.input.is_mouse_down(); self.input.handle_event(&event); let is_mouse_button_changed = self.input.is_mouse_down() != prev_is_mouse_down; if cfg!(feature = "avm_debug") { match event { PlayerEvent::KeyDown { key_code: KeyCode::V, .. } if self.input.is_key_down(KeyCode::Control) && self.input.is_key_down(KeyCode::Alt) => { self.mutate_with_update_context(|context| { let mut dumper = VariableDumper::new(" "); let mut activation = Activation::from_stub( context.reborrow(), ActivationIdentifier::root("[Variable Dumper]"), ); dumper.print_variables( "Global Variables:", "_global", &activation.context.avm1.global_object(), &mut activation, ); for display_object in activation.context.stage.iter_render_list() { let level = display_object.depth(); let object = display_object.object().coerce_to_object(&mut activation); dumper.print_variables( &format!("Level #{level}:"), &format!("_level{level}"), &object, &mut activation, ); } tracing::info!("Variable dump:\n{}", dumper.output()); }); } PlayerEvent::KeyDown { key_code: KeyCode::D, .. } if self.input.is_key_down(KeyCode::Control) && self.input.is_key_down(KeyCode::Alt) => { self.mutate_with_update_context(|context| { if context.avm1.show_debug_output() { tracing::info!( "AVM Debugging turned off! Press CTRL+ALT+D to turn on again." ); context.avm1.set_show_debug_output(false); context.avm2.set_show_debug_output(false); } else { tracing::info!( "AVM Debugging turned on! Press CTRL+ALT+D to turn off." ); context.avm1.set_show_debug_output(true); context.avm2.set_show_debug_output(true); } }); } PlayerEvent::KeyDown { key_code: KeyCode::F, .. } if self.input.is_key_down(KeyCode::Control) && self.input.is_key_down(KeyCode::Alt) => { self.mutate_with_update_context(|context| { context.stage.display_render_tree(0); }); } _ => {} } } self.mutate_with_update_context(|context| { // Propagate button events. let button_event = match event { // ASCII characters convert directly to keyPress button events. PlayerEvent::TextInput { codepoint } if codepoint as u32 >= 32 && codepoint as u32 <= 126 => { Some(ClipEvent::KeyPress { key_code: ButtonKeyCode::from_u8(codepoint as u8).unwrap(), }) } // Special keys have custom values for keyPress. PlayerEvent::KeyDown { key_code, .. } => { if let Some(key_code) = crate::events::key_code_to_button_key_code(key_code) { Some(ClipEvent::KeyPress { key_code }) } else { None } } _ => None, }; let mut key_press_handled = false; if let Some(button_event) = button_event { for level in context.stage.iter_render_list() { let state = if let Some(interactive) = level.as_interactive() { interactive.handle_clip_event(context, button_event) } else { ClipEventResult::NotHandled }; if state == ClipEventResult::Handled { key_press_handled = true; break; } else if let Some(text) = context.focus_tracker.get().and_then(|o| o.as_edit_text()) { // Text fields listen for arrow key presses, etc. if text.handle_text_control_event(context, button_event) == ClipEventResult::Handled { key_press_handled = true; break; } } } } if context.is_action_script_3() { if let PlayerEvent::KeyDown { key_code, key_char } | PlayerEvent::KeyUp { key_code, key_char } = event { let ctrl_key = context.input.is_key_down(KeyCode::Control); let alt_key = context.input.is_key_down(KeyCode::Alt); let shift_key = context.input.is_key_down(KeyCode::Shift); let mut activation = Avm2Activation::from_nothing(context.reborrow()); let event_name = match event { PlayerEvent::KeyDown { .. } => "keyDown", PlayerEvent::KeyUp { .. } => "keyUp", _ => unreachable!(), }; let keyboardevent_class = activation.avm2().classes().keyboardevent; let event_name_val: Avm2Value<'_> = AvmString::new_utf8(activation.context.gc_context, event_name).into(); // TODO: keyLocation should not be a dummy value. // ctrlKey and controlKey can be different from each other on Mac. // commandKey should be supported. let keyboard_event = keyboardevent_class .construct( &mut activation, &[ event_name_val, /* type */ true.into(), /* bubbles */ false.into(), /* cancelable */ key_char.map_or(0, |c| c as u32).into(), /* charCode */ (key_code as u32).into(), /* keyCode */ 0.into(), /* keyLocation */ ctrl_key.into(), /* ctrlKey */ alt_key.into(), /* altKey */ shift_key.into(), /* shiftKey */ ctrl_key.into(), /* controlKey */ ], ) .expect("Failed to construct KeyboardEvent"); let target = activation .context .focus_tracker .get() .unwrap_or_else(|| activation.context.stage.into()) .object2() .coerce_to_object(&mut activation) .expect("DisplayObject is not an object!"); if let Err(e) = Avm2::dispatch_event(&mut activation.context, keyboard_event, target) { tracing::error!( "Encountered AVM2 error when broadcasting `{}` event: {}", event_name, e ); } } } // keyPress events take precedence over text input. if !key_press_handled { if let PlayerEvent::TextInput { codepoint } = event { if let Some(text) = context.focus_tracker.get().and_then(|o| o.as_edit_text()) { text.text_input(codepoint, context); } } } // Propagate clip events. let (clip_event, listener) = match event { PlayerEvent::KeyDown { .. } => { (Some(ClipEvent::KeyDown), Some(("Key", "onKeyDown", vec![]))) } PlayerEvent::KeyUp { .. } => { (Some(ClipEvent::KeyUp), Some(("Key", "onKeyUp", vec![]))) } PlayerEvent::MouseMove { .. } => ( Some(ClipEvent::MouseMove), Some(("Mouse", "onMouseMove", vec![])), ), PlayerEvent::MouseUp { button: MouseButton::Left, .. } => ( Some(ClipEvent::MouseUp), Some(("Mouse", "onMouseUp", vec![])), ), PlayerEvent::MouseDown { button: MouseButton::Left, .. } => ( Some(ClipEvent::MouseDown), Some(("Mouse", "onMouseDown", vec![])), ), PlayerEvent::MouseWheel { delta } => { let delta = Value::from(delta.lines()); (None, Some(("Mouse", "onMouseWheel", vec![delta]))) } _ => (None, None), }; // Fire clip event on all clips. if let Some(clip_event) = clip_event { for level in context.stage.iter_render_list() { if let Some(interactive) = level.as_interactive() { interactive.handle_clip_event(context, clip_event); } } } // Fire event listener on appropriate object if let Some((listener_type, event_name, args)) = listener { context.action_queue.queue_action( context.stage.root_clip(), ActionType::NotifyListeners { listener: listener_type, method: event_name, args, }, false, ); } Self::run_actions(context); }); // Update mouse state. if let PlayerEvent::MouseMove { x, y } | PlayerEvent::MouseDown { x, y, button: MouseButton::Left, } | PlayerEvent::MouseUp { x, y, button: MouseButton::Left, } = event { let inverse_view_matrix = self.mutate_with_update_context(|context| context.stage.inverse_view_matrix()); let old_pos = self.mouse_pos; self.mouse_pos = inverse_view_matrix * (Twips::from_pixels(x), Twips::from_pixels(y)); // Update the dragged object here to keep it constantly in sync with the mouse position. self.mutate_with_update_context(|context| { Self::update_drag(context); }); let is_mouse_moved = old_pos != self.mouse_pos; // This fires button rollover/press events, which should run after the above mouseMove events. if self.update_mouse_state(is_mouse_button_changed, is_mouse_moved) { self.needs_render = true; } } if let PlayerEvent::MouseWheel { delta } = event { self.mutate_with_update_context(|context| { if let Some(over_object) = context.mouse_over_object { if !over_object.as_displayobject().removed() { over_object.handle_clip_event(context, ClipEvent::MouseWheel { delta }); } } else { context .stage .handle_clip_event(context, ClipEvent::MouseWheel { delta }); } }); } } /// Update dragged object, if any. pub fn update_drag(context: &mut UpdateContext<'_, '_>) { let (mouse_x, mouse_y) = *context.mouse_position; if let Some(drag_object) = &mut context.drag_object { let display_object = drag_object.display_object; if drag_object.display_object.removed() { // Be sure to clear the drag if the object was removed. *context.drag_object = None; } else { let (offset_x, offset_y) = drag_object.offset; let mut drag_point = (mouse_x + offset_x, mouse_y + offset_y); if let Some(parent) = display_object.parent() { drag_point = parent.global_to_local(drag_point); } drag_point = drag_object.constraint.clamp(drag_point); display_object.set_x(context.gc_context, drag_point.0.to_pixels()); display_object.set_y(context.gc_context, drag_point.1.to_pixels()); // Update _droptarget property of dragged object. if let Some(movie_clip) = display_object.as_movie_clip() { // Turn the dragged object invisible so that we don't pick it. // TODO: This could be handled via adding a `HitTestOptions::SKIP_DRAGGED`. let was_visible = display_object.visible(); display_object.set_visible(context.gc_context, false); // Set _droptarget to the object the mouse is hovering over. let drop_target_object = context.stage.iter_render_list().rev().find_map(|level| { level .as_interactive() .and_then(|l| l.mouse_pick(context, *context.mouse_position, false)) }); movie_clip.set_drop_target( context.gc_context, drop_target_object.map(|d| d.as_displayobject()), ); display_object.set_visible(context.gc_context, was_visible); } } } } /// Updates the hover state of buttons. fn update_mouse_state(&mut self, is_mouse_button_changed: bool, is_mouse_moved: bool) -> bool { let mut new_cursor = self.mouse_cursor; let mut mouse_cursor_needs_check = self.mouse_cursor_needs_check; // Determine the display object the mouse is hovering over. // Search through levels from top-to-bottom, returning the first display object that is under the mouse. let needs_render = self.mutate_with_update_context(|context| { let new_over_object = context.stage.iter_render_list().rev().find_map(|level| { level .as_interactive() .and_then(|l| l.mouse_pick(context, *context.mouse_position, true)) }); let mut events: smallvec::SmallVec<[(InteractiveObject<'_>, ClipEvent); 2]> = Default::default(); if is_mouse_moved { events.push(( new_over_object.unwrap_or_else(|| context.stage.into()), ClipEvent::MouseMoveInside, )); } // Cancel hover if an object is removed from the stage. if let Some(hovered) = context.mouse_over_object { if hovered.as_displayobject().removed() { context.mouse_over_object = None; } } if let Some(pressed) = context.mouse_down_object { if pressed.as_displayobject().removed() { context.mouse_down_object = None; } } // Update the cursor if the object was removed from the stage. if new_cursor != MouseCursor::Arrow { let object_removed = context.mouse_over_object.is_none() && context.mouse_down_object.is_none(); if !object_removed { mouse_cursor_needs_check = false; if is_mouse_button_changed { // The object is pressed/released and may be removed immediately, we need to check // in the next frame if it still exists. If it doesn't, we'll update the cursor. mouse_cursor_needs_check = true; } } else if mouse_cursor_needs_check { mouse_cursor_needs_check = false; new_cursor = MouseCursor::Arrow; } else if !context.input.is_mouse_down() && (is_mouse_moved || is_mouse_button_changed) { // In every other case, the cursor remains until the user interacts with the mouse again. new_cursor = MouseCursor::Arrow; } } else { mouse_cursor_needs_check = false; } let cur_over_object = context.mouse_over_object; // Check if a new object has been hovered over. if !InteractiveObject::option_ptr_eq(cur_over_object, new_over_object) { // If the mouse button is down, the object the user clicked on grabs the focus // and fires "drag" events. Other objects are ignored. if context.input.is_mouse_down() { context.mouse_over_object = new_over_object; if let Some(down_object) = context.mouse_down_object { if InteractiveObject::option_ptr_eq( context.mouse_down_object, cur_over_object, ) { // Dragged from outside the clicked object to the inside. events.push(( down_object, ClipEvent::DragOut { to: new_over_object, }, )); } else if InteractiveObject::option_ptr_eq( context.mouse_down_object, new_over_object, ) { // Dragged from inside the clicked object to the outside. events.push(( down_object, ClipEvent::DragOver { from: cur_over_object, }, )); } } } else { // The mouse button is up, so fire rollover states for the object we are hovering over. // Rolled out of the previous object. if let Some(cur_over_object) = cur_over_object { events.push(( cur_over_object, ClipEvent::RollOut { to: new_over_object, }, )); } // Rolled over the new object. if let Some(new_over_object) = new_over_object { new_cursor = new_over_object.mouse_cursor(context); events.push(( new_over_object, ClipEvent::RollOver { from: cur_over_object, }, )); } else { new_cursor = MouseCursor::Arrow; } } } context.mouse_over_object = new_over_object; // Handle presses and releases. if is_mouse_button_changed { if context.input.is_mouse_down() { // Pressed on a hovered object. if let Some(over_object) = context.mouse_over_object { events.push((over_object, ClipEvent::Press)); context.mouse_down_object = context.mouse_over_object; } else { events.push((context.stage.into(), ClipEvent::Press)); } } else { if let Some(over_object) = context.mouse_over_object { events.push((over_object, ClipEvent::MouseUpInside)); } else { events.push((context.stage.into(), ClipEvent::MouseUpInside)); } let released_inside = InteractiveObject::option_ptr_eq( context.mouse_down_object, context.mouse_over_object, ); if released_inside { // Released inside the clicked object. if let Some(down_object) = context.mouse_down_object { new_cursor = down_object.mouse_cursor(context); events.push((down_object, ClipEvent::Release)); } else { events.push((context.stage.into(), ClipEvent::Release)); } } else { // Released outside the clicked object. if let Some(down_object) = context.mouse_down_object { events.push((down_object, ClipEvent::ReleaseOutside)); } else { events.push((context.stage.into(), ClipEvent::ReleaseOutside)); } // The new object is rolled over immediately. if let Some(over_object) = context.mouse_over_object { new_cursor = over_object.mouse_cursor(context); events.push(( over_object, ClipEvent::RollOver { from: cur_over_object, }, )); } else { new_cursor = MouseCursor::Arrow; } } context.mouse_down_object = None; } } // Fire any pending mouse events. let needs_render = if events.is_empty() { false } else { let mut refresh = false; for (object, event) in events { let display_object = object.as_displayobject(); if !display_object.removed() { object.handle_clip_event(context, event); } if !refresh && event.is_button_event() { let is_button_mode = display_object.as_avm1_button().is_some() || display_object.as_avm2_button().is_some() || display_object .as_movie_clip() .map(|mc| mc.is_button_mode(context)) .unwrap_or_default(); if is_button_mode { refresh = true; } } } refresh }; Self::run_actions(context); needs_render }); // Update mouse cursor if it has changed. if new_cursor != self.mouse_cursor { self.mouse_cursor = new_cursor; self.ui.set_mouse_cursor(new_cursor) } self.mouse_cursor_needs_check = mouse_cursor_needs_check; needs_render } /// Preload all pending movies in the player, including the root movie. /// /// This should be called periodically with a reasonable execution limit. /// By default, the Player will do so after every `run_frame` using a limit /// derived from the current frame rate and execution time. Clients that /// want synchronous or 'lockstep' preloading may call this function with /// an unlimited execution limit. /// /// Returns true if all preloading work has completed. Clients that want to /// simulate a particular load condition or stress chunked loading may use /// this in lieu of an unlimited execution limit. pub fn preload(&mut self, limit: &mut ExecutionLimit) -> bool { self.mutate_with_update_context(|context| { let mut did_finish = true; if let Some(root) = context.stage.root_clip().as_movie_clip() { let was_root_movie_loaded = root.loaded_bytes() == root.total_bytes(); did_finish = root.preload(context, limit); if !was_root_movie_loaded { if let Some(loader_info) = root.loader_info() { let mut activation = Avm2Activation::from_nothing(context.reborrow()); let progress_evt = activation.avm2().classes().progressevent.construct( &mut activation, &[ "progress".into(), false.into(), false.into(), root.compressed_loaded_bytes().into(), root.compressed_total_bytes().into(), ], ); match progress_evt { Err(e) => tracing::error!( "Encountered AVM2 error when broadcasting `progress` event: {}", e ), Ok(progress_evt) => { if let Err(e) = Avm2::dispatch_event(context, progress_evt, loader_info) { tracing::error!( "Encountered AVM2 error when broadcasting `progress` event: {}", e ); } } } } } } if did_finish { did_finish = LoadManager::preload_tick(context, limit); } did_finish }) } #[instrument(level = "debug", skip_all)] pub fn run_frame(&mut self) { let frame_time = Duration::from_nanos((750_000_000.0 / self.frame_rate) as u64); let (mut execution_limit, may_execute_while_streaming) = match self.load_behavior { LoadBehavior::Streaming => ( ExecutionLimit::with_max_ops_and_time(10000, frame_time), true, ), LoadBehavior::Delayed => ( ExecutionLimit::with_max_ops_and_time(10000, frame_time), false, ), LoadBehavior::Blocking => (ExecutionLimit::none(), false), }; let preload_finished = self.preload(&mut execution_limit); if !preload_finished && !may_execute_while_streaming { return; } self.update(|context| { if context.is_action_script_3() { run_all_phases_avm2(context); } else { Avm1::run_frame(context); } context.update_sounds(); }); self.needs_render = true; } #[instrument(level = "debug", skip_all)] pub fn render(&mut self) { let (renderer, ui, transform_stack) = (&mut self.renderer, &mut self.ui, &mut self.transform_stack); let mut background_color = Color::WHITE; let commands = self.gc_arena.borrow().mutate(|gc_context, gc_root| { let root_data = gc_root.data.read(); let stage = root_data.stage; let mut render_context = RenderContext { renderer: renderer.deref_mut(), commands: CommandList::new(), gc_context, ui: ui.deref_mut(), library: &root_data.library, transform_stack, is_offscreen: false, stage, clip_depth_stack: vec![], allow_mask: true, }; stage.render(&mut render_context); background_color = if stage.window_mode() != WindowMode::Transparent || stage.is_fullscreen() { stage.background_color().unwrap_or(Color::WHITE) } else { Color::from_rgba(0) }; render_context.commands }); renderer.submit_frame(background_color, commands); self.needs_render = false; } /// The current frame of the main timeline, if available. /// The first frame is frame 1. pub fn current_frame(&self) -> Option { self.current_frame } pub fn audio(&self) -> &Audio { &self.audio } pub fn audio_mut(&mut self) -> &mut Audio { &mut self.audio } pub fn navigator(&self) -> &Navigator { &self.navigator } // The frame rate of the current movie in FPS. pub fn frame_rate(&self) -> f64 { self.frame_rate } pub fn renderer(&self) -> &Renderer { &self.renderer } pub fn renderer_mut(&mut self) -> &mut Renderer { &mut self.renderer } pub fn storage(&self) -> &Storage { &self.storage } pub fn storage_mut(&mut self) -> &mut Storage { &mut self.storage } pub fn destroy(self) -> Renderer { self.renderer } pub fn ui(&self) -> &Ui { &self.ui } pub fn ui_mut(&mut self) -> &mut Ui { &mut self.ui } pub fn run_actions(context: &mut UpdateContext<'_, '_>) { // Note that actions can queue further actions, so a while loop is necessary here. while let Some(action) = context.action_queue.pop_action() { // We don't run frame actions if the clip was removed after it queued the action. if !action.is_unload && action.clip.removed() { continue; } match action.action_type { // DoAction/clip event code. ActionType::Normal { bytecode } | ActionType::Initialize { bytecode } => { Avm1::run_stack_frame_for_action(action.clip, "[Frame]", bytecode, context); } // Change the prototype of a MovieClip and run constructor events. ActionType::Construct { constructor: Some(constructor), events, } => { let mut activation = Activation::from_nothing( context.reborrow(), ActivationIdentifier::root("[Construct]"), action.clip, ); if let Ok(prototype) = constructor.get("prototype", &mut activation) { if let Value::Object(object) = action.clip.object() { object.define_value( activation.context.gc_context, "__proto__", prototype, Attribute::empty(), ); for event in events { let _ = activation.run_child_frame_for_action( "[Actions]", action.clip, event, ); } let _ = constructor.construct_on_existing(&mut activation, object, &[]); } } } // Run constructor events without changing the prototype. ActionType::Construct { constructor: None, events, } => { for event in events { Avm1::run_stack_frame_for_action( action.clip, "[Construct]", event, context, ); } } // Event handler method call (e.g. onEnterFrame). ActionType::Method { object, name, args } => { Avm1::run_stack_frame_for_method( action.clip, object, context, name.into(), &args, ); } // Event handler method call (e.g. onEnterFrame). ActionType::NotifyListeners { listener, method, args, } => { // A native function ends up resolving immediately, // so this doesn't require any further execution. Avm1::notify_system_listeners( action.clip, context, listener.into(), method.into(), &args, ); } ActionType::Callable2 { callable, reciever, args, } => { if let Err(e) = Avm2::run_stack_frame_for_callable(callable, reciever, &args[..], context) { tracing::error!("Unhandled AVM2 exception in event handler: {}", e); } } ActionType::Event2 { event_type, target } => { let event = Avm2EventObject::bare_default_event(context, event_type); if let Err(e) = Avm2::dispatch_event(context, event, target) { tracing::error!("Unhandled AVM2 exception in event handler: {}", e); } } } // AVM1 bytecode may leave the stack unbalanced, so do not let garbage values accumulate // across multiple executions and/or frames. context.avm1.clear_stack(); } } /// Runs the closure `f` with an `UpdateContext`. /// This takes cares of populating the `UpdateContext` struct, avoiding borrow issues. pub(crate) fn mutate_with_update_context(&mut self, f: F) -> R where F: for<'a, 'gc> FnOnce(&mut UpdateContext<'a, 'gc>) -> R, { self.gc_arena.borrow().mutate(|gc_context, gc_root| { let mut root_data = gc_root.data.write(gc_context); let mouse_hovered_object = root_data.mouse_hovered_object; let mouse_pressed_object = root_data.mouse_pressed_object; let focus_tracker = root_data.focus_tracker; let ( stage, library, action_queue, avm1, avm2, drag_object, load_manager, avm1_shared_objects, avm2_shared_objects, unbound_text_fields, timers, current_context_menu, external_interface, audio_manager, ) = root_data.update_context_params(); let mut update_context = UpdateContext { player_version: self.player_version, swf: &self.swf, library, rng: &mut self.rng, renderer: self.renderer.deref_mut(), audio: self.audio.deref_mut(), navigator: self.navigator.deref_mut(), ui: self.ui.deref_mut(), action_queue, gc_context, stage, mouse_over_object: mouse_hovered_object, mouse_down_object: mouse_pressed_object, input: &self.input, mouse_position: &self.mouse_pos, drag_object, player: self.self_reference.clone(), load_manager, system: &mut self.system, instance_counter: &mut self.instance_counter, storage: self.storage.deref_mut(), log: self.log.deref_mut(), video: self.video.deref_mut(), avm1_shared_objects, avm2_shared_objects, unbound_text_fields, timers, current_context_menu, needs_render: &mut self.needs_render, avm1, avm2, external_interface, start_time: self.start_time, update_start: Instant::now(), max_execution_duration: self.max_execution_duration, focus_tracker, times_get_time_called: 0, time_offset: &mut self.time_offset, audio_manager, frame_rate: &mut self.frame_rate, actions_since_timeout_check: &mut self.actions_since_timeout_check, frame_phase: &mut self.frame_phase, }; let old_frame_rate = *update_context.frame_rate; let ret = f(&mut update_context); let new_frame_rate = *update_context.frame_rate; // If we changed the framerate, let the audio handler now. #[allow(clippy::float_cmp)] if old_frame_rate != new_frame_rate { update_context.audio.set_frame_rate(new_frame_rate); } self.current_frame = update_context .stage .root_clip() .as_movie_clip() .map(|clip| clip.current_frame()); // Hovered object may have been updated; copy it back to the GC root. let mouse_hovered_object = update_context.mouse_over_object; let mouse_pressed_object = update_context.mouse_down_object; root_data.mouse_hovered_object = mouse_hovered_object; root_data.mouse_pressed_object = mouse_pressed_object; ret }) } pub fn load_device_font<'gc>( gc_context: gc_arena::MutationContext<'gc, '_>, renderer: &mut dyn RenderBackend, ) -> Font<'gc> { const DEVICE_FONT_TAG: &[u8] = include_bytes!("../assets/noto-sans-definefont3.bin"); let mut reader = swf::read::Reader::new(DEVICE_FONT_TAG, 8); Font::from_swf_tag( gc_context, renderer, reader .read_define_font_2(3) .expect("Built-in font should compile"), reader.encoding(), ) } /// Update the current state of the player. /// /// The given function will be called with the current stage root, current /// mouse hover node, AVM, and an update context. /// /// This particular function runs necessary post-update bookkeeping, such /// as executing any actions queued on the update context, keeping the /// hover state up to date, and running garbage collection. pub fn update(&mut self, func: F) -> R where F: for<'a, 'gc> FnOnce(&mut UpdateContext<'a, 'gc>) -> R, { let rval = self.mutate_with_update_context(|context| { let rval = func(context); Self::run_actions(context); rval }); // Update mouse state (check for new hovered button, etc.) self.mutate_with_update_context(|context| { Self::update_drag(context); }); self.update_mouse_state(false, false); // GC self.gc_arena.borrow_mut().collect_debt(); rval } pub fn flush_shared_objects(&mut self) { self.update(|context| { let mut avm1_activation = Activation::from_stub(context.reborrow(), ActivationIdentifier::root("[Flush]")); for so in avm1_activation.context.avm1_shared_objects.clone().values() { if let Err(e) = crate::avm1::flush(&mut avm1_activation, *so, &[]) { tracing::error!("Error flushing AVM1 shared object `{:?}`: {:?}", so, e); } } let mut avm2_activation = Avm2Activation::from_nothing(avm1_activation.context.reborrow()); for so in avm2_activation.context.avm2_shared_objects.clone().values() { if let Err(e) = crate::avm2::globals::flash::net::shared_object::flush( &mut avm2_activation, Some(*so), &[], ) { tracing::error!("Error flushing AVM2 shared object `{:?}`: {:?}", so, e); } } }); } /// Update all AVM-based timers (such as created via setInterval). /// Returns the approximate amount of time until the next timer tick. pub fn update_timers(&mut self, dt: f64) { self.time_til_next_timer = self.mutate_with_update_context(|context| Timers::update_timers(context, dt)); } /// Returns whether this player consumes mouse wheel events. /// Used by web to prevent scrolling. pub fn should_prevent_scrolling(&mut self) -> bool { self.mutate_with_update_context(|context| context.avm1.has_mouse_listener()) } pub fn add_external_interface(&mut self, provider: Box) { self.mutate_with_update_context(|context| { context.external_interface.add_provider(provider) }); } pub fn call_internal_interface( &mut self, name: &str, args: impl IntoIterator, ) -> ExternalValue { self.mutate_with_update_context(|context| { if let Some(callback) = context.external_interface.get_callback(name) { callback.call(context, name, args) } else { ExternalValue::Null } }) } pub fn spoofed_url(&self) -> Option<&str> { self.spoofed_url.as_deref() } pub fn log_backend(&self) -> &Log { &self.log } pub fn max_execution_duration(&self) -> Duration { self.max_execution_duration } pub fn set_max_execution_duration(&mut self, max_execution_duration: Duration) { self.max_execution_duration = max_execution_duration } pub fn callstack(&self) -> StaticCallstack { StaticCallstack { arena: Rc::downgrade(&self.gc_arena), } } } /// Player factory, which can be used to configure the aspects of a Ruffle player. pub struct PlayerBuilder { movie: Option, // Backends audio: Option