From c1cf73ccb30841fb606823e3f4f2142dd1232005 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 9 Nov 2019 22:40:07 -0500 Subject: [PATCH 01/59] Add a public method to trigger a player update. User-defined player updates are treated the same as if a stage frame had been run: queued actions are executed and garbage is collected. --- core/src/player.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/core/src/player.rs b/core/src/player.rs index 73851f56a..60eb6a8fc 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -461,6 +461,8 @@ impl< }); } + /// Checks to see if a recent update has caused the current mouse hover + /// node to change. fn update_roll_over(&mut self) -> bool { // TODO: While the mouse is down, maintain the hovered node. if self.is_mouse_down { @@ -780,6 +782,35 @@ impl< crate::font::Font::from_swf_tag(gc_context, renderer, &reader.read_define_font_2(3)?)?; Ok(device_font) } + + /// 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 Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>) -> R, + { + let rval = self.mutate_with_update_context(|avm, context| { + let rval = func(avm, context); + + Self::run_actions(avm, context); + + rval + }); + + // Update mouse state (check for new hovered button, etc.) + self.update_roll_over(); + + // GC + self.gc_arena.collect_debt(); + + rval + } } pub struct DragObject<'gc> { From 2137b9f1fdd2428a937e5cc326328a23d8fd8f31 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 8 Nov 2019 15:07:01 -0500 Subject: [PATCH 02/59] Migrate `set_frame_rate` into the core `AudioBackend` trait --- core/src/backend/audio.rs | 7 +++++++ web/src/audio.rs | 8 ++++---- web/src/lib.rs | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/core/src/backend/audio.rs b/core/src/backend/audio.rs index 7b94b8dc6..c7eba6da6 100644 --- a/core/src/backend/audio.rs +++ b/core/src/backend/audio.rs @@ -76,6 +76,13 @@ pub trait AudioBackend { true } fn tick(&mut self) {} + + /// Inform the audio backend of the current stage frame rate. + /// + /// This is only necessary if your particular audio backend needs to know + /// what the stage frame rate is. Otherwise, you are free to avoid + /// implementing it. + fn set_frame_rate(&mut self, frame_rate: f64) {} } /// Rust does not auto-impl a Trait for Box or Deref diff --git a/web/src/audio.rs b/web/src/audio.rs index 23f95b070..02cc5d481 100644 --- a/web/src/audio.rs +++ b/web/src/audio.rs @@ -123,10 +123,6 @@ impl WebAudioBackend { }) } - pub fn set_frame_rate(&mut self, frame_rate: f64) { - self.frame_rate = frame_rate - } - fn start_sound_internal( &mut self, handle: SoundHandle, @@ -542,6 +538,10 @@ impl WebAudioBackend { } impl AudioBackend for WebAudioBackend { + fn set_frame_rate(&mut self, frame_rate: f64) { + self.frame_rate = frame_rate + } + fn register_sound(&mut self, sound: &swf::Sound) -> Result { // Slice off latency seek for MP3 data. let (skip_sample_frames, data) = if sound.format.compression == AudioCompression::Mp3 { diff --git a/web/src/lib.rs b/web/src/lib.rs index 12085ce40..e412f4fd3 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -11,7 +11,7 @@ use crate::{ }; use generational_arena::{Arena, Index}; use js_sys::Uint8Array; -use ruffle_core::{backend::render::RenderBackend, PlayerEvent}; +use ruffle_core::{backend::audio::AudioBackend, backend::render::RenderBackend, PlayerEvent}; use std::{cell::RefCell, error::Error, num::NonZeroI32}; use wasm_bindgen::{prelude::*, JsCast, JsValue}; use web_sys::{Element, EventTarget, HtmlCanvasElement, KeyboardEvent, PointerEvent}; From 00d25a768c52b2ee41f01a24e330d3f9bd9691fb Mon Sep 17 00:00:00 2001 From: David Wendt Date: Thu, 7 Nov 2019 14:34:38 -0500 Subject: [PATCH 03/59] Extremely basic impl of fetch/spawn methods for getting data off the web --- Cargo.lock | 1 + core/src/backend/audio.rs | 2 +- core/src/backend/navigator.rs | 22 ++++++++++++++++++++++ desktop/src/navigator.rs | 11 ++++++++++- web/Cargo.toml | 1 + web/src/navigator.rs | 12 +++++++++++- 6 files changed, 46 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb88f1a8b..be37d70b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1640,6 +1640,7 @@ dependencies = [ "svg 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen 0.2.57 (registry+https://github.com/rust-lang/crates.io-index)", + "wasm-bindgen-futures 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)", "wasm-bindgen-test 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "web-sys 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/core/src/backend/audio.rs b/core/src/backend/audio.rs index c7eba6da6..6d6ccb868 100644 --- a/core/src/backend/audio.rs +++ b/core/src/backend/audio.rs @@ -82,7 +82,7 @@ pub trait AudioBackend { /// This is only necessary if your particular audio backend needs to know /// what the stage frame rate is. Otherwise, you are free to avoid /// implementing it. - fn set_frame_rate(&mut self, frame_rate: f64) {} + fn set_frame_rate(&mut self, _frame_rate: f64) {} } /// Rust does not auto-impl a Trait for Box or Deref diff --git a/core/src/backend/navigator.rs b/core/src/backend/navigator.rs index 47093af0d..ee6c71ddc 100644 --- a/core/src/backend/navigator.rs +++ b/core/src/backend/navigator.rs @@ -1,8 +1,11 @@ //! Browser-related platform functions use std::collections::HashMap; +use std::future::Future; use swf::avm1::types::SendVarsMethod; +pub type Error = Box; + /// Enumerates all possible navigation methods. pub enum NavigationMethod { /// Indicates that navigation should generate a GET request. @@ -53,6 +56,19 @@ pub trait NavigatorBackend { window: Option, vars_method: Option<(NavigationMethod, HashMap)>, ); + + /// Fetch data at a given URL and return it some time in the future. + fn fetch(&self, url: String) -> Box, Error>>>; + + /// Arrange for a future to be run at some point in the... well, future. + /// + /// This function must be called to ensure a future is actually computed. + /// The future must output an empty value and not hold any stack references + /// which would cause it to become invalidated. + /// + /// TODO: For some reason, `wasm_bindgen_futures` wants unpinnable futures. + /// This seems highly limiting. + fn spawn_future(&mut self, future: Box + Unpin + 'static>); } /// A null implementation for platforms that do not live in a web browser. @@ -78,4 +94,10 @@ impl NavigatorBackend for NullNavigatorBackend { _vars_method: Option<(NavigationMethod, HashMap)>, ) { } + + fn fetch(&self, _url: String) -> Box, Error>>> { + Box::new(async { Err("Fetch IO not implemented".into()) }) + } + + fn spawn_future(&mut self, _future: Box + Unpin + 'static>) {} } diff --git a/desktop/src/navigator.rs b/desktop/src/navigator.rs index 062e73114..2d10c0204 100644 --- a/desktop/src/navigator.rs +++ b/desktop/src/navigator.rs @@ -1,8 +1,9 @@ //! Navigator backend for web use log; -use ruffle_core::backend::navigator::{NavigationMethod, NavigatorBackend}; +use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend}; use std::collections::HashMap; +use std::future::Future; use url::Url; use webbrowser; @@ -60,4 +61,12 @@ impl NavigatorBackend for ExternalNavigatorBackend { Err(e) => log::error!("Could not open URL {}: {}", modified_url, e), }; } + + fn fetch(&self, _url: String) -> Box, Error>>> { + Box::new(async { Err("Fetch not implemented on desktop!".into()) }) + } + + fn spawn_future(&mut self, _future: Box + Unpin + 'static>) { + unimplemented!(); + } } diff --git a/web/Cargo.toml b/web/Cargo.toml index 4f365a4e5..df9c68892 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -24,6 +24,7 @@ svg = "0.6.0" percent-encoding = "2.1.0" url = "2.1.1" wasm-bindgen = "0.2.57" +wasm-bindgen-futures = "0.4.4" [dependencies.jpeg-decoder] version = "0.1.18" diff --git a/web/src/navigator.rs b/web/src/navigator.rs index 5e866b53e..4a6d59ba2 100644 --- a/web/src/navigator.rs +++ b/web/src/navigator.rs @@ -1,8 +1,10 @@ //! Navigator backend for web -use ruffle_core::backend::navigator::{NavigationMethod, NavigatorBackend}; +use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend}; use std::collections::HashMap; +use std::future::Future; use wasm_bindgen::JsCast; +use wasm_bindgen_futures::spawn_local; use web_sys::window; pub struct WebNavigatorBackend {} @@ -72,4 +74,12 @@ impl NavigatorBackend for WebNavigatorBackend { }; } } + + fn fetch(&self, _url: String) -> Box, Error>>> { + Box::new(async { Ok(Vec::new()) }) + } + + fn spawn_future(&mut self, future: Box + Unpin + 'static>) { + spawn_local(future) + } } From 491d94c94772ee110b605e07993a844304312c68 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 9 Nov 2019 21:19:49 -0500 Subject: [PATCH 04/59] Pinbox all Futures. Due to some strangeness with the way Rust implemented unsafe-to-move behavior, boxed futures are implicitly `Unpin`. Which is useless to us. The reason for this is a little counter-intuitive. Actually, the fact that Rust supports memory pinning at all is a little odd, because the core language explicitly requires all types be movable. To get around this, Pin requires that all !Unpin types be *born pinned*. This is because you can't re-pin an already unpinned value in memory. Anyway, this necessitates this silly API change. --- core/src/backend/navigator.rs | 11 ++++++----- desktop/src/navigator.rs | 7 ++++--- web/src/navigator.rs | 7 ++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/core/src/backend/navigator.rs b/core/src/backend/navigator.rs index ee6c71ddc..7aa5ee2fb 100644 --- a/core/src/backend/navigator.rs +++ b/core/src/backend/navigator.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::future::Future; +use std::pin::Pin; use swf::avm1::types::SendVarsMethod; pub type Error = Box; @@ -58,7 +59,7 @@ pub trait NavigatorBackend { ); /// Fetch data at a given URL and return it some time in the future. - fn fetch(&self, url: String) -> Box, Error>>>; + fn fetch(&self, url: String) -> Pin, Error>>>>; /// Arrange for a future to be run at some point in the... well, future. /// @@ -68,7 +69,7 @@ pub trait NavigatorBackend { /// /// TODO: For some reason, `wasm_bindgen_futures` wants unpinnable futures. /// This seems highly limiting. - fn spawn_future(&mut self, future: Box + Unpin + 'static>); + fn spawn_future(&mut self, future: Pin + 'static>>); } /// A null implementation for platforms that do not live in a web browser. @@ -95,9 +96,9 @@ impl NavigatorBackend for NullNavigatorBackend { ) { } - fn fetch(&self, _url: String) -> Box, Error>>> { - Box::new(async { Err("Fetch IO not implemented".into()) }) + fn fetch(&self, _url: String) -> Pin, Error>>>> { + Box::pin(async { Err("Fetch IO not implemented".into()) }) } - fn spawn_future(&mut self, _future: Box + Unpin + 'static>) {} + fn spawn_future(&mut self, _future: Pin + 'static>>) {} } diff --git a/desktop/src/navigator.rs b/desktop/src/navigator.rs index 2d10c0204..d8d6643e9 100644 --- a/desktop/src/navigator.rs +++ b/desktop/src/navigator.rs @@ -4,6 +4,7 @@ use log; use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend}; use std::collections::HashMap; use std::future::Future; +use std::pin::Pin; use url::Url; use webbrowser; @@ -62,11 +63,11 @@ impl NavigatorBackend for ExternalNavigatorBackend { }; } - fn fetch(&self, _url: String) -> Box, Error>>> { - Box::new(async { Err("Fetch not implemented on desktop!".into()) }) + fn fetch(&self, _url: String) -> Pin, Error>>>> { + Box::pin(async { Err("Fetch not implemented on desktop!".into()) }) } - fn spawn_future(&mut self, _future: Box + Unpin + 'static>) { + fn spawn_future(&mut self, _future: Pin + 'static>>) { unimplemented!(); } } diff --git a/web/src/navigator.rs b/web/src/navigator.rs index 4a6d59ba2..5933a5204 100644 --- a/web/src/navigator.rs +++ b/web/src/navigator.rs @@ -3,6 +3,7 @@ use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend}; use std::collections::HashMap; use std::future::Future; +use std::pin::Pin; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; use web_sys::window; @@ -75,11 +76,11 @@ impl NavigatorBackend for WebNavigatorBackend { } } - fn fetch(&self, _url: String) -> Box, Error>>> { - Box::new(async { Ok(Vec::new()) }) + fn fetch(&self, _url: String) -> Pin, Error>>>> { + Box::pin(async { Ok(Vec::new()) }) } - fn spawn_future(&mut self, future: Box + Unpin + 'static>) { + fn spawn_future(&mut self, future: Pin + 'static>>) { spawn_local(future) } } From 8d83bbb864609c6c52a4a8a7a2a13f2a2982574d Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 8 Nov 2019 21:10:21 -0500 Subject: [PATCH 05/59] Impl fetch for web --- web/Cargo.toml | 2 +- web/src/navigator.rs | 27 +++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/web/Cargo.toml b/web/Cargo.toml index df9c68892..e147ba593 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -42,7 +42,7 @@ features = [ "AudioNode", "CanvasRenderingContext2d", "ChannelMergerNode", "ChannelSplitterNode", "CssStyleDeclaration", "Document", "Element", "Event", "EventTarget", "GainNode", "HtmlCanvasElement", "HtmlElement", "HtmlImageElement", "MouseEvent", "Navigator", "Node", "Performance", "PointerEvent", "ScriptProcessorNode", "UiEvent", "Window", "Location", "HtmlFormElement", - "KeyboardEvent", "Path2d", "CanvasGradient", "CanvasPattern", "SvgMatrix", "SvgsvgElement"] + "KeyboardEvent", "Path2d", "CanvasGradient", "CanvasPattern", "SvgMatrix", "SvgsvgElement", "Response"] [dev-dependencies] wasm-bindgen-test = "0.3.7" diff --git a/web/src/navigator.rs b/web/src/navigator.rs index 5933a5204..1ac743c8d 100644 --- a/web/src/navigator.rs +++ b/web/src/navigator.rs @@ -1,12 +1,13 @@ //! Navigator backend for web +use js_sys::{ArrayBuffer, Uint8Array}; use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend}; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; use wasm_bindgen::JsCast; -use wasm_bindgen_futures::spawn_local; -use web_sys::window; +use wasm_bindgen_futures::{spawn_local, JsFuture}; +use web_sys::{window, Response}; pub struct WebNavigatorBackend {} @@ -76,8 +77,26 @@ impl NavigatorBackend for WebNavigatorBackend { } } - fn fetch(&self, _url: String) -> Pin, Error>>>> { - Box::pin(async { Ok(Vec::new()) }) + fn fetch(&self, url: String) -> Pin, Error>>>> { + Box::pin(async move { + let window = web_sys::window().unwrap(); + let fetchval = JsFuture::from(window.fetch_with_str(&url)).await; + if fetchval.is_err() { + return Err("Could not fetch, got JS Error".into()); + } + + let resp: Response = fetchval.unwrap().dyn_into().unwrap(); + let data: ArrayBuffer = JsFuture::from(resp.array_buffer().unwrap()) + .await + .unwrap() + .dyn_into() + .unwrap(); + let jsarray = Uint8Array::new(&data); + let mut rust_array = vec![0; jsarray.length() as usize]; + jsarray.copy_to(&mut rust_array); + + Ok(rust_array) + }) } fn spawn_future(&mut self, future: Pin + 'static>>) { From 55149b7b7e18013bb53fc9c0363b808a8a110153 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 8 Nov 2019 15:09:57 -0500 Subject: [PATCH 06/59] Reference count the Player and provide a weak reference in UpdateContext. This allows the formation of `'static` futures that can still interact with a player. Async code will need to upgrade the weak reference in order to be able to interact with the player. --- Cargo.lock | 1 + core/Cargo.toml | 1 + core/src/avm1/script_object.rs | 1 + core/src/avm1/test_utils.rs | 1 + core/src/backend/input.rs | 4 +- core/src/context.rs | 15 +++++-- core/src/lib.rs | 3 ++ core/src/player.rs | 66 ++++++++++++++++++------------ core/tests/regression_tests.rs | 12 +++--- desktop/src/main.rs | 46 +++++++++++++-------- web/src/lib.rs | 75 +++++++++++++++++++++------------- 11 files changed, 144 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be37d70b2..4fb34751f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1558,6 +1558,7 @@ version = "0.1.0" dependencies = [ "approx 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "bitstream-io 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)", + "downcast-rs 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "enumset 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "gc-arena 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/core/Cargo.toml b/core/Cargo.toml index cebbc985e..8584adfb7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,6 +20,7 @@ enumset = "0.4.2" smallvec = "1.2.0" num_enum = "0.4.2" quick-xml = "0.17.2" +downcast-rs = "1.1.1" [dependencies.jpeg-decoder] version = "0.1.18" diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index 6f2d16a80..d6a138f03 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -610,6 +610,7 @@ mod tests { mouse_position: &(Twips::new(0), Twips::new(0)), drag_object: &mut None, stage_size: (Twips::from_pixels(550.0), Twips::from_pixels(400.0)), + player: None, }; let object = ScriptObject::object(gc_context, Some(avm.prototypes().object)).into(); diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index 6c77a3db3..94b640ced 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -49,6 +49,7 @@ where mouse_position: &(Twips::new(0), Twips::new(0)), drag_object: &mut None, stage_size: (Twips::from_pixels(550.0), Twips::from_pixels(400.0)), + player: None, }; let globals = avm.global_object_cell(); diff --git a/core/src/backend/input.rs b/core/src/backend/input.rs index e6129d4b9..a12bd952e 100644 --- a/core/src/backend/input.rs +++ b/core/src/backend/input.rs @@ -1,6 +1,7 @@ use crate::events::KeyCode; +use downcast_rs::Downcast; -pub trait InputBackend { +pub trait InputBackend: Downcast { fn is_key_down(&self, key: KeyCode) -> bool; fn get_last_key_code(&self) -> KeyCode; @@ -11,6 +12,7 @@ pub trait InputBackend { fn show_mouse(&mut self); } +impl_downcast!(InputBackend); /// Input backend that does nothing pub struct NullInputBackend {} diff --git a/core/src/context.rs b/core/src/context.rs index ec6197fd4..f1625f9da 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -6,13 +6,14 @@ use crate::avm1::Value; use crate::backend::input::InputBackend; use crate::backend::{audio::AudioBackend, navigator::NavigatorBackend, render::RenderBackend}; use crate::library::Library; +use crate::player::Player; use crate::prelude::*; use crate::tag_utils::SwfSlice; use crate::transform::TransformStack; use core::fmt; use gc_arena::{Collect, MutationContext}; use rand::rngs::SmallRng; -use std::sync::Arc; +use std::sync::{Arc, Mutex, Weak}; /// `UpdateContext` holds shared data that is used by the various subsystems of Ruffle. /// `Player` crates this when it begins a tick and passes it through the call stack to @@ -51,13 +52,13 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { pub swf_data: &'a Arc>, /// The audio backend, used by display objects and AVM to play audio. - pub audio: &'a mut dyn AudioBackend, + pub audio: &'a mut (dyn AudioBackend + 'a), /// The navigator backend, used by the AVM to make HTTP requests and visit webpages. - pub navigator: &'a mut dyn NavigatorBackend, + pub navigator: &'a mut (dyn NavigatorBackend + 'a), /// The renderer, used by the display objects to draw themselves. - pub renderer: &'a mut dyn RenderBackend, + pub renderer: &'a mut (dyn RenderBackend + 'a), /// The input backend, used to detect user interactions. pub input: &'a mut dyn InputBackend, @@ -84,6 +85,12 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { /// The dimensions of the stage. pub stage_size: (Twips, Twips), + + /// Weak reference to the player. + /// + /// Recipients of an update context may upgrade the reference to ensure + /// that the player lives across I/O boundaries. + pub player: Option>>, } /// A queued ActionScript call. diff --git a/core/src/lib.rs b/core/src/lib.rs index b23c5faaa..68d471203 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -6,6 +6,9 @@ mod display_object; #[macro_use] extern crate smallvec; +#[macro_use] +extern crate downcast_rs; + mod avm1; mod bounding_box; mod character; diff --git a/core/src/player.rs b/core/src/player.rs index 60eb6a8fc..054fe4e0e 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -14,7 +14,8 @@ use gc_arena::{make_arena, ArenaParameters, Collect, GcCell}; use log::info; use rand::{rngs::SmallRng, SeedableRng}; use std::convert::TryFrom; -use std::sync::Arc; +use std::ops::DerefMut; +use std::sync::{Arc, Mutex, Weak}; static DEVICE_FONT_TAG: &[u8] = include_bytes!("../assets/noto-sans-definefont3.bin"); @@ -65,12 +66,12 @@ type Error = Box; make_arena!(GcArena, GcRoot); -pub struct Player< - Audio: AudioBackend, - Renderer: RenderBackend, - Navigator: NavigatorBackend, - Input: InputBackend, -> { +type Audio = Box; +type Navigator = Box; +type Renderer = Box; +type Input = Box; + +pub struct Player { /// The version of the player we're emulating. /// /// This serves a few purposes, primarily for compatibility: @@ -90,7 +91,7 @@ pub struct Player< audio: Audio, renderer: Renderer, - navigator: Navigator, + pub navigator: Navigator, input: Input, transform_stack: TransformStack, view_matrix: Matrix, @@ -113,22 +114,23 @@ pub struct Player< mouse_pos: (Twips, Twips), is_mouse_down: bool, + + /// 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: Option>>, } -impl< - Audio: AudioBackend, - Renderer: RenderBackend, - Navigator: NavigatorBackend, - Input: InputBackend, - > Player -{ +impl Player { pub fn new( mut renderer: Renderer, audio: Audio, navigator: Navigator, input: Input, swf_data: Vec, - ) -> Result { + ) -> Result>, Error> { let swf_stream = swf::read::read_swf_header(&swf_data[..]).unwrap(); let header = swf_stream.header; let mut reader = swf_stream.reader; @@ -231,6 +233,7 @@ impl< audio, navigator, input, + self_reference: None, }; player.gc_arena.mutate(|gc_context, gc_root| { @@ -242,7 +245,12 @@ impl< player.build_matrices(); player.preload(); - Ok(player) + let player_box = Arc::new(Mutex::new(player)); + let mut player_lock = player_box.lock().unwrap(); + player_lock.self_reference = Some(Arc::downgrade(&player_box)); + std::mem::drop(player_lock); + + Ok(player_box) } pub fn tick(&mut self, dt: f64) { @@ -554,7 +562,7 @@ impl< self.gc_arena.mutate(|_gc_context, gc_root| { let root_data = gc_root.0.read(); let mut render_context = RenderContext { - renderer, + renderer: renderer.deref_mut(), library: &root_data.library, transform_stack, view_bounds, @@ -597,8 +605,8 @@ impl< &self.input } - pub fn input_mut(&mut self) -> &mut Input { - &mut self.input + pub fn input_mut(&mut self) -> &mut dyn InputBackend { + self.input.deref_mut() } fn run_actions<'gc>(avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>) { @@ -719,20 +727,22 @@ impl< mouse_position, stage_width, stage_height, + player, ) = ( self.player_version, self.global_time, &mut self.swf_data, self.swf_version, &mut self.background_color, - &mut self.renderer, - &mut self.audio, - &mut self.navigator, - &mut self.input, + self.renderer.deref_mut(), + self.audio.deref_mut(), + self.navigator.deref_mut(), + self.input.deref_mut(), &mut self.rng, &self.mouse_pos, Twips::from_pixels(self.movie_width.into()), Twips::from_pixels(self.movie_height.into()), + self.self_reference.clone(), ); self.gc_arena.mutate(|gc_context, gc_root| { @@ -759,6 +769,7 @@ impl< mouse_position, drag_object, stage_size: (stage_width, stage_height), + player, }; let ret = f(avm, &mut update_context); @@ -778,8 +789,11 @@ impl< renderer: &mut Renderer, ) -> Result, Error> { let mut reader = swf::read::Reader::new(data, 8); - let device_font = - crate::font::Font::from_swf_tag(gc_context, renderer, &reader.read_define_font_2(3)?)?; + let device_font = crate::font::Font::from_swf_tag( + gc_context, + renderer.deref_mut(), + &reader.read_define_font_2(3)?, + )?; Ok(device_font) } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 2cb57389b..c3ebd4336 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -271,16 +271,16 @@ fn run_swf(swf_path: &str, num_frames: u32) -> Result { let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info)); let swf_data = std::fs::read(swf_path)?; - let mut player = Player::new( - NullRenderer, - NullAudioBackend::new(), - NullNavigatorBackend::new(), - NullInputBackend::new(), + let player = Player::new( + Box::new(NullRenderer), + Box::new(NullAudioBackend::new()), + Box::new(NullNavigatorBackend::new()), + Box::new(NullInputBackend::new()), swf_data, )?; for _ in 0..num_frames { - player.run_frame(); + player.lock().unwrap().run_frame(); } Ok(trace_log()) diff --git a/desktop/src/main.rs b/desktop/src/main.rs index d14c0a82b..5f331728e 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -15,7 +15,6 @@ use glutin::{ }; use ruffle_core::{ backend::audio::{AudioBackend, NullAudioBackend}, - backend::render::RenderBackend, Player, }; use std::path::PathBuf; @@ -63,14 +62,18 @@ fn run_player(input_path: PathBuf) -> Result<(), Box> { Box::new(NullAudioBackend::new()) } }; - let renderer = GliumRenderBackend::new(windowed_context)?; - let navigator = navigator::ExternalNavigatorBackend::new(); //TODO: actually implement this backend type + let renderer = Box::new(GliumRenderBackend::new(windowed_context)?); + let navigator = Box::new(navigator::ExternalNavigatorBackend::new()); //TODO: actually implement this backend type let display = renderer.display().clone(); - let input = input::WinitInputBackend::new(display.clone()); - let mut player = Player::new(renderer, audio, navigator, input, swf_data)?; - player.set_is_playing(true); // Desktop player will auto-play. + let input = Box::new(input::WinitInputBackend::new(display.clone())); + let player = Player::new(renderer, audio, navigator, input, swf_data)?; - let logical_size: LogicalSize = (player.movie_width(), player.movie_height()).into(); + let logical_size: LogicalSize = { + let mut player_lock = player.lock().unwrap(); + player_lock.set_is_playing(true); // Desktop player will auto-play. + + (player_lock.movie_width(), player_lock.movie_height()).into() + }; let scale_factor = display.gl_window().window().scale_factor(); // Set initial size to movie dimensions. @@ -89,24 +92,27 @@ fn run_player(input_path: PathBuf) -> Result<(), Box> { glutin::event::Event::LoopDestroyed => return, glutin::event::Event::WindowEvent { event, .. } => match event { WindowEvent::Resized(size) => { - player.set_viewport_dimensions(size.width, size.height); - player + let mut player_lock = player.lock().unwrap(); + player_lock.set_viewport_dimensions(size.width, size.height); + player_lock .renderer_mut() .set_viewport_dimensions(size.width, size.height); } WindowEvent::CursorMoved { position, .. } => { + let mut player_lock = player.lock().unwrap(); mouse_pos = position; let event = ruffle_core::PlayerEvent::MouseMove { x: position.x, y: position.y, }; - player.handle_event(event); + player_lock.handle_event(event); } WindowEvent::MouseInput { button: MouseButton::Left, state: pressed, .. } => { + let mut player_lock = player.lock().unwrap(); let event = if pressed == ElementState::Pressed { ruffle_core::PlayerEvent::MouseDown { x: mouse_pos.x, @@ -118,15 +124,22 @@ fn run_player(input_path: PathBuf) -> Result<(), Box> { y: mouse_pos.y, } }; - player.handle_event(event); + player_lock.handle_event(event); } WindowEvent::CursorLeft { .. } => { - player.handle_event(ruffle_core::PlayerEvent::MouseLeft) + let mut player_lock = player.lock().unwrap(); + player_lock.handle_event(ruffle_core::PlayerEvent::MouseLeft) } WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::KeyboardInput { .. } | WindowEvent::ReceivedCharacter(_) => { - if let Some(event) = player.input_mut().handle_event(event) { - player.handle_event(event); + let mut player_lock = player.lock().unwrap(); + if let Some(event) = player_lock + .input_mut() + .downcast_mut::() + .unwrap() + .handle_event(event) + { + player_lock.handle_event(event); } } _ => (), @@ -140,10 +153,11 @@ fn run_player(input_path: PathBuf) -> Result<(), Box> { let dt = new_time.duration_since(time).as_micros(); if dt > 0 { time = new_time; - player.tick(dt as f64 / 1000.0); + player.lock().unwrap().tick(dt as f64 / 1000.0); } - *control_flow = ControlFlow::WaitUntil(new_time + player.time_til_next_frame()); + *control_flow = + ControlFlow::WaitUntil(new_time + player.lock().unwrap().time_til_next_frame()); } }); } diff --git a/web/src/lib.rs b/web/src/lib.rs index e412f4fd3..2861f7a02 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -11,7 +11,9 @@ use crate::{ }; use generational_arena::{Arena, Index}; use js_sys::Uint8Array; -use ruffle_core::{backend::audio::AudioBackend, backend::render::RenderBackend, PlayerEvent}; +use ruffle_core::PlayerEvent; +use std::mem::drop; +use std::sync::{Arc, Mutex}; use std::{cell::RefCell, error::Error, num::NonZeroI32}; use wasm_bindgen::{prelude::*, JsCast, JsValue}; use web_sys::{Element, EventTarget, HtmlCanvasElement, KeyboardEvent, PointerEvent}; @@ -26,12 +28,7 @@ thread_local! { type AnimationHandler = Closure; struct RuffleInstance { - core: ruffle_core::Player< - WebAudioBackend, - WebCanvasRenderBackend, - WebNavigatorBackend, - WebInputBackend, - >, + core: Arc>, canvas: HtmlCanvasElement, canvas_width: i32, canvas_height: i32, @@ -93,14 +90,17 @@ impl Ruffle { swf_data.copy_to(&mut data[..]); let window = web_sys::window().ok_or_else(|| "Expected window")?; - let renderer = WebCanvasRenderBackend::new(&canvas)?; - let audio = WebAudioBackend::new()?; - let navigator = WebNavigatorBackend::new(); - let input = WebInputBackend::new(&canvas); + let renderer = Box::new(WebCanvasRenderBackend::new(&canvas)?); + let audio = Box::new(WebAudioBackend::new()?); + let navigator = Box::new(WebNavigatorBackend::new()); + let input = Box::new(WebInputBackend::new(&canvas)); + + let core = ruffle_core::Player::new(renderer, audio, navigator, input, data)?; + let mut core_lock = core.lock().unwrap(); + let frame_rate = core_lock.frame_rate(); + core_lock.audio_mut().set_frame_rate(frame_rate); + drop(core_lock); - let mut core = ruffle_core::Player::new(renderer, audio, navigator, input, data)?; - let frame_rate = core.frame_rate(); - core.audio_mut().set_frame_rate(frame_rate); // Create instance. let instance = RuffleInstance { core, @@ -149,7 +149,7 @@ impl Ruffle { x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio, y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio, }; - instance.core.handle_event(event); + instance.core.lock().unwrap().handle_event(event); if instance.has_focus { js_event.prevent_default(); } @@ -175,7 +175,7 @@ impl Ruffle { let mut instances = instances.borrow_mut(); if let Some(instance) = instances.get_mut(index) { instance.has_focus = true; - instance.core.set_is_playing(true); + instance.core.lock().unwrap().set_is_playing(true); if let Some(target) = js_event.current_target() { let _ = target .unchecked_ref::() @@ -185,7 +185,7 @@ impl Ruffle { x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio, y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio, }; - instance.core.handle_event(event); + instance.core.lock().unwrap().handle_event(event); js_event.prevent_default(); } }); @@ -242,7 +242,7 @@ impl Ruffle { x: f64::from(js_event.offset_x()) * instance.device_pixel_ratio, y: f64::from(js_event.offset_y()) * instance.device_pixel_ratio, }; - instance.core.handle_event(event); + instance.core.lock().unwrap().handle_event(event); if instance.has_focus { js_event.prevent_default(); } @@ -267,7 +267,7 @@ impl Ruffle { // INSTANCES.with(move |instances| { // let mut instances = instances.borrow_mut(); // if let Some(instance) = instances.get_mut(index) { - // instance.core.set_is_playing(true); + // instance.core.lock().unwrap().set_is_playing(true); // } // }); // }) as Box); @@ -292,19 +292,30 @@ impl Ruffle { if let Some(instance) = instances.borrow_mut().get_mut(index) { if instance.has_focus { let code = js_event.code(); - instance.core.input_mut().keydown(code.clone()); + instance + .core + .lock() + .unwrap() + .input_mut() + .downcast_mut::() + .unwrap() + .keydown(code.clone()); if let Some(codepoint) = input::web_key_to_codepoint(&js_event.key()) { instance .core + .lock() + .unwrap() .handle_event(PlayerEvent::TextInput { codepoint }); } if let Some(key_code) = input::web_to_ruffle_key_code(&code) { instance .core + .lock() + .unwrap() .handle_event(PlayerEvent::KeyDown { key_code }); } @@ -331,7 +342,14 @@ impl Ruffle { INSTANCES.with(|instances| { if let Some(instance) = instances.borrow_mut().get_mut(index) { if instance.has_focus { - instance.core.input_mut().keyup(js_event.code()); + instance + .core + .lock() + .unwrap() + .input_mut() + .downcast_mut::() + .unwrap() + .keyup(js_event.code()); js_event.prevent_default(); } } @@ -376,7 +394,7 @@ impl Ruffle { 0.0 }; - instance.core.tick(dt); + instance.core.lock().unwrap().tick(dt); // Check for canvas resize. let canvas_width = instance.canvas.client_width(); @@ -400,16 +418,17 @@ impl Ruffle { (f64::from(canvas_height) * instance.device_pixel_ratio) as u32; instance.canvas.set_width(viewport_width); instance.canvas.set_height(viewport_height); - instance - .core - .set_viewport_dimensions(viewport_width, viewport_height); - instance - .core + + let mut core_lock = instance.core.lock().unwrap(); + core_lock.set_viewport_dimensions(viewport_width, viewport_height); + core_lock .renderer_mut() .set_viewport_dimensions(viewport_width, viewport_height); // Force a re-render if we resize. - instance.core.render(); + core_lock.render(); + + drop(core_lock); } // Request next animation frame. From 250ec13c120a0bfd608fe60627bcbb4b8e00166c Mon Sep 17 00:00:00 2001 From: David Wendt Date: Mon, 11 Nov 2019 08:58:26 -0500 Subject: [PATCH 07/59] Implement LoadVariablesFlag. This has some subtle problems: we cannot hold references to garbage-collected data in Futures, so we have to arrange for the AVM itself to forcibly root them for us. Then we get them back when our async code is ready to do something to the AVM. --- Cargo.lock | 1 + core/Cargo.toml | 1 + core/src/avm1.rs | 129 ++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 118 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fb34751f..8a9b6767c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1576,6 +1576,7 @@ dependencies = [ "ruffle_macros 0.1.0", "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "swf 0.1.2", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml index 8584adfb7..0bbbb2483 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -21,6 +21,7 @@ smallvec = "1.2.0" num_enum = "0.4.2" quick-xml = "0.17.2" downcast-rs = "1.1.1" +url = "2.1.0" [dependencies.jpeg-decoder] version = "0.1.18" diff --git a/core/src/avm1.rs b/core/src/avm1.rs index d7b463dda..66c2898b2 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -8,6 +8,7 @@ use gc_arena::{GcCell, MutationContext}; use rand::Rng; use std::collections::HashMap; use std::convert::TryInto; +use url::form_urlencoded; use swf::avm1::read::Reader; use swf::avm1::types::{Action, Function}; @@ -88,6 +89,10 @@ pub struct Avm1<'gc> { /// The register slots (also shared across functions). /// `ActionDefineFunction2` defined functions do not use these slots. registers: [Value<'gc>; 4], + + /// Represents externally-held references to objects currently in use by + /// futures that can't actually hold `'gc` pointers. + external_references: Vec>>, } unsafe impl<'gc> gc_arena::Collect for Avm1<'gc> { @@ -128,9 +133,45 @@ impl<'gc> Avm1<'gc> { Value::Undefined, Value::Undefined, ], + external_references: vec![], } } + /// Force the AVM to store a reference to an object that can be retrieved + /// later by ID. + /// + /// This is intended for use by async code which will need to run between + /// GC passes and for various architectural references cannot hold onto any + /// actual GC pointers. + /// + /// Note that this function is the moral equivalent of moving an object + /// from garbage collection into reference counting. Take care to kill + /// roots as soon as possible and avoid creating cycles. + pub fn forcibly_root_object(&mut self, ob: Object<'gc>) -> usize { + if let Some(pos) = self.external_references.iter().position(|&r| r.is_none()) { + *self.external_references.get_mut(pos).unwrap() = Some(ob); + pos + } else { + self.external_references.push(Some(ob)); + self.external_references.len() - 1 + } + } + + /// Get back a previously rooted object. + /// + /// After retrieving the rooted object, the ID you have given will no + /// longer be valid and may refer to a different object or no object at + /// all. + /// + /// Attempting to unroot an object ID that does not exist will panic. + pub fn unroot_object(&mut self, id: usize) -> Object<'gc> { + let root = self.external_references.get(id).and_then(|v| *v).unwrap(); + + *self.external_references.get_mut(id).unwrap() = None; + + root + } + #[allow(dead_code)] pub fn base_clip(&self) -> DisplayObject<'gc> { self.current_stack_frame().unwrap().read().base_clip() @@ -882,6 +923,29 @@ impl<'gc> Avm1<'gc> { Ok(()) } + pub fn resolve_dot_path_clip<'s>( + start: Option>, + root: DisplayObject<'gc>, + path: &'s str, + ) -> Option> { + // If the target clip is invalid, we default to root for the variable path. + let mut clip = Some(start.unwrap_or(root)); + if !path.is_empty() { + for name in path.split('.') { + if clip.is_none() { + break; + } + + clip = clip + .unwrap() + .as_movie_clip() + .and_then(|mc| mc.get_child_by_name(name)); + } + } + + clip + } + fn push(&mut self, value: impl Into>) { let value = value.into(); avm_debug!("Stack push {}: {:?}", self.stack.len(), value); @@ -1601,23 +1665,62 @@ impl<'gc> Avm1<'gc> { return fscommand::handle(fscommand, self, context); } - if is_target_sprite { + if is_load_vars { + let clip_target: Option> = if is_target_sprite { + let is_slashpath = Self::variable_name_is_slash_path(&target); + + if is_slashpath { + if let Some((node, _)) = + Self::resolve_slash_path_variable(self.target_clip(), context.root, &target) + { + Some(node) + } else { + None + } + } else { + Self::resolve_dot_path_clip(self.target_clip(), context.root, &target) + } + } else { + Some(self.target_clip_or_root(context)) + }; + + if let Some(clip_target) = clip_target { + let target_obj = clip_target + .as_movie_clip() + .unwrap() + .object() + .as_object() + .unwrap(); + let player = context.player.clone().unwrap().upgrade().unwrap(); + let fetch = context.navigator.fetch(url); + let slot = self.forcibly_root_object(target_obj); + + context.navigator.spawn_future(Box::pin(async move { + let data = fetch.await.unwrap(); + + player.lock().unwrap().update(|avm, uc| { + let that = avm.unroot_object(slot); + + for (k, v) in form_urlencoded::parse(&data) { + that.set(&k, v.into_owned().into(), avm, uc); + } + }) + })); + } + + return Ok(()); + } else if is_target_sprite { log::warn!("GetURL into target sprite is not yet implemented"); return Ok(()); //maybe error? + } else { + let vars = match NavigationMethod::from_send_vars_method(swf_method) { + Some(method) => Some((method, self.locals_into_form_values(context))), + None => None, + }; + + context.navigator.navigate_to_url(url, Some(target), vars); } - if is_load_vars { - log::warn!("Reading AVM locals from forms is not yet implemented"); - return Ok(()); //maybe error? - } - - let vars = match NavigationMethod::from_send_vars_method(swf_method) { - Some(method) => Some((method, self.locals_into_form_values(context))), - None => None, - }; - - context.navigator.navigate_to_url(url, Some(target), vars); - Ok(()) } From 5ed5876e9ae0b1e594ed3719f77845975a3c68c9 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Tue, 12 Nov 2019 15:05:18 -0500 Subject: [PATCH 08/59] Merge SWF data and version into a single structure. Refactor everything that interacts with it to use `SwfSlice`s. --- Cargo.lock | 2 +- core/src/avm1.rs | 25 ++-- core/src/avm1/activation.rs | 6 +- core/src/avm1/script_object.rs | 5 +- core/src/avm1/test_utils.rs | 5 +- core/src/backend/audio/decoders.rs | 10 +- core/src/context.rs | 9 +- core/src/display_object.rs | 2 +- core/src/display_object/button.rs | 8 +- core/src/display_object/movie_clip.rs | 206 ++++++++++++++------------ core/src/player.rs | 74 +++------ core/src/tag_utils.rs | 198 +++++++++++++++++++++++-- swf/src/types.rs | 4 +- 13 files changed, 359 insertions(+), 195 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a9b6767c..9219cd599 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1576,7 +1576,7 @@ dependencies = [ "ruffle_macros 0.1.0", "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "swf 0.1.2", - "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 66c2898b2..92cb773d3 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1658,7 +1658,7 @@ impl<'gc> Avm1<'gc> { ) -> Result<(), Error> { // TODO: Support `LoadVariablesFlag`, `LoadTargetFlag` // TODO: What happens if there's only one string? - let target = self.pop().into_string(self.current_swf_version()); + let target = self.pop(); let url = self.pop().into_string(self.current_swf_version()); if let Some(fscommand) = fscommand::parse(&url) { @@ -1667,19 +1667,8 @@ impl<'gc> Avm1<'gc> { if is_load_vars { let clip_target: Option> = if is_target_sprite { - let is_slashpath = Self::variable_name_is_slash_path(&target); - - if is_slashpath { - if let Some((node, _)) = - Self::resolve_slash_path_variable(self.target_clip(), context.root, &target) - { - Some(node) - } else { - None - } - } else { - Self::resolve_dot_path_clip(self.target_clip(), context.root, &target) - } + let start = self.target_clip_or_root(context); + self.resolve_target_display_object(context, start, target)? } else { Some(self.target_clip_or_root(context)) }; @@ -1718,7 +1707,11 @@ impl<'gc> Avm1<'gc> { None => None, }; - context.navigator.navigate_to_url(url, Some(target), vars); + context.navigator.navigate_to_url( + url, + Some(target.into_string(self.current_swf_version())), + vars, + ); } Ok(()) @@ -2637,7 +2630,7 @@ pub fn start_drag<'gc>( ) { let lock_center = args .get(0) - .map(|o| o.as_bool(context.swf_version)) + .map(|o| o.as_bool(context.swf.version())) .unwrap_or(false); let offset = if lock_center { diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index ddc741780..34f88d0c7 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -187,6 +187,8 @@ impl<'gc> Activation<'gc> { mc: MutationContext<'gc, '_>, base_clip: DisplayObject<'gc>, ) -> Activation<'gc> { + use crate::tag_utils::SwfMovie; + let global_scope = GcCell::allocate(mc, Scope::from_global_object(globals)); let child_scope = GcCell::allocate(mc, Scope::new_local_scope(global_scope, mc)); let empty_constant_pool = GcCell::allocate(mc, Vec::new()); @@ -194,7 +196,7 @@ impl<'gc> Activation<'gc> { Activation { swf_version, data: SwfSlice { - data: Arc::new(Vec::new()), + movie: Arc::new(SwfMovie::empty(swf_version)), start: 0, end: 0, }, @@ -251,7 +253,7 @@ impl<'gc> Activation<'gc> { /// SwfSlice. #[allow(dead_code)] pub fn is_identical_fn(&self, other: &SwfSlice) -> bool { - Arc::ptr_eq(&self.data.data, &other.data) + Arc::ptr_eq(&self.data.movie, &other.movie) } /// Returns a mutable reference to the current data offset. diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index d6a138f03..88c7e5bb3 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -572,6 +572,7 @@ mod tests { use crate::display_object::MovieClip; use crate::library::Library; use crate::prelude::*; + use crate::tag_utils::SwfMovie; use gc_arena::rootless_arena; use rand::{rngs::SmallRng, SeedableRng}; use std::sync::Arc; @@ -582,6 +583,7 @@ mod tests { { rootless_arena(|gc_context| { let mut avm = Avm1::new(gc_context, swf_version); + let swf = Arc::new(SwfMovie::empty(swf_version)); let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); @@ -589,7 +591,7 @@ mod tests { gc_context, global_time: 0, player_version: 32, - swf_version, + swf: &swf, root, rng: &mut SmallRng::from_seed([0u8; 16]), action_queue: &mut crate::context::ActionQueue::new(), @@ -604,7 +606,6 @@ mod tests { library: &mut Library::new(), navigator: &mut NullNavigatorBackend::new(), renderer: &mut NullRenderer::new(), - swf_data: &mut Arc::new(vec![]), system_prototypes: avm.prototypes().clone(), mouse_hovered_object: None, mouse_position: &(Twips::new(0), Twips::new(0)), diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index 94b640ced..f094de894 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -8,6 +8,7 @@ use crate::context::ActionQueue; use crate::display_object::{MovieClip, TDisplayObject}; use crate::library::Library; use crate::prelude::*; +use crate::tag_utils::SwfMovie; use gc_arena::{rootless_arena, GcCell, MutationContext}; use rand::{rngs::SmallRng, SeedableRng}; use std::sync::Arc; @@ -21,6 +22,7 @@ where F: for<'a> FnOnce(&mut Avm1<'gc>, &mut UpdateContext<'a, 'gc, '_>, Object<'gc>) -> R, { let mut avm = Avm1::new(gc_context, swf_version); + let swf = Arc::new(SwfMovie::empty(swf_version)); let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); @@ -28,7 +30,7 @@ where gc_context, global_time: 0, player_version: 32, - swf_version, + swf: &swf, root, rng: &mut SmallRng::from_seed([0u8; 16]), audio: &mut NullAudioBackend::new(), @@ -43,7 +45,6 @@ where library: &mut Library::new(), navigator: &mut NullNavigatorBackend::new(), renderer: &mut NullRenderer::new(), - swf_data: &mut Arc::new(vec![]), system_prototypes: avm.prototypes().clone(), mouse_hovered_object: None, mouse_position: &(Twips::new(0), Twips::new(0)), diff --git a/core/src/backend/audio/decoders.rs b/core/src/backend/audio/decoders.rs index 8f1a0bc12..a6dc56ff3 100644 --- a/core/src/backend/audio/decoders.rs +++ b/core/src/backend/audio/decoders.rs @@ -124,7 +124,9 @@ pub struct AdpcmStreamDecoder { impl AdpcmStreamDecoder { fn new(format: &SoundFormat, swf_data: SwfSlice, swf_version: u8) -> Self { let mut tag_reader = StreamTagReader::new(format.compression, swf_data, swf_version); - let audio_data = tag_reader.next().unwrap_or_else(SwfSlice::empty); + let audio_data = tag_reader + .next() + .unwrap_or_else(|| SwfSlice::empty(swf_version)); let decoder = AdpcmDecoder::new( Cursor::new(audio_data), format.is_stereo, @@ -222,7 +224,7 @@ impl StreamTagReader { compression, reader: swf::read::Reader::new(Cursor::new(swf_data), swf_version), current_frame: 1, - current_audio_data: SwfSlice::empty(), + current_audio_data: SwfSlice::empty(swf_version), } } } @@ -256,13 +258,13 @@ impl Iterator for StreamTagReader { found = true; if tag_len >= skip_len { *audio_data = SwfSlice { - data: std::sync::Arc::clone(&reader.get_ref().get_ref().data), + movie: std::sync::Arc::clone(&reader.get_ref().get_ref().movie), start: pos + skip_len, end: pos + tag_len, }; } else { *audio_data = SwfSlice { - data: std::sync::Arc::clone(&reader.get_ref().get_ref().data), + movie: std::sync::Arc::clone(&reader.get_ref().get_ref().movie), start: pos, end: pos + tag_len, }; diff --git a/core/src/context.rs b/core/src/context.rs index f1625f9da..9c84fc1da 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -8,7 +8,7 @@ use crate::backend::{audio::AudioBackend, navigator::NavigatorBackend, render::R use crate::library::Library; use crate::player::Player; use crate::prelude::*; -use crate::tag_utils::SwfSlice; +use crate::tag_utils::{SwfMovie, SwfSlice}; use crate::transform::TransformStack; use core::fmt; use gc_arena::{Collect, MutationContext}; @@ -45,11 +45,8 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { /// variables. pub player_version: u8, - /// The version of the SWF file we are running. - pub swf_version: u8, - - /// The raw data of the SWF file. - pub swf_data: &'a Arc>, + /// The root SWF file. + pub swf: &'a Arc, /// The audio backend, used by display objects and AVM to play audio. pub audio: &'a mut (dyn AudioBackend + 'a), diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 9a193e21d..b7b67e654 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -725,7 +725,7 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug { .clip_actions .iter() .cloned() - .map(ClipAction::from) + .map(|a| ClipAction::from_action_and_movie(a, clip.movie())) .collect(), ); } diff --git a/core/src/display_object/button.rs b/core/src/display_object/button.rs index e30ce0d7b..12b4eb38a 100644 --- a/core/src/display_object/button.rs +++ b/core/src/display_object/button.rs @@ -3,6 +3,7 @@ use crate::context::{ActionType, RenderContext, UpdateContext}; use crate::display_object::{DisplayObjectBase, TDisplayObject}; use crate::events::{ButtonEvent, ButtonEventResult, ButtonKeyCode}; use crate::prelude::*; +use crate::tag_utils::SwfSlice; use gc_arena::{Collect, GcCell, MutationContext}; use std::collections::BTreeMap; use std::convert::TryFrom; @@ -26,16 +27,13 @@ pub struct ButtonData<'gc> { impl<'gc> Button<'gc> { pub fn from_swf_tag( button: &swf::Button, + source_movie: &SwfSlice, _library: &crate::library::Library<'gc>, gc_context: gc_arena::MutationContext<'gc, '_>, ) -> Self { let mut actions = vec![]; for action in &button.actions { - let action_data = crate::tag_utils::SwfSlice { - data: std::sync::Arc::new(action.action_data.clone()), - start: 0, - end: action.action_data.len(), - }; + let action_data = source_movie.owned_subslice(action.action_data.clone()); for condition in &action.conditions { let button_action = ButtonAction { action_data: action_data.clone(), diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 0ba7a3fcc..35097f5e7 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -9,13 +9,14 @@ use crate::display_object::{ use crate::events::{ButtonKeyCode, ClipEvent}; use crate::font::Font; use crate::prelude::*; -use crate::tag_utils::{self, DecodeResult, SwfSlice, SwfStream}; +use crate::tag_utils::{self, DecodeResult, SwfMovie, SwfSlice, SwfStream}; use enumset::{EnumSet, EnumSetType}; use gc_arena::{Collect, Gc, GcCell, MutationContext}; use smallvec::SmallVec; use std::cell::Ref; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; +use std::sync::Arc; use swf::read::SwfRead; type FrameNumber = u16; @@ -32,7 +33,6 @@ pub struct MovieClip<'gc>(GcCell<'gc, MovieClipData<'gc>>); #[derive(Clone, Debug)] pub struct MovieClipData<'gc> { base: DisplayObjectBase<'gc>, - swf_version: u8, static_data: Gc<'gc, MovieClipStatic>, tag_stream_pos: u64, current_frame: FrameNumber, @@ -50,8 +50,7 @@ impl<'gc> MovieClip<'gc> { gc_context, MovieClipData { base: Default::default(), - swf_version, - static_data: Gc::allocate(gc_context, MovieClipStatic::default()), + static_data: Gc::allocate(gc_context, MovieClipStatic::empty(swf_version)), tag_stream_pos: 0, current_frame: 0, audio_stream: None, @@ -64,24 +63,20 @@ impl<'gc> MovieClip<'gc> { } pub fn new_with_data( - swf_version: u8, gc_context: MutationContext<'gc, '_>, id: CharacterId, - tag_stream_start: u64, - tag_stream_len: usize, + swf: SwfSlice, num_frames: u16, ) -> Self { MovieClip(GcCell::allocate( gc_context, MovieClipData { base: Default::default(), - swf_version, static_data: Gc::allocate( gc_context, MovieClipStatic { id, - tag_stream_start, - tag_stream_len, + swf, total_frames: num_frames, audio_stream_info: None, frame_labels: HashMap::new(), @@ -98,6 +93,16 @@ impl<'gc> MovieClip<'gc> { )) } + /// Construct a movie clip that represents an entire movie. + pub fn from_movie(gc_context: MutationContext<'gc, '_>, movie: Arc) -> Self { + Self::new_with_data( + gc_context, + 0, + movie.clone().into(), + movie.header().num_frames, + ) + } + pub fn preload( self, context: &mut UpdateContext<'_, 'gc, '_>, @@ -249,7 +254,7 @@ impl<'gc> MovieClip<'gc> { /// Used by the AVM `Call` action. pub fn actions_on_frame( self, - context: &mut UpdateContext<'_, 'gc, '_>, + _context: &mut UpdateContext<'_, 'gc, '_>, frame: FrameNumber, ) -> impl DoubleEndedIterator { use swf::{read::Reader, TagCode}; @@ -257,11 +262,8 @@ impl<'gc> MovieClip<'gc> { let mut actions: SmallVec<[SwfSlice; 2]> = SmallVec::new(); let mut cur_frame = 1; let clip = self.0.read(); - let swf_version = self.swf_version(); - let start = clip.tag_stream_start() as usize; let len = clip.tag_stream_len(); - let cursor = std::io::Cursor::new(&context.swf_data[start..start + len]); - let mut reader = Reader::new(cursor, swf_version); + let mut reader = clip.static_data.swf.read_from(0); // Iterate through this clip's tags, counting frames until we reach the target frame. while cur_frame <= frame && reader.get_ref().position() < len as u64 { @@ -270,15 +272,9 @@ impl<'gc> MovieClip<'gc> { TagCode::ShowFrame => cur_frame += 1, TagCode::DoAction if cur_frame == frame => { // On the target frame, add any DoAction tags to the array. - let start = - (clip.tag_stream_start() + reader.get_ref().position()) as usize; - let end = start + tag_len; - let code = SwfSlice { - data: std::sync::Arc::clone(context.swf_data), - start, - end, - }; - actions.push(code) + if let Some(code) = clip.static_data.swf.resize_to_reader(reader, tag_len) { + actions.push(code) + } } _ => (), } @@ -290,6 +286,10 @@ impl<'gc> MovieClip<'gc> { actions.into_iter() } + + pub fn movie(self) -> Arc { + self.0.read().movie() + } } impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { @@ -453,12 +453,8 @@ impl<'gc> MovieClipData<'gc> { self.stop_audio_stream(context); } - fn tag_stream_start(&self) -> u64 { - self.static_data.tag_stream_start - } - fn tag_stream_len(&self) -> usize { - self.static_data.tag_stream_len + self.static_data.swf.end - self.static_data.swf.start } /// Queues up a goto to the specified frame. @@ -487,18 +483,6 @@ impl<'gc> MovieClipData<'gc> { } } - fn reader<'a>( - &self, - context: &UpdateContext<'a, '_, '_>, - ) -> swf::read::Reader> { - let mut cursor = std::io::Cursor::new( - &context.swf_data[self.tag_stream_start() as usize - ..self.tag_stream_start() as usize + self.tag_stream_len()], - ); - cursor.set_position(self.tag_stream_pos); - swf::read::Reader::new(cursor, context.swf_version) - } - fn run_frame_internal( &mut self, self_display_object: DisplayObject<'gc>, @@ -520,7 +504,8 @@ impl<'gc> MovieClipData<'gc> { } let _tag_pos = self.tag_stream_pos; - let mut reader = self.reader(context); + let data = self.static_data.swf.clone(); + let mut reader = data.read_from(self.tag_stream_pos); let mut has_stream_block = false; use swf::TagCode; @@ -693,10 +678,11 @@ impl<'gc> MovieClipData<'gc> { // Step through the intermediate frames, and aggregate the deltas of each frame. let mut frame_pos = self.tag_stream_pos; - let mut reader = self.reader(context); + let data = self.static_data.swf.clone(); + let mut reader = data.read_from(self.tag_stream_pos); let mut index = 0; - let len = self.static_data.tag_stream_len as u64; + let len = self.tag_stream_len() as u64; // Sanity; let's make sure we don't seek way too far. // TODO: This should be self.frames_loaded() when we implement that. let clamped_frame = if frame <= self.total_frames() { @@ -705,7 +691,7 @@ impl<'gc> MovieClipData<'gc> { self.total_frames() }; - while self.current_frame < clamped_frame && frame_pos < len { + while self.current_frame() < clamped_frame && frame_pos < len { self.current_frame += 1; frame_pos = reader.get_inner().position(); @@ -875,7 +861,7 @@ impl<'gc> MovieClipData<'gc> { event: ClipEvent, ) { // TODO: What's the behavior for loaded SWF files? - if context.swf_version >= 5 { + if context.swf.version() >= 5 { for clip_action in self .clip_actions .iter() @@ -892,7 +878,7 @@ impl<'gc> MovieClipData<'gc> { // Queue ActionScript-defined event handlers after the SWF defined ones. // (e.g., clip.onEnterFrame = foo). - if context.swf_version >= 6 { + if context.swf.version() >= 6 { let name = match event { ClipEvent::Construct => None, ClipEvent::Data => Some("onData"), @@ -951,6 +937,10 @@ impl<'gc> MovieClipData<'gc> { context.audio.stop_stream(audio_stream); } } + + pub fn movie(&self) -> Arc { + self.static_data.swf.movie.clone() + } } // Preloading of definition tags @@ -965,7 +955,8 @@ impl<'gc, 'a> MovieClipData<'gc> { // TODO: Re-creating static data because preload step occurs after construction. // Should be able to hoist this up somewhere, or use MaybeUninit. let mut static_data = (&*self.static_data).clone(); - let mut reader = self.reader(context); + let data = self.static_data.swf.clone(); + let mut reader = data.read_from(self.tag_stream_pos); let mut cur_frame = 1; let mut ids = fnv::FnvHashMap::default(); let tag_callback = |reader: &mut _, tag_code, tag_len| match tag_code { @@ -1324,7 +1315,12 @@ impl<'gc, 'a> MovieClipData<'gc> { reader: &mut SwfStream<&'a [u8]>, ) -> DecodeResult { let swf_button = reader.read_define_button_1()?; - let button = Button::from_swf_tag(&swf_button, &context.library, context.gc_context); + let button = Button::from_swf_tag( + &swf_button, + &self.static_data.swf, + &context.library, + context.gc_context, + ); context .library .register_character(swf_button.id, Character::Button(button)); @@ -1338,7 +1334,12 @@ impl<'gc, 'a> MovieClipData<'gc> { reader: &mut SwfStream<&'a [u8]>, ) -> DecodeResult { let swf_button = reader.read_define_button_2()?; - let button = Button::from_swf_tag(&swf_button, &context.library, context.gc_context); + let button = Button::from_swf_tag( + &swf_button, + &self.static_data.swf, + &context.library, + context.gc_context, + ); context .library .register_character(swf_button.id, Character::Button(button)); @@ -1487,8 +1488,10 @@ impl<'gc, 'a> MovieClipData<'gc> { ) -> DecodeResult { // TODO(Herschel): Can we use a slice of the sound data instead of copying the data? use std::io::Read; - let mut reader = - swf::read::Reader::new(reader.get_mut().take(tag_len as u64), context.swf_version); + let mut reader = swf::read::Reader::new( + reader.get_mut().take(tag_len as u64), + self.static_data.swf.version(), + ); let sound = reader.read_define_sound()?; let handle = context.audio.register_sound(&sound).unwrap(); context @@ -1507,11 +1510,17 @@ impl<'gc, 'a> MovieClipData<'gc> { let id = reader.read_character_id()?; let num_frames = reader.read_u16()?; let movie_clip = MovieClip::new_with_data( - context.swf_version, context.gc_context, id, - reader.get_ref().position(), - tag_len - 4, + self.static_data + .swf + .resize_to_reader(reader, tag_len - 4) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + "Cannot define sprite with invalid offset and length!", + ) + })?, num_frames, ); @@ -1632,15 +1641,16 @@ impl<'gc, 'a> MovieClipData<'gc> { tag_len: usize, ) -> DecodeResult { // Queue the actions. - // TODO: The reader is actually reading the tag slice at this point (tag_stream.take()), - // so make sure to get the proper offsets. This feels kind of bad. - let start = (self.tag_stream_start() + reader.get_ref().position()) as usize; - let end = start + tag_len; - let slice = SwfSlice { - data: std::sync::Arc::clone(context.swf_data), - start, - end, - }; + let slice = self + .static_data + .swf + .resize_to_reader(reader, tag_len) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + "Invalid source or tag length when running action", + ) + })?; context.action_queue.queue_actions( self_display_object, ActionType::Normal { bytecode: slice }, @@ -1664,19 +1674,20 @@ impl<'gc, 'a> MovieClipData<'gc> { let sprite_id = reader.read_u16()?; log::info!("Init Action sprite ID {}", sprite_id); - // TODO: The reader is actually reading the tag slice at this point (tag_stream.take()), - // so make sure to get the proper offsets. This feels kind of bad. - let start = (self.tag_stream_start() + reader.get_ref().position()) as usize; - let end = start + tag_len; - let slice = SwfSlice { - data: std::sync::Arc::clone(context.swf_data), - start, - end, - }; + let slice = self + .static_data + .swf + .resize_to_reader(reader, tag_len) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + "Invalid source or tag length when running init action", + ) + })?; context.action_queue.queue_actions( self_display_object, ActionType::Init { bytecode: slice }, - false, + true, ); Ok(()) } @@ -1765,18 +1776,23 @@ impl<'gc, 'a> MovieClipData<'gc> { ) -> DecodeResult { if let (Some(stream_info), None) = (&self.static_data.audio_stream_info, self.audio_stream) { - let pos = self.tag_stream_start() + self.tag_stream_pos; - let slice = SwfSlice { - data: std::sync::Arc::clone(context.swf_data), - start: pos as usize, - end: self.tag_stream_start() as usize + self.tag_stream_len(), - }; - self.audio_stream = Some(context.audio.start_stream( + let slice = self + .static_data + .swf + .to_start_and_end(self.tag_stream_pos as usize, self.tag_stream_len()) + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + "Invalid slice generated when constructing sound stream block", + ) + })?; + let audio_stream = context.audio.start_stream( self.id(), self.current_frame() + 1, slice, &stream_info, - )); + ); + self.audio_stream = Some(audio_stream); } Ok(()) @@ -1818,19 +1834,17 @@ impl<'gc, 'a> MovieClipData<'gc> { #[derive(Clone)] struct MovieClipStatic { id: CharacterId, - tag_stream_start: u64, - tag_stream_len: usize, + swf: SwfSlice, frame_labels: HashMap, audio_stream_info: Option, total_frames: FrameNumber, } -impl Default for MovieClipStatic { - fn default() -> Self { +impl MovieClipStatic { + fn empty(swf_version: u8) -> Self { Self { id: 0, - tag_stream_start: 0, - tag_stream_len: 0, + swf: SwfSlice::empty(swf_version), total_frames: 1, frame_labels: HashMap::new(), audio_stream_info: None, @@ -1975,9 +1989,17 @@ pub struct ClipAction { action_data: SwfSlice, } -impl From for ClipAction { - fn from(other: swf::ClipAction) -> Self { +impl ClipAction { + /// Build a clip action from a SWF movie and a parsed ClipAction. + /// + /// TODO: Our underlying SWF parser currently does not yield slices of the + /// underlying movie, so we cannot convert those slices into a `SwfSlice`. + /// Instead, we have to construct a fake `SwfMovie` just to hold one clip + /// action. + pub fn from_action_and_movie(other: swf::ClipAction, movie: Arc) -> Self { use swf::ClipEventFlag; + + let len = other.action_data.len(); Self { events: other .events @@ -2010,9 +2032,9 @@ impl From for ClipAction { }) .collect(), action_data: SwfSlice { - data: std::sync::Arc::new(other.action_data.clone()), + movie: Arc::new(movie.from_movie_and_subdata(other.action_data)), start: 0, - end: other.action_data.len(), + end: len, }, } } diff --git a/core/src/player.rs b/core/src/player.rs index 054fe4e0e..99a773fa3 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -9,6 +9,7 @@ use crate::display_object::{MorphShape, MovieClip}; use crate::events::{ButtonEvent, ButtonKeyCode, ClipEvent, PlayerEvent}; use crate::library::Library; use crate::prelude::*; +use crate::tag_utils::SwfMovie; use crate::transform::TransformStack; use gc_arena::{make_arena, ArenaParameters, Collect, GcCell}; use log::info; @@ -84,8 +85,7 @@ pub struct Player { /// Player can be enabled by setting a particular player version. player_version: u8, - swf_data: Arc>, - swf_version: u8, + swf: Arc, is_playing: bool, @@ -131,42 +131,23 @@ impl Player { input: Input, swf_data: Vec, ) -> Result>, Error> { - let swf_stream = swf::read::read_swf_header(&swf_data[..]).unwrap(); - let header = swf_stream.header; - let mut reader = swf_stream.reader; + let movie = Arc::new(SwfMovie::from_data(&swf_data)); - // Decompress the entire SWF in memory. - // Sometimes SWFs will have an incorrectly compressed stream, - // but will otherwise decompress fine up to the End tag. - // So just warn on this case and try to continue gracefully. - let data = if header.compression == swf::Compression::Lzma { - // TODO: The LZMA decoder is still funky. - // It always errors, and doesn't return all the data if you use read_to_end, - // but read_exact at least returns the data... why? - // Does the decoder need to be flushed somehow? - let mut data = vec![0u8; swf_stream.uncompressed_length]; - let _ = reader.get_mut().read_exact(&mut data); - data - } else { - let mut data = Vec::with_capacity(swf_stream.uncompressed_length); - if let Err(e) = reader.get_mut().read_to_end(&mut data) { - log::error!("Error decompressing SWF, may be corrupt: {}", e); - } - data - }; + info!( + "{}x{}", + movie.header().stage_size.x_max, + movie.header().stage_size.y_max + ); - let swf_len = data.len(); - - info!("{}x{}", header.stage_size.x_max, header.stage_size.y_max); - - let movie_width = (header.stage_size.x_max - header.stage_size.x_min).to_pixels() as u32; - let movie_height = (header.stage_size.y_max - header.stage_size.y_min).to_pixels() as u32; + let movie_width = + (movie.header().stage_size.x_max - movie.header().stage_size.x_min).to_pixels() as u32; + let movie_height = + (movie.header().stage_size.y_max - movie.header().stage_size.y_min).to_pixels() as u32; let mut player = Player { player_version: NEWEST_PLAYER_VERSION, - swf_data: Arc::new(data), - swf_version: header.version, + swf: movie.clone(), is_playing: false, @@ -199,15 +180,7 @@ impl Player { gc_context, GcRootData { library, - root: MovieClip::new_with_data( - header.version, - gc_context, - 0, - 0, - swf_len, - header.num_frames, - ) - .into(), + root: MovieClip::from_movie(gc_context, movie.clone()).into(), mouse_hovered_object: None, drag_object: None, avm: Avm1::new(gc_context, NEWEST_PLAYER_VERSION), @@ -216,7 +189,7 @@ impl Player { )) }), - frame_rate: header.frame_rate.into(), + frame_rate: movie.header().frame_rate.into(), frame_accumulator: 0.0, global_time: 0, @@ -620,7 +593,7 @@ impl Player { ActionType::Normal { bytecode } => { avm.insert_stack_frame_for_action( actions.clip, - context.swf_version, + context.swf.header().version, bytecode, context, ); @@ -629,7 +602,7 @@ impl Player { ActionType::Init { bytecode } => { avm.insert_stack_frame_for_init_action( actions.clip, - context.swf_version, + context.swf.header().version, bytecode, context, ); @@ -639,7 +612,7 @@ impl Player { ActionType::Method { name } => { avm.insert_stack_frame_for_avm_function( actions.clip, - context.swf_version, + context.swf.header().version, context, name, ); @@ -655,7 +628,7 @@ impl Player { // so this doesn't require any further execution. avm.notify_system_listeners( actions.clip, - context.swf_version, + context.swf.version(), context, listener, method, @@ -716,8 +689,7 @@ impl Player { let ( player_version, global_time, - swf_data, - swf_version, + swf, background_color, renderer, audio, @@ -731,8 +703,7 @@ impl Player { ) = ( self.player_version, self.global_time, - &mut self.swf_data, - self.swf_version, + &self.swf, &mut self.background_color, self.renderer.deref_mut(), self.audio.deref_mut(), @@ -752,8 +723,7 @@ impl Player { let mut update_context = UpdateContext { player_version, global_time, - swf_data, - swf_version, + swf, library, background_color, rng, diff --git a/core/src/tag_utils.rs b/core/src/tag_utils.rs index 4b68fd253..123426db8 100644 --- a/core/src/tag_utils.rs +++ b/core/src/tag_utils.rs @@ -1,47 +1,152 @@ use gc_arena::Collect; use std::sync::Arc; -use swf::TagCode; +use swf::{Header, TagCode}; pub type DecodeResult = Result<(), Box>; pub type SwfStream = swf::read::Reader>; -/// A shared-ownership reference to some portion of an immutable datastream. +/// An open, fully parsed SWF movie ready to play back, either in a Player or a +/// MovieClip. +#[derive(Debug, Clone, Collect)] +#[collect(require_static)] +pub struct SwfMovie { + /// The SWF header parsed from the data stream. + header: Header, + + /// Uncompressed SWF data. + data: Vec, +} + +impl SwfMovie { + /// Construct an empty movie. + pub fn empty(swf_version: u8) -> Self { + Self { + header: Header { + version: swf_version, + compression: swf::Compression::None, + stage_size: swf::Rectangle::default(), + frame_rate: 1.0, + num_frames: 0, + }, + data: vec![], + } + } + + /// Construct a movie from an existing movie with any particular data on it. + pub fn from_movie_and_subdata(&self, data: Vec) -> Self { + Self { + header: self.header.clone(), + data, + } + } + + /// Construct a movie based on the contents of the SWF datastream. + pub fn from_data(swf_data: &[u8]) -> Self { + let swf_stream = swf::read::read_swf_header(&swf_data[..]).unwrap(); + let header = swf_stream.header; + let mut reader = swf_stream.reader; + + // Decompress the entire SWF in memory. + // Sometimes SWFs will have an incorrectly compressed stream, + // but will otherwise decompress fine up to the End tag. + // So just warn on this case and try to continue gracefully. + let data = if header.compression == swf::Compression::Lzma { + // TODO: The LZMA decoder is still funky. + // It always errors, and doesn't return all the data if you use read_to_end, + // but read_exact at least returns the data... why? + // Does the decoder need to be flushed somehow? + let mut data = vec![0u8; swf_stream.uncompressed_length]; + let _ = reader.get_mut().read_exact(&mut data); + data + } else { + let mut data = Vec::with_capacity(swf_stream.uncompressed_length); + if let Err(e) = reader.get_mut().read_to_end(&mut data) { + log::error!("Error decompressing SWF, may be corrupt: {}", e); + } + data + }; + + Self { header, data } + } + + pub fn header(&self) -> &Header { + &self.header + } + + /// Get the version of the SWF. + pub fn version(&self) -> u8 { + self.header.version + } + + pub fn data(&self) -> &[u8] { + &self.data + } +} + +/// A shared-ownership reference to some portion of an SWF datastream. #[derive(Debug, Clone, Collect)] #[collect(no_drop)] pub struct SwfSlice { - pub data: Arc>, + pub movie: Arc, pub start: usize, pub end: usize, } +impl From> for SwfSlice { + fn from(movie: Arc) -> Self { + let end = movie.data().len(); + + Self { + movie, + start: 0, + end, + } + } +} + impl AsRef<[u8]> for SwfSlice { #[inline] fn as_ref(&self) -> &[u8] { - &self.data[self.start..self.end] + &self.movie.data()[self.start..self.end] } } impl SwfSlice { /// Creates an empty SwfSlice. #[inline] - pub fn empty() -> Self { + pub fn empty(swf_version: u8) -> Self { Self { - data: Arc::new(vec![]), + movie: Arc::new(SwfMovie::empty(swf_version)), start: 0, end: 0, } } + + /// Construct a new slice with a given dataset only. + /// + /// This is used primarily for converting owned data back into a slice: we + /// reattach the SWF data that we can + pub fn owned_subslice(&self, data: Vec) -> Self { + let len = data.len(); + + Self { + movie: Arc::new(self.movie.from_movie_and_subdata(data)), + start: 0, + end: len, + } + } + /// Construct a new SwfSlice from a regular slice. /// /// This function returns None if the given slice is not a subslice of the /// current slice. pub fn to_subslice(&self, slice: &[u8]) -> Option { - let self_pval = self.data.as_ptr() as usize; + let self_pval = self.movie.data().as_ptr() as usize; let slice_pval = slice.as_ptr() as usize; if (self_pval + self.start) <= slice_pval && slice_pval < (self_pval + self.end) { Some(SwfSlice { - data: self.data.clone(), + movie: self.movie.clone(), start: slice_pval - self_pval, end: (slice_pval - self_pval) + slice.len(), }) @@ -49,6 +154,79 @@ impl SwfSlice { None } } + + /// Construct a new SwfSlice from a Reader and a size. + /// + /// This is intended to allow constructing references to the contents of a + /// given SWF tag. You just need the current reader and the size of the tag + /// you want to reference. + /// + /// The returned slice may or may not be a subslice of the current slice. + /// If the resulting slice would be outside the bounds of the underlying + /// movie, or the given reader refers to a different underlying movie, this + /// function returns None. + pub fn resize_to_reader(&self, reader: &mut SwfStream<&[u8]>, size: usize) -> Option { + if self.movie.data().as_ptr() as usize <= reader.get_ref().get_ref().as_ptr() as usize + && (reader.get_ref().get_ref().as_ptr() as usize) + < self.movie.data().as_ptr() as usize + self.movie.data().len() + { + let outer_offset = + reader.get_ref().get_ref().as_ptr() as usize - self.movie.data().as_ptr() as usize; + let inner_offset = reader.get_ref().position() as usize; + let new_start = outer_offset + inner_offset; + let new_end = outer_offset + inner_offset + size; + + let len = self.movie.data().len(); + + if new_start < len && new_end < len { + Some(SwfSlice { + movie: self.movie.clone(), + start: new_start, + end: new_end, + }) + } else { + None + } + } else { + None + } + } + + /// Construct a new SwfSlice from a start and an end. + /// + /// The start and end values will be relative to the current slice. + /// Furthermore, this function will yield None if the calculated slice + /// would be invalid (e.g. negative length) or would extend past the end of + /// the current slice. + pub fn to_start_and_end(&self, start: usize, end: usize) -> Option { + let new_start = self.start + start; + let new_end = self.start + end; + + if new_start <= new_end { + self.to_subslice(&self.movie.data().get(new_start..new_end)?) + } else { + None + } + } + + /// Convert the SwfSlice into a standard data slice. + pub fn data(&self) -> &[u8] { + &self.movie.data()[self.start..self.end] + } + + /// Get the version of the SWF this data comes from. + pub fn version(&self) -> u8 { + self.movie.header().version + } + + /// Construct a reader for this slice. + /// + /// The `from` paramter is the offset to start reading the slice from. + pub fn read_from(&self, from: u64) -> swf::read::Reader> { + let mut cursor = std::io::Cursor::new(self.data()); + cursor.set_position(from); + swf::read::Reader::new(cursor, self.movie.version()) + } } pub fn decode_tags<'a, R, F>( @@ -69,8 +247,8 @@ where if let Some(tag) = tag { let result = tag_callback(reader, tag, tag_len); - if let Err(_e) = result { - log::error!("Error running definition tag: {:?}", tag); + if let Err(e) = result { + log::error!("Error running definition tag: {:?}, got {}", tag, e); } if stop_tag == tag { diff --git a/swf/src/types.rs b/swf/src/types.rs index 081d1172e..8dba0f02c 100644 --- a/swf/src/types.rs +++ b/swf/src/types.rs @@ -27,7 +27,7 @@ pub struct SwfStream<'a> { /// Notably contains the compression format used by the rest of the SWF data. /// /// [SWF19 p.27](https://www.adobe.com/content/dam/acom/en/devnet/pdf/swf-file-format-spec.pdf#page=27) -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct Header { pub version: u8, pub compression: Compression, @@ -40,7 +40,7 @@ pub struct Header { /// /// The vast majority of SWFs will use zlib compression. /// [SWF19 p.27](https://www.adobe.com/content/dam/acom/en/devnet/pdf/swf-file-format-spec.pdf#page=27) -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum Compression { None, Zlib, From 8c9d290db723c2ba1f41549bf72490f52854ad83 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Tue, 12 Nov 2019 20:53:04 -0500 Subject: [PATCH 09/59] Implement the MovieClip loading portion of `ActionGetURL2`. --- core/src/avm1.rs | 41 ++++++++++++++++++------- core/src/display_object/movie_clip.rs | 43 +++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 92cb773d3..a3d736cd4 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -8,12 +8,14 @@ use gc_arena::{GcCell, MutationContext}; use rand::Rng; use std::collections::HashMap; use std::convert::TryInto; +use std::sync::Arc; use url::form_urlencoded; use swf::avm1::read::Reader; use swf::avm1::types::{Action, Function}; -use crate::tag_utils::SwfSlice; +use crate::display_object::DisplayObject; +use crate::tag_utils::{SwfMovie, SwfSlice}; #[cfg(test)] #[macro_use] @@ -1665,14 +1667,14 @@ impl<'gc> Avm1<'gc> { return fscommand::handle(fscommand, self, context); } - if is_load_vars { - let clip_target: Option> = if is_target_sprite { - let start = self.target_clip_or_root(context); - self.resolve_target_display_object(context, start, target)? - } else { - Some(self.target_clip_or_root(context)) - }; + let clip_target: Option> = if is_target_sprite { + let start = self.target_clip_or_root(context); + self.resolve_target_display_object(context, start, target.clone())? + } else { + Some(self.target_clip_or_root(context)) + }; + if is_load_vars { if let Some(clip_target) = clip_target { let target_obj = clip_target .as_movie_clip() @@ -1699,8 +1701,27 @@ impl<'gc> Avm1<'gc> { return Ok(()); } else if is_target_sprite { - log::warn!("GetURL into target sprite is not yet implemented"); - return Ok(()); //maybe error? + if let Some(clip_target) = clip_target { + let player = context.player.clone().unwrap().upgrade().unwrap(); + let fetch = context.navigator.fetch(url); + let slot = self.forcibly_root_object(clip_target.object().as_object()?); + + context.navigator.spawn_future(Box::pin(async move { + let data = fetch.await.unwrap(); + let movie = Arc::new(SwfMovie::from_data(&data)); + + player.lock().unwrap().update(|avm, uc| { + let that = avm.unroot_object(slot); + that.as_display_object() + .unwrap() + .as_movie_clip() + .unwrap() + .replace_with_movie(uc.gc_context, movie); + }) + })) + } + + return Ok(()); } else { let vars = match NavigationMethod::from_send_vars_method(swf_method) { Some(method) => Some((method, self.locals_into_form_values(context))), diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 35097f5e7..4d5e72b74 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -103,6 +103,21 @@ impl<'gc> MovieClip<'gc> { ) } + /// Replace the current MovieClip with a completely new SwfMovie. + /// + /// Playback will start at position zero, any existing streamed audio will + /// be terminated, and so on. Children and AVM data will be kept across the + /// load boundary. + pub fn replace_with_movie( + &mut self, + gc_context: MutationContext<'gc, '_>, + movie: Arc, + ) { + self.0 + .write(gc_context) + .replace_with_movie(gc_context, movie) + } + pub fn preload( self, context: &mut UpdateContext<'_, 'gc, '_>, @@ -406,6 +421,34 @@ unsafe impl<'gc> Collect for MovieClipData<'gc> { } impl<'gc> MovieClipData<'gc> { + /// Replace the current MovieClipData with a completely new SwfMovie. + /// + /// Playback will start at position zero, any existing streamed audio will + /// be terminated, and so on. Children and AVM data will be kept across the + /// load boundary. + pub fn replace_with_movie( + &mut self, + gc_context: MutationContext<'gc, '_>, + movie: Arc, + ) { + let total_frames = movie.header().num_frames; + + self.static_data = Gc::allocate( + gc_context, + MovieClipStatic { + id: self.static_data.id, //TODO: This is WRONG; This is VERRRRY WRONG! + swf: movie.into(), + total_frames, + audio_stream_info: None, + frame_labels: HashMap::new(), + }, + ); + self.tag_stream_pos = 0; + self.flags = EnumSet::empty(); + self.current_frame = 0; + self.audio_stream = None; + } + fn id(&self) -> CharacterId { self.static_data.id } From ed799fd2b97d65b229e4abc72ca44d31448ae6b1 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Tue, 12 Nov 2019 22:00:19 -0500 Subject: [PATCH 10/59] Split the player up into nine layers, each of which is a separate root movie clip. --- core/src/avm1/script_object.rs | 3 +- core/src/avm1/test_utils.rs | 3 +- core/src/context.rs | 12 +++- core/src/display_object/button.rs | 1 + core/src/display_object/movie_clip.rs | 4 ++ core/src/player.rs | 84 ++++++++++++++++++--------- 6 files changed, 77 insertions(+), 30 deletions(-) diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index 88c7e5bb3..773fd3e65 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -586,12 +586,13 @@ mod tests { let swf = Arc::new(SwfMovie::empty(swf_version)); let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); - + let mut layers = [root; 9]; let mut context = UpdateContext { gc_context, global_time: 0, player_version: 32, swf: &swf, + layers: &mut layers, root, rng: &mut SmallRng::from_seed([0u8; 16]), action_queue: &mut crate::context::ActionQueue::new(), diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index f094de894..c0d63fc1d 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -25,12 +25,13 @@ where let swf = Arc::new(SwfMovie::empty(swf_version)); let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); - + let mut layers = [root; 9]; let mut context = UpdateContext { gc_context, global_time: 0, player_version: 32, swf: &swf, + layers: &mut layers, root, rng: &mut SmallRng::from_seed([0u8; 16]), audio: &mut NullAudioBackend::new(), diff --git a/core/src/context.rs b/core/src/context.rs index 9c84fc1da..cd540b71e 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -63,8 +63,11 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { /// The RNG, used by the AVM `RandomNumber` opcode, `Math.random(),` and `random()`. pub rng: &'a mut SmallRng, - /// The root of the current timeline. - /// This will generally be `_level0`, except for loadMovie/loadMovieNum. + /// All nine layers of the current player. + pub layers: &'a mut [DisplayObject<'gc>; 9], + + /// The root of the current timeline being updated. + /// This will always be one of the layers in `layers`. pub root: DisplayObject<'gc>, /// The current set of system-specified prototypes to use when constructing @@ -95,6 +98,9 @@ pub struct QueuedActions<'gc> { /// The movie clip this ActionScript is running on. pub clip: DisplayObject<'gc>, + /// The root timeline this action was queued in. + pub root: DisplayObject<'gc>, + /// The type of action this is, along with the corresponding bytecode/method data. pub action_type: ActionType<'gc>, @@ -131,11 +137,13 @@ impl<'gc> ActionQueue<'gc> { pub fn queue_actions( &mut self, clip: DisplayObject<'gc>, + root: DisplayObject<'gc>, action_type: ActionType<'gc>, is_unload: bool, ) { self.queue.push_back(QueuedActions { clip, + root, action_type, is_unload, }) diff --git a/core/src/display_object/button.rs b/core/src/display_object/button.rs index 12b4eb38a..5072fefe7 100644 --- a/core/src/display_object/button.rs +++ b/core/src/display_object/button.rs @@ -357,6 +357,7 @@ impl<'gc> ButtonData<'gc> { handled = ButtonEventResult::Handled; context.action_queue.queue_actions( parent, + context.root, ActionType::Normal { bytecode: action.action_data.clone(), }, diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 4d5e72b74..f7905942f 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -912,6 +912,7 @@ impl<'gc> MovieClipData<'gc> { { context.action_queue.queue_actions( self_display_object, + context.root, ActionType::Normal { bytecode: clip_action.action_data.clone(), }, @@ -946,6 +947,7 @@ impl<'gc> MovieClipData<'gc> { if let Some(name) = name { context.action_queue.queue_actions( self_display_object, + context.root, ActionType::Method { name }, event == ClipEvent::Unload, ); @@ -1696,6 +1698,7 @@ impl<'gc, 'a> MovieClipData<'gc> { })?; context.action_queue.queue_actions( self_display_object, + context.root, ActionType::Normal { bytecode: slice }, false, ); @@ -1729,6 +1732,7 @@ impl<'gc, 'a> MovieClipData<'gc> { })?; context.action_queue.queue_actions( self_display_object, + context.root, ActionType::Init { bytecode: slice }, true, ); diff --git a/core/src/player.rs b/core/src/player.rs index 99a773fa3..20aa71b03 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -32,7 +32,7 @@ struct GcRoot<'gc>(GcCell<'gc, GcRootData<'gc>>); #[collect(no_drop)] struct GcRootData<'gc> { library: Library<'gc>, - root: DisplayObject<'gc>, + layers: [DisplayObject<'gc>; 9], mouse_hovered_object: Option>, // TODO: Remove GcCell wrapped inside GcCell. /// The object being dragged via a `startDrag` action. @@ -48,14 +48,14 @@ impl<'gc> GcRootData<'gc> { fn update_context_params( &mut self, ) -> ( - DisplayObject<'gc>, + &mut [DisplayObject<'gc>; 9], &mut Library<'gc>, &mut ActionQueue<'gc>, &mut Avm1<'gc>, &mut Option>, ) { ( - self.root, + &mut self.layers, &mut self.library, &mut self.action_queue, &mut self.avm, @@ -180,7 +180,17 @@ impl Player { gc_context, GcRootData { library, - root: MovieClip::from_movie(gc_context, movie.clone()).into(), + layers: [ + MovieClip::from_movie(gc_context, movie.clone()).into(), + MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), + MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), + MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), + MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), + MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), + MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), + MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), + MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), + ], mouse_hovered_object: None, drag_object: None, avm: Avm1::new(gc_context, NEWEST_PLAYER_VERSION), @@ -210,9 +220,13 @@ impl Player { }; player.gc_arena.mutate(|gc_context, gc_root| { - let root_data = gc_root.0.write(gc_context); - let mut root = root_data.root; - root.post_instantiation(gc_context, root, root_data.avm.prototypes().movie_clip); + let mut root_data = gc_root.0.write(gc_context); + let mc_proto = root_data.avm.prototypes().movie_clip; + + for (i, layer) in root_data.layers.iter_mut().enumerate() { + layer.post_instantiation(gc_context, *layer, mc_proto); + layer.set_name(gc_context, &format!("_level{}", i)) + } }); player.build_matrices(); @@ -371,6 +385,7 @@ impl Player { if let Some(mouse_event_name) = mouse_event_name { context.action_queue.queue_actions( + root, root, ActionType::NotifyListeners { listener: SystemListener::Mouse, @@ -450,11 +465,20 @@ impl Player { return false; } let mouse_pos = self.mouse_pos; - // Check hovered object. + self.mutate_with_update_context(|avm, context| { - let root = context.root; - let new_hovered = root.mouse_pick(root, (mouse_pos.0, mouse_pos.1)); + // Check hovered object. + let mut new_hovered = None; + for layer in context.layers.iter().rev() { + if new_hovered.is_none() { + new_hovered = layer.mouse_pick(*layer, (mouse_pos.0, mouse_pos.1)); + } else { + break; + } + } + let cur_hovered = context.mouse_hovered_object; + if cur_hovered.map(|d| d.as_ptr()) != new_hovered.map(|d| d.as_ptr()) { // RollOut of previous node. if let Some(node) = cur_hovered { @@ -480,6 +504,10 @@ impl Player { }) } + /// Preload the first movie in the player. + /// + /// This should only be called once. Further movie loads should preload the + /// specific `MovieClip` referenced. fn preload(&mut self) { self.mutate_with_update_context(|_avm, context| { let mut morph_shapes = fnv::FnvHashMap::default(); @@ -499,18 +527,14 @@ impl Player { } pub fn run_frame(&mut self) { - self.mutate_with_update_context(|avm, context| { - let mut root = context.root; - root.run_frame(context); - Self::run_actions(avm, context); - }); + self.update(|_avm, update_context| { + // TODO: In what order are layers run? + for layer in update_context.layers.clone().iter_mut() { + update_context.root = *layer; - // Update mouse state (check for new hovered button, etc.) - self.update_drag(); - self.update_roll_over(); - - // GC - self.gc_arena.collect_debt(); + layer.run_frame(update_context); + } + }) } pub fn render(&mut self) { @@ -541,7 +565,10 @@ impl Player { view_bounds, clip_depth_stack: vec![], }; - root_data.root.render(&mut render_context); + + for layer in &root_data.layers { + layer.render(&mut render_context); + } }); transform_stack.pop(); @@ -588,6 +615,9 @@ impl Player { if !actions.is_unload && actions.clip.removed() { continue; } + + context.root = actions.root; + match actions.action_type { // DoAction/clip event code ActionType::Normal { bytecode } => { @@ -607,7 +637,6 @@ impl Player { context, ); } - // Event handler method call (e.g. onEnterFrame) ActionType::Method { name } => { avm.insert_stack_frame_for_avm_function( @@ -719,7 +748,8 @@ impl Player { self.gc_arena.mutate(|gc_context, gc_root| { let mut root_data = gc_root.0.write(gc_context); let mouse_hovered_object = root_data.mouse_hovered_object; - let (root, library, action_queue, avm, drag_object) = root_data.update_context_params(); + let (layers, library, action_queue, avm, drag_object) = + root_data.update_context_params(); let mut update_context = UpdateContext { player_version, global_time, @@ -733,12 +763,13 @@ impl Player { input, action_queue, gc_context, - root, - system_prototypes: avm.prototypes().clone(), + root: layers[0], + layers, mouse_hovered_object, mouse_position, drag_object, stage_size: (stage_width, stage_height), + system_prototypes: avm.prototypes().clone(), player, }; @@ -788,6 +819,7 @@ impl Player { }); // Update mouse state (check for new hovered button, etc.) + self.update_drag(); self.update_roll_over(); // GC From 6da374c56761cd01d57bc8c4f239b6a8e2d38130 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Wed, 13 Nov 2019 14:11:22 -0500 Subject: [PATCH 11/59] Implement loading movies into `_leveln` via `ActionGetUrl` --- core/src/avm1.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index a3d736cd4..1b833349b 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1627,16 +1627,30 @@ impl<'gc> Avm1<'gc> { fn action_get_url( &mut self, - context: &mut UpdateContext, + context: &mut UpdateContext<'_, 'gc, '_>, url: &str, target: &str, ) -> Result<(), Error> { - //TODO: support `_level0` thru `_level9` - if target.starts_with("_level") { - log::warn!( - "Remote SWF loads into target {} not yet implemented", - target - ); + if target.starts_with("_level") && target.len() > 6 { + let level_id = target[6..].parse::().unwrap(); + let player = context.player.clone().unwrap().upgrade().unwrap(); + let fetch = context.navigator.fetch(url.to_string()); + let slot = self.forcibly_root_object(context.layers[level_id].object().as_object()?); + + context.navigator.spawn_future(Box::pin(async move { + let data = fetch.await.unwrap(); + let movie = Arc::new(SwfMovie::from_data(&data)); + + player.lock().unwrap().update(|avm, uc| { + let that = avm.unroot_object(slot); + that.as_display_object() + .unwrap() + .as_movie_clip() + .unwrap() + .replace_with_movie(uc.gc_context, movie); + }) + })); + return Ok(()); } From e0c0779bd0443575acbb53427cedc38c1f367c3d Mon Sep 17 00:00:00 2001 From: David Wendt Date: Wed, 13 Nov 2019 14:40:57 -0500 Subject: [PATCH 12/59] Make sure to preload all loaded clips. --- core/src/avm1.rs | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 1b833349b..d3b0a8c55 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -14,7 +14,7 @@ use url::form_urlencoded; use swf::avm1::read::Reader; use swf::avm1::types::{Action, Function}; -use crate::display_object::DisplayObject; +use crate::display_object::{DisplayObject, MorphShape}; use crate::tag_utils::{SwfMovie, SwfSlice}; #[cfg(test)] @@ -1642,12 +1642,22 @@ impl<'gc> Avm1<'gc> { let movie = Arc::new(SwfMovie::from_data(&data)); player.lock().unwrap().update(|avm, uc| { - let that = avm.unroot_object(slot); - that.as_display_object() - .unwrap() - .as_movie_clip() - .unwrap() - .replace_with_movie(uc.gc_context, movie); + let that = avm.unroot_object(slot).as_display_object().unwrap(); + let mut mc = that.as_movie_clip().unwrap(); + + mc.replace_with_movie(uc.gc_context, movie); + + let mut morph_shapes = fnv::FnvHashMap::default(); + mc.preload(uc, &mut morph_shapes); + + // Finalize morph shapes. + for (id, static_data) in morph_shapes { + let morph_shape = MorphShape::new(uc.gc_context, static_data); + uc.library.register_character( + id, + crate::character::Character::MorphShape(morph_shape), + ); + } }) })); @@ -1725,12 +1735,22 @@ impl<'gc> Avm1<'gc> { let movie = Arc::new(SwfMovie::from_data(&data)); player.lock().unwrap().update(|avm, uc| { - let that = avm.unroot_object(slot); - that.as_display_object() - .unwrap() - .as_movie_clip() - .unwrap() - .replace_with_movie(uc.gc_context, movie); + let that = avm.unroot_object(slot).as_display_object().unwrap(); + let mut mc = that.as_movie_clip().unwrap(); + + mc.replace_with_movie(uc.gc_context, movie); + + let mut morph_shapes = fnv::FnvHashMap::default(); + mc.preload(uc, &mut morph_shapes); + + // Finalize morph shapes. + for (id, static_data) in morph_shapes { + let morph_shape = MorphShape::new(uc.gc_context, static_data); + uc.library.register_character( + id, + crate::character::Character::MorphShape(morph_shape), + ); + } }) })) } From 5ce499d11e08b8bd07a15861801f672288bbe6b5 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Wed, 13 Nov 2019 21:41:38 -0500 Subject: [PATCH 13/59] Add separate libraries for each loaded movie. --- Cargo.lock | 7 ++ core/Cargo.toml | 1 + core/src/avm1.rs | 24 +++--- core/src/avm1/globals/movie_clip.rs | 28 ++++--- core/src/avm1/globals/sound.rs | 51 +++++++++---- core/src/avm1/script_object.rs | 2 +- core/src/avm1/test_utils.rs | 2 +- core/src/display_object.rs | 13 +++- core/src/display_object/button.rs | 42 ++++++++--- core/src/display_object/edit_text.rs | 103 ++++++++++++++++++-------- core/src/display_object/movie_clip.rs | 73 +++++++++++++----- core/src/display_object/text.rs | 21 +++++- core/src/library.rs | 52 +++++++++++-- core/src/player.rs | 8 +- 14 files changed, 322 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9219cd599..e2b3a5f41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1577,6 +1577,7 @@ dependencies = [ "smallvec 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "swf 0.1.2", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "weak-table 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2242,6 +2243,11 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "weak-table" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "web-sys" version = "0.3.34" @@ -2642,6 +2648,7 @@ dependencies = [ "checksum wayland-protocols 0.23.6 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc286643656742777d55dc8e70d144fa4699e426ca8e9d4ef454f4bf15ffcf9" "checksum wayland-scanner 0.23.6 (registry+https://github.com/rust-lang/crates.io-index)" = "93b02247366f395b9258054f964fe293ddd019c3237afba9be2ccbe9e1651c3d" "checksum wayland-sys 0.23.6 (registry+https://github.com/rust-lang/crates.io-index)" = "d94e89a86e6d6d7c7c9b19ebf48a03afaac4af6bc22ae570e9a24124b75358f4" +"checksum weak-table 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a5862bb244c852a56c6f3c39668ff181271bda44513ef30d2073a3eedd9898d" "checksum web-sys 0.3.34 (registry+https://github.com/rust-lang/crates.io-index)" = "ba09295448c0b93bc87d2769614d371a924749e5e6c87e4c1df8b2416b49b775" "checksum webbrowser 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "97d468a911faaaeb783693b004e1c62e0063e646b0afae5c146cd144e566e66d" "checksum weedle 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164" diff --git a/core/Cargo.toml b/core/Cargo.toml index 0bbbb2483..674e9b635 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,6 +22,7 @@ num_enum = "0.4.2" quick-xml = "0.17.2" downcast-rs = "1.1.1" url = "2.1.0" +weak-table = "0.2.3" [dependencies.jpeg-decoder] version = "0.1.18" diff --git a/core/src/avm1.rs b/core/src/avm1.rs index d3b0a8c55..918db73c0 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1645,7 +1645,7 @@ impl<'gc> Avm1<'gc> { let that = avm.unroot_object(slot).as_display_object().unwrap(); let mut mc = that.as_movie_clip().unwrap(); - mc.replace_with_movie(uc.gc_context, movie); + mc.replace_with_movie(uc.gc_context, movie.clone()); let mut morph_shapes = fnv::FnvHashMap::default(); mc.preload(uc, &mut morph_shapes); @@ -1653,10 +1653,12 @@ impl<'gc> Avm1<'gc> { // Finalize morph shapes. for (id, static_data) in morph_shapes { let morph_shape = MorphShape::new(uc.gc_context, static_data); - uc.library.register_character( - id, - crate::character::Character::MorphShape(morph_shape), - ); + uc.library + .library_for_movie_mut(movie.clone()) + .register_character( + id, + crate::character::Character::MorphShape(morph_shape), + ); } }) })); @@ -1738,7 +1740,7 @@ impl<'gc> Avm1<'gc> { let that = avm.unroot_object(slot).as_display_object().unwrap(); let mut mc = that.as_movie_clip().unwrap(); - mc.replace_with_movie(uc.gc_context, movie); + mc.replace_with_movie(uc.gc_context, movie.clone()); let mut morph_shapes = fnv::FnvHashMap::default(); mc.preload(uc, &mut morph_shapes); @@ -1746,10 +1748,12 @@ impl<'gc> Avm1<'gc> { // Finalize morph shapes. for (id, static_data) in morph_shapes { let morph_shape = MorphShape::new(uc.gc_context, static_data); - uc.library.register_character( - id, - crate::character::Character::MorphShape(morph_shape), - ); + uc.library + .library_for_movie_mut(movie.clone()) + .register_character( + id, + crate::character::Character::MorphShape(morph_shape), + ); } }) })) diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index 9e8c10a07..149b412bc 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -246,11 +246,15 @@ fn attach_movie<'gc>( if depth < 0 || depth > AVM_MAX_DEPTH { return Ok(Value::Undefined.into()); } - if let Ok(mut new_clip) = context.library.instantiate_by_export_name( - &export_name, - context.gc_context, - &avm.prototypes, - ) { + + if let Ok(mut new_clip) = context + .library + .library_for_movie(movie_clip.movie().unwrap()) + .ok_or_else(|| "Movie is missing!".into()) + .and_then(|l| { + l.instantiate_by_export_name(&export_name, context.gc_context, &avm.prototypes) + }) + { // Set name and attach to parent. new_clip.set_name(context.gc_context, &new_instance_name); movie_clip.add_child_from_avm(context, new_clip, depth); @@ -310,6 +314,7 @@ fn create_text_field<'gc>( context: &mut UpdateContext<'_, 'gc, '_>, args: &[Value<'gc>], ) -> Result, Error> { + let movie = avm.base_clip().movie().unwrap(); let instance_name = args .get(0) .cloned() @@ -341,7 +346,8 @@ fn create_text_field<'gc>( .unwrap_or(Value::Undefined) .as_number(avm, context)?; - let mut text_field: DisplayObject<'gc> = EditText::new(context, x, y, width, height).into(); + let mut text_field: DisplayObject<'gc> = + EditText::new(context, movie, x, y, width, height).into(); text_field.post_instantiation(context.gc_context, text_field, avm.prototypes().text_field); text_field.set_name(context.gc_context, &instance_name); movie_clip.add_child_from_avm(context, text_field, depth as Depth); @@ -385,10 +391,12 @@ pub fn duplicate_movie_clip<'gc>( if depth < 0 || depth > AVM_MAX_DEPTH { return Ok(Value::Undefined.into()); } - if let Ok(mut new_clip) = - context - .library - .instantiate_by_id(movie_clip.id(), context.gc_context, &avm.prototypes) + + if let Ok(mut new_clip) = context + .library + .library_for_movie(movie_clip.movie().unwrap()) + .ok_or_else(|| "Movie is missing!".into()) + .and_then(|l| l.instantiate_by_id(movie_clip.id(), context.gc_context, &avm.prototypes)) { // Set name and attach to parent. new_clip.set_name(context.gc_context, &new_instance_name); diff --git a/core/src/avm1/globals/sound.rs b/core/src/avm1/globals/sound.rs index c5b760c62..5da6fe8ac 100644 --- a/core/src/avm1/globals/sound.rs +++ b/core/src/avm1/globals/sound.rs @@ -6,6 +6,7 @@ use crate::avm1::property::Attribute::*; use crate::avm1::return_value::ReturnValue; use crate::avm1::{Avm1, Error, Object, SoundObject, TObject, UpdateContext, Value}; use crate::character::Character; +use crate::display_object::TDisplayObject; use gc_arena::MutationContext; /// Implements `Sound` @@ -167,15 +168,27 @@ fn attach_sound<'gc>( let name = args.get(0).unwrap_or(&Value::Undefined); if let Some(sound_object) = this.as_sound_object() { let name = name.clone().coerce_to_string(avm, context)?; - if let Some(Character::Sound(sound)) = context.library.get_character_by_export_name(&name) { - sound_object.set_sound(context.gc_context, Some(*sound)); - sound_object.set_duration( - context.gc_context, - context.audio.get_sound_duration(*sound).unwrap_or(0), - ); - sound_object.set_position(context.gc_context, 0); + let movie = sound_object.owner().and_then(|o| o.movie()); + if let Some(movie) = movie { + if let Some(Character::Sound(sound)) = context + .library + .library_for_movie_mut(movie) + .get_character_by_export_name(&name) + { + sound_object.set_sound(context.gc_context, Some(*sound)); + sound_object.set_duration( + context.gc_context, + context.audio.get_sound_duration(*sound).unwrap_or(0), + ); + sound_object.set_position(context.gc_context, 0); + } else { + log::warn!("Sound.attachSound: Sound '{}' not found", name); + } } else { - log::warn!("Sound.attachSound: Sound '{}' not found", name); + log::warn!( + "Sound.attachSound: Cannot attach Sound '{}' without a library to reference", + name + ); } } else { log::warn!("Sound.attachSound: this is not a Sound"); @@ -395,13 +408,23 @@ fn stop<'gc>( if let Some(name) = args.get(0) { // Usage 1: Stop all instances of a particular sound, using the name parameter. let name = name.clone().coerce_to_string(avm, context)?; - if let Some(Character::Sound(sound)) = - context.library.get_character_by_export_name(&name) - { - // Stop all sounds with the given name. - context.audio.stop_sounds_with_handle(*sound); + let movie = sound.owner().and_then(|o| o.movie()); + if let Some(movie) = movie { + if let Some(Character::Sound(sound)) = context + .library + .library_for_movie_mut(movie) + .get_character_by_export_name(&name) + { + // Stop all sounds with the given name. + context.audio.stop_sounds_with_handle(*sound); + } else { + log::warn!("Sound.stop: Sound '{}' not found", name); + } } else { - log::warn!("Sound.stop: Sound '{}' not found", name); + log::warn!( + "Sound.stop: Cannot stop Sound '{}' without a library to reference", + name + ) } } else if let Some(_owner) = sound.owner() { // Usage 2: Stop all sound running within a given clip. diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index 773fd3e65..ffb495878 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -604,7 +604,7 @@ mod tests { b: 0, a: 0, }, - library: &mut Library::new(), + library: &mut Library::default(), navigator: &mut NullNavigatorBackend::new(), renderer: &mut NullRenderer::new(), system_prototypes: avm.prototypes().clone(), diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index c0d63fc1d..5456f1983 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -43,7 +43,7 @@ where b: 0, a: 0, }, - library: &mut Library::new(), + library: &mut Library::default(), navigator: &mut NullNavigatorBackend::new(), renderer: &mut NullRenderer::new(), system_prototypes: avm.prototypes().clone(), diff --git a/core/src/display_object.rs b/core/src/display_object.rs index b7b67e654..d8677b159 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -2,12 +2,14 @@ use crate::avm1::{Object, Value}; use crate::context::{RenderContext, UpdateContext}; use crate::player::NEWEST_PLAYER_VERSION; use crate::prelude::*; +use crate::tag_utils::SwfMovie; use crate::transform::Transform; use enumset::{EnumSet, EnumSetType}; use gc_arena::{Collect, MutationContext}; use ruffle_macros::enum_trait_object; use std::cell::{Ref, RefMut}; use std::fmt::Debug; +use std::sync::Arc; mod bitmap; mod button; @@ -345,6 +347,10 @@ impl<'gc> DisplayObjectBase<'gc> { .map(|p| p.swf_version()) .unwrap_or(NEWEST_PLAYER_VERSION) } + + fn movie(&self) -> Option> { + self.parent.and_then(|p| p.movie()) + } } #[enum_trait_object( @@ -725,7 +731,7 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug { .clip_actions .iter() .cloned() - .map(|a| ClipAction::from_action_and_movie(a, clip.movie())) + .map(|a| ClipAction::from_action_and_movie(a, clip.movie().unwrap())) .collect(), ); } @@ -784,6 +790,11 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug { .unwrap_or(NEWEST_PLAYER_VERSION) } + /// Return the SWF that defines this display object. + fn movie(&self) -> Option> { + self.parent().and_then(|p| p.movie()) + } + fn instantiate(&self, gc_context: MutationContext<'gc, '_>) -> DisplayObject<'gc>; fn as_ptr(&self) -> *const DisplayObjectPtr; diff --git a/core/src/display_object/button.rs b/core/src/display_object/button.rs index 5072fefe7..2e51e1979 100644 --- a/core/src/display_object/button.rs +++ b/core/src/display_object/button.rs @@ -3,10 +3,11 @@ use crate::context::{ActionType, RenderContext, UpdateContext}; use crate::display_object::{DisplayObjectBase, TDisplayObject}; use crate::events::{ButtonEvent, ButtonEventResult, ButtonKeyCode}; use crate::prelude::*; -use crate::tag_utils::SwfSlice; +use crate::tag_utils::{SwfMovie, SwfSlice}; use gc_arena::{Collect, GcCell, MutationContext}; use std::collections::BTreeMap; use std::convert::TryFrom; +use std::sync::Arc; #[derive(Clone, Debug, Collect, Copy)] #[collect(no_drop)] @@ -47,6 +48,7 @@ impl<'gc> Button<'gc> { } let static_data = ButtonStatic { + swf: source_movie.movie.clone(), id: button.id, records: button.records.clone(), actions, @@ -120,6 +122,10 @@ impl<'gc> TDisplayObject<'gc> for Button<'gc> { self.0.read().static_data.read().id } + fn movie(&self) -> Option> { + Some(self.0.read().static_data.read().swf.clone()) + } + fn post_instantiation( &mut self, gc_context: MutationContext<'gc, '_>, @@ -223,11 +229,11 @@ impl<'gc> ButtonData<'gc> { self.children.clear(); for record in &self.static_data.read().records { if record.states.contains(&swf_state) { - if let Ok(mut child) = context.library.instantiate_by_id( - record.id, - context.gc_context, - &context.system_prototypes, - ) { + if let Ok(mut child) = context + .library + .library_for_movie_mut(self.movie()) + .instantiate_by_id(record.id, context.gc_context, &context.system_prototypes) + { child.set_parent(context.gc_context, Some(self_display_object)); child.set_matrix(context.gc_context, &record.matrix.clone().into()); child.set_color_transform( @@ -253,11 +259,14 @@ impl<'gc> ButtonData<'gc> { for record in &self.static_data.read().records { if record.states.contains(&swf::ButtonState::HitTest) { - match context.library.instantiate_by_id( - record.id, - context.gc_context, - &context.system_prototypes, - ) { + match context + .library + .library_for_movie_mut(self.static_data.read().swf.clone()) + .instantiate_by_id( + record.id, + context.gc_context, + &context.system_prototypes, + ) { Ok(mut child) => { { child.set_matrix(context.gc_context, &record.matrix.clone().into()); @@ -335,7 +344,11 @@ impl<'gc> ButtonData<'gc> { sound: Option<&swf::ButtonSound>, ) { if let Some((id, sound_info)) = sound { - if let Some(sound_handle) = context.library.get_sound(*id) { + if let Some(sound_handle) = context + .library + .library_for_movie_mut(self.movie()) + .get_sound(*id) + { context.audio.start_sound(sound_handle, sound_info); } } @@ -368,6 +381,10 @@ impl<'gc> ButtonData<'gc> { } handled } + + fn movie(&self) -> Arc { + self.static_data.read().swf.clone() + } } unsafe impl<'gc> gc_arena::Collect for ButtonData<'gc> { @@ -410,6 +427,7 @@ enum ButtonTracking { #[allow(dead_code)] #[derive(Clone, Debug)] struct ButtonStatic { + swf: Arc, id: CharacterId, records: Vec, actions: Vec, diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index c84d4ff8f..9f1abdd33 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -6,8 +6,10 @@ use crate::display_object::{DisplayObjectBase, TDisplayObject}; use crate::font::{Glyph, TextFormat}; use crate::library::Library; use crate::prelude::*; +use crate::tag_utils::SwfMovie; use crate::transform::Transform; use gc_arena::{Collect, Gc, GcCell, MutationContext}; +use std::sync::Arc; /// A dynamic text field. /// The text in this text field can be changed dynamically. @@ -51,7 +53,11 @@ pub struct EditTextData<'gc> { impl<'gc> EditText<'gc> { /// Creates a new `EditText` from an SWF `DefineEditText` tag. - pub fn from_swf_tag(context: &mut UpdateContext<'_, 'gc, '_>, swf_tag: swf::EditText) -> Self { + pub fn from_swf_tag( + context: &mut UpdateContext<'_, 'gc, '_>, + swf_movie: Arc, + swf_tag: swf::EditText, + ) -> Self { let is_multiline = swf_tag.is_multiline; let is_word_wrap = swf_tag.is_word_wrap; @@ -84,7 +90,13 @@ impl<'gc> EditText<'gc> { base: Default::default(), text, new_format: TextFormat::default(), - static_data: gc_arena::Gc::allocate(context.gc_context, EditTextStatic(swf_tag)), + static_data: gc_arena::Gc::allocate( + context.gc_context, + EditTextStatic { + swf: swf_movie, + text: swf_tag, + }, + ), is_multiline, is_word_wrap, object: None, @@ -96,6 +108,7 @@ impl<'gc> EditText<'gc> { /// Create a new, dynamic `EditText`. pub fn new( context: &mut UpdateContext<'_, 'gc, '_>, + swf_movie: Arc, x: f64, y: f64, width: f64, @@ -140,7 +153,7 @@ impl<'gc> EditText<'gc> { is_device_font: false, }; - Self::from_swf_tag(context, swf_tag) + Self::from_swf_tag(context, swf_movie, swf_tag) } // TODO: This needs to strip away HTML @@ -191,16 +204,20 @@ impl<'gc> EditText<'gc> { /// `DisplayObject`. pub fn text_transform(self) -> Transform { let edit_text = self.0.read(); - let static_data = &edit_text.static_data.0; + let static_data = &edit_text.static_data; // TODO: Many of these properties should change be instance members instead // of static data, because they can be altered via ActionScript. - let color = static_data.color.as_ref().unwrap_or_else(|| &swf::Color { - r: 0, - g: 0, - b: 0, - a: 255, - }); + let color = static_data + .text + .color + .as_ref() + .unwrap_or_else(|| &swf::Color { + r: 0, + g: 0, + b: 0, + a: 255, + }); let mut transform: Transform = Default::default(); transform.color_transform.r_mult = f32::from(color.r) / 255.0; @@ -208,7 +225,7 @@ impl<'gc> EditText<'gc> { transform.color_transform.b_mult = f32::from(color.b) / 255.0; transform.color_transform.a_mult = f32::from(color.a) / 255.0; - if let Some(layout) = &static_data.layout { + if let Some(layout) = &static_data.text.layout { transform.matrix.tx += layout.left_margin.get() as f32; transform.matrix.tx += layout.indent.get() as f32; transform.matrix.ty -= layout.leading.get() as f32; @@ -224,11 +241,11 @@ impl<'gc> EditText<'gc> { /// and returns the adjusted transform. pub fn newline(self, height: f32, mut transform: Transform) -> Transform { let edit_text = self.0.read(); - let static_data = &edit_text.static_data.0; + let static_data = &edit_text.static_data; transform.matrix.tx = 0.0; transform.matrix.ty += height * Twips::TWIPS_PER_PIXEL as f32; - if let Some(layout) = &static_data.layout { + if let Some(layout) = &static_data.text.layout { transform.matrix.tx += layout.left_margin.get() as f32; transform.matrix.tx += layout.indent.get() as f32; transform.matrix.ty += layout.leading.get() as f32; @@ -239,11 +256,11 @@ impl<'gc> EditText<'gc> { pub fn line_width(self) -> f32 { let edit_text = self.0.read(); - let static_data = &edit_text.static_data.0; + let static_data = &edit_text.static_data; let mut base_width = self.width() as f32; - if let Some(layout) = &static_data.layout { + if let Some(layout) = &static_data.text.layout { base_width -= layout.left_margin.to_pixels() as f32; base_width -= layout.indent.to_pixels() as f32; base_width -= layout.right_margin.to_pixels() as f32; @@ -273,18 +290,26 @@ impl<'gc> EditText<'gc> { /// calculating them is a relatively expensive operation. fn line_breaks(self, library: &Library<'gc>) -> Vec { let edit_text = self.0.read(); - let static_data = &edit_text.static_data.0; - let font_id = static_data.font_id.unwrap_or(0); + let static_data = &edit_text.static_data; + let font_id = static_data.text.font_id.unwrap_or(0); if edit_text.is_multiline { if let Some(font) = library + .library_for_movie(self.movie().unwrap()) + .unwrap() .get_font(font_id) .filter(|font| font.has_glyphs()) - .or_else(|| library.device_font()) + .or_else(|| { + library + .library_for_movie(self.movie().unwrap()) + .unwrap() + .device_font() + }) { let mut breakpoints = vec![]; let mut break_base = 0; let height = static_data + .text .height .map(|v| v.to_pixels() as f32) .unwrap_or_else(|| font.scale()); @@ -343,16 +368,24 @@ impl<'gc> EditText<'gc> { let breakpoints = self.line_breaks_cached(context.gc_context, context.library); let edit_text = self.0.read(); - let static_data = &edit_text.static_data.0; - let font_id = static_data.font_id.unwrap_or(0); + let static_data = &edit_text.static_data; + let font_id = static_data.text.font_id.unwrap_or(0); let mut size: (f32, f32) = (0.0, 0.0); if let Some(font) = context .library + .library_for_movie(self.movie().unwrap()) + .unwrap() .get_font(font_id) .filter(|font| font.has_glyphs()) - .or_else(|| context.library.device_font()) + .or_else(|| { + context + .library + .library_for_movie(self.movie().unwrap()) + .unwrap() + .device_font() + }) { let mut start = 0; let mut chunks = vec![]; @@ -364,6 +397,7 @@ impl<'gc> EditText<'gc> { chunks.push(&edit_text.text[start..]); let height = static_data + .text .height .map(|v| v.to_pixels() as f32) .unwrap_or_else(|| font.scale()); @@ -372,7 +406,7 @@ impl<'gc> EditText<'gc> { let chunk_size = font.measure(chunk, height); size.0 = size.0.max(chunk_size.0); - if let Some(layout) = &static_data.layout { + if let Some(layout) = &static_data.text.layout { size.1 += layout.leading.to_pixels() as f32; } size.1 += chunk_size.1; @@ -387,7 +421,11 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { impl_display_object!(base); fn id(&self) -> CharacterId { - self.0.read().static_data.0.id + self.0.read().static_data.text.id + } + + fn movie(&self) -> Option> { + Some(self.0.read().static_data.swf.clone()) } fn run_frame(&mut self, _context: &mut UpdateContext) { @@ -424,7 +462,7 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { } fn self_bounds(&self) -> BoundingBox { - self.0.read().static_data.0.bounds.clone().into() + self.0.read().static_data.text.bounds.clone().into() } fn render(&self, context: &mut RenderContext<'_, 'gc>) { @@ -433,20 +471,24 @@ impl<'gc> TDisplayObject<'gc> for EditText<'gc> { let mut text_transform = self.text_transform(); let edit_text = self.0.read(); - let static_data = &edit_text.static_data.0; - let font_id = static_data.font_id.unwrap_or(0); + let static_data = &edit_text.static_data; + let font_id = static_data.text.font_id.unwrap_or(0); // If the font can't be found or has no glyph information, use the "device font" instead. // We're cheating a bit and not actually rendering text using the OS/web. // Instead, we embed an SWF version of Noto Sans to use as the "device font", and render // it the same as any other SWF outline text. - if let Some(font) = context + let library = context .library + .library_for_movie(edit_text.static_data.swf.clone()) + .unwrap(); + if let Some(font) = library .get_font(font_id) .filter(|font| font.has_glyphs()) - .or_else(|| context.library.device_font()) + .or_else(|| library.device_font()) { let height = static_data + .text .height .map(|v| v.to_pixels() as f32) .unwrap_or_else(|| font.scale()); @@ -503,7 +545,10 @@ unsafe impl<'gc> gc_arena::Collect for EditTextData<'gc> { /// Static data shared between all instances of a text object. #[allow(dead_code)] #[derive(Debug, Clone)] -struct EditTextStatic(swf::EditText); +struct EditTextStatic { + swf: Arc, + text: swf::EditText, +} unsafe impl<'gc> gc_arena::Collect for EditTextStatic { #[inline] diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index f7905942f..9552c7d6a 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -301,10 +301,6 @@ impl<'gc> MovieClip<'gc> { actions.into_iter() } - - pub fn movie(self) -> Arc { - self.0.read().movie() - } } impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { @@ -314,6 +310,10 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { self.0.read().id() } + fn movie(&self) -> Option> { + Some(self.0.read().movie()) + } + fn run_frame(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) { // Children must run first. for mut child in self.children() { @@ -595,10 +595,10 @@ impl<'gc> MovieClipData<'gc> { place_object: &swf::PlaceObject, copy_previous_properties: bool, ) -> Option> { - if let Ok(mut child) = - context - .library - .instantiate_by_id(id, context.gc_context, &context.system_prototypes) + if let Ok(mut child) = context + .library + .library_for_movie_mut(self.movie()) + .instantiate_by_id(id, context.gc_context, &context.system_prototypes) { // Remove previous child from children list, // and add new childonto front of the list. @@ -1095,6 +1095,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(define_bits_lossless.id, Character::Bitmap(bitmap)); Ok(()) } @@ -1125,6 +1126,7 @@ impl<'gc, 'a> MovieClipData<'gc> { let graphic = Graphic::from_swf_tag(context, &swf_shape); context .library + .library_for_movie_mut(self.movie()) .register_character(swf_shape.id, Character::Graphic(graphic)); Ok(()) } @@ -1232,10 +1234,14 @@ impl<'gc, 'a> MovieClipData<'gc> { .get_mut() .take(data_len as u64) .read_to_end(&mut jpeg_data)?; - let bitmap_info = + let bitmap_info = context.renderer.register_bitmap_jpeg( + id, + &jpeg_data, context - .renderer - .register_bitmap_jpeg(id, &jpeg_data, context.library.jpeg_tables()); + .library + .library_for_movie_mut(self.movie()) + .jpeg_tables(), + ); let bitmap = crate::display_object::Bitmap::new( context, id, @@ -1245,6 +1251,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::Bitmap(bitmap)); Ok(()) } @@ -1274,6 +1281,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::Bitmap(bitmap)); Ok(()) } @@ -1311,6 +1319,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::Bitmap(bitmap)); Ok(()) } @@ -1349,6 +1358,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::Bitmap(bitmap)); Ok(()) } @@ -1368,6 +1378,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(swf_button.id, Character::Button(button)); Ok(()) } @@ -1387,6 +1398,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(swf_button.id, Character::Button(button)); Ok(()) } @@ -1399,7 +1411,11 @@ impl<'gc, 'a> MovieClipData<'gc> { tag_len: usize, ) -> DecodeResult { let button_colors = reader.read_define_button_cxform(tag_len)?; - if let Some(button) = context.library.get_character_by_id(button_colors.id) { + if let Some(button) = context + .library + .library_for_movie_mut(self.movie()) + .get_character_by_id(button_colors.id) + { if let Character::Button(button) = button { button.set_colors(context.gc_context, &button_colors.color_transforms[..]); } else { @@ -1424,7 +1440,11 @@ impl<'gc, 'a> MovieClipData<'gc> { reader: &mut SwfStream<&'a [u8]>, ) -> DecodeResult { let button_sounds = reader.read_define_button_sound()?; - if let Some(button) = context.library.get_character_by_id(button_sounds.id) { + if let Some(button) = context + .library + .library_for_movie_mut(self.movie()) + .get_character_by_id(button_sounds.id) + { if let Character::Button(button) = button { button.set_sounds(context.gc_context, button_sounds); } else { @@ -1450,9 +1470,10 @@ impl<'gc, 'a> MovieClipData<'gc> { reader: &mut SwfStream<&'a [u8]>, ) -> DecodeResult { let swf_edit_text = reader.read_define_edit_text()?; - let edit_text = EditText::from_swf_tag(context, swf_edit_text); + let edit_text = EditText::from_swf_tag(context, self.movie(), swf_edit_text); context .library + .library_for_movie_mut(self.movie()) .register_character(edit_text.id(), Character::EditText(edit_text)); Ok(()) } @@ -1491,6 +1512,7 @@ impl<'gc, 'a> MovieClipData<'gc> { let font_object = Font::from_swf_tag(context.gc_context, context.renderer, &font).unwrap(); context .library + .library_for_movie_mut(self.movie()) .register_character(font.id, Character::Font(font_object)); Ok(()) } @@ -1505,6 +1527,7 @@ impl<'gc, 'a> MovieClipData<'gc> { let font_object = Font::from_swf_tag(context.gc_context, context.renderer, &font).unwrap(); context .library + .library_for_movie_mut(self.movie()) .register_character(font.id, Character::Font(font_object)); Ok(()) } @@ -1519,6 +1542,7 @@ impl<'gc, 'a> MovieClipData<'gc> { let font_object = Font::from_swf_tag(context.gc_context, context.renderer, &font).unwrap(); context .library + .library_for_movie_mut(self.movie()) .register_character(font.id, Character::Font(font_object)); Ok(()) @@ -1541,6 +1565,7 @@ impl<'gc, 'a> MovieClipData<'gc> { let handle = context.audio.register_sound(&sound).unwrap(); context .library + .library_for_movie_mut(self.movie()) .register_character(sound.id, Character::Sound(handle)); Ok(()) } @@ -1573,6 +1598,7 @@ impl<'gc, 'a> MovieClipData<'gc> { context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::MovieClip(movie_clip)); Ok(()) @@ -1586,9 +1612,10 @@ impl<'gc, 'a> MovieClipData<'gc> { version: u8, ) -> DecodeResult { let text = reader.read_define_text(version)?; - let text_object = Text::from_swf_tag(context, &text); + let text_object = Text::from_swf_tag(context, self.movie(), &text); context .library + .library_for_movie_mut(self.movie()) .register_character(text.id, Character::Text(text_object)); Ok(()) } @@ -1601,7 +1628,10 @@ impl<'gc, 'a> MovieClipData<'gc> { ) -> DecodeResult { let exports = reader.read_export_assets()?; for export in exports { - context.library.register_export(export.id, &export.name); + context + .library + .library_for_movie_mut(self.movie()) + .register_export(export.id, &export.name); } Ok(()) } @@ -1642,7 +1672,10 @@ impl<'gc, 'a> MovieClipData<'gc> { .get_mut() .take(tag_len as u64) .read_to_end(&mut jpeg_data)?; - context.library.set_jpeg_tables(jpeg_data); + context + .library + .library_for_movie_mut(self.movie()) + .set_jpeg_tables(jpeg_data); Ok(()) } @@ -1852,7 +1885,11 @@ impl<'gc, 'a> MovieClipData<'gc> { reader: &mut SwfStream<&'a [u8]>, ) -> DecodeResult { let start_sound = reader.read_start_sound_1()?; - if let Some(handle) = context.library.get_sound(start_sound.id) { + if let Some(handle) = context + .library + .library_for_movie_mut(self.movie()) + .get_sound(start_sound.id) + { use swf::SoundEvent; // The sound event type is controlled by the "Sync" setting in the Flash IDE. match start_sound.sound_info.event { diff --git a/core/src/display_object/text.rs b/core/src/display_object/text.rs index 92f183cc2..6b1009f43 100644 --- a/core/src/display_object/text.rs +++ b/core/src/display_object/text.rs @@ -1,8 +1,10 @@ use crate::context::{RenderContext, UpdateContext}; use crate::display_object::{DisplayObjectBase, TDisplayObject}; use crate::prelude::*; +use crate::tag_utils::SwfMovie; use crate::transform::Transform; use gc_arena::{Collect, GcCell}; +use std::sync::Arc; #[derive(Clone, Debug, Collect, Copy)] #[collect(no_drop)] @@ -15,7 +17,11 @@ pub struct TextData<'gc> { } impl<'gc> Text<'gc> { - pub fn from_swf_tag(context: &mut UpdateContext<'_, 'gc, '_>, tag: &swf::Text) -> Self { + pub fn from_swf_tag( + context: &mut UpdateContext<'_, 'gc, '_>, + swf: Arc, + tag: &swf::Text, + ) -> Self { Text(GcCell::allocate( context.gc_context, TextData { @@ -23,6 +29,7 @@ impl<'gc> Text<'gc> { static_data: gc_arena::Gc::allocate( context.gc_context, TextStatic { + swf, id: tag.id, text_transform: tag.matrix.clone().into(), text_blocks: tag.records.clone(), @@ -40,6 +47,10 @@ impl<'gc> TDisplayObject<'gc> for Text<'gc> { self.0.read().static_data.id } + fn movie(&self) -> Option> { + Some(self.0.read().static_data.swf.clone()) + } + fn run_frame(&mut self, _context: &mut UpdateContext) { // Noop } @@ -71,7 +82,12 @@ impl<'gc> TDisplayObject<'gc> for Text<'gc> { color = block.color.as_ref().unwrap_or(&color).clone(); font_id = block.font_id.unwrap_or(font_id); height = block.height.map(|h| h.get() as f32).unwrap_or(height); - if let Some(font) = context.library.get_font(font_id) { + if let Some(font) = context + .library + .library_for_movie(self.movie().unwrap()) + .unwrap() + .get_font(font_id) + { let scale = height / font.scale(); transform.matrix.a = scale; transform.matrix.d = scale; @@ -108,6 +124,7 @@ unsafe impl<'gc> gc_arena::Collect for TextData<'gc> { #[allow(dead_code)] #[derive(Debug, Clone)] struct TextStatic { + swf: Arc, id: CharacterId, text_transform: Matrix, text_blocks: Vec, diff --git a/core/src/library.rs b/core/src/library.rs index dcacac487..6e1a1a43e 100644 --- a/core/src/library.rs +++ b/core/src/library.rs @@ -5,20 +5,24 @@ use crate::character::Character; use crate::display_object::TDisplayObject; use crate::font::Font; use crate::prelude::*; +use crate::tag_utils::SwfMovie; use gc_arena::MutationContext; use std::collections::HashMap; +use std::sync::{Arc, Weak}; use swf::CharacterId; +use weak_table::PtrWeakKeyHashMap; -pub struct Library<'gc> { +/// Symbol library for a single given SWF. +pub struct MovieLibrary<'gc> { characters: HashMap>, export_characters: HashMap>, jpeg_tables: Option>, device_font: Option>, } -impl<'gc> Library<'gc> { +impl<'gc> MovieLibrary<'gc> { pub fn new() -> Self { - Library { + MovieLibrary { characters: HashMap::new(), export_characters: HashMap::new(), jpeg_tables: None, @@ -181,7 +185,7 @@ impl<'gc> Library<'gc> { } } -unsafe impl<'gc> gc_arena::Collect for Library<'gc> { +unsafe impl<'gc> gc_arena::Collect for MovieLibrary<'gc> { #[inline] fn trace(&self, cc: gc_arena::CollectionContext) { for character in self.characters.values() { @@ -191,8 +195,46 @@ unsafe impl<'gc> gc_arena::Collect for Library<'gc> { } } -impl Default for Library<'_> { +impl Default for MovieLibrary<'_> { fn default() -> Self { Self::new() } } + +/// Symbol library for multiple movies. +pub struct Library<'gc> { + /// All the movie libraries. + movie_libraries: PtrWeakKeyHashMap, MovieLibrary<'gc>>, +} + +unsafe impl<'gc> gc_arena::Collect for Library<'gc> { + #[inline] + fn trace(&self, cc: gc_arena::CollectionContext) { + for (_, val) in self.movie_libraries.iter() { + val.trace(cc); + } + } +} + +impl<'gc> Library<'gc> { + pub fn library_for_movie(&self, movie: Arc) -> Option<&MovieLibrary<'gc>> { + self.movie_libraries.get(&movie) + } + + pub fn library_for_movie_mut(&mut self, movie: Arc) -> &mut MovieLibrary<'gc> { + if !self.movie_libraries.contains_key(&movie) { + self.movie_libraries + .insert(movie.clone(), MovieLibrary::default()); + }; + + self.movie_libraries.get_mut(&movie).unwrap() + } +} + +impl<'gc> Default for Library<'gc> { + fn default() -> Self { + Self { + movie_libraries: PtrWeakKeyHashMap::new(), + } + } +} diff --git a/core/src/player.rs b/core/src/player.rs index 20aa71b03..b3d5135fe 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -174,8 +174,11 @@ impl Player { } }; - let mut library = Library::new(); - library.set_device_font(device_font); + let mut library = Library::default(); + library + .library_for_movie_mut(movie.clone()) + .set_device_font(device_font); + GcRoot(GcCell::allocate( gc_context, GcRootData { @@ -521,6 +524,7 @@ impl Player { let morph_shape = MorphShape::new(context.gc_context, static_data); context .library + .library_for_movie_mut(root.as_movie_clip().unwrap().movie().unwrap()) .register_character(id, crate::character::Character::MorphShape(morph_shape)); } }); From f4e4171ebe848bfcd846c480a4bb82a08c74e24d Mon Sep 17 00:00:00 2001 From: David Wendt Date: Thu, 14 Nov 2019 14:52:13 -0500 Subject: [PATCH 14/59] Make spawned futures falliable, and report those errors. --- core/src/avm1.rs | 14 ++++++++++---- core/src/backend/navigator.rs | 8 ++++++-- desktop/src/navigator.rs | 5 ++++- web/src/navigator.rs | 8 ++++++-- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 918db73c0..ca8971d5a 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1638,7 +1638,7 @@ impl<'gc> Avm1<'gc> { let slot = self.forcibly_root_object(context.layers[level_id].object().as_object()?); context.navigator.spawn_future(Box::pin(async move { - let data = fetch.await.unwrap(); + let data = fetch.await?; let movie = Arc::new(SwfMovie::from_data(&data)); player.lock().unwrap().update(|avm, uc| { @@ -1660,6 +1660,8 @@ impl<'gc> Avm1<'gc> { crate::character::Character::MorphShape(morph_shape), ); } + + Ok(()) }) })); @@ -1713,14 +1715,16 @@ impl<'gc> Avm1<'gc> { let slot = self.forcibly_root_object(target_obj); context.navigator.spawn_future(Box::pin(async move { - let data = fetch.await.unwrap(); + let data = fetch.await?; player.lock().unwrap().update(|avm, uc| { let that = avm.unroot_object(slot); for (k, v) in form_urlencoded::parse(&data) { - that.set(&k, v.into_owned().into(), avm, uc); + that.set(&k, v.into_owned().into(), avm, uc)?; } + + Ok(()) }) })); } @@ -1733,7 +1737,7 @@ impl<'gc> Avm1<'gc> { let slot = self.forcibly_root_object(clip_target.object().as_object()?); context.navigator.spawn_future(Box::pin(async move { - let data = fetch.await.unwrap(); + let data = fetch.await?; let movie = Arc::new(SwfMovie::from_data(&data)); player.lock().unwrap().update(|avm, uc| { @@ -1755,6 +1759,8 @@ impl<'gc> Avm1<'gc> { crate::character::Character::MorphShape(morph_shape), ); } + + Ok(()) }) })) } diff --git a/core/src/backend/navigator.rs b/core/src/backend/navigator.rs index 7aa5ee2fb..5fbe86367 100644 --- a/core/src/backend/navigator.rs +++ b/core/src/backend/navigator.rs @@ -69,7 +69,7 @@ pub trait NavigatorBackend { /// /// TODO: For some reason, `wasm_bindgen_futures` wants unpinnable futures. /// This seems highly limiting. - fn spawn_future(&mut self, future: Pin + 'static>>); + fn spawn_future(&mut self, future: Pin> + 'static>>); } /// A null implementation for platforms that do not live in a web browser. @@ -100,5 +100,9 @@ impl NavigatorBackend for NullNavigatorBackend { Box::pin(async { Err("Fetch IO not implemented".into()) }) } - fn spawn_future(&mut self, _future: Pin + 'static>>) {} + fn spawn_future( + &mut self, + _future: Pin> + 'static>>, + ) { + } } diff --git a/desktop/src/navigator.rs b/desktop/src/navigator.rs index d8d6643e9..eedcae2ba 100644 --- a/desktop/src/navigator.rs +++ b/desktop/src/navigator.rs @@ -67,7 +67,10 @@ impl NavigatorBackend for ExternalNavigatorBackend { Box::pin(async { Err("Fetch not implemented on desktop!".into()) }) } - fn spawn_future(&mut self, _future: Pin + 'static>>) { + fn spawn_future( + &mut self, + _future: Pin> + 'static>>, + ) { unimplemented!(); } } diff --git a/web/src/navigator.rs b/web/src/navigator.rs index 1ac743c8d..2bc6fe8d6 100644 --- a/web/src/navigator.rs +++ b/web/src/navigator.rs @@ -99,7 +99,11 @@ impl NavigatorBackend for WebNavigatorBackend { }) } - fn spawn_future(&mut self, future: Pin + 'static>>) { - spawn_local(future) + fn spawn_future(&mut self, future: Pin> + 'static>>) { + spawn_local(async move { + if let Err(e) = future.await { + log::error!("Asynchronous error occured: {}", e); + } + }) } } From 2a82d21966af081be1b9bc2b461b1530ba8183fe Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 1 Dec 2019 21:00:01 -0500 Subject: [PATCH 15/59] Change the layer list from a static array to a `BTreeMap`. Flash allows creating layers at any 31-bit height without issue, so this should support similar limitations. --- core/src/avm1.rs | 63 ++++++++++++++++++++++------------ core/src/avm1/script_object.rs | 5 ++- core/src/avm1/test_utils.rs | 5 ++- core/src/context.rs | 5 +-- core/src/player.rs | 48 ++++++++++++++++---------- 5 files changed, 81 insertions(+), 45 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index ca8971d5a..e3bb9c71c 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -14,7 +14,8 @@ use url::form_urlencoded; use swf::avm1::read::Reader; use swf::avm1::types::{Action, Function}; -use crate::display_object::{DisplayObject, MorphShape}; +use crate::display_object::{DisplayObject, MorphShape, MovieClip}; +use crate::player::NEWEST_PLAYER_VERSION; use crate::tag_utils::{SwfMovie, SwfSlice}; #[cfg(test)] @@ -1632,37 +1633,55 @@ impl<'gc> Avm1<'gc> { target: &str, ) -> Result<(), Error> { if target.starts_with("_level") && target.len() > 6 { - let level_id = target[6..].parse::().unwrap(); + let url = url.to_string(); + let level_id = target[6..].parse::()?; let player = context.player.clone().unwrap().upgrade().unwrap(); - let fetch = context.navigator.fetch(url.to_string()); - let slot = self.forcibly_root_object(context.layers[level_id].object().as_object()?); + let fetch = context.navigator.fetch(url); + let layer = if let Some(layer) = context.layers.get(&level_id) { + *layer + } else { + let mut layer: DisplayObject<'_> = + MovieClip::new(NEWEST_PLAYER_VERSION, context.gc_context).into(); + + layer.post_instantiation(context.gc_context, layer, self.prototypes.movie_clip); + context.layers.insert(level_id, layer); + + layer + }; + let slot = self.forcibly_root_object(layer.object().as_object()?); context.navigator.spawn_future(Box::pin(async move { let data = fetch.await?; let movie = Arc::new(SwfMovie::from_data(&data)); - player.lock().unwrap().update(|avm, uc| { - let that = avm.unroot_object(slot).as_display_object().unwrap(); - let mut mc = that.as_movie_clip().unwrap(); + player + .lock() + .expect("Could not lock player!!") + .update(|avm, uc| { + let that = avm + .unroot_object(slot) + .as_display_object() + .expect("Locked object not a display node!"); + let mut mc = that.as_movie_clip().expect("Locked clip not a clip!"); - mc.replace_with_movie(uc.gc_context, movie.clone()); + mc.replace_with_movie(uc.gc_context, movie.clone()); - let mut morph_shapes = fnv::FnvHashMap::default(); - mc.preload(uc, &mut morph_shapes); + let mut morph_shapes = fnv::FnvHashMap::default(); + mc.preload(uc, &mut morph_shapes); - // Finalize morph shapes. - for (id, static_data) in morph_shapes { - let morph_shape = MorphShape::new(uc.gc_context, static_data); - uc.library - .library_for_movie_mut(movie.clone()) - .register_character( - id, - crate::character::Character::MorphShape(morph_shape), - ); - } + // Finalize morph shapes. + for (id, static_data) in morph_shapes { + let morph_shape = MorphShape::new(uc.gc_context, static_data); + uc.library + .library_for_movie_mut(movie.clone()) + .register_character( + id, + crate::character::Character::MorphShape(morph_shape), + ); + } - Ok(()) - }) + Ok(()) + }) })); return Ok(()); diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index ffb495878..ee4679137 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -575,6 +575,7 @@ mod tests { use crate::tag_utils::SwfMovie; use gc_arena::rootless_arena; use rand::{rngs::SmallRng, SeedableRng}; + use std::collections::BTreeMap; use std::sync::Arc; fn with_object(swf_version: u8, test: F) -> R @@ -586,7 +587,9 @@ mod tests { let swf = Arc::new(SwfMovie::empty(swf_version)); let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); - let mut layers = [root; 9]; + let mut layers = BTreeMap::new(); + layers.insert(0, root); + let mut context = UpdateContext { gc_context, global_time: 0, diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index 5456f1983..ea57f12da 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -11,6 +11,7 @@ use crate::prelude::*; use crate::tag_utils::SwfMovie; use gc_arena::{rootless_arena, GcCell, MutationContext}; use rand::{rngs::SmallRng, SeedableRng}; +use std::collections::BTreeMap; use std::sync::Arc; pub fn with_avm(swf_version: u8, test: F) -> R @@ -25,7 +26,9 @@ where let swf = Arc::new(SwfMovie::empty(swf_version)); let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); - let mut layers = [root; 9]; + let mut layers = BTreeMap::new(); + layers.insert(0, root); + let mut context = UpdateContext { gc_context, global_time: 0, diff --git a/core/src/context.rs b/core/src/context.rs index cd540b71e..0cb8106b8 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -13,6 +13,7 @@ use crate::transform::TransformStack; use core::fmt; use gc_arena::{Collect, MutationContext}; use rand::rngs::SmallRng; +use std::collections::BTreeMap; use std::sync::{Arc, Mutex, Weak}; /// `UpdateContext` holds shared data that is used by the various subsystems of Ruffle. @@ -63,8 +64,8 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { /// The RNG, used by the AVM `RandomNumber` opcode, `Math.random(),` and `random()`. pub rng: &'a mut SmallRng, - /// All nine layers of the current player. - pub layers: &'a mut [DisplayObject<'gc>; 9], + /// All loaded layers of the current player. + pub layers: &'a mut BTreeMap>, /// The root of the current timeline being updated. /// This will always be one of the layers in `layers`. diff --git a/core/src/player.rs b/core/src/player.rs index b3d5135fe..7430d262b 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -14,6 +14,7 @@ use crate::transform::TransformStack; use gc_arena::{make_arena, ArenaParameters, Collect, GcCell}; use log::info; use rand::{rngs::SmallRng, SeedableRng}; +use std::collections::BTreeMap; use std::convert::TryFrom; use std::ops::DerefMut; use std::sync::{Arc, Mutex, Weak}; @@ -32,7 +33,11 @@ struct GcRoot<'gc>(GcCell<'gc, GcRootData<'gc>>); #[collect(no_drop)] struct GcRootData<'gc> { library: Library<'gc>, - layers: [DisplayObject<'gc>; 9], + + /// The list of layers on the current stage. + /// + /// Each layer is a `_root` MovieClip that holds a particular SWF movie. + layers: BTreeMap>, mouse_hovered_object: Option>, // TODO: Remove GcCell wrapped inside GcCell. /// The object being dragged via a `startDrag` action. @@ -48,7 +53,7 @@ impl<'gc> GcRootData<'gc> { fn update_context_params( &mut self, ) -> ( - &mut [DisplayObject<'gc>; 9], + &mut BTreeMap>, &mut Library<'gc>, &mut ActionQueue<'gc>, &mut Avm1<'gc>, @@ -175,6 +180,10 @@ impl Player { }; let mut library = Library::default(); + let root = MovieClip::from_movie(gc_context, movie.clone()).into(); + let mut layers = BTreeMap::new(); + layers.insert(0, root); + library .library_for_movie_mut(movie.clone()) .set_device_font(device_font); @@ -183,17 +192,7 @@ impl Player { gc_context, GcRootData { library, - layers: [ - MovieClip::from_movie(gc_context, movie.clone()).into(), - MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), - MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), - MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), - MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), - MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), - MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), - MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), - MovieClip::new(NEWEST_PLAYER_VERSION, gc_context).into(), - ], + layers, mouse_hovered_object: None, drag_object: None, avm: Avm1::new(gc_context, NEWEST_PLAYER_VERSION), @@ -226,7 +225,7 @@ impl Player { let mut root_data = gc_root.0.write(gc_context); let mc_proto = root_data.avm.prototypes().movie_clip; - for (i, layer) in root_data.layers.iter_mut().enumerate() { + for (i, layer) in root_data.layers.iter_mut() { layer.post_instantiation(gc_context, *layer, mc_proto); layer.set_name(gc_context, &format!("_level{}", i)) } @@ -472,7 +471,7 @@ impl Player { self.mutate_with_update_context(|avm, context| { // Check hovered object. let mut new_hovered = None; - for layer in context.layers.iter().rev() { + for (_depth, layer) in context.layers.iter().rev() { if new_hovered.is_none() { new_hovered = layer.mouse_pick(*layer, (mouse_pos.0, mouse_pos.1)); } else { @@ -533,8 +532,17 @@ impl Player { pub fn run_frame(&mut self) { self.update(|_avm, update_context| { // TODO: In what order are layers run? - for layer in update_context.layers.clone().iter_mut() { - update_context.root = *layer; + // NOTE: We have to copy all the layer pointers into a separate list + // because layer updates can create more layers, which we don't + // want to run frames on + let mut layers = vec![]; + + for (_depth, layer) in update_context.layers.iter() { + layers.push(*layer); + } + + for mut layer in layers { + update_context.root = layer; layer.run_frame(update_context); } @@ -570,7 +578,7 @@ impl Player { clip_depth_stack: vec![], }; - for layer in &root_data.layers { + for (_depth, layer) in root_data.layers.iter() { layer.render(&mut render_context); } }); @@ -754,6 +762,8 @@ impl Player { let mouse_hovered_object = root_data.mouse_hovered_object; let (layers, library, action_queue, avm, drag_object) = root_data.update_context_params(); + let layer0 = layers.get(&0).expect("Layer 0 should always exist"); + let mut update_context = UpdateContext { player_version, global_time, @@ -767,7 +777,7 @@ impl Player { input, action_queue, gc_context, - root: layers[0], + root: *layer0, layers, mouse_hovered_object, mouse_position, From 6b78ec6e033d6ae42c2d5aa1afa77eec7a6cb876 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 1 Dec 2019 21:03:42 -0500 Subject: [PATCH 16/59] More thoroughly clean `MovieClip`s that have movies loaded into them. This does everything except wipe the object it's attached to - I need further testing to see how Flash handles that. --- core/src/avm1.rs | 1 + core/src/display_object/movie_clip.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index e3bb9c71c..f5259e348 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1665,6 +1665,7 @@ impl<'gc> Avm1<'gc> { let mut mc = that.as_movie_clip().expect("Locked clip not a clip!"); mc.replace_with_movie(uc.gc_context, movie.clone()); + mc.post_instantiation(uc.gc_context, that, avm.prototypes.movie_clip); let mut morph_shapes = fnv::FnvHashMap::default(); mc.preload(uc, &mut morph_shapes); diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 9552c7d6a..130a0ef01 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -433,10 +433,11 @@ impl<'gc> MovieClipData<'gc> { ) { let total_frames = movie.header().num_frames; + self.base = Default::default(); self.static_data = Gc::allocate( gc_context, MovieClipStatic { - id: self.static_data.id, //TODO: This is WRONG; This is VERRRRY WRONG! + id: 0, swf: movie.into(), total_frames, audio_stream_info: None, @@ -447,6 +448,7 @@ impl<'gc> MovieClipData<'gc> { self.flags = EnumSet::empty(); self.current_frame = 0; self.audio_stream = None; + self.children = BTreeMap::new(); } fn id(&self) -> CharacterId { From 2d7a4fef285d1aefdeac2056c1a7aa2a05a76a67 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 5 Jan 2020 14:54:31 -0700 Subject: [PATCH 17/59] `Sound` methods that reference sounds by library export name should default to `_layer0`'s library if the `Sound` was created without a movie clip target. --- core/src/avm1/globals/sound.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/avm1/globals/sound.rs b/core/src/avm1/globals/sound.rs index 5da6fe8ac..2fccc38c5 100644 --- a/core/src/avm1/globals/sound.rs +++ b/core/src/avm1/globals/sound.rs @@ -168,7 +168,10 @@ fn attach_sound<'gc>( let name = args.get(0).unwrap_or(&Value::Undefined); if let Some(sound_object) = this.as_sound_object() { let name = name.clone().coerce_to_string(avm, context)?; - let movie = sound_object.owner().and_then(|o| o.movie()); + let movie = sound_object + .owner() + .or_else(|| context.layers.get(&0).copied()) + .and_then(|o| o.movie()); if let Some(movie) = movie { if let Some(Character::Sound(sound)) = context .library @@ -408,7 +411,10 @@ fn stop<'gc>( if let Some(name) = args.get(0) { // Usage 1: Stop all instances of a particular sound, using the name parameter. let name = name.clone().coerce_to_string(avm, context)?; - let movie = sound.owner().and_then(|o| o.movie()); + let movie = sound + .owner() + .or_else(|| context.layers.get(&0).copied()) + .and_then(|o| o.movie()); if let Some(movie) = movie { if let Some(Character::Sound(sound)) = context .library From d84e11ed404f9b12c3722629a66dcae872428c64 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 10 Jan 2020 18:28:49 -0500 Subject: [PATCH 18/59] Introduce a new top-level object, `LoadManager`, which is responsible for all asynchronous behavior in the program. Migrate the existing async impls to it. --- core/src/avm1.rs | 145 +++----------------- core/src/avm1/script_object.rs | 2 + core/src/avm1/test_utils.rs | 2 + core/src/context.rs | 7 + core/src/lib.rs | 1 + core/src/loader.rs | 237 +++++++++++++++++++++++++++++++++ core/src/player.rs | 11 +- 7 files changed, 279 insertions(+), 126 deletions(-) create mode 100644 core/src/loader.rs diff --git a/core/src/avm1.rs b/core/src/avm1.rs index f5259e348..461306bbf 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -8,15 +8,13 @@ use gc_arena::{GcCell, MutationContext}; use rand::Rng; use std::collections::HashMap; use std::convert::TryInto; -use std::sync::Arc; -use url::form_urlencoded; use swf::avm1::read::Reader; use swf::avm1::types::{Action, Function}; -use crate::display_object::{DisplayObject, MorphShape, MovieClip}; +use crate::display_object::{DisplayObject, MovieClip}; use crate::player::NEWEST_PLAYER_VERSION; -use crate::tag_utils::{SwfMovie, SwfSlice}; +use crate::tag_utils::SwfSlice; #[cfg(test)] #[macro_use] @@ -92,10 +90,6 @@ pub struct Avm1<'gc> { /// The register slots (also shared across functions). /// `ActionDefineFunction2` defined functions do not use these slots. registers: [Value<'gc>; 4], - - /// Represents externally-held references to objects currently in use by - /// futures that can't actually hold `'gc` pointers. - external_references: Vec>>, } unsafe impl<'gc> gc_arena::Collect for Avm1<'gc> { @@ -136,45 +130,9 @@ impl<'gc> Avm1<'gc> { Value::Undefined, Value::Undefined, ], - external_references: vec![], } } - /// Force the AVM to store a reference to an object that can be retrieved - /// later by ID. - /// - /// This is intended for use by async code which will need to run between - /// GC passes and for various architectural references cannot hold onto any - /// actual GC pointers. - /// - /// Note that this function is the moral equivalent of moving an object - /// from garbage collection into reference counting. Take care to kill - /// roots as soon as possible and avoid creating cycles. - pub fn forcibly_root_object(&mut self, ob: Object<'gc>) -> usize { - if let Some(pos) = self.external_references.iter().position(|&r| r.is_none()) { - *self.external_references.get_mut(pos).unwrap() = Some(ob); - pos - } else { - self.external_references.push(Some(ob)); - self.external_references.len() - 1 - } - } - - /// Get back a previously rooted object. - /// - /// After retrieving the rooted object, the ID you have given will no - /// longer be valid and may refer to a different object or no object at - /// all. - /// - /// Attempting to unroot an object ID that does not exist will panic. - pub fn unroot_object(&mut self, id: usize) -> Object<'gc> { - let root = self.external_references.get(id).and_then(|v| *v).unwrap(); - - *self.external_references.get_mut(id).unwrap() = None; - - root - } - #[allow(dead_code)] pub fn base_clip(&self) -> DisplayObject<'gc> { self.current_stack_frame().unwrap().read().base_clip() @@ -1635,7 +1593,6 @@ impl<'gc> Avm1<'gc> { if target.starts_with("_level") && target.len() > 6 { let url = url.to_string(); let level_id = target[6..].parse::()?; - let player = context.player.clone().unwrap().upgrade().unwrap(); let fetch = context.navigator.fetch(url); let layer = if let Some(layer) = context.layers.get(&level_id) { *layer @@ -1648,42 +1605,13 @@ impl<'gc> Avm1<'gc> { layer }; - let slot = self.forcibly_root_object(layer.object().as_object()?); - context.navigator.spawn_future(Box::pin(async move { - let data = fetch.await?; - let movie = Arc::new(SwfMovie::from_data(&data)); - - player - .lock() - .expect("Could not lock player!!") - .update(|avm, uc| { - let that = avm - .unroot_object(slot) - .as_display_object() - .expect("Locked object not a display node!"); - let mut mc = that.as_movie_clip().expect("Locked clip not a clip!"); - - mc.replace_with_movie(uc.gc_context, movie.clone()); - mc.post_instantiation(uc.gc_context, that, avm.prototypes.movie_clip); - - let mut morph_shapes = fnv::FnvHashMap::default(); - mc.preload(uc, &mut morph_shapes); - - // Finalize morph shapes. - for (id, static_data) in morph_shapes { - let morph_shape = MorphShape::new(uc.gc_context, static_data); - uc.library - .library_for_movie_mut(movie.clone()) - .register_character( - id, - crate::character::Character::MorphShape(morph_shape), - ); - } - - Ok(()) - }) - })); + let process = context.load_manager.load_movie_into_clip( + context.player.clone().unwrap(), + layer, + fetch, + ); + context.navigator.spawn_future(process); return Ok(()); } @@ -1730,59 +1658,26 @@ impl<'gc> Avm1<'gc> { .object() .as_object() .unwrap(); - let player = context.player.clone().unwrap().upgrade().unwrap(); let fetch = context.navigator.fetch(url); - let slot = self.forcibly_root_object(target_obj); + let process = context.load_manager.load_form_into_object( + context.player.clone().unwrap(), + target_obj, + fetch, + ); - context.navigator.spawn_future(Box::pin(async move { - let data = fetch.await?; - - player.lock().unwrap().update(|avm, uc| { - let that = avm.unroot_object(slot); - - for (k, v) in form_urlencoded::parse(&data) { - that.set(&k, v.into_owned().into(), avm, uc)?; - } - - Ok(()) - }) - })); + context.navigator.spawn_future(process); } return Ok(()); } else if is_target_sprite { if let Some(clip_target) = clip_target { - let player = context.player.clone().unwrap().upgrade().unwrap(); let fetch = context.navigator.fetch(url); - let slot = self.forcibly_root_object(clip_target.object().as_object()?); - - context.navigator.spawn_future(Box::pin(async move { - let data = fetch.await?; - let movie = Arc::new(SwfMovie::from_data(&data)); - - player.lock().unwrap().update(|avm, uc| { - let that = avm.unroot_object(slot).as_display_object().unwrap(); - let mut mc = that.as_movie_clip().unwrap(); - - mc.replace_with_movie(uc.gc_context, movie.clone()); - - let mut morph_shapes = fnv::FnvHashMap::default(); - mc.preload(uc, &mut morph_shapes); - - // Finalize morph shapes. - for (id, static_data) in morph_shapes { - let morph_shape = MorphShape::new(uc.gc_context, static_data); - uc.library - .library_for_movie_mut(movie.clone()) - .register_character( - id, - crate::character::Character::MorphShape(morph_shape), - ); - } - - Ok(()) - }) - })) + let process = context.load_manager.load_movie_into_clip( + context.player.clone().unwrap(), + clip_target, + fetch, + ); + context.navigator.spawn_future(process); } return Ok(()); diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index ee4679137..0b7c1f760 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -571,6 +571,7 @@ mod tests { use crate::backend::render::NullRenderer; use crate::display_object::MovieClip; use crate::library::Library; + use crate::loader::LoadManager; use crate::prelude::*; use crate::tag_utils::SwfMovie; use gc_arena::rootless_arena; @@ -616,6 +617,7 @@ mod tests { drag_object: &mut None, stage_size: (Twips::from_pixels(550.0), Twips::from_pixels(400.0)), player: None, + load_manager: &mut LoadManager::new(), }; let object = ScriptObject::object(gc_context, Some(avm.prototypes().object)).into(); diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index ea57f12da..a5b48fcc4 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -7,6 +7,7 @@ use crate::backend::render::NullRenderer; use crate::context::ActionQueue; use crate::display_object::{MovieClip, TDisplayObject}; use crate::library::Library; +use crate::loader::LoadManager; use crate::prelude::*; use crate::tag_utils::SwfMovie; use gc_arena::{rootless_arena, GcCell, MutationContext}; @@ -55,6 +56,7 @@ where drag_object: &mut None, stage_size: (Twips::from_pixels(550.0), Twips::from_pixels(400.0)), player: None, + load_manager: &mut LoadManager::new(), }; let globals = avm.global_object_cell(); diff --git a/core/src/context.rs b/core/src/context.rs index 0cb8106b8..9c3b72be9 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -6,6 +6,7 @@ use crate::avm1::Value; use crate::backend::input::InputBackend; use crate::backend::{audio::AudioBackend, navigator::NavigatorBackend, render::RenderBackend}; use crate::library::Library; +use crate::loader::LoadManager; use crate::player::Player; use crate::prelude::*; use crate::tag_utils::{SwfMovie, SwfSlice}; @@ -92,6 +93,12 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { /// Recipients of an update context may upgrade the reference to ensure /// that the player lives across I/O boundaries. pub player: Option>>, + + /// The player's load manager. + /// + /// This is required for asynchronous behavior, such as fetching data from + /// a URL. + pub load_manager: &'a mut LoadManager<'gc>, } /// A queued ActionScript call. diff --git a/core/src/lib.rs b/core/src/lib.rs index 68d471203..148601271 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -17,6 +17,7 @@ mod context; pub mod events; mod font; mod library; +mod loader; pub mod matrix; mod player; mod prelude; diff --git a/core/src/loader.rs b/core/src/loader.rs new file mode 100644 index 000000000..bacc8b46a --- /dev/null +++ b/core/src/loader.rs @@ -0,0 +1,237 @@ +//! Management of async loaders + +use crate::avm1::{Object, TObject}; +use crate::display_object::{DisplayObject, MorphShape, TDisplayObject}; +use crate::player::Player; +use crate::tag_utils::SwfMovie; +use gc_arena::{Collect, CollectionContext}; +use generational_arena::{Arena, Index}; +use std::future::Future; +use std::pin::Pin; +use std::sync::{Arc, Mutex, Weak}; +use url::form_urlencoded; + +pub type Handle = Index; + +type Error = Box; + +/// Holds all in-progress loads for the player. +pub struct LoadManager<'gc>(Arena>); + +unsafe impl<'gc> Collect for LoadManager<'gc> { + fn trace(&self, cc: CollectionContext) { + for (_, loader) in self.0.iter() { + loader.trace(cc) + } + } +} + +impl<'gc> LoadManager<'gc> { + /// Construct a new `LoadManager`. + pub fn new() -> Self { + Self(Arena::new()) + } + + /// Add a new loader to the `LoadManager`. + /// + /// This function returns the loader handle for later inspection. A loader + /// handle is valid for as long as the load operation. Once the load + /// finishes, the handle will be invalidated (and the underlying loader + /// deleted). + pub fn add_loader(&mut self, loader: Loader<'gc>) -> Handle { + let handle = self.0.insert(loader); + self.0 + .get_mut(handle) + .unwrap() + .introduce_loader_handle(handle); + + handle + } + + /// Retrieve a loader by handle. + pub fn get_loader(&self, handle: Handle) -> Option<&Loader<'gc>> { + self.0.get(handle) + } + + /// Retrieve a loader by handle for mutation. + pub fn get_loader_mut(&mut self, handle: Handle) -> Option<&mut Loader<'gc>> { + self.0.get_mut(handle) + } + + /// Kick off a movie clip load. + /// + /// Returns the loader's async process, which you will need to spawn. + pub fn load_movie_into_clip( + &mut self, + player: Weak>, + target_clip: DisplayObject<'gc>, + fetch: Pin, Error>>>>, + ) -> Pin> + 'static>> { + let loader = Loader::Movie { + self_handle: None, + target_clip, + }; + let handle = self.add_loader(loader); + + let loader = self.get_loader_mut(handle).unwrap(); + loader.introduce_loader_handle(handle); + + loader.movie_loader(player, fetch) + } + + /// Kick off a form data load into an AVM1 object. + /// + /// Returns the loader's async process, which you will need to spawn. + pub fn load_form_into_object( + &mut self, + player: Weak>, + target_object: Object<'gc>, + fetch: Pin, Error>>>>, + ) -> Pin> + 'static>> { + let loader = Loader::Form { + self_handle: None, + target_object, + }; + let handle = self.add_loader(loader); + + let loader = self.get_loader_mut(handle).unwrap(); + loader.introduce_loader_handle(handle); + + loader.form_loader(player, fetch) + } +} + +impl<'gc> Default for LoadManager<'gc> { + fn default() -> Self { + Self::new() + } +} + +/// A struct that holds garbage-collected pointers for asynchronous code. +pub enum Loader<'gc> { + /// Loader that is loading a new movie into a movieclip. + Movie { + /// The handle to refer to this loader instance. + self_handle: Option, + + /// The target movie clip to load the movie into. + target_clip: DisplayObject<'gc>, + }, + + /// Loader that is loading form data into an AVM1 object scope. + Form { + /// The handle to refer to this loader instance. + self_handle: Option, + + /// The target AVM1 object to load form data into. + target_object: Object<'gc>, + }, +} + +impl<'gc> Loader<'gc> { + /// Set the loader handle for this loader. + /// + /// An active loader handle is required before asynchronous loader code can + /// run. + pub fn introduce_loader_handle(&mut self, handle: Handle) { + match self { + Loader::Movie { self_handle, .. } => *self_handle = Some(handle), + Loader::Form { self_handle, .. } => *self_handle = Some(handle), + } + } + + /// Construct a future for the given movie loader. + /// + /// The given future should be passed immediately to an executor; it will + /// take responsibility for running the loader to completion. + /// + /// If the loader is not a movie then the returned future will yield an + /// error immediately once spawned. + pub fn movie_loader( + &mut self, + player: Weak>, + fetch: Pin, Error>>>>, + ) -> Pin> + 'static>> { + let handle = match self { + Loader::Movie { self_handle, .. } => self_handle.expect("Loader not self-introduced"), + _ => return Box::pin(async { Err("Non-movie loader spawned as movie loader".into()) }), + }; + + let player = player + .upgrade() + .expect("Could not upgrade weak reference to player"); + + Box::pin(async move { + let data = fetch.await?; + let movie = Arc::new(SwfMovie::from_data(&data)); + + player + .lock() + .expect("Could not lock player!!") + .update(|avm, uc| { + let loader = uc.load_manager.get_loader(handle); + let that = match loader { + Some(Loader::Movie { target_clip, .. }) => target_clip, + None => return Err("Loader expired during loading".into()), + _ => return Err("Non-movie loader spawned as movie loader".into()), + }; + let mut mc = that + .as_movie_clip() + .expect("Attempted to load movie into not movie clip"); + + mc.replace_with_movie(uc.gc_context, movie.clone()); + mc.post_instantiation(uc.gc_context, *that, avm.prototypes().movie_clip); + + let mut morph_shapes = fnv::FnvHashMap::default(); + mc.preload(uc, &mut morph_shapes); + + // Finalize morph shapes. + for (id, static_data) in morph_shapes { + let morph_shape = MorphShape::new(uc.gc_context, static_data); + uc.library + .library_for_movie_mut(movie.clone()) + .register_character( + id, + crate::character::Character::MorphShape(morph_shape), + ); + } + + Ok(()) + }) + }) + } + + pub fn form_loader( + &mut self, + player: Weak>, + fetch: Pin, Error>>>>, + ) -> Pin> + 'static>> { + let handle = match self { + Loader::Form { self_handle, .. } => self_handle.expect("Loader not self-introduced"), + _ => return Box::pin(async { Err("Non-form loader spawned as form loader".into()) }), + }; + + let player = player + .upgrade() + .expect("Could not upgrade weak reference to player"); + + Box::pin(async move { + let data = fetch.await?; + + player.lock().unwrap().update(|avm, uc| { + let loader = uc.load_manager.get_loader(handle); + let that = match loader { + Some(Loader::Form { target_object, .. }) => *target_object, + None => return Err("Loader expired during loading".into()), + _ => return Err("Non-movie loader spawned as movie loader".into()), + }; + + for (k, v) in form_urlencoded::parse(&data) { + that.set(&k, v.into_owned().into(), avm, uc)?; + } + + Ok(()) + }) + }) + } +} diff --git a/core/src/player.rs b/core/src/player.rs index 7430d262b..aea0f2c45 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -8,6 +8,7 @@ use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext}; use crate::display_object::{MorphShape, MovieClip}; use crate::events::{ButtonEvent, ButtonKeyCode, ClipEvent, PlayerEvent}; use crate::library::Library; +use crate::loader::LoadManager; use crate::prelude::*; use crate::tag_utils::SwfMovie; use crate::transform::TransformStack; @@ -45,6 +46,10 @@ struct GcRootData<'gc> { avm: Avm1<'gc>, action_queue: ActionQueue<'gc>, + + /// Object which manages asynchronous processes that need to interact with + /// data in the GC arena. + load_manager: LoadManager<'gc>, } impl<'gc> GcRootData<'gc> { @@ -58,6 +63,7 @@ impl<'gc> GcRootData<'gc> { &mut ActionQueue<'gc>, &mut Avm1<'gc>, &mut Option>, + &mut LoadManager<'gc>, ) { ( &mut self.layers, @@ -65,6 +71,7 @@ impl<'gc> GcRootData<'gc> { &mut self.action_queue, &mut self.avm, &mut self.drag_object, + &mut self.load_manager, ) } } @@ -197,6 +204,7 @@ impl Player { drag_object: None, avm: Avm1::new(gc_context, NEWEST_PLAYER_VERSION), action_queue: ActionQueue::new(), + load_manager: LoadManager::new(), }, )) }), @@ -760,7 +768,7 @@ impl Player { self.gc_arena.mutate(|gc_context, gc_root| { let mut root_data = gc_root.0.write(gc_context); let mouse_hovered_object = root_data.mouse_hovered_object; - let (layers, library, action_queue, avm, drag_object) = + let (layers, library, action_queue, avm, drag_object, load_manager) = root_data.update_context_params(); let layer0 = layers.get(&0).expect("Layer 0 should always exist"); @@ -785,6 +793,7 @@ impl Player { stage_size: (stage_width, stage_height), system_prototypes: avm.prototypes().clone(), player, + load_manager, }; let ret = f(avm, &mut update_context); From 64b787b73fbf8a7044593cdfbd14c838750f11e8 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 10 Jan 2020 18:43:27 -0500 Subject: [PATCH 19/59] Implement `Collect` for `Loader`. (How did this compile without that?) --- core/src/loader.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/loader.rs b/core/src/loader.rs index bacc8b46a..793b2eeab 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -128,6 +128,15 @@ pub enum Loader<'gc> { }, } +unsafe impl<'gc> Collect for Loader<'gc> { + fn trace(&self, cc: CollectionContext) { + match self { + Loader::Movie { target_clip, .. } => target_clip.trace(cc), + Loader::Form { target_object, .. } => target_object.trace(cc), + } + } +} + impl<'gc> Loader<'gc> { /// Set the loader handle for this loader. /// From 07637fe676be8c2d62e89c174fdf4f7f9eb2acc0 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 11 Jan 2020 17:39:55 -0500 Subject: [PATCH 20/59] Replaced movies should start out playing. This fixes a regression from a previous rebase. --- core/src/display_object/movie_clip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 130a0ef01..aa64c1e71 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -445,7 +445,7 @@ impl<'gc> MovieClipData<'gc> { }, ); self.tag_stream_pos = 0; - self.flags = EnumSet::empty(); + self.flags = MovieClipFlags::Playing.into(); self.current_frame = 0; self.audio_stream = None; self.children = BTreeMap::new(); From ea28c2c4a23bea95216fc1e91271ebe54028b575 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 11 Jan 2020 19:33:02 -0500 Subject: [PATCH 21/59] Impl `loadMovie` / `loadMovieNum` (sans local variable support) --- core/src/avm1/globals.rs | 102 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index 491e63ce2..e6753d7c6 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -4,6 +4,8 @@ use crate::avm1::listeners::SystemListeners; use crate::avm1::return_value::ReturnValue; use crate::avm1::{Avm1, Error, Object, ScriptObject, TObject, UpdateContext, Value}; use crate::backend::navigator::NavigationMethod; +use crate::display_object::{DisplayObject, MovieClip, TDisplayObject}; +use crate::player::NEWEST_PLAYER_VERSION; use enumset::EnumSet; use gc_arena::MutationContext; use rand::Rng; @@ -107,6 +109,92 @@ pub fn get_nan<'gc>( } } +pub fn load_movie<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let url = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let target_or_path = args.get(1).cloned().unwrap_or(Value::Undefined); + let target = if let Value::Object(target) = target_or_path { + let target = target.as_display_object(); + + if target.is_none() { + log::warn!("loadMovie: Object passed as target is not a movie clip"); + } + + target + } else { + let target_path = target_or_path.into_string(); + let target_clip = + Avm1::resolve_dot_path_clip(avm.target_clip(), context.root, &target_path); + + if target_clip.is_none() { + log::warn!("loadMovie: Could not resolve target path {}", target_path) + } + + target_clip + }; + let _method = args.get(2).cloned().unwrap_or(Value::Undefined); + + if let Some(target) = target { + let fetch = context.navigator.fetch(url); + let process = context.load_manager.load_movie_into_clip( + context.player.clone().unwrap(), + target, + fetch, + ); + + context.navigator.spawn_future(process); + } + + Ok(Value::Undefined.into()) +} + +pub fn load_movie_num<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let url = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let level_id = args + .get(1) + .cloned() + .unwrap_or(Value::Undefined) + .as_number(avm, context)? as u32; + let layer = if let Some(layer) = context.layers.get(&level_id) { + *layer + } else { + let mut layer: DisplayObject<'_> = + MovieClip::new(NEWEST_PLAYER_VERSION, context.gc_context).into(); + + layer.post_instantiation(context.gc_context, layer, avm.prototypes().movie_clip); + context.layers.insert(level_id, layer); + + layer + }; + let _method = args.get(2).cloned().unwrap_or(Value::Undefined); + let fetch = context.navigator.fetch(url); + let process = + context + .load_manager + .load_movie_into_clip(context.player.clone().unwrap(), layer, fetch); + + context.navigator.spawn_future(process); + + Ok(Value::Undefined.into()) +} + /// This structure represents all system builtins that are used regardless of /// whatever the hell happens to `_global`. These are, of course, /// user-modifiable. @@ -333,6 +421,20 @@ pub fn create_globals<'gc>( EnumSet::empty(), Some(function_proto), ); + globals.force_set_function( + "loadMovie", + load_movie, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + globals.force_set_function( + "loadMovieNum", + load_movie_num, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); globals.add_property( gc_context, "NaN", From 3fe6884c90a7fbcc9b339f0c4cec2e77d03e85b8 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 12 Jan 2020 12:34:16 -0500 Subject: [PATCH 22/59] Refactor layer resolution into a separate function. This function is part of `Avm1`, rather than a hypothetical `LayerManager`, because we're going to need to eventually construct layers differently for AVM2. --- core/src/avm1.rs | 34 +++++++++++++++++++++++----------- core/src/avm1/globals.rs | 14 +------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 461306bbf..5ea6e31f8 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -907,6 +907,28 @@ impl<'gc> Avm1<'gc> { clip } + /// Resolve a layer by ID. + /// + /// If the layer does not exist, then it will be created and instantiated + /// with a script object. + pub fn resolve_layer( + &self, + level_id: u32, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> DisplayObject<'gc> { + if let Some(layer) = context.layers.get(&level_id) { + *layer + } else { + let mut layer: DisplayObject<'_> = + MovieClip::new(NEWEST_PLAYER_VERSION, context.gc_context).into(); + + layer.post_instantiation(context.gc_context, layer, self.prototypes.movie_clip); + context.layers.insert(level_id, layer); + + layer + } + } + fn push(&mut self, value: impl Into>) { let value = value.into(); avm_debug!("Stack push {}: {:?}", self.stack.len(), value); @@ -1594,17 +1616,7 @@ impl<'gc> Avm1<'gc> { let url = url.to_string(); let level_id = target[6..].parse::()?; let fetch = context.navigator.fetch(url); - let layer = if let Some(layer) = context.layers.get(&level_id) { - *layer - } else { - let mut layer: DisplayObject<'_> = - MovieClip::new(NEWEST_PLAYER_VERSION, context.gc_context).into(); - - layer.post_instantiation(context.gc_context, layer, self.prototypes.movie_clip); - context.layers.insert(level_id, layer); - - layer - }; + let layer = self.resolve_layer(level_id, context); let process = context.load_manager.load_movie_into_clip( context.player.clone().unwrap(), diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index e6753d7c6..c2e40c9b0 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -4,8 +4,6 @@ use crate::avm1::listeners::SystemListeners; use crate::avm1::return_value::ReturnValue; use crate::avm1::{Avm1, Error, Object, ScriptObject, TObject, UpdateContext, Value}; use crate::backend::navigator::NavigationMethod; -use crate::display_object::{DisplayObject, MovieClip, TDisplayObject}; -use crate::player::NEWEST_PLAYER_VERSION; use enumset::EnumSet; use gc_arena::MutationContext; use rand::Rng; @@ -172,17 +170,7 @@ pub fn load_movie_num<'gc>( .cloned() .unwrap_or(Value::Undefined) .as_number(avm, context)? as u32; - let layer = if let Some(layer) = context.layers.get(&level_id) { - *layer - } else { - let mut layer: DisplayObject<'_> = - MovieClip::new(NEWEST_PLAYER_VERSION, context.gc_context).into(); - - layer.post_instantiation(context.gc_context, layer, avm.prototypes().movie_clip); - context.layers.insert(level_id, layer); - - layer - }; + let layer = avm.resolve_layer(level_id, context); let _method = args.get(2).cloned().unwrap_or(Value::Undefined); let fetch = context.navigator.fetch(url); let process = From c7539872f7414d861a7b41c588ad68efc6cbda53 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 12 Jan 2020 15:06:27 -0700 Subject: [PATCH 23/59] Add the ability to POST data from `fetch`, and allow methods that read AVM locals into form data to `GET` or `POST` them. --- core/src/avm1.rs | 54 ++++++++++++++++++++++++++++--- core/src/avm1/globals.rs | 12 ++++--- core/src/backend/navigator.rs | 61 +++++++++++++++++++++++++++++++++-- desktop/src/navigator.rs | 8 +++-- web/Cargo.toml | 3 +- web/src/navigator.rs | 46 +++++++++++++++++++++++--- 6 files changed, 166 insertions(+), 18 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 5ea6e31f8..7497d0ac0 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1,13 +1,14 @@ use crate::avm1::function::{Avm1Function, FunctionObject}; use crate::avm1::globals::create_globals; use crate::avm1::return_value::ReturnValue; -use crate::backend::navigator::NavigationMethod; +use crate::backend::navigator::{NavigationMethod, RequestOptions}; use crate::context::UpdateContext; use crate::prelude::*; use gc_arena::{GcCell, MutationContext}; use rand::Rng; use std::collections::HashMap; use std::convert::TryInto; +use url::form_urlencoded; use swf::avm1::read::Reader; use swf::avm1::types::{Action, Function}; @@ -196,6 +197,41 @@ impl<'gc> Avm1<'gc> { form_values } + /// Construct request options for a fetch operation that may send locals as + /// form data in the request body or URL. + pub fn locals_into_request_options( + &mut self, + context: &mut UpdateContext<'_, 'gc, '_>, + url: String, + method: Option, + ) -> (String, RequestOptions) { + match method { + Some(method) => { + let vars = self.locals_into_form_values(context); + let qstring = form_urlencoded::Serializer::new(String::new()) + .extend_pairs(vars.iter()) + .finish(); + + match method { + NavigationMethod::GET if url.find('?').is_none() => { + (format!("{}?{}", url, qstring), RequestOptions::get()) + } + NavigationMethod::GET => { + (format!("{}&{}", url, qstring), RequestOptions::get()) + } + NavigationMethod::POST => ( + url, + RequestOptions::post(Some(( + qstring.as_bytes().to_owned(), + "application/x-www-form-urlencoded".to_string(), + ))), + ), + } + } + None => (url, RequestOptions::get()), + } + } + /// Add a stack frame that executes code in timeline scope pub fn insert_stack_frame_for_action( &mut self, @@ -1615,7 +1651,7 @@ impl<'gc> Avm1<'gc> { if target.starts_with("_level") && target.len() > 6 { let url = url.to_string(); let level_id = target[6..].parse::()?; - let fetch = context.navigator.fetch(url); + let fetch = context.navigator.fetch(url, RequestOptions::get()); let layer = self.resolve_layer(level_id, context); let process = context.load_manager.load_movie_into_clip( @@ -1670,7 +1706,12 @@ impl<'gc> Avm1<'gc> { .object() .as_object() .unwrap(); - let fetch = context.navigator.fetch(url); + let (url, opts) = self.locals_into_request_options( + context, + url, + NavigationMethod::from_send_vars_method(swf_method), + ); + let fetch = context.navigator.fetch(url, opts); let process = context.load_manager.load_form_into_object( context.player.clone().unwrap(), target_obj, @@ -1683,7 +1724,12 @@ impl<'gc> Avm1<'gc> { return Ok(()); } else if is_target_sprite { if let Some(clip_target) = clip_target { - let fetch = context.navigator.fetch(url); + let (url, opts) = self.locals_into_request_options( + context, + url, + NavigationMethod::from_send_vars_method(swf_method), + ); + let fetch = context.navigator.fetch(url, opts); let process = context.load_manager.load_movie_into_clip( context.player.clone().unwrap(), clip_target, diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index c2e40c9b0..c88ae7b0d 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -138,10 +138,12 @@ pub fn load_movie<'gc>( target_clip }; - let _method = args.get(2).cloned().unwrap_or(Value::Undefined); + let method = args.get(2).cloned().unwrap_or(Value::Undefined); if let Some(target) = target { - let fetch = context.navigator.fetch(url); + let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); + let (url, opts) = avm.locals_into_request_options(context, url, method); + let fetch = context.navigator.fetch(url, opts); let process = context.load_manager.load_movie_into_clip( context.player.clone().unwrap(), target, @@ -171,8 +173,10 @@ pub fn load_movie_num<'gc>( .unwrap_or(Value::Undefined) .as_number(avm, context)? as u32; let layer = avm.resolve_layer(level_id, context); - let _method = args.get(2).cloned().unwrap_or(Value::Undefined); - let fetch = context.navigator.fetch(url); + let method = args.get(2).cloned().unwrap_or(Value::Undefined); + let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); + let (url, opts) = avm.locals_into_request_options(context, url, method); + let fetch = context.navigator.fetch(url, opts); let process = context .load_manager diff --git a/core/src/backend/navigator.rs b/core/src/backend/navigator.rs index 5fbe86367..4ddf81df5 100644 --- a/core/src/backend/navigator.rs +++ b/core/src/backend/navigator.rs @@ -8,6 +8,7 @@ use swf::avm1::types::SendVarsMethod; pub type Error = Box; /// Enumerates all possible navigation methods. +#[derive(Copy, Clone)] pub enum NavigationMethod { /// Indicates that navigation should generate a GET request. GET, @@ -25,6 +26,54 @@ impl NavigationMethod { SendVarsMethod::Post => Some(Self::POST), } } + + pub fn from_method_str(method: &str) -> Option { + match method { + "GET" => Some(Self::GET), + "POST" => Some(Self::POST), + _ => None, + } + } +} + +/// Represents request options to be sent as part of a fetch. +pub struct RequestOptions { + /// The HTTP method to be used to make the request. + method: NavigationMethod, + + /// The contents of the request body, if the request's HTTP method supports + /// having a body. + /// + /// The body consists of data and a mime type. + body: Option<(Vec, String)>, +} + +impl RequestOptions { + /// Construct request options for a GET request. + pub fn get() -> Self { + Self { + method: NavigationMethod::GET, + body: None, + } + } + + /// Construct request options for a POST request. + pub fn post(body: Option<(Vec, String)>) -> Self { + Self { + method: NavigationMethod::POST, + body, + } + } + + /// Retrieve the navigation method for this request. + pub fn method(&self) -> NavigationMethod { + self.method + } + + /// Retrieve the body of this request, if it exists. + pub fn body(&self) -> &Option<(Vec, String)> { + &self.body + } } /// A backend interacting with a browser environment. @@ -59,7 +108,11 @@ pub trait NavigatorBackend { ); /// Fetch data at a given URL and return it some time in the future. - fn fetch(&self, url: String) -> Pin, Error>>>>; + fn fetch( + &self, + url: String, + request_options: RequestOptions, + ) -> Pin, Error>>>>; /// Arrange for a future to be run at some point in the... well, future. /// @@ -96,7 +149,11 @@ impl NavigatorBackend for NullNavigatorBackend { ) { } - fn fetch(&self, _url: String) -> Pin, Error>>>> { + fn fetch( + &self, + _url: String, + _opts: RequestOptions, + ) -> Pin, Error>>>> { Box::pin(async { Err("Fetch IO not implemented".into()) }) } diff --git a/desktop/src/navigator.rs b/desktop/src/navigator.rs index eedcae2ba..38137aaa1 100644 --- a/desktop/src/navigator.rs +++ b/desktop/src/navigator.rs @@ -1,7 +1,7 @@ //! Navigator backend for web use log; -use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend}; +use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend, RequestOptions}; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; @@ -63,7 +63,11 @@ impl NavigatorBackend for ExternalNavigatorBackend { }; } - fn fetch(&self, _url: String) -> Pin, Error>>>> { + fn fetch( + &self, + _url: String, + _options: RequestOptions, + ) -> Pin, Error>>>> { Box::pin(async { Err("Fetch not implemented on desktop!".into()) }) } diff --git a/web/Cargo.toml b/web/Cargo.toml index e147ba593..e888540a6 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -42,7 +42,8 @@ features = [ "AudioNode", "CanvasRenderingContext2d", "ChannelMergerNode", "ChannelSplitterNode", "CssStyleDeclaration", "Document", "Element", "Event", "EventTarget", "GainNode", "HtmlCanvasElement", "HtmlElement", "HtmlImageElement", "MouseEvent", "Navigator", "Node", "Performance", "PointerEvent", "ScriptProcessorNode", "UiEvent", "Window", "Location", "HtmlFormElement", - "KeyboardEvent", "Path2d", "CanvasGradient", "CanvasPattern", "SvgMatrix", "SvgsvgElement", "Response"] + "KeyboardEvent", "Path2d", "CanvasGradient", "CanvasPattern", "SvgMatrix", "SvgsvgElement", "Response", "Request", "RequestInit", + "Blob", "BlobPropertyBag"] [dev-dependencies] wasm-bindgen-test = "0.3.7" diff --git a/web/src/navigator.rs b/web/src/navigator.rs index 2bc6fe8d6..5f399f085 100644 --- a/web/src/navigator.rs +++ b/web/src/navigator.rs @@ -1,13 +1,13 @@ //! Navigator backend for web -use js_sys::{ArrayBuffer, Uint8Array}; -use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend}; +use js_sys::{Array, ArrayBuffer, Uint8Array}; +use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend, RequestOptions}; use std::collections::HashMap; use std::future::Future; use std::pin::Pin; use wasm_bindgen::JsCast; use wasm_bindgen_futures::{spawn_local, JsFuture}; -use web_sys::{window, Response}; +use web_sys::{window, Blob, BlobPropertyBag, Request, RequestInit, Response}; pub struct WebNavigatorBackend {} @@ -77,10 +77,46 @@ impl NavigatorBackend for WebNavigatorBackend { } } - fn fetch(&self, url: String) -> Pin, Error>>>> { + fn fetch( + &self, + url: String, + options: RequestOptions, + ) -> Pin, Error>>>> { Box::pin(async move { + let mut init = RequestInit::new(); + + init.method(match options.method() { + NavigationMethod::GET => "GET", + NavigationMethod::POST => "POST", + }); + + if let Some((data, mime)) = options.body() { + let arraydata = ArrayBuffer::new(data.len() as u32); + let u8data = Uint8Array::new(&arraydata); + + for (i, byte) in data.iter().enumerate() { + u8data.fill(*byte, i as u32, i as u32 + 1); + } + + let blobparts = Array::new(); + blobparts.push(&arraydata); + + let mut blobprops = BlobPropertyBag::new(); + blobprops.type_(mime); + + let datablob = + Blob::new_with_buffer_source_sequence_and_options(&blobparts, &blobprops) + .unwrap() + .dyn_into() + .unwrap(); + + init.body(Some(&datablob)); + } + + let request = Request::new_with_str_and_init(&url, &init).unwrap(); + let window = web_sys::window().unwrap(); - let fetchval = JsFuture::from(window.fetch_with_str(&url)).await; + let fetchval = JsFuture::from(window.fetch_with_request(&request)).await; if fetchval.is_err() { return Err("Could not fetch, got JS Error".into()); } From b2b2a165fcf12c8e0c96f60f5cd4e13ddc7a9d3f Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 12 Jan 2020 20:03:11 -0500 Subject: [PATCH 24/59] Implement `loadVariables` and `loadVariablesNum`. --- core/src/avm1/globals.rs | 80 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index c88ae7b0d..e5cf1d4a3 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -4,6 +4,7 @@ use crate::avm1::listeners::SystemListeners; use crate::avm1::return_value::ReturnValue; use crate::avm1::{Avm1, Error, Object, ScriptObject, TObject, UpdateContext, Value}; use crate::backend::navigator::NavigationMethod; +use crate::display_object::TDisplayObject; use enumset::EnumSet; use gc_arena::MutationContext; use rand::Rng; @@ -187,6 +188,71 @@ pub fn load_movie_num<'gc>( Ok(Value::Undefined.into()) } +pub fn load_variables<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let url = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let target = args.get(1).cloned().unwrap_or(Value::Undefined); + + if let Value::Object(target) = target { + let method = args.get(2).cloned().unwrap_or(Value::Undefined); + let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); + let (url, opts) = avm.locals_into_request_options(context, url, method); + let fetch = context.navigator.fetch(url, opts); + let process = context.load_manager.load_form_into_object( + context.player.clone().unwrap(), + target, + fetch, + ); + + context.navigator.spawn_future(process); + } else { + log::warn!("loadVariables: Was not called with a valid target"); + } + + Ok(Value::Undefined.into()) +} + +pub fn load_variables_num<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let url = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let level_id = args + .get(1) + .cloned() + .unwrap_or(Value::Undefined) + .as_number(avm, context)? as u32; + let layer = avm.resolve_layer(level_id, context); + let target = layer.object().as_object()?; + + let method = args.get(2).cloned().unwrap_or(Value::Undefined); + let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); + let (url, opts) = avm.locals_into_request_options(context, url, method); + let fetch = context.navigator.fetch(url, opts); + let process = + context + .load_manager + .load_form_into_object(context.player.clone().unwrap(), target, fetch); + + context.navigator.spawn_future(process); + + Ok(Value::Undefined.into()) +} + /// This structure represents all system builtins that are used regardless of /// whatever the hell happens to `_global`. These are, of course, /// user-modifiable. @@ -427,6 +493,20 @@ pub fn create_globals<'gc>( EnumSet::empty(), Some(function_proto), ); + globals.force_set_function( + "loadVariables", + load_variables, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + globals.force_set_function( + "loadVariablesNum", + load_variables_num, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); globals.add_property( gc_context, "NaN", From 31b1364b82309df3b3121ca61a4e64026e2a851e Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sun, 12 Jan 2020 21:44:46 -0500 Subject: [PATCH 25/59] Implement `unloadMovie` / `unloadMovieNum` --- core/src/avm1/globals.rs | 93 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index e5cf1d4a3..eca5e88f5 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -4,7 +4,7 @@ use crate::avm1::listeners::SystemListeners; use crate::avm1::return_value::ReturnValue; use crate::avm1::{Avm1, Error, Object, ScriptObject, TObject, UpdateContext, Value}; use crate::backend::navigator::NavigationMethod; -use crate::display_object::TDisplayObject; +use crate::display_object::{DisplayObject, TDisplayObject}; use enumset::EnumSet; use gc_arena::MutationContext; use rand::Rng; @@ -253,6 +253,83 @@ pub fn load_variables_num<'gc>( Ok(Value::Undefined.into()) } +pub fn unload_movie<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let target_or_path = args.get(0).cloned().unwrap_or(Value::Undefined); + let target = if let Value::Object(target) = target_or_path { + let target = target.as_display_object(); + + if target.is_none() { + log::warn!("unloadMovie: Object passed as target is not a movie clip"); + } + + target + } else { + let target_path = target_or_path.into_string(); + let target_clip = + Avm1::resolve_dot_path_clip(avm.target_clip(), context.root, &target_path); + + if target_clip.is_none() { + log::warn!("unloadMovie: Could not resolve target path {}", target_path) + } + + target_clip + }; + + if let Some(mut target) = target { + //TODO: How do we reclaim resources from unloaded movies? + if let Some(parent) = target.parent() { + parent + .as_movie_clip() + .unwrap() + .remove_child_from_avm(context, target); + } else { + //Removing a layer is a little different. + let mut layer_depth = None; + + for (depth, layer) in context.layers.iter() { + if DisplayObject::ptr_eq(*layer, target) { + layer_depth = Some(*depth); + break; + } + } + + if let Some(depth) = layer_depth { + context.layers.remove(&depth); + } + + target.unload(context); + } + } + + Ok(Value::Undefined.into()) +} + +pub fn unload_movie_num<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let level_id = args + .get(1) + .cloned() + .unwrap_or(Value::Undefined) + .as_number(avm, context)? as u32; + + if let Some(mut layer) = context.layers.get_mut(&level_id).copied() { + layer.unload(context); + } + + context.layers.remove(&level_id); + + Ok(Value::Undefined.into()) +} + /// This structure represents all system builtins that are used regardless of /// whatever the hell happens to `_global`. These are, of course, /// user-modifiable. @@ -507,6 +584,20 @@ pub fn create_globals<'gc>( EnumSet::empty(), Some(function_proto), ); + globals.force_set_function( + "unloadMovie", + unload_movie, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); + globals.force_set_function( + "unloadMovieNum", + unload_movie_num, + gc_context, + EnumSet::empty(), + Some(function_proto), + ); globals.add_property( gc_context, "NaN", From b7d318a897bde794321982022fbdc4bc74444910 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Mon, 13 Jan 2020 19:28:52 -0500 Subject: [PATCH 26/59] Implement `MovieClip.loadMovie`, `MovieClip.loadVariables`, and `MovieClip.unloadMovie`. *De*implement the free function versions of the above, as well as their `Num` variants, since they don't actually exist as callables. Instead, the ActionScript compiler treats them as preprocessor functions that represent various forms of `ActionGetURL`/`ActionGetURL2`. --- core/src/avm1/globals.rs | 265 ---------------------------- core/src/avm1/globals/movie_clip.rs | 70 +++++++- 2 files changed, 69 insertions(+), 266 deletions(-) diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index eca5e88f5..491e63ce2 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -4,7 +4,6 @@ use crate::avm1::listeners::SystemListeners; use crate::avm1::return_value::ReturnValue; use crate::avm1::{Avm1, Error, Object, ScriptObject, TObject, UpdateContext, Value}; use crate::backend::navigator::NavigationMethod; -use crate::display_object::{DisplayObject, TDisplayObject}; use enumset::EnumSet; use gc_arena::MutationContext; use rand::Rng; @@ -108,228 +107,6 @@ pub fn get_nan<'gc>( } } -pub fn load_movie<'gc>( - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - _this: Object<'gc>, - args: &[Value<'gc>], -) -> Result, Error> { - let url = args - .get(0) - .cloned() - .unwrap_or(Value::Undefined) - .coerce_to_string(avm, context)?; - let target_or_path = args.get(1).cloned().unwrap_or(Value::Undefined); - let target = if let Value::Object(target) = target_or_path { - let target = target.as_display_object(); - - if target.is_none() { - log::warn!("loadMovie: Object passed as target is not a movie clip"); - } - - target - } else { - let target_path = target_or_path.into_string(); - let target_clip = - Avm1::resolve_dot_path_clip(avm.target_clip(), context.root, &target_path); - - if target_clip.is_none() { - log::warn!("loadMovie: Could not resolve target path {}", target_path) - } - - target_clip - }; - let method = args.get(2).cloned().unwrap_or(Value::Undefined); - - if let Some(target) = target { - let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); - let (url, opts) = avm.locals_into_request_options(context, url, method); - let fetch = context.navigator.fetch(url, opts); - let process = context.load_manager.load_movie_into_clip( - context.player.clone().unwrap(), - target, - fetch, - ); - - context.navigator.spawn_future(process); - } - - Ok(Value::Undefined.into()) -} - -pub fn load_movie_num<'gc>( - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - _this: Object<'gc>, - args: &[Value<'gc>], -) -> Result, Error> { - let url = args - .get(0) - .cloned() - .unwrap_or(Value::Undefined) - .coerce_to_string(avm, context)?; - let level_id = args - .get(1) - .cloned() - .unwrap_or(Value::Undefined) - .as_number(avm, context)? as u32; - let layer = avm.resolve_layer(level_id, context); - let method = args.get(2).cloned().unwrap_or(Value::Undefined); - let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); - let (url, opts) = avm.locals_into_request_options(context, url, method); - let fetch = context.navigator.fetch(url, opts); - let process = - context - .load_manager - .load_movie_into_clip(context.player.clone().unwrap(), layer, fetch); - - context.navigator.spawn_future(process); - - Ok(Value::Undefined.into()) -} - -pub fn load_variables<'gc>( - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - _this: Object<'gc>, - args: &[Value<'gc>], -) -> Result, Error> { - let url = args - .get(0) - .cloned() - .unwrap_or(Value::Undefined) - .coerce_to_string(avm, context)?; - let target = args.get(1).cloned().unwrap_or(Value::Undefined); - - if let Value::Object(target) = target { - let method = args.get(2).cloned().unwrap_or(Value::Undefined); - let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); - let (url, opts) = avm.locals_into_request_options(context, url, method); - let fetch = context.navigator.fetch(url, opts); - let process = context.load_manager.load_form_into_object( - context.player.clone().unwrap(), - target, - fetch, - ); - - context.navigator.spawn_future(process); - } else { - log::warn!("loadVariables: Was not called with a valid target"); - } - - Ok(Value::Undefined.into()) -} - -pub fn load_variables_num<'gc>( - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - _this: Object<'gc>, - args: &[Value<'gc>], -) -> Result, Error> { - let url = args - .get(0) - .cloned() - .unwrap_or(Value::Undefined) - .coerce_to_string(avm, context)?; - let level_id = args - .get(1) - .cloned() - .unwrap_or(Value::Undefined) - .as_number(avm, context)? as u32; - let layer = avm.resolve_layer(level_id, context); - let target = layer.object().as_object()?; - - let method = args.get(2).cloned().unwrap_or(Value::Undefined); - let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); - let (url, opts) = avm.locals_into_request_options(context, url, method); - let fetch = context.navigator.fetch(url, opts); - let process = - context - .load_manager - .load_form_into_object(context.player.clone().unwrap(), target, fetch); - - context.navigator.spawn_future(process); - - Ok(Value::Undefined.into()) -} - -pub fn unload_movie<'gc>( - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - _this: Object<'gc>, - args: &[Value<'gc>], -) -> Result, Error> { - let target_or_path = args.get(0).cloned().unwrap_or(Value::Undefined); - let target = if let Value::Object(target) = target_or_path { - let target = target.as_display_object(); - - if target.is_none() { - log::warn!("unloadMovie: Object passed as target is not a movie clip"); - } - - target - } else { - let target_path = target_or_path.into_string(); - let target_clip = - Avm1::resolve_dot_path_clip(avm.target_clip(), context.root, &target_path); - - if target_clip.is_none() { - log::warn!("unloadMovie: Could not resolve target path {}", target_path) - } - - target_clip - }; - - if let Some(mut target) = target { - //TODO: How do we reclaim resources from unloaded movies? - if let Some(parent) = target.parent() { - parent - .as_movie_clip() - .unwrap() - .remove_child_from_avm(context, target); - } else { - //Removing a layer is a little different. - let mut layer_depth = None; - - for (depth, layer) in context.layers.iter() { - if DisplayObject::ptr_eq(*layer, target) { - layer_depth = Some(*depth); - break; - } - } - - if let Some(depth) = layer_depth { - context.layers.remove(&depth); - } - - target.unload(context); - } - } - - Ok(Value::Undefined.into()) -} - -pub fn unload_movie_num<'gc>( - avm: &mut Avm1<'gc>, - context: &mut UpdateContext<'_, 'gc, '_>, - _this: Object<'gc>, - args: &[Value<'gc>], -) -> Result, Error> { - let level_id = args - .get(1) - .cloned() - .unwrap_or(Value::Undefined) - .as_number(avm, context)? as u32; - - if let Some(mut layer) = context.layers.get_mut(&level_id).copied() { - layer.unload(context); - } - - context.layers.remove(&level_id); - - Ok(Value::Undefined.into()) -} - /// This structure represents all system builtins that are used regardless of /// whatever the hell happens to `_global`. These are, of course, /// user-modifiable. @@ -556,48 +333,6 @@ pub fn create_globals<'gc>( EnumSet::empty(), Some(function_proto), ); - globals.force_set_function( - "loadMovie", - load_movie, - gc_context, - EnumSet::empty(), - Some(function_proto), - ); - globals.force_set_function( - "loadMovieNum", - load_movie_num, - gc_context, - EnumSet::empty(), - Some(function_proto), - ); - globals.force_set_function( - "loadVariables", - load_variables, - gc_context, - EnumSet::empty(), - Some(function_proto), - ); - globals.force_set_function( - "loadVariablesNum", - load_variables_num, - gc_context, - EnumSet::empty(), - Some(function_proto), - ); - globals.force_set_function( - "unloadMovie", - unload_movie, - gc_context, - EnumSet::empty(), - Some(function_proto), - ); - globals.force_set_function( - "unloadMovieNum", - unload_movie_num, - gc_context, - EnumSet::empty(), - Some(function_proto), - ); globals.add_property( gc_context, "NaN", diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index 149b412bc..27033b1b9 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -4,6 +4,7 @@ use crate::avm1::function::Executable; use crate::avm1::property::Attribute::*; use crate::avm1::return_value::ReturnValue; use crate::avm1::{Avm1, Error, Object, ScriptObject, TObject, UpdateContext, Value}; +use crate::backend::navigator::NavigationMethod; use crate::display_object::{DisplayObject, EditText, MovieClip, TDisplayObject}; use crate::prelude::*; use enumset::EnumSet; @@ -184,7 +185,74 @@ pub fn create_proto<'gc>( Ok(movie_clip.path().into()) }, "localToGlobal" => local_to_global, - "globalToLocal" => global_to_local + "globalToLocal" => global_to_local, + "loadMovie" => |target: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, args: &[Value<'gc>]| { + let url = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let method = args.get(1).cloned().unwrap_or(Value::Undefined); + let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); + let (url, opts) = avm.locals_into_request_options(context, url, method); + let fetch = context.navigator.fetch(url, opts); + let process = context.load_manager.load_movie_into_clip( + context.player.clone().unwrap(), + DisplayObject::MovieClip(target), + fetch, + ); + + context.navigator.spawn_future(process); + + Ok(Value::Undefined.into()) + }, + "loadVariables" => |target: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, args: &[Value<'gc>]| { + let url = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let method = args.get(1).cloned().unwrap_or(Value::Undefined); + let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); + let (url, opts) = avm.locals_into_request_options(context, url, method); + let fetch = context.navigator.fetch(url, opts); + let process = context.load_manager.load_form_into_object( + context.player.clone().unwrap(), + target.object().as_object()?, + fetch, + ); + + context.navigator.spawn_future(process); + + Ok(Value::Undefined.into()) + }, + "unloadMovie" => |mut target: MovieClip<'gc>, _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _args: &[Value<'gc>]| { + //TODO: How do we reclaim resources from unloaded movies? + if let Some(parent) = target.parent() { + parent + .as_movie_clip() + .unwrap() + .remove_child_from_avm(context, DisplayObject::MovieClip(target)); + } else { + //Removing a layer is a little different. + let mut layer_depth = None; + + for (depth, layer) in context.layers.iter() { + if DisplayObject::ptr_eq(*layer, DisplayObject::MovieClip(target)) { + layer_depth = Some(*depth); + break; + } + } + + if let Some(depth) = layer_depth { + context.layers.remove(&depth); + } + + target.unload(context); + } + + Ok(Value::Undefined.into()) + } ); object.add_property( From db41bec91eac35d3aeafc4f5c682c060909ccb55 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Tue, 14 Jan 2020 19:58:32 -0500 Subject: [PATCH 27/59] Implement `MovieClipLoader`'s `addListener`, `removeListener`, and `broadcastMessage` methods. Interestingly, this constitutes an implementation of `AsBroadcaster`. It appears Macromedia decided to implement event handling on `MovieClipLoader` in a very similar fashion to `AsBroadcaster`, down to invoking `broadcastMessage` and searching a `_listeners` property for listeners. --- core/src/avm1/globals.rs | 16 ++ core/src/avm1/globals/movie_clip_loader.rs | 162 ++++++++++++++++++ core/tests/regression_tests.rs | 1 + .../swfs/avm1/mcl_as_broadcaster/output.txt | 12 ++ .../swfs/avm1/mcl_as_broadcaster/test.fla | Bin 0 -> 37376 bytes .../swfs/avm1/mcl_as_broadcaster/test.swf | Bin 0 -> 354 bytes 6 files changed, 191 insertions(+) create mode 100644 core/src/avm1/globals/movie_clip_loader.rs create mode 100644 core/tests/swfs/avm1/mcl_as_broadcaster/output.txt create mode 100644 core/tests/swfs/avm1/mcl_as_broadcaster/test.fla create mode 100644 core/tests/swfs/avm1/mcl_as_broadcaster/test.swf diff --git a/core/src/avm1/globals.rs b/core/src/avm1/globals.rs index 491e63ce2..93a30c0b2 100644 --- a/core/src/avm1/globals.rs +++ b/core/src/avm1/globals.rs @@ -17,6 +17,7 @@ mod key; mod math; pub(crate) mod mouse; pub(crate) mod movie_clip; +mod movie_clip_loader; pub(crate) mod number; mod object; mod sound; @@ -154,6 +155,9 @@ pub fn create_globals<'gc>( let movie_clip_proto: Object<'gc> = movie_clip::create_proto(gc_context, object_proto, function_proto); + let movie_clip_loader_proto: Object<'gc> = + movie_clip_loader::create_proto(gc_context, object_proto, function_proto); + let sound_proto: Object<'gc> = sound::create_proto(gc_context, object_proto, function_proto); let text_field_proto: Object<'gc> = @@ -200,6 +204,12 @@ pub fn create_globals<'gc>( Some(function_proto), Some(movie_clip_proto), ); + let movie_clip_loader = FunctionObject::function( + gc_context, + Executable::Native(movie_clip_loader::constructor), + Some(function_proto), + Some(movie_clip_loader_proto), + ); let sound = FunctionObject::function( gc_context, Executable::Native(sound::constructor), @@ -249,6 +259,12 @@ pub fn create_globals<'gc>( globals.define_value(gc_context, "Object", object.into(), EnumSet::empty()); globals.define_value(gc_context, "Function", function.into(), EnumSet::empty()); globals.define_value(gc_context, "MovieClip", movie_clip.into(), EnumSet::empty()); + globals.define_value( + gc_context, + "MovieClipLoader", + movie_clip_loader.into(), + EnumSet::empty(), + ); globals.define_value(gc_context, "Sound", sound.into(), EnumSet::empty()); globals.define_value(gc_context, "TextField", text_field.into(), EnumSet::empty()); globals.define_value( diff --git a/core/src/avm1/globals/movie_clip_loader.rs b/core/src/avm1/globals/movie_clip_loader.rs new file mode 100644 index 000000000..34ddff673 --- /dev/null +++ b/core/src/avm1/globals/movie_clip_loader.rs @@ -0,0 +1,162 @@ +//! `MovieClipLoader` impl + +use crate::avm1::object::TObject; +use crate::avm1::property::Attribute; +use crate::avm1::return_value::ReturnValue; +use crate::avm1::script_object::ScriptObject; +use crate::avm1::{Avm1, Error, Object, UpdateContext, Value}; +use enumset::EnumSet; +use gc_arena::MutationContext; + +pub fn constructor<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + _args: &[Value<'gc>], +) -> Result, Error> { + let listeners = ScriptObject::array(context.gc_context, Some(avm.prototypes().array)); + this.define_value( + context.gc_context, + "_listeners", + Value::Object(listeners.into()), + Attribute::DontEnum.into(), + ); + listeners.set("0", Value::Object(this), avm, context)?; + + Ok(Value::Undefined.into()) +} + +pub fn add_listener<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let new_listener = args.get(0).cloned().unwrap_or(Value::Undefined); + let listeners = this + .get("_listeners", avm, context)? + .resolve(avm, context)?; + + if let Value::Object(listeners) = listeners { + let length = listeners.length(); + listeners.set_length(context.gc_context, length + 1); + listeners.set_array_element(length, new_listener, context.gc_context); + } + + Ok(true.into()) +} + +pub fn remove_listener<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let old_listener = args.get(0).cloned().unwrap_or(Value::Undefined); + let listeners = this + .get("_listeners", avm, context)? + .resolve(avm, context)?; + + if let Value::Object(listeners) = listeners { + let length = listeners.length(); + let mut position = None; + + for i in 0..length { + let other_listener = listeners + .get(&format!("{}", i), avm, context)? + .resolve(avm, context)?; + if old_listener == other_listener { + position = Some(i); + break; + } + } + + if let Some(position) = position { + if length > 0 { + let new_length = length - 1; + for i in position..new_length { + listeners.set_array_element( + i, + listeners.array_element(i + 1), + context.gc_context, + ); + } + + listeners.delete_array_element(new_length, context.gc_context); + listeners.delete(context.gc_context, &new_length.to_string()); + + listeners.set_length(context.gc_context, new_length); + } + } + } + + Ok(true.into()) +} + +pub fn broadcast_message<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let event_name = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let call_args = &args[0..]; + + let listeners = this + .get("_listeners", avm, context)? + .resolve(avm, context)?; + if let Value::Object(listeners) = listeners { + for i in 0..listeners.length() { + let listener = listeners + .get(&format!("{}", i), avm, context)? + .resolve(avm, context)?; + + if let Value::Object(listener) = listener { + let handler = listener + .get(&event_name, avm, context)? + .resolve(avm, context)?; + handler + .call(avm, context, listener, call_args)? + .resolve(avm, context)?; + } + } + } + + Ok(Value::Undefined.into()) +} + +pub fn create_proto<'gc>( + gc_context: MutationContext<'gc, '_>, + proto: Object<'gc>, + fn_proto: Object<'gc>, +) -> Object<'gc> { + let mcl_proto = ScriptObject::object(gc_context, Some(proto)); + + mcl_proto.as_script_object().unwrap().force_set_function( + "addListener", + add_listener, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); + mcl_proto.as_script_object().unwrap().force_set_function( + "removeListener", + remove_listener, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); + mcl_proto.as_script_object().unwrap().force_set_function( + "broadcastMessage", + broadcast_message, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); + + mcl_proto.into() +} diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index c3ebd4336..098bde10c 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -158,6 +158,7 @@ swf_tests! { (undefined_to_string_swf6, "avm1/undefined_to_string_swf6", 1), (define_function2_preload, "avm1/define_function2_preload", 1), (define_function2_preload_order, "avm1/define_function2_preload_order", 1), + (mcl_as_broadcaster, "avm1/mcl_as_broadcaster", 1), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/mcl_as_broadcaster/output.txt b/core/tests/swfs/avm1/mcl_as_broadcaster/output.txt new file mode 100644 index 000000000..c8bf05af1 --- /dev/null +++ b/core/tests/swfs/avm1/mcl_as_broadcaster/output.txt @@ -0,0 +1,12 @@ +Called from MovieClipLoader +[object Object] +true +false +Called from New Listener +[object Object] +false +true +Called from New Listener +[object Object] +false +true diff --git a/core/tests/swfs/avm1/mcl_as_broadcaster/test.fla b/core/tests/swfs/avm1/mcl_as_broadcaster/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..324f8c0bad4c5eef87f0fd0fda092423b055a46c GIT binary patch literal 37376 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 b6l;s;w{!#q(rPxm_nkqtkN*8T`TzbOIVfiV literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/mcl_as_broadcaster/test.swf b/core/tests/swfs/avm1/mcl_as_broadcaster/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..7c1b211006eb335713dd2c1d93ac9f0bfaa07f44 GIT binary patch literal 354 zcmV-o0iFIsS5pXm0ssJboU349jb~usU%MZO zDRVS4bP6wsFfpkxHqlGbw+QtxWVpcpc7ExsnvF-@x^!-xmo9jA-JV&2iGiWx5Cb1W zZgLKTZ+=;3s&h_eflq#7N@@{9GLXqol95@=kX)3SSd!|RTToI7SHzH)S{|R1SzMBu z2UMG%2h!wPRFq%D;GCF~lbWKCR+OKsfNmzToL_3Wf)C7q#FP{`+buIERlzU6M8Pe; zG%tn0#K43hsR*b)IkC9JH?_DpF+G)`C^a{~EETS08WRHt!!%9?HU=mF2{1a?fG8$( zai(dk4D3u$4zmo1WC;Q*Wkpu%3>IPl%d^d91qo?_wXmx}G^!!=GsvVN@mPU8e+UCZ zGY2V}f$n64x)b6K&LD9R#RVd`!G3|*08+_0PpXJDns^~ AJpcdz literal 0 HcmV?d00001 From 7ff885a0de9f9fc3b3f4c117dabc1bf636f07428 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Wed, 15 Jan 2020 19:25:50 -0500 Subject: [PATCH 28/59] Implement `MovieClipLoader` event broadcasts for `onLoadStart`, `onLoadProgress`, `onLoadComplete`, and `onLoadError`. Note that we do not implement `onLoadInit` yet - this requires some ability to trigger an event when another movie clip loads. --- core/src/avm1.rs | 43 ++++---- core/src/avm1/globals/movie_clip.rs | 1 + core/src/loader.rs | 162 ++++++++++++++++++++++------ core/src/player.rs | 16 +-- 4 files changed, 160 insertions(+), 62 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 7497d0ac0..0bd1245a6 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -295,38 +295,33 @@ impl<'gc> Avm1<'gc> { )); } - /// Add a stack frame that executes code in timeline scope for an event handler. - pub fn insert_stack_frame_for_avm_function( + /// Add a stack frame that executes code in timeline scope for an object + /// method, such as an event handler. + pub fn insert_stack_frame_for_method( &mut self, active_clip: DisplayObject<'gc>, + obj: Object<'gc>, swf_version: u8, context: &mut UpdateContext<'_, 'gc, '_>, name: &str, + args: &[Value<'gc>], ) { // Grab the property with the given name. // Requires a dummy stack frame. - let clip = active_clip.object().as_object(); - if let Ok(clip) = clip { - self.stack_frames.push(GcCell::allocate( - context.gc_context, - Activation::from_nothing( - swf_version, - self.globals, - context.gc_context, - active_clip, - ), - )); - let callback = clip - .get(name, self, context) - .and_then(|prop| prop.resolve(self, context)); - self.stack_frames.pop(); + self.stack_frames.push(GcCell::allocate( + context.gc_context, + Activation::from_nothing(swf_version, self.globals, context.gc_context, active_clip), + )); + let callback = obj + .get(name, self, context) + .and_then(|prop| prop.resolve(self, context)); + self.stack_frames.pop(); - // Run the callback. - // The function exec pushes its own stack frame. - // The function is now ready to execute with `run_stack_till_empty`. - if let Ok(callback) = callback { - let _ = callback.call(self, context, clip, &[]); - } + // Run the callback. + // The function exec pushes its own stack frame. + // The function is now ready to execute with `run_stack_till_empty`. + if let Ok(callback) = callback { + let _ = callback.call(self, context, obj, args); } } @@ -1658,6 +1653,7 @@ impl<'gc> Avm1<'gc> { context.player.clone().unwrap(), layer, fetch, + None, ); context.navigator.spawn_future(process); @@ -1734,6 +1730,7 @@ impl<'gc> Avm1<'gc> { context.player.clone().unwrap(), clip_target, fetch, + None, ); context.navigator.spawn_future(process); } diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index 27033b1b9..829a91b70 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -200,6 +200,7 @@ pub fn create_proto<'gc>( context.player.clone().unwrap(), DisplayObject::MovieClip(target), fetch, + None ); context.navigator.spawn_future(process); diff --git a/core/src/loader.rs b/core/src/loader.rs index 793b2eeab..cbd2ecd27 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -1,8 +1,8 @@ //! Management of async loaders -use crate::avm1::{Object, TObject}; +use crate::avm1::{Object, TObject, Value}; use crate::display_object::{DisplayObject, MorphShape, TDisplayObject}; -use crate::player::Player; +use crate::player::{Player, NEWEST_PLAYER_VERSION}; use crate::tag_utils::SwfMovie; use gc_arena::{Collect, CollectionContext}; use generational_arena::{Arena, Index}; @@ -66,10 +66,12 @@ impl<'gc> LoadManager<'gc> { player: Weak>, target_clip: DisplayObject<'gc>, fetch: Pin, Error>>>>, + target_broadcaster: Option>, ) -> Pin> + 'static>> { let loader = Loader::Movie { self_handle: None, target_clip, + target_broadcaster, }; let handle = self.add_loader(loader); @@ -116,6 +118,10 @@ pub enum Loader<'gc> { /// The target movie clip to load the movie into. target_clip: DisplayObject<'gc>, + + /// Event broadcaster (typically a `MovieClipLoader`) to fire events + /// into. + target_broadcaster: Option>, }, /// Loader that is loading form data into an AVM1 object scope. @@ -171,42 +177,132 @@ impl<'gc> Loader<'gc> { .expect("Could not upgrade weak reference to player"); Box::pin(async move { - let data = fetch.await?; - let movie = Arc::new(SwfMovie::from_data(&data)); - - player - .lock() - .expect("Could not lock player!!") - .update(|avm, uc| { - let loader = uc.load_manager.get_loader(handle); - let that = match loader { - Some(Loader::Movie { target_clip, .. }) => target_clip, - None => return Err("Loader expired during loading".into()), - _ => return Err("Non-movie loader spawned as movie loader".into()), + player.lock().expect("Could not lock player!!").update( + |avm, uc| -> Result<(), Error> { + let (clip, broadcaster) = match uc.load_manager.get_loader(handle) { + Some(Loader::Movie { + target_clip, + target_broadcaster, + .. + }) => (*target_clip, *target_broadcaster), + _ => unreachable!(), }; - let mut mc = that - .as_movie_clip() - .expect("Attempted to load movie into not movie clip"); - mc.replace_with_movie(uc.gc_context, movie.clone()); - mc.post_instantiation(uc.gc_context, *that, avm.prototypes().movie_clip); - - let mut morph_shapes = fnv::FnvHashMap::default(); - mc.preload(uc, &mut morph_shapes); - - // Finalize morph shapes. - for (id, static_data) in morph_shapes { - let morph_shape = MorphShape::new(uc.gc_context, static_data); - uc.library - .library_for_movie_mut(movie.clone()) - .register_character( - id, - crate::character::Character::MorphShape(morph_shape), - ); + if let Some(broadcaster) = broadcaster { + avm.insert_stack_frame_for_method( + clip, + broadcaster, + NEWEST_PLAYER_VERSION, + uc, + "onLoadStart", + &[Value::Object(broadcaster)], + ); + avm.run_stack_till_empty(uc)?; } Ok(()) - }) + }, + )?; + + let data = fetch.await; + if let Ok(data) = data { + let movie = Arc::new(SwfMovie::from_data(&data)); + + player + .lock() + .expect("Could not lock player!!") + .update(|avm, uc| { + let (clip, broadcaster) = match uc.load_manager.get_loader(handle) { + Some(Loader::Movie { + target_clip, + target_broadcaster, + .. + }) => (*target_clip, *target_broadcaster), + _ => unreachable!(), + }; + + if let Some(broadcaster) = broadcaster { + avm.insert_stack_frame_for_method( + clip, + broadcaster, + NEWEST_PLAYER_VERSION, + uc, + "onLoadProgress", + &[ + Value::Object(broadcaster), + data.len().into(), + data.len().into(), + ], + ); + avm.run_stack_till_empty(uc)?; + } + + let mut mc = clip + .as_movie_clip() + .expect("Attempted to load movie into not movie clip"); + + mc.replace_with_movie(uc.gc_context, movie.clone()); + mc.post_instantiation(uc.gc_context, clip, avm.prototypes().movie_clip); + + let mut morph_shapes = fnv::FnvHashMap::default(); + mc.preload(uc, &mut morph_shapes); + + // Finalize morph shapes. + for (id, static_data) in morph_shapes { + let morph_shape = MorphShape::new(uc.gc_context, static_data); + uc.library + .library_for_movie_mut(movie.clone()) + .register_character( + id, + crate::character::Character::MorphShape(morph_shape), + ); + } + + if let Some(broadcaster) = broadcaster { + avm.insert_stack_frame_for_method( + clip, + broadcaster, + NEWEST_PLAYER_VERSION, + uc, + "onLoadComplete", + &[Value::Object(broadcaster)], + ); + avm.run_stack_till_empty(uc)?; + } + + Ok(()) + }) + } else { + //TODO: Inspect the fetch error. + //This requires cooperation from the backend to send abstract + //error types we can actually inspect. + player.lock().expect("Could not lock player!!").update( + |avm, uc| -> Result<(), Error> { + let (clip, broadcaster) = match uc.load_manager.get_loader(handle) { + Some(Loader::Movie { + target_clip, + target_broadcaster, + .. + }) => (*target_clip, *target_broadcaster), + _ => unreachable!(), + }; + + if let Some(broadcaster) = broadcaster { + avm.insert_stack_frame_for_method( + clip, + broadcaster, + NEWEST_PLAYER_VERSION, + uc, + "onLoadError", + &[Value::Object(broadcaster), "LoadNeverCompleted".into()], + ); + avm.run_stack_till_empty(uc)?; + } + + Ok(()) + }, + ) + } }) } diff --git a/core/src/player.rs b/core/src/player.rs index aea0f2c45..809d70979 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -659,12 +659,16 @@ impl Player { } // Event handler method call (e.g. onEnterFrame) ActionType::Method { name } => { - avm.insert_stack_frame_for_avm_function( - actions.clip, - context.swf.header().version, - context, - name, - ); + if let Ok(clip) = actions.clip.object().as_object() { + avm.insert_stack_frame_for_method( + actions.clip, + clip, + context.swf.header().version, + context, + name, + &[], + ); + } } // Event handler method call (e.g. onEnterFrame) From a8545ee27763e6f9de3c6b77ca090f401b49993d Mon Sep 17 00:00:00 2001 From: David Wendt Date: Thu, 16 Jan 2020 14:20:07 -0500 Subject: [PATCH 29/59] Implement `onLoadInit` by pulling `LoadManager` into all clip load events. --- core/src/context.rs | 12 +++-- core/src/display_object/movie_clip.rs | 15 ++++++- core/src/loader.rs | 63 +++++++++++++++++++++++++++ core/src/player.rs | 20 ++++----- 4 files changed, 95 insertions(+), 15 deletions(-) diff --git a/core/src/context.rs b/core/src/context.rs index 9c3b72be9..8f71f0d6f 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -2,7 +2,7 @@ use crate::avm1; use crate::avm1::listeners::SystemListener; -use crate::avm1::Value; +use crate::avm1::{Object, Value}; use crate::backend::input::InputBackend; use crate::backend::{audio::AudioBackend, navigator::NavigatorBackend, render::RenderBackend}; use crate::library::Library; @@ -204,7 +204,11 @@ pub enum ActionType<'gc> { Init { bytecode: SwfSlice }, /// An event handler method, e.g. `onEnterFrame`. - Method { name: &'static str }, + Method { + object: Object<'gc>, + name: &'static str, + args: Vec>, + }, /// A system listener method, NotifyListeners { @@ -225,9 +229,11 @@ impl fmt::Debug for ActionType<'_> { .debug_struct("ActionType::Init") .field("bytecode", bytecode) .finish(), - ActionType::Method { name } => f + ActionType::Method { object, name, args } => f .debug_struct("ActionType::Method") + .field("object", object) .field("name", name) + .field("args", args) .finish(), ActionType::NotifyListeners { listener, diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index aa64c1e71..796ec0791 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -950,11 +950,24 @@ impl<'gc> MovieClipData<'gc> { context.action_queue.queue_actions( self_display_object, context.root, - ActionType::Method { name }, + ActionType::Method { + object: self.object.unwrap(), + name, + args: vec![], + }, event == ClipEvent::Unload, ); } } + + // Finally, queue any loaders that may be waiting for this event. + if let ClipEvent::Load = event { + context.load_manager.movie_clip_on_load( + self_display_object, + context.root, + context.action_queue, + ); + } } } diff --git a/core/src/loader.rs b/core/src/loader.rs index cbd2ecd27..8439c1ce2 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -1,6 +1,7 @@ //! Management of async loaders use crate::avm1::{Object, TObject, Value}; +use crate::context::{ActionQueue, ActionType}; use crate::display_object::{DisplayObject, MorphShape, TDisplayObject}; use crate::player::{Player, NEWEST_PLAYER_VERSION}; use crate::tag_utils::SwfMovie; @@ -81,6 +82,28 @@ impl<'gc> LoadManager<'gc> { loader.movie_loader(player, fetch) } + /// Indicates that a movie clip has initialized (ran it's first frame). + /// + /// Interested loaders will be invoked from here. + pub fn movie_clip_on_load( + &mut self, + loaded_clip: DisplayObject<'gc>, + root: DisplayObject<'gc>, + queue: &mut ActionQueue<'gc>, + ) { + let mut invalidated_loaders = vec![]; + + for (index, loader) in self.0.iter_mut() { + if loader.movie_clip_loaded(loaded_clip, root, queue) { + invalidated_loaders.push(index); + } + } + + for index in invalidated_loaders { + self.0.remove(index); + } + } + /// Kick off a form data load into an AVM1 object. /// /// Returns the loader's async process, which you will need to spawn. @@ -339,4 +362,44 @@ impl<'gc> Loader<'gc> { }) }) } + + /// Event handler morally equivalent to `onLoad` on a movie clip. + /// + /// Returns `true` if the loader has completed and should be removed. + /// + /// Used to fire listener events on clips and terminate completed loaders. + pub fn movie_clip_loaded( + &mut self, + loaded_clip: DisplayObject<'gc>, + root: DisplayObject<'gc>, + queue: &mut ActionQueue<'gc>, + ) -> bool { + let (clip, broadcaster) = match self { + Loader::Movie { + target_clip, + target_broadcaster, + .. + } => (*target_clip, *target_broadcaster), + _ => return false, + }; + + if DisplayObject::ptr_eq(loaded_clip, clip) { + if let Some(broadcaster) = broadcaster { + queue.queue_actions( + clip, + root, + ActionType::Method { + object: broadcaster, + name: "broadcastMessage", + args: vec!["onLoadInit".into(), loaded_clip.object()], + }, + false, + ); + } + + true + } else { + false + } + } } diff --git a/core/src/player.rs b/core/src/player.rs index 809d70979..c3f22816a 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -658,17 +658,15 @@ impl Player { ); } // Event handler method call (e.g. onEnterFrame) - ActionType::Method { name } => { - if let Ok(clip) = actions.clip.object().as_object() { - avm.insert_stack_frame_for_method( - actions.clip, - clip, - context.swf.header().version, - context, - name, - &[], - ); - } + ActionType::Method { object, name, args } => { + avm.insert_stack_frame_for_method( + actions.clip, + object, + context.swf.header().version, + context, + name, + &args, + ); } // Event handler method call (e.g. onEnterFrame) From a9621da47d39929e5598d30980a3144670b4cb59 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Thu, 16 Jan 2020 19:20:45 -0500 Subject: [PATCH 30/59] Add tests for `loadMovie`. This test also includes changes to the SWF testing environment to allow asynchronous movie loads to execute. --- core/src/backend/navigator.rs | 169 ++++++++++++++++++++-- core/tests/regression_tests.rs | 13 +- core/tests/swfs/avm1/loadmovie/output.txt | 2 + core/tests/swfs/avm1/loadmovie/target.fla | Bin 0 -> 20992 bytes core/tests/swfs/avm1/loadmovie/target.swf | Bin 0 -> 68 bytes core/tests/swfs/avm1/loadmovie/test.fla | Bin 0 -> 40448 bytes core/tests/swfs/avm1/loadmovie/test.swf | Bin 0 -> 142 bytes 7 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 core/tests/swfs/avm1/loadmovie/output.txt create mode 100644 core/tests/swfs/avm1/loadmovie/target.fla create mode 100644 core/tests/swfs/avm1/loadmovie/target.swf create mode 100644 core/tests/swfs/avm1/loadmovie/test.fla create mode 100644 core/tests/swfs/avm1/loadmovie/test.swf diff --git a/core/src/backend/navigator.rs b/core/src/backend/navigator.rs index 4ddf81df5..36ec31627 100644 --- a/core/src/backend/navigator.rs +++ b/core/src/backend/navigator.rs @@ -1,8 +1,13 @@ //! Browser-related platform functions -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; +use std::fs; use std::future::Future; +use std::path::{Path, PathBuf}; use std::pin::Pin; +use std::ptr::null; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; use swf::avm1::types::SendVarsMethod; pub type Error = Box; @@ -76,6 +81,10 @@ impl RequestOptions { } } +/// Type alias for pinned, boxed, and owned futures that output a falliable +/// result of type `Result`. +pub type OwnedFuture = Pin> + 'static>>; + /// A backend interacting with a browser environment. pub trait NavigatorBackend { /// Cause a browser navigation to a given URL. @@ -108,11 +117,7 @@ pub trait NavigatorBackend { ); /// Fetch data at a given URL and return it some time in the future. - fn fetch( - &self, - url: String, - request_options: RequestOptions, - ) -> Pin, Error>>>>; + fn fetch(&self, url: String, request_options: RequestOptions) -> OwnedFuture, Error>; /// Arrange for a future to be run at some point in the... well, future. /// @@ -122,15 +127,144 @@ pub trait NavigatorBackend { /// /// TODO: For some reason, `wasm_bindgen_futures` wants unpinnable futures. /// This seems highly limiting. - fn spawn_future(&mut self, future: Pin> + 'static>>); + fn spawn_future(&mut self, future: OwnedFuture<(), Error>); +} + +/// A null implementation of an event loop that only supports blocking. +pub struct NullExecutor { + /// The list of outstanding futures spawned on this executor. + futures_queue: VecDeque>, + + /// The source of any additional futures. + channel: Receiver>, +} + +unsafe fn do_nothing(_data: *const ()) {} + +unsafe fn clone(_data: *const ()) -> RawWaker { + NullExecutor::raw_waker() +} + +const NULL_VTABLE: RawWakerVTable = RawWakerVTable::new(clone, do_nothing, do_nothing, do_nothing); + +impl NullExecutor { + /// Construct a new executor. + /// + /// The sender yielded as part of construction should be given to a + /// `NullNavigatorBackend` so that it can spawn futures on this executor. + pub fn new() -> (Self, Sender>) { + let (send, recv) = channel(); + + ( + Self { + futures_queue: VecDeque::new(), + channel: recv, + }, + send, + ) + } + + /// Construct a do-nothing raw waker. + /// + /// The RawWaker, because the RawWaker + /// interface normally deals with unchecked pointers. We instead just hand + /// it a null pointer and do nothing with it, which is trivially sound. + fn raw_waker() -> RawWaker { + RawWaker::new(null(), &NULL_VTABLE) + } + + /// Copy all outstanding futures into the local queue. + fn flush_channel(&mut self) { + for future in self.channel.try_iter() { + self.futures_queue.push_back(future); + } + } + + /// Poll all in-progress futures. + /// + /// If any task in the executor yields an error, then this function will + /// stop polling futures and return that error. Otherwise, it will yield + /// `Ok`, indicating that no errors occured. More work may still be + /// available, + pub fn poll_all(&mut self) -> Result<(), Error> { + self.flush_channel(); + + let mut unfinished_futures = VecDeque::new(); + let mut result = Ok(()); + + while let Some(mut future) = self.futures_queue.pop_front() { + let waker = unsafe { Waker::from_raw(Self::raw_waker()) }; + let mut context = Context::from_waker(&waker); + + match future.as_mut().poll(&mut context) { + Poll::Ready(v) if v.is_err() => { + result = v; + break; + } + Poll::Ready(_) => continue, + Poll::Pending => unfinished_futures.push_back(future), + } + } + + for future in unfinished_futures { + self.futures_queue.push_back(future); + } + + result + } + + /// Check if work remains in the executor. + pub fn has_work(&mut self) -> bool { + self.flush_channel(); + + !self.futures_queue.is_empty() + } + + /// Block until all futures complete or an error occurs. + pub fn block_all(&mut self) -> Result<(), Error> { + while self.has_work() { + self.poll_all()?; + } + + Ok(()) + } } /// A null implementation for platforms that do not live in a web browser. -pub struct NullNavigatorBackend {} +/// +/// The NullNavigatorBackend includes a trivial executor that holds owned +/// futures and runs them to completion, blockingly. +pub struct NullNavigatorBackend { + /// The channel upon which all spawned futures will be sent. + channel: Option>>, + + /// The base path for all relative fetches. + relative_base_path: PathBuf, +} impl NullNavigatorBackend { + /// Construct a default navigator backend with no async or fetch + /// capability. pub fn new() -> Self { - NullNavigatorBackend {} + NullNavigatorBackend { + channel: None, + relative_base_path: PathBuf::new(), + } + } + + /// Construct a navigator backend with fetch and async capability. + pub fn with_base_path>( + path: P, + channel: Sender>, + ) -> Self { + let mut relative_base_path = PathBuf::new(); + + relative_base_path.push(path); + + NullNavigatorBackend { + channel: Some(channel), + relative_base_path, + } } } @@ -151,15 +285,20 @@ impl NavigatorBackend for NullNavigatorBackend { fn fetch( &self, - _url: String, + url: String, _opts: RequestOptions, ) -> Pin, Error>>>> { - Box::pin(async { Err("Fetch IO not implemented".into()) }) + let mut path = self.relative_base_path.clone(); + path.push(url); + + Box::pin(async move { fs::read(path).map_err(|e| e.into()) }) } - fn spawn_future( - &mut self, - _future: Pin> + 'static>>, - ) { + fn spawn_future(&mut self, future: OwnedFuture<(), Error>) { + self.channel + .as_ref() + .expect("Expected ability to execute futures") + .send(future) + .unwrap(); } } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 098bde10c..bfcff97ef 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -4,12 +4,13 @@ use approx::assert_abs_diff_eq; use log::{Metadata, Record}; +use ruffle_core::backend::navigator::{NullExecutor, NullNavigatorBackend}; use ruffle_core::backend::{ - audio::NullAudioBackend, input::NullInputBackend, navigator::NullNavigatorBackend, - render::NullRenderer, + audio::NullAudioBackend, input::NullInputBackend, render::NullRenderer, }; use ruffle_core::Player; use std::cell::RefCell; +use std::path::Path; type Error = Box; @@ -159,6 +160,7 @@ swf_tests! { (define_function2_preload, "avm1/define_function2_preload", 1), (define_function2_preload_order, "avm1/define_function2_preload_order", 1), (mcl_as_broadcaster, "avm1/mcl_as_broadcaster", 1), + (loadmovie, "avm1/loadmovie", 2), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. @@ -271,19 +273,24 @@ fn test_swf_approx( fn run_swf(swf_path: &str, num_frames: u32) -> Result { let _ = log::set_logger(&TRACE_LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info)); + let base_path = Path::new(swf_path).parent().unwrap(); let swf_data = std::fs::read(swf_path)?; + let (mut executor, channel) = NullExecutor::new(); let player = Player::new( Box::new(NullRenderer), Box::new(NullAudioBackend::new()), - Box::new(NullNavigatorBackend::new()), + Box::new(NullNavigatorBackend::with_base_path(base_path, channel)), Box::new(NullInputBackend::new()), swf_data, )?; for _ in 0..num_frames { player.lock().unwrap().run_frame(); + executor.poll_all().unwrap(); } + executor.block_all().unwrap(); + Ok(trace_log()) } diff --git a/core/tests/swfs/avm1/loadmovie/output.txt b/core/tests/swfs/avm1/loadmovie/output.txt new file mode 100644 index 000000000..5b75abd83 --- /dev/null +++ b/core/tests/swfs/avm1/loadmovie/output.txt @@ -0,0 +1,2 @@ +Loading movie +Child movie loaded! diff --git a/core/tests/swfs/avm1/loadmovie/target.fla b/core/tests/swfs/avm1/loadmovie/target.fla new file mode 100644 index 0000000000000000000000000000000000000000..3b639cffc5e43da0805302853a75ff565ba05ba5 GIT binary patch literal 20992 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadmovie/target.swf b/core/tests/swfs/avm1/loadmovie/target.swf new file mode 100644 index 0000000000000000000000000000000000000000..1a1a83455cade7036d1025e2bc10ef0cb88d2e36 GIT binary patch literal 68 zcmZ<@59V-TU|^_VV2x*B;9tPNz{AMkA_x>=aAx}d|Gz!66p%Sh6v)lU%t=wm%`eML TRmjOtOi4{qWKd&p0LlUY&fpHG literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadmovie/test.fla b/core/tests/swfs/avm1/loadmovie/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..d6cae71a08bf644760c38ee73df62a245aa11e26 GIT binary patch literal 40448 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 literal 0 HcmV?d00001 From 82d305a0f527263dc47c9a9ef919109a6f432ed8 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Thu, 16 Jan 2020 19:33:09 -0500 Subject: [PATCH 31/59] Add test for `loadmovienum` --- core/tests/regression_tests.rs | 1 + core/tests/swfs/avm1/loadmovienum/output.txt | 2 ++ core/tests/swfs/avm1/loadmovienum/target.fla | Bin 0 -> 20992 bytes core/tests/swfs/avm1/loadmovienum/target.swf | Bin 0 -> 68 bytes core/tests/swfs/avm1/loadmovienum/test.fla | Bin 0 -> 21504 bytes core/tests/swfs/avm1/loadmovienum/test.swf | Bin 0 -> 142 bytes 6 files changed, 3 insertions(+) create mode 100644 core/tests/swfs/avm1/loadmovienum/output.txt create mode 100644 core/tests/swfs/avm1/loadmovienum/target.fla create mode 100644 core/tests/swfs/avm1/loadmovienum/target.swf create mode 100644 core/tests/swfs/avm1/loadmovienum/test.fla create mode 100644 core/tests/swfs/avm1/loadmovienum/test.swf diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index bfcff97ef..1db2c32a9 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -161,6 +161,7 @@ swf_tests! { (define_function2_preload_order, "avm1/define_function2_preload_order", 1), (mcl_as_broadcaster, "avm1/mcl_as_broadcaster", 1), (loadmovie, "avm1/loadmovie", 2), + (loadmovienum, "avm1/loadmovienum", 2), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/loadmovienum/output.txt b/core/tests/swfs/avm1/loadmovienum/output.txt new file mode 100644 index 000000000..5b75abd83 --- /dev/null +++ b/core/tests/swfs/avm1/loadmovienum/output.txt @@ -0,0 +1,2 @@ +Loading movie +Child movie loaded! diff --git a/core/tests/swfs/avm1/loadmovienum/target.fla b/core/tests/swfs/avm1/loadmovienum/target.fla new file mode 100644 index 0000000000000000000000000000000000000000..3b639cffc5e43da0805302853a75ff565ba05ba5 GIT binary patch literal 20992 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadmovienum/target.swf b/core/tests/swfs/avm1/loadmovienum/target.swf new file mode 100644 index 0000000000000000000000000000000000000000..1a1a83455cade7036d1025e2bc10ef0cb88d2e36 GIT binary patch literal 68 zcmZ<@59V-TU|^_VV2x*B;9tPNz{AMkA_x>=aAx}d|Gz!66p%Sh6v)lU%t=wm%`eML TRmjOtOi4{qWKd&p0LlUY&fpHG literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadmovienum/test.fla b/core/tests/swfs/avm1/loadmovienum/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..d10618bf14f4245402545520b3ab6fd481856232 GIT binary patch literal 21504 zcmeHPUyNK;8NbT{g=&Ff9K>eHf!LlK9{Q#>DUjk3I2#{KNVWe(}p2{+2|quP4LFr|rv=OLNzU@cAN7UzH>m<1?>) z+HSXbc^CyBKY#rXN#IbjfPX7VEqN@N$NN%pic8t|>zd?}u;$c-#k1f4*4d>$tpD-N zhxSbDE5f|H_V(G|wca1Pi_l>1RO zqj1%>qSR5gp=?Ka0OdiH9Vic>JdE-+l$|J#pnM%=7s_swJt&W&jG|B_9z)rS@;D0B zcR$KEP@X_Jfbt{?dHH{N{U`1Jg-c^ednmsJj!9 zeH}a$%PVNLg!-vu4z-K;^a#pIG7D-MS;G`+8)(C9^D&?-;?pTabdNUb`f*H;acgo9 zFgAyA<{U3sucwj|z+#^jjn8ay1*0t|uO=z3vi}zKxA_(SQUXJ{OZm?fFv}OvXw3nD zp8_-|xP@J)2M6(won5aXdq)>`TXT`W4!YNCm+KnUQw!E5qhkwGCyqDfSEw4-UYgI9 z$4=7ulaXeKV2+bB|>l(L-N1)@K5cLl4LjIWHLJh zJNp^l_%SQC-0WxCaPHhWOoUN-_&xl=#^lrX`Z+Am40LA-+H*iA)JP_v`n+NWg;qnG z*>=M?v}*x%6EK+u{;eQ&ei$k86h0k=c{P&5==UPqUc3cHu> z-k-xJp%)*^kSZlQjm^q-Y*3ychVihei1avQk@mzPAC$)m{uSg!yqk(h{j9)C;%g@3 zW@8bfO1^Z>)ELf2>vMClzsVzY>)T5@^O%)e_cddfR|@(B=D9-Ljqw_>#fzw)!9O1o z#&M0>Mtz}Vrn4Dc9ru4mTJRKFt-wWd<&R~pBmH_B8sCfSC#4fp(26V=jDdsWnKU9T zDMQgw)WNt=t@&I$_YZ2G2 z!dHuPskV8O9qsjylhYpPEfbjq@glD~FskmE6Z{*#Mu_}Kd6b-h9PC(I8fY_LMRX)T%uj;!BCG00D;D8>W(Lp* zYmX5}iYry+_oxk)T}?r*bI97b37Wy^ls1pp!z`j0U4{HKd=?S8{-=bpjeoC<^^aOl z-J?`as-0FYil|k((LmotAaE{4Zz=taB4HI;mUq>Wt!^vHyQ+}#d`-Zz_9L4P5{>3q zuAR&g=o`6S-t*>S{k_F???=a&qjDR@O|#{)72a)Aaf~VrAR0H^+>nzWuT5Itj#zq7 zJB1~1!zjSb;RO1eK(A>RPt-5++*R_=d2D<% zTc{VBV`RhqAoz^DVg*-$%sl8-%PYz?98P6D1xkf>% zTZ9romBYZNMEba z={xAtEH`};e9ptNf*&}&aLhMN84XU#U; zg4?1Kf`<^tapjics+If5{g@ZIUeZkIcLMK`#L98}0m>flUTmMK;V5LsEQv#a*V2TmVQ<<%jIcH-A-$&th=X|7*M`;3_q7VD{E{(Z~XokoL$P3MgMQ` zwac_=%;l{ed*j=RdQ~p@K2CWC!YiKOb;&v+|5ol#^8T$0*5=|&`_YnKJMN0CA!<{pr1iJ_7ctj z&024aGo<*8fTvh~?^MHlxgVfc4E7$`X_dVXuKVTMdZFGMA5PVQW0c$@q(wCRRe6AwbqEPpvHtz*W<3?|F6b0@kT zy(#I=6Mdeg@g^$I0(72FJ&wF%l?I!I%>cp9#Oy9su+9jrTzm7rwregK#euRMZCp9p zDcpuOTR#<9t9(ba=KcHBFu!QM`$6!##DKAio#rg^MxL8hSkMLW?RFmAzeUX^FT+b> zb_4Drd)*5uKEUev=(rCO#epizvxqef-G5c5xYN+zup&{ct-?BtzWIH$=P`HY{p=|2 zejZ8e&KM)l5u6h+a&WiFeaa+cb_Djkh<{_+k57VTR%)k>N5Cb|t9e?V<>5!5)&5Mp zhTQ%L5a;pNWR}XeoRGip_q&eZp-a=9Ba!ypr_*6~8r!jdJ&bY58Rcbr9$Cve@#>vZr^AcFN^mssEjFKi1!Qk`v|@-4XHY)GLRn zw2N!I-4XdNbjZ$4?995b>X9d^;(*zhogvT~`N?a3&N{E}&IM0xihT>5++EBosyI0) zt?Y{*>e3al)*b@yKCcVw5ot-)o;KSTWzQdI4;bN;>#X00a&>v4tCKtO9QmYj6w49j z;ae85GK|ec>au?sh&OtmlkvUFVLorMog7B_TDetG;&z_Vz<+)7IgO2leKJ!AEGWV}Z9{Ib1?DrshZ-}+bauKcs_YTaujsqh%QisgaiL5vAiU zZ+?|%g|h)D(T@lFOznD?EUGP43C50cW4>cKEac_e#Xa>{+6FL*@Qr5A=^u z4;ojwCm16MmZNmft(bd;dr8CeYKqjGig|3g-{_9ab?$!BS@y(P@#HRN;gni*$F(f0 zvPi-f*={b5irFb@w^Nlebf4|=^f>p7Y!n6h@o3z?XHT{I`C29^D_I5RBJ`~IY@FpJ zVMSJf=}&+BPRVl$%qzR?l=uW2b1gk1bx%>7s=P1P&Lms4KMFI7_5oZN(1pXr5QHDtiH~|UpNw_`0rgyg|)g2nY^k7~@&)6r)zwnh0ej#sk zbYX5`X{?c6%;)}5^cp@rAX#VM@#G_Xr-FU))QXXtMK(RTPh`uJFNlP5v&JV$;=8wE z!$H-BJmQh-Lf#2d$aNu)f)sLH$a_Hwxh~{=AcdR;Wc5vjyum}ILgqkLE9C7Ug`5hR z;|e(y@@|kqhRnn>WydMhF|uAZ{bDV?UX0o^=xef4q34#kPg|`w%GSrCOVQ_BPTkNzxK$fkl*uA zsgTbESvlk&I)e(LBS|~zCc0Lug<8JObrCGS-;G*|&Zb`{`SU=gB&mc}+a&p;Knj_X zwAwan7ePAFkv!{b>q0&gq#@)t11aR!0_l*0!4isV~hl3&{cOb{K( zw*x6;4x}l-$WY5QGE&G~BO`^(H8L_Fi$oki-*-`KHYjASk<|*B14$atK{Tcv87XAi zk&ywptLU7Cdf^lewi#;oQLs%#t93v&QqgK-AQi1f0;y=V6{J=lst+c2fYgjy0a>*z zA+Pm~TS8`7v|1tG97rK^AgdMfW{^V0DEOZQQplGDQpi^YQpnc@QpmMH3Yi0$<`jj4 zVKC5mkJ_f9)!$L;M8{Z0-7#q>T5SlVqSZ(s6|J^ou{CZ}(TZ_jtKSmxKR`Os8M15K z67o9Vwg<2Ku@rFx$;(UJTUNGCdy z@1v&^9Z9Z{3CE=Y4yq+&=4L`}37PsV2IxF~HQ{}H4U9Rn`c$Y~u(+L03-Nh-gf z+78)637Mi3awHrm9r|(#-9^XRU-?=%29S*VCM!iN=4K`-^^n($k#pD#k>YRdUNA zJLlP3LYJDz_&5Ee{Q{-*J^ne}MEE}2MwHgeT2;zhzoekH;GumNIB7r6x%`kYXUW^2 zIWuzT7D7KB!zK6)%E#d5RbY(Z*Mqt(OLlFD=OzQ3nAj;FzeO~GQxiVlZl_1f;ASYf zDj9wH@SYRXM;9g z1)e?=*S#F#yE~IzupmCNQge@a`A0e*!S8}>!Ec0Y#(NEunb%_vya~#KM%F%qBh9_~ z(SrxnW?9EWoIGz3^3bWFcJug5j9vIuv$gnzGv2i3R|$AX6^n^2)}n00001ZoU349jb~usU% Date: Thu, 16 Jan 2020 19:37:38 -0500 Subject: [PATCH 32/59] Add a test for `MovieClip.loadMovie`. --- core/tests/regression_tests.rs | 1 + .../tests/swfs/avm1/loadmovie_method/output.txt | 2 ++ .../tests/swfs/avm1/loadmovie_method/target.fla | Bin 0 -> 20992 bytes .../tests/swfs/avm1/loadmovie_method/target.swf | Bin 0 -> 68 bytes core/tests/swfs/avm1/loadmovie_method/test.fla | Bin 0 -> 21504 bytes core/tests/swfs/avm1/loadmovie_method/test.swf | Bin 0 -> 150 bytes 6 files changed, 3 insertions(+) create mode 100644 core/tests/swfs/avm1/loadmovie_method/output.txt create mode 100644 core/tests/swfs/avm1/loadmovie_method/target.fla create mode 100644 core/tests/swfs/avm1/loadmovie_method/target.swf create mode 100644 core/tests/swfs/avm1/loadmovie_method/test.fla create mode 100644 core/tests/swfs/avm1/loadmovie_method/test.swf diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 1db2c32a9..8244a1d37 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -162,6 +162,7 @@ swf_tests! { (mcl_as_broadcaster, "avm1/mcl_as_broadcaster", 1), (loadmovie, "avm1/loadmovie", 2), (loadmovienum, "avm1/loadmovienum", 2), + (loadmovie_method, "avm1/loadmovie_method", 2), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/loadmovie_method/output.txt b/core/tests/swfs/avm1/loadmovie_method/output.txt new file mode 100644 index 000000000..5b75abd83 --- /dev/null +++ b/core/tests/swfs/avm1/loadmovie_method/output.txt @@ -0,0 +1,2 @@ +Loading movie +Child movie loaded! diff --git a/core/tests/swfs/avm1/loadmovie_method/target.fla b/core/tests/swfs/avm1/loadmovie_method/target.fla new file mode 100644 index 0000000000000000000000000000000000000000..3b639cffc5e43da0805302853a75ff565ba05ba5 GIT binary patch literal 20992 zcmeHPU5s5t9iL?@MdYhcKBR!RfUqE7+uK5w3e>H&#Y&fM@hicw?(W&GbU)k==@O%j z#>59rOiYyfR^Lby5>0q8F=`+Ca1)G?ga;BKCdM~-^g+4)e!u_B+;isMd(OEFBoaC8 z?B4&GncvL(zh}<5`{!4-z47bc-u$LZu1DOcd#CeJcWLha2tHpD=xdzYg3rA7PN&o1 z?NJnb{P6k!ao|6gE9Lj$_5aC%6K)Cr)?CAV!7bwVirb(m*S>CWA1T;;osPj@)M;C% zOtDmcObBLeUx=ThFlY$KDbY)CCH*PH_+?1Qc=nc2KPS>6`S0N&XvH82F!GIgb z{Q3t}4|RJp`MN|;gzWcw9s`H zKT)7Ic}Ld$Og-veH=)AsBzzO_;Zc-#I&l~A=Hu2L_ikk#GUcY>m&f28#@)O-1Y9s}G4;8p;>n_Vca zX((womz0E8$Yx(y}mHJxJK2u@zQ*(B)cXhp+(8U zN6)}&3OkR?&#(To@lA#hUJNgejYE+WiUezM1q6ygE#2RM5n5?0$|fwE1e8(|kXIZI z^k}%riopb1#d+krup8-cFZ{;sv&BLUB808P+s(i3#T{IaF;2Sq5!l(!@QWY2#Z=K| zO%`3aZ~+S;i$`C^j}Z>O?KH$QCu7=h$6z=V*Q8t49a3{_HL^>?zrfEsC)~RG7L4nZ zTZQ3$5x+Ax0h?GtFWSbM%x~3w7XPRc9AQl<+!LS{Kyk4sjUv(s_k{bZ`_e$HRpHZ^ z_qcuTLA11);pfM{0@P`Yv4rtC?m?`f=^7e;+C7VLEs5iN)-dbUFo)ZVb2x(91_JJQyN(C%B1%pCri{!$C5mkXdx4|5W`ttc^*Qihh~_#l}r zVijJGw{pA;^g4R1X+}lFDvdgZ71462Lt6i>#r}14=-aZ4V5va5xrk9Fk*{K{Gnn~2 zXE3@CmLPKY~t>!_R{s+_W+Hu9%(Ky6!3>*0=UapwbUfB1d7@hcFj% zOY|zxZXCiI5rO34^OnMAzx%``PXz#EHtDfIQ&87$Fa)6(L$I4kl-#M2@ zh)GzI9WhA*4I$xVm9EehDZwde-ZG@J0%{KYl|^1QhS+cqWVQ#o&41wT9YwBGjgVpW zQ{d{n`@HxYbO%UVc6(fWz9H2cTEYUXa|JV8#lJ?*??oA3pY~#GDUDi**4Ob7-4z76 z(RBZp6NFu7gWnF1c?5H^h6!lxbOvB%4H| za_wX6q3@$*2QQY3kUNX(uF{fOc!_1qjycdIBz+VaRg`jLIIM2A({%gY-h8%`tZa4(mp{jfT&P0f+$^J<78<<}EqhV% zmj{E9ze&WZr%Q}0_3dF9JVQr4)fBWcZVxLtS(e;iV zVt7PgJAw)?l*HJwZ0WG#fMt=&qs@;cwoxuiiy)j6Ij*R;Z4WH`e&nsRmIl18d8Gz& zlaO~qZ<2YIkJ2};COTK&H&V{Dyn>uRQIt(N+EK}sQeh;etZ507agwb*4!&?tIw<=- zgqox5y>+j92s@(v$XP#w7>qkTzO2U!U9#Wee@{yrjTXkZ66cn1tXh}3D_+(bHxL1+ ziR(g>uf4a(u`0A9;5u#Xd2bc;rzIY6&v5AGTbb?A=BSTh^>*hd;`c#V<`h~`X591V zq;^djH%_9KxenVBDe^E`_2z82zL#d!A_*q3rp~y`z@Ni;L50WI1ajD;RUjueUT4mh zX+>@))>LklJ-g2xMI9>dU2pGLKN@`rTC)NEQ|rB_p%(2d&Z$1$NocN}odAVMiN~r! z>gHg3;AQN*Np_s<9_mQxJD5p*9kyd^%sBBs#jJsGD8Z?tN!^_s=3)6q zB8ze@8CJ68Xv0gkidQudZ%JcMx?|>fP1uPwV0X*~{fV7%Eb%E8{WC?n!RLOK{f3Ac zxu5v#s;J(L;6&JLmfO1zrZ-+3!pCs8swkc89_r|z?Sv{~*{-7Bu7}%ycV975qW@(X z{Dkt84ZO}|(%eY2qu&`qxdr6CZr?mP@!?haZ@M@KowYNgQ<`bim77!DCvSRBym?y1 zOvqv>dNcm@^gRx%usz2~*$DG;?mCzQTNaqx*Co5&Dm^~}tlTNw=wA6x%wkA;8IWZp zXM}aME97AH<1l+@$A8c8y4>~C722C*CFR;yvX4smS{~mAN+-zxtF)D;^E~s9`gvVH z{S5p2?s-9M-?I}t37uU=2F|l_+hfZn6E9Vzed%tbkIb(q%H;u_7yC1sBtBG)X1;eu zbLNw+nLI~^+?Vlrmp!E@&ae0?Tv$)f>H8*5OnPI9ovDT~a_6=KPKxKcl$ABF#VxOFUP(y`O+4*=}GKPvDrx z@JUB_9Wt&g@ge25PwAdXpVE!;sro+JabjuZSQa-Hb?U($QUD6KUMVlRbJq@JQnjio`2Z0LVgN%l(SW?C9#z%^xC+| z)1fNdO25LnCOJ$U%jrFbjeJdZX5|_dbgA^|ax%ZF-6NmZ(~g!P8)irgu!!8LUr$V; zw(x1T5zs~Pq_ z%YK|PPD<)LF`dnyHJHS1O?4};DK#;A-j2y<_Iy1rJoo9Oo*7fJD?X>N86LyV`rW*{WbX^v^Fw>OIg6~BZ(8Nv7#ecz z`F9rV=_cQDp`?{3CH7I}4+r@Co;O_bzHC6GOx985MT#DbyW|Xf!8#t>@9x9NLnsg9 znPK?M-w)$W(jvVtq4y?qGokk+^zMYl9FlyePV}CGW@j;T%-QmQ9hkMpScz@c8H<@h zg`Z5MQRe07h1QWP@tv%LK<>UXORbVuOLjnIXdyf0x!`Fpow$0?`&-PJ_0CDc@jB!6o(fn*eK%{EfmU?imJKVg zGe-CuUgmZ6zPWS%#yx(?Y~lX4}cVMQ^?~Wh1?YKVUR*@3i${~A!iX; zeKR3%4^WwqIgr%~c^^n2XF}$S8lQ<|#@ZQ> z*0)9SX^@_EB)_d>sU+H8FQK>e;P^k zB`2-6#oA{VZWn@r7rj@-Y!znVS)oP(JFEz$*PdaPNTC&c#(Iokc zNMkKNgjwJ1Yurr8FNMCD zkSQZ;EM&^aNRJ#y&6#%8k97L%E(9|Q$|KcWRZvi==&SgY9RPFkd%?t3Yi0m8_+@2 z(vFN2GVRF7h}@TT-a-R&iU!+?wMWR{V zirNubwQV6^9vZiW%&=&+LcT4MLgqkLE96}ug^W?~-$hc$mqk*@*F;jtH$_s&jYtZa z0|^$)(Q11n6|Kf1sc5wi!q&JgMJvXANxv=R ze}J^*`#9(uw}pIV=-U?Zjz|i5ERsT|jD*|~@;^ZuCp~hg6*6UHjfG4Z87XAS$VefR zbt19y0_&vSRN#J$Z7Ev4idxS)2B6m?b9{96KDMQ3H5NQ@OVNsPU*onEt(cp=NRhRL z%oM`2j>vyUQ%$4ok$vB`kSQZUwS`O>87XAuWgyWbHfHl2Fvjo~ar%j+dr#lFAL1;{o)z%>HIq0w;rCAL zoM{!a8FAOR@h49nT%S3$G&#Grc4~1>e;)~>VX`CLnea%(>5l=}MCM1rAq(c-b9EdNM;~{=YVOH%H(T^C9x<~N0hYn!0C4F^B%z$Ha6dtyQSE5#9n*;b%wU)?l2|^|lZoMzR{|imr{A&OJ literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadmovie_method/target.swf b/core/tests/swfs/avm1/loadmovie_method/target.swf new file mode 100644 index 0000000000000000000000000000000000000000..1a1a83455cade7036d1025e2bc10ef0cb88d2e36 GIT binary patch literal 68 zcmZ<@59V-TU|^_VV2x*B;9tPNz{AMkA_x>=aAx}d|Gz!66p%Sh6v)lU%t=wm%`eML TRmjOtOi4{qWKd&p0LlUY&fpHG literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadmovie_method/test.fla b/core/tests/swfs/avm1/loadmovie_method/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..21c33acbe2ab4061c8250a50e936faca1a425092 GIT binary patch literal 21504 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 E;U3gJjQ{`u literal 0 HcmV?d00001 From 8ef4a94672bd713617d04c351a95d0e01cba50c9 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Thu, 16 Jan 2020 22:20:11 -0500 Subject: [PATCH 33/59] Add new tests for `unloadMovie`, `unloadMovieNum`, and `MovieClip.unloadMovie`. They currently fail in Ruffle, so they're ignored. --- core/tests/regression_tests.rs | 3 +++ core/tests/swfs/avm1/unloadmovie/output.txt | 3 +++ core/tests/swfs/avm1/unloadmovie/target.fla | Bin 0 -> 39936 bytes core/tests/swfs/avm1/unloadmovie/target.swf | Bin 0 -> 121 bytes core/tests/swfs/avm1/unloadmovie/test.fla | Bin 0 -> 40960 bytes core/tests/swfs/avm1/unloadmovie/test.swf | Bin 0 -> 168 bytes .../swfs/avm1/unloadmovie_method/output.txt | 3 +++ .../swfs/avm1/unloadmovie_method/target.fla | Bin 0 -> 39936 bytes .../swfs/avm1/unloadmovie_method/target.swf | Bin 0 -> 121 bytes .../tests/swfs/avm1/unloadmovie_method/test.fla | Bin 0 -> 40960 bytes .../tests/swfs/avm1/unloadmovie_method/test.swf | Bin 0 -> 188 bytes core/tests/swfs/avm1/unloadmovienum/output.txt | 3 +++ core/tests/swfs/avm1/unloadmovienum/target.fla | Bin 0 -> 39936 bytes core/tests/swfs/avm1/unloadmovienum/target.swf | Bin 0 -> 121 bytes core/tests/swfs/avm1/unloadmovienum/test.fla | Bin 0 -> 40960 bytes core/tests/swfs/avm1/unloadmovienum/test.swf | Bin 0 -> 109 bytes 16 files changed, 12 insertions(+) create mode 100644 core/tests/swfs/avm1/unloadmovie/output.txt create mode 100644 core/tests/swfs/avm1/unloadmovie/target.fla create mode 100644 core/tests/swfs/avm1/unloadmovie/target.swf create mode 100644 core/tests/swfs/avm1/unloadmovie/test.fla create mode 100644 core/tests/swfs/avm1/unloadmovie/test.swf create mode 100644 core/tests/swfs/avm1/unloadmovie_method/output.txt create mode 100644 core/tests/swfs/avm1/unloadmovie_method/target.fla create mode 100644 core/tests/swfs/avm1/unloadmovie_method/target.swf create mode 100644 core/tests/swfs/avm1/unloadmovie_method/test.fla create mode 100644 core/tests/swfs/avm1/unloadmovie_method/test.swf create mode 100644 core/tests/swfs/avm1/unloadmovienum/output.txt create mode 100644 core/tests/swfs/avm1/unloadmovienum/target.fla create mode 100644 core/tests/swfs/avm1/unloadmovienum/target.swf create mode 100644 core/tests/swfs/avm1/unloadmovienum/test.fla create mode 100644 core/tests/swfs/avm1/unloadmovienum/test.swf diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 8244a1d37..ecfdc3a3a 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -163,6 +163,9 @@ swf_tests! { (loadmovie, "avm1/loadmovie", 2), (loadmovienum, "avm1/loadmovienum", 2), (loadmovie_method, "avm1/loadmovie_method", 2), + #[ignore] (unloadmovie, "avm1/unloadmovie", 3), + #[ignore] (unloadmovienum, "avm1/unloadmovienum", 3), + #[ignore] (unloadmovie_method, "avm1/unloadmovie_method", 3), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/unloadmovie/output.txt b/core/tests/swfs/avm1/unloadmovie/output.txt new file mode 100644 index 000000000..e65d6331c --- /dev/null +++ b/core/tests/swfs/avm1/unloadmovie/output.txt @@ -0,0 +1,3 @@ +Loading movie +Child movie loaded! +Unloading movie diff --git a/core/tests/swfs/avm1/unloadmovie/target.fla b/core/tests/swfs/avm1/unloadmovie/target.fla new file mode 100644 index 0000000000000000000000000000000000000000..fde35408b67a3b1e966091682c9f95b250c81866 GIT binary patch literal 39936 zcmeHQO^h7Jb?zl4QKqe5%aSZg@=&tnm24>_hm;vxGR2UhNSiiST*{6EDA0<#vrCKI zrMaX@3PD07Fpz@-7%-4{Zp=*(V88)%2w)`Pk%vKS1QFnaf!Hu$_+}k*h&B1X_o}M9 zUw2n^56S2!(^T!uo9_Oq>ec(J>guPT|Lm?`{o_A9@axPRU(PmW?~Xr`-BS2{rtm$x zGs|W{^S5`$<1tV1JHAej4-f~AW^4F&HtS{2XRG);lU?Lcwm$C7ZVqG4@jw0MUwrG$ zGe5iQZyr9ncm7}*=F)w?UH$jb+cQt#=Sv&(+&_pjx@-)g9P_>SX2?Y-9j!><3rglqg$l*g`6{$6hfyuNJ$(5)yRLD`J*QIwCN+=g;H${i>O0kXSL z?ne1I$~`EbK)Dy?K9u`WK8f-G$`+IdQ655h80AwaJ(R5|kDxq?@@bUMpge~1S(Fc( zu6bU+pFSyubVVCcZbG4MPWk?R8lfB8mYMN*28zCK?!@uAh2^E25X{U#=}8}6xVUtt z_``uS3#V{}9GCDH1!7Y)Wchc>QGVQy6K*EqEk44;`;)AT|(K4>mC;`uPDX8ti@F;KNr8V6D%La zMVk%-?jqpM0DL=dA@7giA8&Ja&FCj?;cc5X_}@W)5RW~sTc+)lg4?ov``3=2JGHcW zmZEXrEyY-w?3$MmT9vVI(X(eY2fK*O?{ELE_vdsWJm_9H(xHeE@;~q-50AiMkSnwQ z0*1eeW=k&NWfKC5P!iB%@sspM|092BCU{k7NAVVqrlKG@Hxolpc8Aji@I@E)NdVO^%wE`0j&va=|?GSX3;|0V+ICaiVAr!wzA82>b`)Ry7zC%*yIS)nd+1Q$f| zPGeNgfLaEBMI&#Wg>QHqW3~gb&41wTqh+q7Y8asR3*hPs7K6xNFFQcGX}iP4_j|3} zP!mo;JI|nn>-g6z*u5~upRjk)w?w^`k@_|^BJY78w}Pj+z0T7-h4!9@Z9_XhTjo}q z`e=Oz(AA=KEYw&#&TVkX%eCy=!1ex~KIOP*jEt=DMsew*@1gCZW_v4E@Q_=}{Z2{A z60F2&w2m>*JVyEuBC0UTy>3`}wA05z<<4TXV|v)7Rb^Wz7Um8seUuN%D@Wr(&R63p zQk?Bw6sG8a-PY@0@u#|4d6h?>1B{+AC10)LIiY9~wdEA92+H0l-G zJb{mh z#L;TUjBdooQauRUM2?HsyK4tD{7J;E)RrEsu3DuYViO;CLvBnvolZ6n8- znpYa{{eR_3t&JK+zY^MJIF^=W&WcYFGV&jb;O8VhxrmKCI+_=abkwaO#5JI3b!+2f zW5ZMOud=udnbkF;6VlQkD-T%NCvU1N-dwF>B&5C+chmn>?mYsn&^gC3(GkYwoOLh; z)+n$ruQRjWl$;*~R$&!x<4&=kXhlbTX^1k+8etph@-bL@Kg>I1#{YPCSq~1Z5vjKSF-QuTHypUqZQa1a=qN!LY?ZdjrD1x?|wEHWX)(u8;r2# zr=HqqN6gbSYM8NHaeY4uNz&QC5}v>@j^UFIza2XC%-E2)?sIf6*r#+G`BZ%u_1I`y z983Mis;qkO36{33X1>s=g??U2ndkbV%;&FZ2AA&`kQvwvc zu~>}nxc;Hf3i-|JQ9)PP7o#gF^wzlM=}-zcX;;`*6T`I8oa#Dk;H#OL#U;$kQuOJv zX1bh7xokTrdQH5t28#MXGRy7z`F4!mEyr?Gr-o6~5yOe^9j?c%oh*tfKP6F*zq zh4ted#b;~A%cP>8Mov{`sJUe5ewN)>Wt^8$=Zfi4ajii|>>(>#MM-IiQP~|+jO@8R z&p-F6%D1eiSDt6*y@<%Fk+qclVLQ0)Qivh1%gj+@vP4v2+8Xoc-Y02n7BtJOZpRRG z)ObZ62iqM2xrGtEHF0QMLg$Rmvpc7KPR}XzQ9wx5C^;q;~x}yxJ8>xk2QO$Z- z9Cezp}%nXv-QrrRhh?ij*xF zXUPNb1^sw#H(m_e7Rs0K%&<&9ZRlNw-f8H*p?4U1yP?qr6YsR~-pi2eL5Lg+y4)}W z)9X=}(akn>F;Ym_Ni!NTD@QAI4zUvVWbFlVzGs$FC05PMfMRGqI_0|HN%0buojQtX z<`;2GnV126zFXW+yRKd#H3MkkK$}tWvspizD9FSeP5ZYPGs~V6!|^utRaXVnqh3oN z79kb+Zdu<0U1Nm3;h|Pn_M2zf|KK;iWVUbL+R3#u`TK~F8e)R zJ>$LD+ot3;A%~tkDCEeKuLucepch{tiT6pR84jv1$g>`~FUZe;6y&}j?*l2weL+3| zQjq(Cd=R7{4+65>I}qf1Jk&stS&^O<Ic_7HFSC9vS{2WL@2G69;N!;9mzif=~zedPeD4?k-Q30kWDlCf;@*& zQeFx29PU(Jjd<@|_5%ksR`U1w0X-T>n|}#pWsd8hgBl3(tVbRQ@-rTJAjtbX@<5O| zMuH4Bp$(EzNKLpxYLn`Jyp)+uxPB)=a>xtE;OvqPSJ4WwfoNix>4j^ryK9qUNG zef?k}vxX_QxbSM$*B7w3&ka6 zkwOVFwd@U1oHBEU!yzu_As6G*k(pl1vt*reqfhdOfgF(hULXaEoOGQNo*MxznV^0-pN*ev5&vQE3-C;9h*9FU|C>e(U5zYe4zlaqRO%(JIKI@XbV z)1U1N^5wt{LH=PN1^Ih{bjU$r3B@sTtRqQzmU;(*{9}LbK#(~`da)pLjFfc9o|HCI zkLp-Q$D*uzrMg9AC24%QhGEB$Z~ch$eaDe zBSEHH)U$&8a3BSl73o<)-Ud>TQ49Xd0x8J122zmk45T36A4oy&1yYb%kst$9kckd% zfB-qyaCRu(>Nhy+SVv!$0Z7_B6mNA;AjMnF22#A$E)2HRI}~q4zb~Ud66D{4)b2Y! z=q?@!@@@X!ksxmgq#(}*Qjj@Df;<-F-+@$4I%I!VkU2(ru^@AdloVu+k&=Q;)(MH8 zmsls_O#!ZB>`=Vb+c@i3M+fxFz#QkDosAuex0>}9cqrbAeqZVxinn5H_6CP+B*+XQ z9P5bu8(b>Q7&&Bj??{k2MglbwWR8)Ng3Q=VkjH}jE0B(LMCKUD#bbx;FBW9#k=`rF z)FUMYnXD7iunwvY`sN(OJG{T1XZI4kwS#^7R(k&2lhGd5FR2{P-|dj**lNsmC`UXH$yrbjSW zILi(?)_Kyg&Xa-+R=JB6N{~55@9EQr(sJlkNq$Rq0J#=z z7(aoQj9+G7{)#Yfk{$oap0R~&B#(_K9D;vAc?YdI4~$u4#HkyW{5N^$zQbCYJ}cn+ zHS<_G;d>{#&a{r!%w%_F`(8Y@_uS%%wfUv9XHTpyOMW!ehQ`k1E#Z<%CdmM_AuBU} zkp*q99C>3kb59P$Gs0Ei$#>$q!y*3UnQRXd|beG=)z{XeLa|E zz5t;w20wy4WW^S+Zq~xZR=*``JzVn)zNPvS_$>n=D&v2;A)hlqrA?u@(`em{Lfxar z`20`Pd_Zn6K7=2+6~TaGnk(g6lHpLGrpE_{1OK1%Kiw>08iwkU7TCa|^yc4b6!W7^ zGOLr!Gl>Ik^I7iy(KjNQ%<33+rqAOqpNj64H^^VY-jL^!)20Wi2lv6Jz|1znbXTv6 z1LF=kJ_Yv4uK4>!oTZ~YA5vf421iKe!`-#BZIfYr!16zB*syh)|H{#P3yc)VT-#s%XwC0&r4@i zYAhvu9-L~-e`R(S#dp1UNr~>HHt&zeWVvCtml?@6_Nltpgj!d~>SNwXXoIOcq7wU> z%&4@%&EM;)@n;RYvqC1N%1$_1`e?4F${t+qh$`+8ugxj-Xg3+o`=q-IkBZHc9l6E) zR%T>Vnvw6TWSd*%ns zEE9L7kc#c93d27ip!E5-BKo|iZ5-M4a!*V z6#sLZ!PYe9rZOWD_j{_9>i26#!q}WP+OEDO#4bjM+aUOeJTZb3Ap3oy31su$LYp>M z<*&WH3t4&E=MC!k-fVdXID5|#&);NE*);|k+V z;|Ll>Q3kIq$6GA=*0(h(x#&#f*G8`=%uQrQI%U~&A8oQJ884N>$vs3RzmtjE+9Ne& zo-(T>aT#ow4H?afW2Dwu+Kgr%%hL|jt=6QpyU|K?8 z#VSuHyz&ZOP1IrRTil)$?OP4jb$MS>XWWUX!9;UyY9PRpVB@iVXQB6 zT;m;5-kEE7nuRuaT4Tf19}jR9SA~?SzM=hHoGZUoF4|goci65Ga23nvDyYqFWppW} z@7-lTF0I!aFR4p6GCPl1^JpXSKDKW!iM&Hr=Q~PbgU>N*ASrone2e|s@5Gp>tmSMStbqe4cajlFWxT4)6&vz!bZ(c5IrH=(HNr_5e2Xm$=lo%(Y|mIp+Nu zwb5X-(V|i|qsm$|_kZYYGnz%Id%mWMRV=SEQj;ZbiHR^$lF1ASi?DPJ`KL_$9+xaH zL(!+Y*JXyN-m2^u3*JRNpt08}`%{zsr}D(JRo0AzDjBhi4T;O*WHuyjmm20cM$Y0GjeIg5xUD5h=z!7RHmJ+61{39bF_tH# zZ)N+%NM`^m1UvSA$dgmq9Z|OHMLyE<*3F($8q*+6hKnitU3^Nc>+q$lNk*ztXnrqK zBD0f;?9!-y2N5HeXeO$Tz0X(R`Q+Z@%QDYN;`X93gO}l89Hl zCDX$+|C4G;COIUg`JeET9qa}ji@b1`y(6NMzsIj6&oU2$Wdg!h!;st7Q4?H2;%ho#dV|tOMiR z*?OsCwj`#p5vMtnY=z7%B=OZyvK3xMcp>+iWR1gV{wK{Xq`aEue<~;}gwDxTz0~pS pH2)LAk8GO%$%~C-mhpr}Xpj;f$&5|Y{7=*TPt*KQG^#iD_+MrT=VJf> literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovie/target.swf b/core/tests/swfs/avm1/unloadmovie/target.swf new file mode 100644 index 0000000000000000000000000000000000000000..7d79ab01006a3c95c55229c9273a8bdf343c27db GIT binary patch literal 121 zcmV-<0EYiVS5pXn0001ZoU349jb~usU%$IA+Et83T}>`KA}ObRtla9#TogfKrKnB b3Z;1v9STX63VHeEK#lA`oeT^BP`n(hTh}rd literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovie/test.fla b/core/tests/swfs/avm1/unloadmovie/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..8bdc3b486130462a8b9b303eba37bb27820c4709 GIT binary patch literal 40960 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovie/test.swf b/core/tests/swfs/avm1/unloadmovie/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..6a91c99442af651bb3e6a6c9064eba7f968e83a6 GIT binary patch literal 168 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovie_method/target.swf b/core/tests/swfs/avm1/unloadmovie_method/target.swf new file mode 100644 index 0000000000000000000000000000000000000000..7d79ab01006a3c95c55229c9273a8bdf343c27db GIT binary patch literal 121 zcmV-<0EYiVS5pXn0001ZoU349jb~usU%$IA+Et83T}>`KA}ObRtla9#TogfKrKnB b3Z;1v9STX63VHeEK#lA`oeT^BP`n(hTh}rd literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovie_method/test.fla b/core/tests/swfs/avm1/unloadmovie_method/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..521cbad989544feba3ab0a192080e2049b9bf618 GIT binary patch literal 40960 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 Lo^F2E=fM8~j}K%0 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovie_method/test.swf b/core/tests/swfs/avm1/unloadmovie_method/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..18417deae62e0b71ac6410167790e317a9a55dc6 GIT binary patch literal 188 zcmV;t07L&nS5pY_0001ZoU349jb~usU%EWI(daK{nGk88}$j8NpIuG1ee)1_vOtXSM+f qP6N6vG%p9mEo#&F8Q=y&TqZM(mw}-)4`FwZI6Ke`1_l6xSTmHaL`*IK literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovienum/output.txt b/core/tests/swfs/avm1/unloadmovienum/output.txt new file mode 100644 index 000000000..e65d6331c --- /dev/null +++ b/core/tests/swfs/avm1/unloadmovienum/output.txt @@ -0,0 +1,3 @@ +Loading movie +Child movie loaded! +Unloading movie diff --git a/core/tests/swfs/avm1/unloadmovienum/target.fla b/core/tests/swfs/avm1/unloadmovienum/target.fla new file mode 100644 index 0000000000000000000000000000000000000000..fde35408b67a3b1e966091682c9f95b250c81866 GIT binary patch literal 39936 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovienum/target.swf b/core/tests/swfs/avm1/unloadmovienum/target.swf new file mode 100644 index 0000000000000000000000000000000000000000..7d79ab01006a3c95c55229c9273a8bdf343c27db GIT binary patch literal 121 zcmV-<0EYiVS5pXn0001ZoU349jb~usU%$IA+Et83T}>`KA}ObRtla9#TogfKrKnB b3Z;1v9STX63VHeEK#lA`oeT^BP`n(hTh}rd literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovienum/test.fla b/core/tests/swfs/avm1/unloadmovienum/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..371fdfbdc1d630f6bc5e73b7206e20291d484a03 GIT binary patch literal 40960 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 atD(+*e5%T2WzI;WMLC*0-Tbc4f&T?5m1y7q literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/unloadmovienum/test.swf b/core/tests/swfs/avm1/unloadmovienum/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..6570a73e366e686400dddda2d0e76e4253582b81 GIT binary patch literal 109 zcmV-z0FwVhS5pXq0001ZoU349jb~usU% Date: Fri, 17 Jan 2020 14:50:28 -0500 Subject: [PATCH 34/59] Allow unloading a movie by sending `None` to `replace_with_movie`. This also adjusts `MovieClip.unloadMovie` to do just that, instead of removing the clip from the display list. We also have to unload clips when loading new movies into them, since `unloadMovie` desugars to loading `""` as the URL. --- core/src/avm1/globals/movie_clip.rs | 25 ++----------------------- core/src/display_object/movie_clip.rs | 12 ++++++++---- core/src/loader.rs | 8 +++++++- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index 829a91b70..73db6ad7f 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -228,29 +228,8 @@ pub fn create_proto<'gc>( Ok(Value::Undefined.into()) }, "unloadMovie" => |mut target: MovieClip<'gc>, _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _args: &[Value<'gc>]| { - //TODO: How do we reclaim resources from unloaded movies? - if let Some(parent) = target.parent() { - parent - .as_movie_clip() - .unwrap() - .remove_child_from_avm(context, DisplayObject::MovieClip(target)); - } else { - //Removing a layer is a little different. - let mut layer_depth = None; - - for (depth, layer) in context.layers.iter() { - if DisplayObject::ptr_eq(*layer, DisplayObject::MovieClip(target)) { - layer_depth = Some(*depth); - break; - } - } - - if let Some(depth) = layer_depth { - context.layers.remove(&depth); - } - - target.unload(context); - } + target.unload(context); + target.replace_with_movie(context.gc_context, None); Ok(Value::Undefined.into()) } diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 796ec0791..65e72fa3f 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -111,7 +111,7 @@ impl<'gc> MovieClip<'gc> { pub fn replace_with_movie( &mut self, gc_context: MutationContext<'gc, '_>, - movie: Arc, + movie: Option>, ) { self.0 .write(gc_context) @@ -424,13 +424,17 @@ impl<'gc> MovieClipData<'gc> { /// Replace the current MovieClipData with a completely new SwfMovie. /// /// Playback will start at position zero, any existing streamed audio will - /// be terminated, and so on. Children and AVM data will be kept across the - /// load boundary. + /// be terminated, and so on. Children and AVM data will NOT be kept across + /// the load boundary. + /// + /// If no movie is provided, then the movie clip will be replaced with an + /// empty movie of the same SWF version. pub fn replace_with_movie( &mut self, gc_context: MutationContext<'gc, '_>, - movie: Arc, + movie: Option>, ) { + let movie = movie.unwrap_or_else(|| Arc::new(SwfMovie::empty(self.movie().version()))); let total_frames = movie.header().num_frames; self.base = Default::default(); diff --git a/core/src/loader.rs b/core/src/loader.rs index 8439c1ce2..39e111244 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -211,6 +211,12 @@ impl<'gc> Loader<'gc> { _ => unreachable!(), }; + clip.as_movie_clip().unwrap().unload(uc); + + clip.as_movie_clip() + .unwrap() + .replace_with_movie(uc.gc_context, None); + if let Some(broadcaster) = broadcaster { avm.insert_stack_frame_for_method( clip, @@ -264,7 +270,7 @@ impl<'gc> Loader<'gc> { .as_movie_clip() .expect("Attempted to load movie into not movie clip"); - mc.replace_with_movie(uc.gc_context, movie.clone()); + mc.replace_with_movie(uc.gc_context, Some(movie.clone())); mc.post_instantiation(uc.gc_context, clip, avm.prototypes().movie_clip); let mut morph_shapes = fnv::FnvHashMap::default(); From 89e5dd97f37e945b51003b866526e69df56af671 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 17 Jan 2020 18:09:27 -0500 Subject: [PATCH 35/59] Implement `MovieClipLoader.loadClip` --- core/src/avm1/globals/movie_clip_loader.rs | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/core/src/avm1/globals/movie_clip_loader.rs b/core/src/avm1/globals/movie_clip_loader.rs index 34ddff673..d541697a4 100644 --- a/core/src/avm1/globals/movie_clip_loader.rs +++ b/core/src/avm1/globals/movie_clip_loader.rs @@ -5,6 +5,8 @@ use crate::avm1::property::Attribute; use crate::avm1::return_value::ReturnValue; use crate::avm1::script_object::ScriptObject; use crate::avm1::{Avm1, Error, Object, UpdateContext, Value}; +use crate::backend::navigator::RequestOptions; +use crate::display_object::{DisplayObject, TDisplayObject}; use enumset::EnumSet; use gc_arena::MutationContext; @@ -129,6 +131,41 @@ pub fn broadcast_message<'gc>( Ok(Value::Undefined.into()) } +pub fn load_clip<'gc>( + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let url = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let target = args.get(1).cloned().unwrap_or(Value::Undefined); + + if let Value::Object(target) = target { + if let Some(movieclip) = target + .as_display_object() + .and_then(|dobj| dobj.as_movie_clip()) + { + let fetch = context.navigator.fetch(url, RequestOptions::get()); + let process = context.load_manager.load_movie_into_clip( + context.player.clone().unwrap(), + DisplayObject::MovieClip(movieclip), + fetch, + Some(this), + ); + + context.navigator.spawn_future(process); + } + + Ok(true.into()) + } else { + Ok(false.into()) + } +} + pub fn create_proto<'gc>( gc_context: MutationContext<'gc, '_>, proto: Object<'gc>, @@ -157,6 +194,13 @@ pub fn create_proto<'gc>( EnumSet::empty(), Some(fn_proto), ); + mcl_proto.as_script_object().unwrap().force_set_function( + "loadClip", + load_clip, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); mcl_proto.into() } From b56c3b6aed1ce9a0589cccf6ada0d0d5bbe93667 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 17 Jan 2020 18:24:06 -0500 Subject: [PATCH 36/59] Fire events directly onto `broadcastMessage` instead of the individual event handlers on the `MovieClipLoader`, so that listeners run correctly. Also, this fixes a double-borrow for `onClipInit`. --- core/src/display_object/movie_clip.rs | 1 + core/src/loader.rs | 28 ++++++++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 65e72fa3f..d961b906a 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -968,6 +968,7 @@ impl<'gc> MovieClipData<'gc> { if let ClipEvent::Load = event { context.load_manager.movie_clip_on_load( self_display_object, + self.object, context.root, context.action_queue, ); diff --git a/core/src/loader.rs b/core/src/loader.rs index 39e111244..6eda0a857 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -88,13 +88,14 @@ impl<'gc> LoadManager<'gc> { pub fn movie_clip_on_load( &mut self, loaded_clip: DisplayObject<'gc>, + clip_object: Option>, root: DisplayObject<'gc>, queue: &mut ActionQueue<'gc>, ) { let mut invalidated_loaders = vec![]; for (index, loader) in self.0.iter_mut() { - if loader.movie_clip_loaded(loaded_clip, root, queue) { + if loader.movie_clip_loaded(loaded_clip, clip_object, root, queue) { invalidated_loaders.push(index); } } @@ -223,8 +224,8 @@ impl<'gc> Loader<'gc> { broadcaster, NEWEST_PLAYER_VERSION, uc, - "onLoadStart", - &[Value::Object(broadcaster)], + "broadcastMessage", + &["onLoadStart".into(), Value::Object(broadcaster)], ); avm.run_stack_till_empty(uc)?; } @@ -256,8 +257,9 @@ impl<'gc> Loader<'gc> { broadcaster, NEWEST_PLAYER_VERSION, uc, - "onLoadProgress", + "broadcastMessage", &[ + "onLoadProgress".into(), Value::Object(broadcaster), data.len().into(), data.len().into(), @@ -293,8 +295,8 @@ impl<'gc> Loader<'gc> { broadcaster, NEWEST_PLAYER_VERSION, uc, - "onLoadComplete", - &[Value::Object(broadcaster)], + "broadcastMessage", + &["onLoadComplete".into(), Value::Object(broadcaster)], ); avm.run_stack_till_empty(uc)?; } @@ -322,8 +324,12 @@ impl<'gc> Loader<'gc> { broadcaster, NEWEST_PLAYER_VERSION, uc, - "onLoadError", - &[Value::Object(broadcaster), "LoadNeverCompleted".into()], + "broadcastMessage", + &[ + "onLoadError".into(), + Value::Object(broadcaster), + "LoadNeverCompleted".into(), + ], ); avm.run_stack_till_empty(uc)?; } @@ -377,6 +383,7 @@ impl<'gc> Loader<'gc> { pub fn movie_clip_loaded( &mut self, loaded_clip: DisplayObject<'gc>, + clip_object: Option>, root: DisplayObject<'gc>, queue: &mut ActionQueue<'gc>, ) -> bool { @@ -397,7 +404,10 @@ impl<'gc> Loader<'gc> { ActionType::Method { object: broadcaster, name: "broadcastMessage", - args: vec!["onLoadInit".into(), loaded_clip.object()], + args: vec![ + "onLoadInit".into(), + clip_object.map(|co| co.into()).unwrap_or(Value::Undefined), + ], }, false, ); From 8ece2d1b31261508ff51d09b07e2209e5f0400ba Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 17 Jan 2020 18:25:00 -0500 Subject: [PATCH 37/59] Add another ignored test, this time measuring the timing of load events broadcasted by `MovieClipLoader`. We're loading too early, which is why this is ignored. --- core/tests/regression_tests.rs | 1 + core/tests/swfs/avm1/mcl_loadclip/output.txt | 8 ++++++++ core/tests/swfs/avm1/mcl_loadclip/target.fla | Bin 0 -> 20992 bytes core/tests/swfs/avm1/mcl_loadclip/target.swf | Bin 0 -> 68 bytes core/tests/swfs/avm1/mcl_loadclip/test.fla | Bin 0 -> 42496 bytes core/tests/swfs/avm1/mcl_loadclip/test.swf | Bin 0 -> 252 bytes 6 files changed, 9 insertions(+) create mode 100644 core/tests/swfs/avm1/mcl_loadclip/output.txt create mode 100644 core/tests/swfs/avm1/mcl_loadclip/target.fla create mode 100644 core/tests/swfs/avm1/mcl_loadclip/target.swf create mode 100644 core/tests/swfs/avm1/mcl_loadclip/test.fla create mode 100644 core/tests/swfs/avm1/mcl_loadclip/test.swf diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index ecfdc3a3a..3fc8d0e6f 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -166,6 +166,7 @@ swf_tests! { #[ignore] (unloadmovie, "avm1/unloadmovie", 3), #[ignore] (unloadmovienum, "avm1/unloadmovienum", 3), #[ignore] (unloadmovie_method, "avm1/unloadmovie_method", 3), + #[ignore] (mcl_loadclip, "avm1/mcl_loadclip", 3), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/mcl_loadclip/output.txt b/core/tests/swfs/avm1/mcl_loadclip/output.txt new file mode 100644 index 000000000..8719df9dd --- /dev/null +++ b/core/tests/swfs/avm1/mcl_loadclip/output.txt @@ -0,0 +1,8 @@ +Parent frame 1 +Parent frame 2 +Event: onLoadStart +Event: onLoadProgress +Event: onLoadComplete +Child movie loaded! +Parent frame 3 +Event: onLoadInit diff --git a/core/tests/swfs/avm1/mcl_loadclip/target.fla b/core/tests/swfs/avm1/mcl_loadclip/target.fla new file mode 100644 index 0000000000000000000000000000000000000000..3b639cffc5e43da0805302853a75ff565ba05ba5 GIT binary patch literal 20992 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/mcl_loadclip/target.swf b/core/tests/swfs/avm1/mcl_loadclip/target.swf new file mode 100644 index 0000000000000000000000000000000000000000..1a1a83455cade7036d1025e2bc10ef0cb88d2e36 GIT binary patch literal 68 zcmZ<@59V-TU|^_VV2x*B;9tPNz{AMkA_x>=aAx}d|Gz!66p%Sh6v)lU%t=wm%`eML TRmjOtOi4{qWKd&p0LlUY&fpHG literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/mcl_loadclip/test.fla b/core/tests/swfs/avm1/mcl_loadclip/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..b0848d6b24fae2e1da0cc2933324020c42283663 GIT binary patch literal 42496 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 z Date: Fri, 17 Jan 2020 19:00:41 -0500 Subject: [PATCH 38/59] Rewrite some of the unload and cliploader tests to be more generous with load timing. Loads in Flash Player, like all web technologies, are asynchronous tasks of some kind (probably a separate thread). They appear to operate on some kind of a delay. If I `trace` each frame out, like in the previous version of `mcl_loadclip`, you get a series of events that look like this: 1. Parent frame 1 2. Parent frame 2 3. Event: onLoadStart 4. Event: onLoadProgress 5. Event: onLoadComplete 6. Parent frame 3 7. Event: onLoadInit If I run that version of the test on Ruffle, everything happens after frame 1. This is an artifact of how we're testing asynchronous behavior in Ruffle. In order to guarantee test determinism, we have a dummy implementation of `fetch` that does a blocking load, and we poll all futures every frame of execution. This means that there is a very specific order of execution with these tests, which is good for testing, but probably isn't 100% accurate. Flash Player appears to delay all loads by at least one frame, even loads that are coming from disk which should load immediately. I don't know if this is intentional or not, so I don't want to implement a load delay just for the sake of making tests pass. Ergo, I'm loosening the tests to just test the ability to load and unload movies, and fire events from a loader. Specifically: 1. `mcl_loadclip` no longer traces out frames of the parent timeline 2. `unloadmovie` et. all use a target movie that doesn't fail the test until 10 frames have passed. If someone can find a movie network that breaks with fast loading, then I'll consider implementing explicit frame delays for async tasks. Otherwise, this is how we're testing this. --- core/tests/regression_tests.rs | 8 ++++---- core/tests/swfs/avm1/mcl_loadclip/output.txt | 3 --- core/tests/swfs/avm1/mcl_loadclip/test.fla | Bin 42496 -> 40960 bytes core/tests/swfs/avm1/mcl_loadclip/test.swf | Bin 252 -> 222 bytes core/tests/swfs/avm1/unloadmovie/target.fla | Bin 39936 -> 40448 bytes core/tests/swfs/avm1/unloadmovie/target.swf | Bin 121 -> 122 bytes .../swfs/avm1/unloadmovie_method/target.fla | Bin 39936 -> 40448 bytes .../swfs/avm1/unloadmovie_method/target.swf | Bin 121 -> 122 bytes .../tests/swfs/avm1/unloadmovienum/target.fla | Bin 39936 -> 40448 bytes .../tests/swfs/avm1/unloadmovienum/target.swf | Bin 121 -> 122 bytes 10 files changed, 4 insertions(+), 7 deletions(-) diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 3fc8d0e6f..6b4fe0eed 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -163,10 +163,10 @@ swf_tests! { (loadmovie, "avm1/loadmovie", 2), (loadmovienum, "avm1/loadmovienum", 2), (loadmovie_method, "avm1/loadmovie_method", 2), - #[ignore] (unloadmovie, "avm1/unloadmovie", 3), - #[ignore] (unloadmovienum, "avm1/unloadmovienum", 3), - #[ignore] (unloadmovie_method, "avm1/unloadmovie_method", 3), - #[ignore] (mcl_loadclip, "avm1/mcl_loadclip", 3), + #[ignore] (unloadmovie, "avm1/unloadmovie", 11), + (unloadmovienum, "avm1/unloadmovienum", 11), + (unloadmovie_method, "avm1/unloadmovie_method", 11), + #[ignore] (mcl_loadclip, "avm1/mcl_loadclip", 11), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/mcl_loadclip/output.txt b/core/tests/swfs/avm1/mcl_loadclip/output.txt index 8719df9dd..d21cd4a56 100644 --- a/core/tests/swfs/avm1/mcl_loadclip/output.txt +++ b/core/tests/swfs/avm1/mcl_loadclip/output.txt @@ -1,8 +1,5 @@ -Parent frame 1 -Parent frame 2 Event: onLoadStart Event: onLoadProgress Event: onLoadComplete Child movie loaded! -Parent frame 3 Event: onLoadInit diff --git a/core/tests/swfs/avm1/mcl_loadclip/test.fla b/core/tests/swfs/avm1/mcl_loadclip/test.fla index b0848d6b24fae2e1da0cc2933324020c42283663..c66b9622229301d99bc5d3d54af9938c98642fa6 100644 GIT binary patch delta 280 zcmZoT!_;tqX+sVZ?}QH;_7l!tWz=E-g2|Ok)r>lmzcMv5y+Qb z`~UyHE=a)^mX66ToO>oFa7<##;boAT>j2Ws3fIhzu9@8jteFF-VzUB=1S3Q@&m=Az z#(x_F*E3E`5Shd?mGR2vb39f|lLZBICr3s`Pd>oU&B#9aM`Y9F2ZEe}MGOiIh761h p3_w@>_XomBLhG6I-8XNH@pmTDpe`W>tfuHfO<8ni^O<=ji~uugY5M>G delta 2106 zcmb_dYiyHM82(NfQbxzO-mhP;>%CpueWmNywQD=ZTn21I<^>lBC@?Qv5@aG80*(+6 zjF3JE0lELuM8c&c5-@=oBQZcGAsQ2nc#Q%7fFeIkpq}$}j7(!f)E_5(PtSMW^FGh} zyzjf=ykf&8#qni|I_L0U-{#AolMu&o=do7t8j0d}#YV+Y4PKux9dX5^l2VCF6TVeO z=oj}Yl?qPPY?;{0Hs#{PFsWPk;jgFOq#h=Yc~Ht8-Wu;=(k$hqQ4eLO(nMiBF zKQ7f~kyWY)byxcVDXHF;Zm#B5af`TTxg}hHb924aUPbK{+-lCrEtSuf)4h{R(9NYiys zlfiLKH##glMobM@rxA%0YJCV-^aA~YheL0}8`S>H&g?wC*EQmZu7F|vP0V&*m!Jrx#724Q4$tY995>SdW<;jt>Rzl=>+c) zsUHcyxb-+A@-o8RZY!MbF5Gd2=u+dF%ZLrGZn#|waLTy?Tb&_joUg#$D1t92@tebe z(+tD1zEOrOJR9Hru7gB5)Zow(t70VV$| zwEMep+t-UDzQ^#Tu?lg}7bSMM+>MC(%+$J1@^>B+4u`ZiGUzAbYOnG1!atSrQ ziN|GBgtjtP|V7zBd}#h4$50y8k&C{gO4mW55}uVFM}SkmEG zz&Di@-5BpyS@JAGRz6Jf{gPi!^U-9EqGwqNw;Hf5SwJjlrtC{org155rtBht9b$-t z{oaDXbO7(P*dV6;_@cRhL(M6AazYcw z_GUHB*jl;t^ExHGc^*4k7oe-Pi}H5G&QLkza%Kg~5dNGqVnMD0eW|MIX6CdEk1dT` zS=U(MBl{rsprAreR!d)GQL)VKZAsEr6vN%jep-;rMoT=O@yK{t8(Ld*q1~dEcd2exP{olvKjqz>v;l~4Zsdge; z7dNtbGQbVej$EB8FLC``oSR=>SJz3J&lYJnuQv35F-iK{ulvUC|A(P6Z!3mW+Q|Pq iQc`I;QhKK2|v+s;T9`QRSBr$%uN(V0e)6t(~?0>lc diff --git a/core/tests/swfs/avm1/mcl_loadclip/test.swf b/core/tests/swfs/avm1/mcl_loadclip/test.swf index 7fce01b7a86af32c7d098d87f5bb9ff59d878c32..74e69843a1a37e4c8475cb78f91e328ac9fa974b 100644 GIT binary patch literal 222 zcmV<403rWFS5pWT0RRAaoU349jb~usU%gIZ~#JkW{@B|kj20N04;wxWn*$-;s5{u literal 252 zcmV!@-mJ*AfpHU44h!c6Z6>7&XRk7{ zPA+AtXJp^}m5G~i5{C!}8&t<+8IA@PUm#~<;FQS=SXd^8oS4KVvRNeP4dW!9bVkm} z89Yi%E$Nec!cH??)0rF@zI?JT5BKDMJZ_Afn-fFkGfp;O;-AE~p82<%(&kNkevXTR Pye5C}XW7gW@P`oqa!fV- delta 153 zcmZqJ!_=^YX@bGz0)I9s1_lNJ1_liv#R$az{{R0Eq`>UWhFlvMH!+vKlI&5S;i14Fzgad1pN!|uTW(#sB1Fhs wabkkNB%bui>0whRbMkpi4i4EfNnkzm{TB+GHwpMTE}F$OQHNtQN6-&X0CK=FR{#J2 diff --git a/core/tests/swfs/avm1/unloadmovie/target.swf b/core/tests/swfs/avm1/unloadmovie/target.swf index 7d79ab01006a3c95c55229c9273a8bdf343c27db..989920a746eb9f1394f0c7422066792096ca51eb 100644 GIT binary patch delta 88 zcmV-e0H^*YLF_=C3=9CZV;;1e3>_D9i3;!@-mJ*AfpHU44h!c6Z6>7&XRk7{ zPA+AtXJp^}m5G~i5{C!}8&t<+8IA@PUm#~<;FQS=SXd^8oS4KVvRNeP4dW!9bVkm} z89Yi%E$Nec!cH??)0rF@zI?JT5BKDMJZ_Afn-fFkGfp;O;-AE~p82<%(&kNkevXTR Pye5C}XW7gW@P`oqa!fV- delta 153 zcmZqJ!_=^YX@bGz0)I9s1_lNJ1_liv#R$az{{R0Eq`>UWhFlvMH!+vKlI&5S;i14Fzgad1pN!|uTW(#sB1Fhs wabkkNB%bui>0whRbMkpi4i4EfNnkzm{TB+GHwpMTE}F$OQHNtQN6-&X0CK=FR{#J2 diff --git a/core/tests/swfs/avm1/unloadmovie_method/target.swf b/core/tests/swfs/avm1/unloadmovie_method/target.swf index 7d79ab01006a3c95c55229c9273a8bdf343c27db..989920a746eb9f1394f0c7422066792096ca51eb 100644 GIT binary patch delta 88 zcmV-e0H^*YLF_=C3=9CZV;;1e3>_D9i3;!@-mJ*AfpHU44h!c6Z6>7&XRk7{ zPA+AtXJp^}m5G~i5{C!}8&t<+8IA@PUm#~<;FQS=SXd^8oS4KVvRNeP4dW!9bVkm} z89Yi%E$Nec!cH??)0rF@zI?JT5BKDMJZ_Afn-fFkGfp;O;-AE~p82<%(&kNkevXTR Pye5C}XW7gW@P`oqa!fV- delta 153 zcmZqJ!_=^YX@bGz0)I9s1_lNJ1_liv#R$az{{R0Eq`>UWhFlvMH!+vKlI&5S;i14Fzgad1pN!|uTW(#sB1Fhs wabkkNB%bui>0whRbMkpi4i4EfNnkzm{TB+GHwpMTE}F$OQHNtQN6-&X0CK=FR{#J2 diff --git a/core/tests/swfs/avm1/unloadmovienum/target.swf b/core/tests/swfs/avm1/unloadmovienum/target.swf index 7d79ab01006a3c95c55229c9273a8bdf343c27db..989920a746eb9f1394f0c7422066792096ca51eb 100644 GIT binary patch delta 88 zcmV-e0H^*YLF_=C3=9CZV;;1e3>_D9i3; Date: Fri, 17 Jan 2020 20:03:53 -0500 Subject: [PATCH 39/59] `GetUrl2` can accept a `DisplayObject` as target in `LoadTargetFlag` mode. I have no idea what happens to non-MovieClip objects, or if I'm really supposed to `coerce_to_string` here. --- core/src/avm1.rs | 17 ++++++++++------- core/tests/regression_tests.rs | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 0bd1245a6..9ece32cfa 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1687,9 +1687,14 @@ impl<'gc> Avm1<'gc> { return fscommand::handle(fscommand, self, context); } + let window_target = target.clone().into_string(self.current_swf_version()); let clip_target: Option> = if is_target_sprite { - let start = self.target_clip_or_root(context); - self.resolve_target_display_object(context, start, target.clone())? + if let Value::Object(target) = target { + target.as_display_object() + } else { + let start = self.target_clip_or_root(context); + self.resolve_target_display_object(context, start, target.clone())? + } } else { Some(self.target_clip_or_root(context)) }; @@ -1742,11 +1747,9 @@ impl<'gc> Avm1<'gc> { None => None, }; - context.navigator.navigate_to_url( - url, - Some(target.into_string(self.current_swf_version())), - vars, - ); + context + .navigator + .navigate_to_url(url, Some(window_target), vars); } Ok(()) diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 6b4fe0eed..d21a0a5b5 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -163,7 +163,7 @@ swf_tests! { (loadmovie, "avm1/loadmovie", 2), (loadmovienum, "avm1/loadmovienum", 2), (loadmovie_method, "avm1/loadmovie_method", 2), - #[ignore] (unloadmovie, "avm1/unloadmovie", 11), + (unloadmovie, "avm1/unloadmovie", 11), (unloadmovienum, "avm1/unloadmovienum", 11), (unloadmovie_method, "avm1/unloadmovie_method", 11), #[ignore] (mcl_loadclip, "avm1/mcl_loadclip", 11), From a132226da4ec2992562e277f8f3a4d875b249693 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 17 Jan 2020 20:20:49 -0500 Subject: [PATCH 40/59] Run `onLoadInit` at the *end* of a frame, rather than before the movie clip's own actions. This is technically better, but it may make more sense to trigger `ClipEvent::Load` at the start of the next frame instead. Furthermore, I don't know if other forms of load events should trigger on the next frame (or end of the current one) like this. --- core/src/display_object/movie_clip.rs | 41 ++++++++++++++++++++------- core/tests/regression_tests.rs | 2 +- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index d961b906a..a39044a01 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -322,7 +322,8 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { // Run my load/enterFrame clip event. let mut mc = self.0.write(context.gc_context); - if !mc.initialized() { + let is_load_frame = !mc.initialized(); + if is_load_frame { mc.run_clip_action((*self).into(), context, ClipEvent::Load); mc.set_initialized(true); } else { @@ -333,6 +334,10 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> { if mc.playing() { mc.run_frame_internal((*self).into(), context, true); } + + if is_load_frame { + mc.run_clip_postaction((*self).into(), context, ClipEvent::Load); + } } fn render(&self, context: &mut RenderContext<'_, 'gc>) { @@ -963,16 +968,32 @@ impl<'gc> MovieClipData<'gc> { ); } } + } + } - // Finally, queue any loaders that may be waiting for this event. - if let ClipEvent::Load = event { - context.load_manager.movie_clip_on_load( - self_display_object, - self.object, - context.root, - context.action_queue, - ); - } + /// Run clip actions that trigger after the clip's own actions. + /// + /// Currently, this is purely limited to `MovieClipLoader`'s `onLoadInit` + /// event, delivered via the `LoadManager`. We need to be called here so + /// that external init code runs after the event. + /// + /// TODO: If it turns out other `Load` events need to be delayed, perhaps + /// we should change which frame triggers a `Load` event, rather than + /// making sure our actions run after the clip's. + fn run_clip_postaction( + &self, + self_display_object: DisplayObject<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + event: ClipEvent, + ) { + // Finally, queue any loaders that may be waiting for this event. + if let ClipEvent::Load = event { + context.load_manager.movie_clip_on_load( + self_display_object, + self.object, + context.root, + context.action_queue, + ); } } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index d21a0a5b5..00d478503 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -166,7 +166,7 @@ swf_tests! { (unloadmovie, "avm1/unloadmovie", 11), (unloadmovienum, "avm1/unloadmovienum", 11), (unloadmovie_method, "avm1/unloadmovie_method", 11), - #[ignore] (mcl_loadclip, "avm1/mcl_loadclip", 11), + (mcl_loadclip, "avm1/mcl_loadclip", 11), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. From 3f7e3a9ed8d3a71e09f8eb721b2c21298b6cf4a9 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 17 Jan 2020 20:29:56 -0500 Subject: [PATCH 41/59] Implement `MovieClipLoader.unloadClip`, with tests. --- core/src/avm1/globals/movie_clip_loader.rs | 30 ++++++++++++++++++ core/tests/regression_tests.rs | 1 + .../tests/swfs/avm1/mcl_unloadclip/output.txt | 5 +++ .../tests/swfs/avm1/mcl_unloadclip/target.fla | Bin 0 -> 40448 bytes .../tests/swfs/avm1/mcl_unloadclip/target.swf | Bin 0 -> 118 bytes core/tests/swfs/avm1/mcl_unloadclip/test.fla | Bin 0 -> 41984 bytes core/tests/swfs/avm1/mcl_unloadclip/test.swf | Bin 0 -> 244 bytes 7 files changed, 36 insertions(+) create mode 100644 core/tests/swfs/avm1/mcl_unloadclip/output.txt create mode 100644 core/tests/swfs/avm1/mcl_unloadclip/target.fla create mode 100644 core/tests/swfs/avm1/mcl_unloadclip/target.swf create mode 100644 core/tests/swfs/avm1/mcl_unloadclip/test.fla create mode 100644 core/tests/swfs/avm1/mcl_unloadclip/test.swf diff --git a/core/src/avm1/globals/movie_clip_loader.rs b/core/src/avm1/globals/movie_clip_loader.rs index d541697a4..c8c494353 100644 --- a/core/src/avm1/globals/movie_clip_loader.rs +++ b/core/src/avm1/globals/movie_clip_loader.rs @@ -166,6 +166,29 @@ pub fn load_clip<'gc>( } } +pub fn unload_clip<'gc>( + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let target = args.get(0).cloned().unwrap_or(Value::Undefined); + + if let Value::Object(target) = target { + if let Some(mut movieclip) = target + .as_display_object() + .and_then(|dobj| dobj.as_movie_clip()) + { + movieclip.unload(context); + movieclip.replace_with_movie(context.gc_context, None); + + return Ok(true.into()); + } + } + + Ok(false.into()) +} + pub fn create_proto<'gc>( gc_context: MutationContext<'gc, '_>, proto: Object<'gc>, @@ -201,6 +224,13 @@ pub fn create_proto<'gc>( EnumSet::empty(), Some(fn_proto), ); + mcl_proto.as_script_object().unwrap().force_set_function( + "unloadClip", + unload_clip, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); mcl_proto.into() } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 00d478503..fa18762fa 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -167,6 +167,7 @@ swf_tests! { (unloadmovienum, "avm1/unloadmovienum", 11), (unloadmovie_method, "avm1/unloadmovie_method", 11), (mcl_loadclip, "avm1/mcl_loadclip", 11), + (mcl_unloadclip, "avm1/mcl_unloadclip", 11), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/mcl_unloadclip/output.txt b/core/tests/swfs/avm1/mcl_unloadclip/output.txt new file mode 100644 index 000000000..d21cd4a56 --- /dev/null +++ b/core/tests/swfs/avm1/mcl_unloadclip/output.txt @@ -0,0 +1,5 @@ +Event: onLoadStart +Event: onLoadProgress +Event: onLoadComplete +Child movie loaded! +Event: onLoadInit diff --git a/core/tests/swfs/avm1/mcl_unloadclip/target.fla b/core/tests/swfs/avm1/mcl_unloadclip/target.fla new file mode 100644 index 0000000000000000000000000000000000000000..856b2e1eacb7b5147f8f9122e58babd97d16d6c8 GIT binary patch literal 40448 zcmeHQU5s5NJ9RYBoHQn1l!jRZZL6NJB|$wnRskyD}`q8%$+f@XUxo) z*r-yiwn#isDH2k}eW>KVrIC=Rl!r*DmDMLd+={3o5|wxWAyvGQ$3B$Ye&1Sq?{m&N z=j?sXwcR-3-ek|c>->Cs@3q(8-e;fXw|{=yum9;^?)gn-K2K+xviC+G$*wOPpTh5J zJbi1HU5np5_TFeT;^9pw_&ND}fH-hCTg87H*>v`7wu0}q>@0_}_29UiqvpHhr{F4@8PMte<%W6 zWxv@zmPgUx&!aq!@&w8kP`03O)V87YP`0D&KzS17izqu${siSIlrN#|LisYv(ihkYo|qU=X`4u#@-5alZQRsssA^i z{`Xt|=dKd{_pursYWn^#*?=kV zn4-wojqx0HljBbv@#Ftt>hIMgM^{5zrbeSF4D{aF=CUJb{(b}>2N6^p%?{((Q#f)2-C2`xlmYoMbntj~3cprd zh<+Ns>N>p{CVCw~^eV2N>C6Uc={fvG`6zgG!yz=|EV#Ocs<)DJ6x0{+AFl){U;2Fx zzfo?u2FK|tCZ7)&2X?#hG|i?J*qrU%w|eaK$;FionufcsFV2-HCv&2qE24{T_T*KK z`cXvY?{|JT{Y{1t{LsBn(?by_8!gwztRn%W}O|CqlYoXt*W z-xAxi4#WE#?&)W4NRn( zu+{~i#(W#5aZln(Z5jT4^6Nl7CKhf@>fM7r^s;HG|5)}4>eWd++p~dIulqJURBpoo zoIQyaH??67=V^mC1o<>@*q+di&y>;jgC}diW1UTi%dW=#;2Jqq;L%M-!GCgdL(VK< za=W#LZ!f|xG@<9F`UP;0SX@p)dpqLnK?_cSZ}V9Vj^SafJMi_xuf2L7uKN}wbP4~d{89=imnVgjl*7V^Jy0Go zZ15B%Vw9pjKS)idqaBW1+)+CB$V z+98g}LFn~9w1wP~c6LV1u?~wYE+Wc04GvH<0_t5>s-DfxfJc-fj{OO-EGO_w!yw$p zdyV#|MxNGl6%x&;ZV8r;c4Y;6u?~tY5ox%u%(WCIEe7p{*f-_S_O1x)j^Aa;rUl43 zz+c{C6||wY&2bMg2Tjr?W|2TeFq}-u3U!eqI4_cS3Zt?HY6<)mjl6LNzTr`f**3^F z{{wd)E^{SSLm$1L2UnM|7)btl*#XkETOBTb-)ZHBns5@@xrP?5paa9Xz#1AZD{9b%iL;HAFXd6x>~f3g&J$exeYFPxte_& zxZdB>ryLWFk&!jtC@y{UJ+yt)Y;VO19`eC*zf)4O2rF?4tz&#Jhmq#&EsXMXH>^C` z>0zOIdokKEJ?zq|vaJ&fbBmQe$_M3@qj4tZtMQa6OY|bFDlH}Xdsa}k^Y2WV3n>(~ zO(V)-`+YK++iMuJ?<;i&mh z6>WaJEH7~G4$9UU#q+-h|R_L0Y>i;WOYHido`jyZ& z!?Cn1ABQHKl0Ke=2cRUL7BsQ7kCi!=LOUSR%a~fVg7!3G0k?HSSKG>HkLZ*~-+MiB z5dM1)G;jmUS zKC7$5En^`2Ia&hQ=y;npYorypZL}$_m6h4&y=57S-`#HR*xnm`6zCh{%|i7WV7n=N2J_g<0P9Tj4kSe;60z@l!sl z(GMA%HjHPc5bv|T$Ih6!UgKwCe3Hgk(5=`h$Ba!; z@81_s*Szkh(XaQIf&0d0r@VUZ!HF<#lv|w#(;BaIurZviikCLChc+^(Gogep+fugM z?QrX7_lcT}{Kq2rS&2{1;)$w`=2;^hb!!N54JcaO+Bn(R@RaXp{u#I&27_7Y?<{dKQf3&+S=XN?Fy(TJ&OIu_fW%wHR?~S8lBEXci za&?|-{y{!JUMHV^exI)wgzGCau_KV#Q;5L1Hm-AQ(Me;aQqpH^r(vIL6Fq8McO8FZZ@kr+RE-ecI@|pUnkXGaAwcBdqzUr#9LV^E8bbW-M1+-w#8Q zbT+VvCvc2o_@u*chYvk7HYBe59NjbaDcvSMRo_89Hkua4Qopexs~&uUr7f$OFLY|5 zpO;eRxxOg#`AeF?Yh3emD21D}D{QNYVcKX;bsaYF)y&M|66R$o`gGZ}FExAQ^LpyhD#nHp(n)AU zVb!nVlPE2GnyoPr`>~T;xx-44MwOIT&N^z4D{Jc9Q9`ZZ{A5~mviQ7^HGPpa8M{-& z)_AeH_l7GDykEShv3zlx(`dO&E8-~a;>@_2X^DXKTjGq@tfjPE}^8 zxn$^mmYrB-oRd-Kis@o;twBfZek)r=Nok2u*&S1i?72P9KliE1x2&gEo@eL1h{&pu zwUqr~JGkyrh#@b_%u!>qL{wqg8uRDgCuwXJG|Q}R#}IVXctsuu+Z_VAg%Q0qacErQ z$}2mC2#R)~6H>ib4}CTsCa%NvSoCJPqYS8pQ+2K;OWeuLv^dn_O$`*{XBi@2ps%z!@ME$*jXSFezo z0W@)-%_#ZVte;I3Wa5^l{acKgWzUJ>c$@mFs{-m#FQpF)kc#SaRb69*z2Qf#uIx9@ zvj4>~zGS#}@9K%wwS9|yzLBH5@wts%UeM`4_IU4X_Itc~$a}H3P01}n4m^24$e|}+ z781@tFFr#O?-NVC4yq@}Gak7o$h$xaa!-)=f)wPQAnyk$$UQ+m08)_q0a@P}7i88e$bCV62BaVx*7bU*Sw7l*%UGU0k9(C@a`Bl!_IY*zq~1Fq z`4ULSI+A|@(y@-@pMi9&BY6>|Ae(0N1bG&tq`VU3S=_0-8uH%R>@^29Qu6mWfI;bJ zB!2~DWsd8hgX#-WK0`Dh0%>v)!|Q|9zY{xFbo zFFC1a2R!=#T>M{*gYV;#w#_=|gjd>-fI;+`P?Fpv(}pB?hvS8~>o z9B>y3h zeUcPHJv$)zH-Qvna#GKZc=i-X$2yX4__IAhJ|DOt$Uh3CAb&rQ4ml_+p*Tj4btEaz zQg2_7f8y`$3o^$@FBW8uk&+JClhS7DQ61~(Sd?||*0PRg$vWvTtO&>-2eMD{wLlI? z{veP;lIH>`6_JyUbv*leAO)EfNf)4GILk3oQjj@DN(wT^NXdXKB>VvG{VC2$MX+rk zIYxR`kXey%12TxS)FUMYnR=vTK(1w-(@@Wxvce9+*#l(kK)lsvAS)@}>fS(#x0(s0 zc&lw7W%MEXXyi_i(xYKOma{`azR6!a6lA(ZJuAo$1X7S$k)9RgEg%IMwc!6Okb-<; zAO-o>Knn8RffVHFKngM|5@di1GSL}e$l2M3vjg!~zr|U{Iy$cmK+@)cc&j@CDc))( zkm9X&V6dg$fp{zWeHs0sApbW=?Y{Ga?&6^!Z}#^N1^M1U3i3=K1({4#Zo%gR_oxbU?of%yHh?+1P=2 zs~K;B2jZ>h_od!}cq_(cuXD(Tg3J)Yv5v^U#ii1Wp+k1}4h5NGBv3;^<`^j{$c)Vd zc_hfc2I*KwWR8(sJaWkXVnL=J>AiwXJyKGT$vPno>!9jDZj#ACMi+At_a0D%RWeAO zl7V$3`B7epA7pGM^$tcnOYy#-tRwlKARX&SzJog*>qv5plsPU8a8N@*W^5+NLqVoI z3-U$QM2GP>?Tr39?}hgbw$z3sR9e z3nPlNZ~3z@3?S+El~>}e7@H}t#9LAKr5WfhE~Z;lGI%TPuOMH*S*aH`1{ZUTRLlgK zv6=EpkXf(ZE6A)!dISpha`c5XJ%YKyS$5E|&XbOHo)l!T%3Z8bg3K{`m#?Bz(ja)u zk2A%C?vwwPSH|TiZX}YIQgf~1!e_xh6QQdfTqM75~i5shRHgo8D$1{BO|1f%) zT>2jWoEyM>zcGTBmXBVQZfEKm7G7PP(c$s5beO{6GO>@J1&Iu!Dr9Gm3-5gGe% z|CwZafu#TOey*PUPk*99FtjHiTqDmSNShpK+)6} ze`7g=OB&4f^a0ENb8(XY=St`Q8RJRPGU68Q@T_@q)s*AL#7L>9zxgx9;?qaSxIP;k zKR?iP`AJ~=r~j24MP~m+d}l*+Cx_Xi)aF?p_p$Sx+Q1RBlPkqvzxRdr%f0}!Peb>h z_2p?}^ZPS}{f{+ybL@U6K9R1z*T8*v`pyD(Ru=YrbjurL{k zkjLkdt~#%Cvf?hja-I#@^UxWU8cPZ18y1cEkIc@J_^t;pDbby@=KbNAEH~`-F(cW= zK2`S`Q0od=UCf^JHki62Cb6%@j7l5a{JowUf7Y-&CuGv8?1ZAFkLLQR?7_4<25WN) zJ={%(^FBrM8<~+!dBc2P9oyV0FO%7C7Wb01IWETVj>|4S3Es_URU4_$_e9!DM0l^n z>@Q5&J>BQVb)aNM9OI)?$dz}Lr?%5NSN6iQkKD0Nt`zbE8IPKhjpDw;vbB`>!g#_I zTE6SDk}b)^T`8nuyDB-HOdsRYS$r?lTi$}NALBi(>Khda>t_0%k^rLlH}yEWrA#~( zU)nMokg?n;{^vG>t!d0nWJV(H_f#v@@7Iiku{mwDU42W4U5pO5LGTfIVg$!P_WJ}A z$Y$Sqn>JVFuf4qsSy|fWjp_K_WO)ZLd(RQi-(*hYvoZWg=}O1kJVstq=(HV?JVF(l ztn)A5-yoNj>nb+>8kY~rM184!-jN(kW*=_cMjjL!Q=1jaa4e2fw6(SG;2Rx}+l$(` z!g$j-!sW{G6^p)gw|*rTnTh<`==GSnY0OBcEPL*wO*S3lrBXP#hp6QDF>zaaq=w8x zW|br^gAKDGqgio`)H+L>(adA{5YHOfO=)+dmFUP{eXp@)MxBn>ximHA|1p{n=X{G* zo=$k>6}+0L!`QdDJt^9^8m!CmzMRgu6H$YS=Gw?(t6VW-M8nlJ1ARZCeN4hwU*0-2 z-XZ0kxrV1%NQ0*}Hmu!x=yy?j`K@Ao=gO{x+BE{MV)=K~YqJ{}T}tVDciE3i>*dBv z>e7|W&SKU)+DN>Q?b}Nt?~v8`j*{5mbIck@N}e0vV!pI|L6y5{TdFJiwV8^HbxcXH z>RV&ZB!d>0-lDCojU|mAh{b5FvQ3#WZzEx0^=L}gmd$v#LGZK0V8OdT-;Tl)t=hbDDa})l)sQ#H_}UcK5olL2|Eh0e z;XT1v#-qwyu^|sr*ZfWnWqn_ z5l+e=*K3U)r!X1I=*k#nnZLZ;6l9yYS2h<;yL>!m6jSrNe2!qU{}`OOO!+w(xnIgf zWb`wRQlAB(CYJBiHd@6I|B#8_zGWRiZ zkkXh2X);_)+3(_0VqJ$XWz8{Cl|u7-nG%_OOk|fv^*e~@52KlgI`%$af#;KZlP}4v zHOK8mV+JR~!8l5NY?be{gRJBkrYb9Sv47dF9y!QfC#>=+R>5yl`^?kFSX4De7BQnE zF?1j8>YlUEQ{x4Sny70p)%=4Cm`g-*y;T0ayf&CkYrdD#PQMc3=RJjyN$1tM@|l9n zd?4=Uy3Ge-a^UBh$-GO(cEm8L0nyA*e3fdF|3}|344GI+^8Mv4d~bA;{|DTdyy-v5|D!nsGRYw@$^U~l*^$d90HU4 zKaN+*I>#%|oHFYs`G3GG=EP9RDKm%f;cJ4Fy0eq~KaO>hPXnr1lsZr#bCoQgo#g*< ztdra$hIL?^J6kVx%$CD6CgL;)lC6%pWnQKw97wjhiwG~|UX!eEFv#{6TLJH5{vReZ@~I*C%|Mz< zH1eByh1|z2+x&D_wi~X4FZ)lse_+r5qbXlTa*M6m6L2jL;T*To9K*6SAA<7bR-QeI z&hE!QgS|C-Qm$FY((*jcF5vG8T+4sdo7b)2H!*f+PiHq}yHMLIwv{o_^n)`3+p$sL4A0rU&GO$6h&I3w9-;?5>Sp-+?`!Y+Y)VF&X`2nyT4((^X$py?XWDtLo~fpZ#?6um0gjxBNOW*O!xx$p^zLlgo0ycXHp8 zYm?+|(ERO#;c&=X{En~V>tn=$XOm_8TTdpE$CD-eT}{q%C>vilB$xUz=lHL@{TF}o z_UcbJ|IHoG?wdJSgjwnA`?v4^%X^)B@$=qG^xhO|XT9_CxmGODfVwW((Aj`|j^z!C zB=;lJV87;^PG*w@)IO1HZ&I|B=jvpGubXWsWvKaKYuf*s+pT_feQ#60tAKO&_j!Pi z;CHr>={_|29;6459z@!P#8KOU)J58fv5q`ELb@918YH*? z$!4VMkUoucJh`TksLxKcdlS>&Hb&aLnwT;7L~XDiaZNL2{h%D>FD*7f~(+Ar<*da{U?tiks= zj`q)^1@xI8M>|&V?Q8`<*baxWHMtMP-NlPtN88dk+KUPqm4;CT7n6b)m7Jr(4>=eG;yKo%PC{a7$=oV{k`~E*Fed24xdP$ zBGh3F%UKLOr^9x#nL6uf{A13P(;fYbA}n_K2KPJYa=ATeA>Pbg)B;x}Q~Q^XpFTCe zv`*D<<7N4^5*srk^3ECQS5K}Zoa}jIe*dfAPJErN1TUHw87>xJ5+mdZ-o@n**h$Ux z;1gTF?sCy=Nfu@s5m1zp)Ss^o?b+ylWF_?kbNP1Ui?A4J_nZ5RpU)K&$A=K?MMvx} zzWnz+_`tQ7ep0g7fu8+4{NdLu=|Hob%E9^b=fOyNX&c|c2NtFu3^$*|@GL-Tjzew^ zLxpg-W+06Gh13Fbb++;I`f11q1$73>W*-06v2=PEOC(i;RB1<1?j`2EWINQ!ZY;N0 z{Tuw{^V3k7FNxAxL-_+hT7`O|qH?&DTPjqlTOu!lI)xGk@EhCd6f4j+J%_e2@4DQX z!=imVYuQ??_OD@)Sl5mS55|QEn>v@OQTDPAxC9=?kJX5e)Q7ut>exTdPRc@l*{+yZx8fo^mCoR z+-k>&IbOp31^iP9=6F%IPKmsbGr2Lo&X#{pq~Iy!T8CNV$iE`(mpNeM>lsLVU8|3X zOdN+)WX{NbwBS_6jfo;RLTLNY-xXlZVtaumUEL1!pJ**`G7C|Gv$NQq*yxf|%HiUu zu&w0sD(0vZGNO03P)J0md zI(t8KkVpMov8~g%_e8BEJ4xaE`6ke~DNCntRrQ_n?3N;8NS40_oH_7wk?q;I3mQ*) zYDTh&ep*2rmXnv!Ca0x2>yBsr{6eu+O)#l7df--G&*UtR_STF_6Wktn#svj!U&)?b zU1iUlV+}5S#!4hLeb55kU`ue+xE$c}fXkWuB8hS#`uWZxzhW|+_310<)4G>q;K2jf zfpM)G*%&HyJ!iqJNZJZ!*(#_7a85M-6_e;W7gvfeH5fe-wvsd8gFe;}3DlmCU>(^W z`X`=yW>z(kiWTv(noES{M~#!kk&5X(N`rb=T-Kh1*T!Y>0%|9>=}Y-u7#kf!eg-;= zh#dd3g0hpJ*I4;uA1Qm}${EQgBWEmXOg83Gb_EFROHo^j{2EIbL(BZGF1*#90lzDT zlyBDz?8zZ`)1ITIk74Pgk3ikX@p77%TMR&By1zK?O=OHdD*gIZjFbAZ5$(36SVv3( zh{nwpZf~$XPMTzVOSbfB+5A{VYiI?yIGjNlZl}zp_C(4V%^l-^rZd=@HME8JB~QtW zMA+`>tW-~?VkC^tX*LqHEtCuCqnX1^5S&LI8Nm^d=?A?AUpIn$Pf<3Sw7D#Q&Gj9( zmP9vQYxT)dj5F;Stt!XJb9G0tg)!+>%}7s{lFQx`E!)~Vgb~{ZnVm%r*2Oh!t~SGu zSZxPIchQg1c@ojZMqXp+)E(4m>YE-xJC`uC$QjX&C7rBm7uR3U!BW1Gj^D6bExYmh zzKXiJpHR0Z+K)Sj*q+OG;t}k2*&+B6>^Oe;Y;n}WI`RNXImLW}|mNUUX+fqkHW zjqWxcLo2yo&E5LU4?hN}c6;Iz@a>NQaS5Nwv!3Xd)1oij^R8n!bgQy+%t+4zvOApa z#+KHvM^P_ZMtAs2XSr z&YP=S!CjkT*+NV1E&3I)mOL#|*{D5~rK_#fK8v(-$hUGC`0&K=uqbW#{aeS{_GmKBT)Z8MQ7)-SEvMh$evKD*q=^A@|)JnKR3E?3uT)+}MkaxxTrl6^r|#cH7= z9*(gGF+ITpb<{jLi&A>Zn;#|4!pgBNotepd;;h9zJ1LK#?A6|GWg{Lu)khcF{D z#@*=bgoBsM(rPtpc|!!_y~eC@?F<|Fnc*(%VDHF3Gc-CREQ_P*kg#5=S=Sjk3uDys z$yoTj4yd#8)I^avVCHWV)CFjRp10OHKegr*h2}47p4faVN(_Hd3pJrQTBXW5ar*8kyr9 z%gu0d%{*}pc~xpjm?K-wS*w`sqE_vygwUP0%iZJL8ChcmnrJkx-?LM#uD|L@$}AZH za|Lo%JR7Gzi64;>Fx~EtTPeAg0`r=xl@d>|(btkQQuh_5DW-incFNh<`pEaltOvsK z+HO4%k^@QpV@~@$ECZBZSEra2g!oNw|H$rgqnp>M0sudQdGQXY7;Y zfAF#gU&x%AT0XhFx_`dM_i6BpUK{20nigc2Wv7$h;WbT`#igDt{_i30Hh#y1^FOILGF2EDccj|>m5{2 zkXe!5735tY1-U24tXGhGg1i@`ARE?oyQo<%?Y?C!@4kt$$}7oy#*;nXodc<5`y^ig znX-=LpMp$TNAizBrmQ3RE=WN(&FBj9G)75zCCJk#sk|Dn>~!*a3N=*nclZH=(o0DG z63EJ&w1X*BPmm`aa!-&Sb;vzIo^r@NLFO0qwHZDeFkS2r^|I$#-1Yt{}hZdelvkJ?j+8vyS8ou7lknNsf_pu$PeJ zph&BFg8Z&SHvR2Mo2QXgTGbn{EXPQp1evq!Etxn)=Jfl0WacHA@i!$C&CI)GopPf~ z@_U}_k^HtN1&W+ZS;xCeAXCc%V1T>4&Q~xN@Au;_S*P9alKgv5 z_DE6)^=_Z!w>&Ax zS?cWx@(*0uo*;9KG_xRcjFe1~9Vu<*JgQ=yyiF&pd%u!(v@&F!^cPwLtR{>c`u~xTwQmoaa zC&gOr04bvn(MKb9gOnZ(JhI#!2=XSEc_7F%i+Wd(@9?A`vm(7K$lE{)GHStpi6;g5 z3Qr31wVo8@n>;DV6P^@gR-}?sWDcr9MbkBR`(mwrgS#o~XuK#p$_=qr*LzZ|)uboI zTJ6GMOTB%uR|*?}P6>PbPK^rRqjj0AZo$iD@toJ=jO zyDP{XBh4(x93v$KnPa4+Ad_`MqUX|mvVgvsh&Kheim`pMR`20%$~qdLUwY=G);ZO& zeX&-PP6PMFTG8%Hy?wD(^v&L4y#qm}3z4#p$iG2WX~rN$PRkAinPVhS13~5(DJjVG z%>;QU$iD)avX00cBgs5WkzHm%<~-7}g3NiOq#%=ZLK@a#ssp)BCi@9Z%)2OiP$#UC zUh0(etRu-wc_ns`zM0h9AM!56`@FJ_;QM z$dqS69!i^k0WxKsLV4Da2%-z0N*ndV@ z>0n>1)lXap`(mwV_mx*-t>~L6uTpE}NU>IQA*2}tLFS+c@<5Q^b;tuje%B!n1ev~> z@=B1|1%(o14$3b$C__R18OW4%G`Jk2RO(1_9;K`!N#Pf$VT$aa1evT8WWyW?9m=u` z)vV*)AG^Cy3?OOul~-b|=$k38#9DFgOEb`2WTshE(vOk$SL!{FyHYQ73^H?!RLlgK zzM1k$kXf&m6=YT|n||N2aWEq#%P;X=XtNFi2kS^R<1^Z^@9c zggWx8eq`U}^?Lb{C0R5ro|a_vp)7a)`vobf%6Ei1QDnr{vdVHl>p66pvW%a--w$6T zm%ho5bA6caw}-IOa%rw8za^4_OmKGuSoZ`F7nsT0?6($+gMUGe`HGo;$HTGrzum zVrfBQ38FSMwv)AlTPleJPSA!dcHAb5vZwf&HCCfsWk9?mTm_zdCvJMBh(CNZ*@GFx zBP$bf>@0g!`WRxVY)4#`ZTQQ$FpM;_jE-rCc=tJMX&#WE2M|0@?UX&nCxI@-nA8pr>N zhK~F9`&5gGao4YfPyU`NI{Dpwzlb5p{&CeeX$$SOi(>bH9Gi2$WD(cgXuG&|wD&yH zda+}Meazy2{nrcQ_+LH-*hee=mpMxiKBW+R{y$NCj@X+sHh}=9;VpPIj)O0>|($)E+*o zUx&#H#L{XP$E$P#lBd+l?-=n(zP`D{)84r=H|FGyo`2&T3tU*$orYUNFVgqzD&m+G zr8*{2?(7L=NKS~D(~wP0oxBVK!ngIk9GZ8ILnBgiHq4yn3!lJnG7&tDdsmS!&1XB` zzTmkWj!5Yp-`1!ZmU|bu7=1?Oap|lz-?+@)6R3$BW@dkE~T$m-F$lN~gHlp8Uz} z%2-{c6QHy$!I)r1>YM0@tOpgArslk@h66?)&08N$>RW&uMx(Epac+WJJ`G2kl8Y2) ztWj9{^6z>&Eta12;tVprPD~E-CuB>pql~qUVP)^}lp$RJtt#p|u{Wi+Jt!Z1 zuY{wKi|=JDWlR&5Vn}p$lB4E4Zwy6GZgU4X8tu$jtY)%AyV86<_?T@Y*yzR7&jSzC z&YMvTTUc$QTA&I!HS;qj5t+WEw^SQrt7HWmM*K3fCN{=wgL_w#qn5sH))7lQ!n?!e zcNPIu`TZ0RrnmO&>HOf82_#L*wn`VYKf@tQa6lNW-NZpX4TpugVRKk zvXz|oV9Z#ArKIi1hSA!OY>Zb85KHeI+Z8kSYg{CS$_QCTr!?F5dlfb^JKW*9Z*+Fd zs&g+@BQ|@TJ%X28UI*)2Va@F%*M;&1x>-FmkwIMvMQqsxdH-Hl_mP^6{D(aFMiH4t z*1DB`vO$ZK8M@ zobgJ+`ZlnRGJFl&_uA1h9$-vbORpRj0H#%9Ghp!T39 zwph!xV)hPiQ4@22oBK6-V9L#_-19Nr>$)KJO)Y_|!yV3o4XxR|_7UJlSgkFlyLH9h z=IR{zS)E&9dfRViZFB4E5zLGYS-;v@Z?tJx2Q`d%V)R;AA8~X8QJu`}4`WqFFsN9= zv}&PSD^unRF|iCv^OcK<(A6v+*u{-WX68yv$IzGM?59*~JPS8Jb2?zO*lXq)SF9yb zP1HUPo$=e6%*^t>m7k?%H5!&+C#%6H>ZW}$xnmrHK>V$e9WL$O?S^qDOktks;`<_1$p-*&q z7Kt zxm>ft)BNbKk*X$jHHc_vMkA!$CNepD~B#%wZ5 zZnaz?6Y)4@Bh**7t4`f8>Iv^T_%!9yCwf8?`i2+YTVc}oRmSnZ#__-KlJPXQLtf-- z^IeG{GLHY1Y!hyb<9}(qhJzAPK_Y#O*@>`x1WL6|GM8@&Iz?^aX zFBgev9REwFXw{S~PS|n$FHD?Cs1b=h1g?S9OLQGc-c@Z@ygQEnl`==-G#S=$1(J*2 zXzq^Vf2FLGm{x{4jE^*q|Ak2>ay5?sMRk29+$#4dmvN}LiD~msV6CeEqjD|rpv0(h5vcfh zn6>bsgnP|rEpd4s#kW{2E3%arxFy#({#X0>U&_IJsJ3zZH^-AaS3arn-#$t6KO494 Ix27EUU-`{PM*si- literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/mcl_unloadclip/test.swf b/core/tests/swfs/avm1/mcl_unloadclip/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..3db34d03a579b7b83bac98f10930dc247e8017d1 GIT binary patch literal 244 zcmV1|K@ot}Q6^$cA60Nn(4m*ASox~?b5FE76c9c=dI zg(@ue$C757^6=2?mlgNIShpZ+&6+Sj($3uuQ1BhN*V3Y>$T*sB5AO3!! Date: Fri, 17 Jan 2020 20:54:48 -0500 Subject: [PATCH 42/59] Basic, stub implementation of `MovieClipLoader.getProgress`, plus test. This implementation just returns the size of the current loaded movie. The test is also deliberately written to be loose on timings so that it likely won't see a partially loaded movie. (I don't want it to be a test of load events, so I just wait a few frames, rather than the correct way of waiting for `onLoadComplete`.) Until we support streaming file loads, we can't faithfully support these properties. Still, it's better to have them, just in case. --- core/src/avm1/globals/movie_clip_loader.rs | 47 ++++++++++++++++++ core/tests/regression_tests.rs | 1 + .../swfs/avm1/mcl_getprogress/output.txt | 3 ++ .../swfs/avm1/mcl_getprogress/target.fla | Bin 0 -> 39424 bytes .../swfs/avm1/mcl_getprogress/target.swf | Bin 0 -> 68 bytes core/tests/swfs/avm1/mcl_getprogress/test.fla | Bin 0 -> 41984 bytes core/tests/swfs/avm1/mcl_getprogress/test.swf | Bin 0 -> 224 bytes 7 files changed, 51 insertions(+) create mode 100644 core/tests/swfs/avm1/mcl_getprogress/output.txt create mode 100644 core/tests/swfs/avm1/mcl_getprogress/target.fla create mode 100644 core/tests/swfs/avm1/mcl_getprogress/target.swf create mode 100644 core/tests/swfs/avm1/mcl_getprogress/test.fla create mode 100644 core/tests/swfs/avm1/mcl_getprogress/test.swf diff --git a/core/src/avm1/globals/movie_clip_loader.rs b/core/src/avm1/globals/movie_clip_loader.rs index c8c494353..3742a82d1 100644 --- a/core/src/avm1/globals/movie_clip_loader.rs +++ b/core/src/avm1/globals/movie_clip_loader.rs @@ -189,6 +189,46 @@ pub fn unload_clip<'gc>( Ok(false.into()) } +pub fn get_progress<'gc>( + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let target = args.get(0).cloned().unwrap_or(Value::Undefined); + + if let Value::Object(target) = target { + if let Some(movieclip) = target + .as_display_object() + .and_then(|dobj| dobj.as_movie_clip()) + { + let ret_obj = ScriptObject::object(context.gc_context, None); + ret_obj.define_value( + context.gc_context, + "bytesLoaded", + movieclip + .movie() + .map(|mv| (mv.data().len() + 21).into()) + .unwrap_or(Value::Undefined), + EnumSet::empty(), + ); + ret_obj.define_value( + context.gc_context, + "bytesTotal", + movieclip + .movie() + .map(|mv| (mv.data().len() + 21).into()) + .unwrap_or(Value::Undefined), + EnumSet::empty(), + ); + + return Ok(ret_obj.into()); + } + } + + Ok(Value::Undefined.into()) +} + pub fn create_proto<'gc>( gc_context: MutationContext<'gc, '_>, proto: Object<'gc>, @@ -231,6 +271,13 @@ pub fn create_proto<'gc>( EnumSet::empty(), Some(fn_proto), ); + mcl_proto.as_script_object().unwrap().force_set_function( + "getProgress", + get_progress, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); mcl_proto.into() } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index fa18762fa..c8e7a3141 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -168,6 +168,7 @@ swf_tests! { (unloadmovie_method, "avm1/unloadmovie_method", 11), (mcl_loadclip, "avm1/mcl_loadclip", 11), (mcl_unloadclip, "avm1/mcl_unloadclip", 11), + (mcl_getprogress, "avm1/mcl_getprogress", 6), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/mcl_getprogress/output.txt b/core/tests/swfs/avm1/mcl_getprogress/output.txt new file mode 100644 index 000000000..dddff57ef --- /dev/null +++ b/core/tests/swfs/avm1/mcl_getprogress/output.txt @@ -0,0 +1,3 @@ +Child movie loaded! +68 +68 diff --git a/core/tests/swfs/avm1/mcl_getprogress/target.fla b/core/tests/swfs/avm1/mcl_getprogress/target.fla new file mode 100644 index 0000000000000000000000000000000000000000..3ec29ba191447179d3ce0759edfbaf96b9c421d1 GIT binary patch literal 39424 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/mcl_getprogress/target.swf b/core/tests/swfs/avm1/mcl_getprogress/target.swf new file mode 100644 index 0000000000000000000000000000000000000000..1a1a83455cade7036d1025e2bc10ef0cb88d2e36 GIT binary patch literal 68 zcmZ<@59V-TU|^_VV2x*B;9tPNz{AMkA_x>=aAx}d|Gz!66p%Sh6v)lU%t=wm%`eML TRmjOtOi4{qWKd&p0LlUY&fpHG literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/mcl_getprogress/test.fla b/core/tests/swfs/avm1/mcl_getprogress/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..c2499cee00e7c698ae17c18471d3ed7ab0712789 GIT binary patch literal 41984 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/mcl_getprogress/test.swf b/core/tests/swfs/avm1/mcl_getprogress/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..8a53aecf941af0d4a1fa836b33c57a4b12e3188a GIT binary patch literal 224 zcmV<603ZKDS5pW`0RRAaoJCK|3c@fDoTiUNP(%>C^z6|O5J5q`sfc)&s!)2$L2}jY(OSL;q>uV+M*WPxf43o?lWv&eCG$ z=AD61r&MAmc%u09T&Vl+2r3aru6pu%umYCT4KPIb6!S&{-3B9nhEP&9Kh^{+SPiO1 zVqF6Z-3E3OtvVR}-q4p-)V@(JM1B-pN*C@E+{9RZ`R!eCX+N-cc%7>xgAR-|^I}aw aJsZUO_%4An4Hn)15H7mZSOaf?>_!o{3u5U2 literal 0 HcmV?d00001 From 538a5f05e5ca5e12b81b403522198a74fe5019a3 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 17 Jan 2020 21:04:10 -0500 Subject: [PATCH 43/59] Add tests for various forms of `loadVariables`. Surprisingly enough these tests passed without any changes, somehow. --- core/tests/regression_tests.rs | 3 +++ core/tests/swfs/avm1/loadvariables/output.txt | 2 ++ core/tests/swfs/avm1/loadvariables/test.fla | Bin 0 -> 40448 bytes core/tests/swfs/avm1/loadvariables/test.swf | Bin 0 -> 141 bytes core/tests/swfs/avm1/loadvariables/testvars.txt | 1 + .../swfs/avm1/loadvariables_method/output.txt | 2 ++ .../swfs/avm1/loadvariables_method/test.fla | Bin 0 -> 40448 bytes .../swfs/avm1/loadvariables_method/test.swf | Bin 0 -> 159 bytes .../swfs/avm1/loadvariables_method/testvars.txt | 1 + .../tests/swfs/avm1/loadvariablesnum/output.txt | 2 ++ core/tests/swfs/avm1/loadvariablesnum/test.fla | Bin 0 -> 39936 bytes core/tests/swfs/avm1/loadvariablesnum/test.swf | Bin 0 -> 148 bytes .../swfs/avm1/loadvariablesnum/testvars.txt | 1 + 13 files changed, 12 insertions(+) create mode 100644 core/tests/swfs/avm1/loadvariables/output.txt create mode 100644 core/tests/swfs/avm1/loadvariables/test.fla create mode 100644 core/tests/swfs/avm1/loadvariables/test.swf create mode 100644 core/tests/swfs/avm1/loadvariables/testvars.txt create mode 100644 core/tests/swfs/avm1/loadvariables_method/output.txt create mode 100644 core/tests/swfs/avm1/loadvariables_method/test.fla create mode 100644 core/tests/swfs/avm1/loadvariables_method/test.swf create mode 100644 core/tests/swfs/avm1/loadvariables_method/testvars.txt create mode 100644 core/tests/swfs/avm1/loadvariablesnum/output.txt create mode 100644 core/tests/swfs/avm1/loadvariablesnum/test.fla create mode 100644 core/tests/swfs/avm1/loadvariablesnum/test.swf create mode 100644 core/tests/swfs/avm1/loadvariablesnum/testvars.txt diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index c8e7a3141..5f09f9406 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -169,6 +169,9 @@ swf_tests! { (mcl_loadclip, "avm1/mcl_loadclip", 11), (mcl_unloadclip, "avm1/mcl_unloadclip", 11), (mcl_getprogress, "avm1/mcl_getprogress", 6), + (loadvariables, "avm1/loadvariables", 3), + (loadvariablesnum, "avm1/loadvariablesnum", 3), + (loadvariables_method, "avm1/loadvariables_method", 3), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/loadvariables/output.txt b/core/tests/swfs/avm1/loadvariables/output.txt new file mode 100644 index 000000000..5b653ae6a --- /dev/null +++ b/core/tests/swfs/avm1/loadvariables/output.txt @@ -0,0 +1,2 @@ +Hurray +The test passed diff --git a/core/tests/swfs/avm1/loadvariables/test.fla b/core/tests/swfs/avm1/loadvariables/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..2e09001279ffb09141a1dcabd23791f3ec29ab9f GIT binary patch literal 40448 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadvariables/test.swf b/core/tests/swfs/avm1/loadvariables/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..25cf5428530bc4887d99ad18f794cdf727430a19 GIT binary patch literal 141 zcmV;80CN9BS5pY40001ZoU349jb~usU% v42Wd(o5sk%oF*hRmx)0EMO+QRVe(UBpUK3)#?Z*bzzC!qfCd8qwu>H|`?xt% literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadvariables/testvars.txt b/core/tests/swfs/avm1/loadvariables/testvars.txt new file mode 100644 index 000000000..c78b9c1a8 --- /dev/null +++ b/core/tests/swfs/avm1/loadvariables/testvars.txt @@ -0,0 +1 @@ +loaded=Hurray&also=The%20test%20passed \ No newline at end of file diff --git a/core/tests/swfs/avm1/loadvariables_method/output.txt b/core/tests/swfs/avm1/loadvariables_method/output.txt new file mode 100644 index 000000000..5b653ae6a --- /dev/null +++ b/core/tests/swfs/avm1/loadvariables_method/output.txt @@ -0,0 +1,2 @@ +Hurray +The test passed diff --git a/core/tests/swfs/avm1/loadvariables_method/test.fla b/core/tests/swfs/avm1/loadvariables_method/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..c245e19c77f8dd31684b1a79d7d9391be3524362 GIT binary patch literal 40448 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadvariables_method/test.swf b/core/tests/swfs/avm1/loadvariables_method/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..4a768f7278edd59c294f5183ab991c60a5d248da GIT binary patch literal 159 zcmV;Q0AT+^S5pYP0001ZoU349jb~usU%e}bWEL~XOyg%@$jMJk2}>->Oiap2EoKN3XK(;Qd*)D}q7GpO zW{66VqSO?I#GK-MhG|R;96+r=lF@G(BLj1qkkDKv1_cyxH3WyrPmO&h69XGVBNGE7 Nka7ST3;_PbBdR#SKVtv@ literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadvariables_method/testvars.txt b/core/tests/swfs/avm1/loadvariables_method/testvars.txt new file mode 100644 index 000000000..c78b9c1a8 --- /dev/null +++ b/core/tests/swfs/avm1/loadvariables_method/testvars.txt @@ -0,0 +1 @@ +loaded=Hurray&also=The%20test%20passed \ No newline at end of file diff --git a/core/tests/swfs/avm1/loadvariablesnum/output.txt b/core/tests/swfs/avm1/loadvariablesnum/output.txt new file mode 100644 index 000000000..5b653ae6a --- /dev/null +++ b/core/tests/swfs/avm1/loadvariablesnum/output.txt @@ -0,0 +1,2 @@ +Hurray +The test passed diff --git a/core/tests/swfs/avm1/loadvariablesnum/test.fla b/core/tests/swfs/avm1/loadvariablesnum/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..2c254693a0207b9a23d23cfb319a2d7695f57f1c GIT binary patch literal 39936 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 Cay)bZ literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/loadvariablesnum/testvars.txt b/core/tests/swfs/avm1/loadvariablesnum/testvars.txt new file mode 100644 index 000000000..c78b9c1a8 --- /dev/null +++ b/core/tests/swfs/avm1/loadvariablesnum/testvars.txt @@ -0,0 +1 @@ +loaded=Hurray&also=The%20test%20passed \ No newline at end of file From aab339880d65b7c14a3d62d157a52ce84a019f2b Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 17 Jan 2020 22:58:48 -0500 Subject: [PATCH 44/59] Implement `XML.load()`, with tests. Interestingly enough, very little actually has to be done inside the async process for XML. The async process basically just fetches data and fires an event handler when it's done. Everything else is handled via a system builtin, `XML.onData`. --- core/src/avm1/globals/xml.rs | 80 +++++++++++ core/src/loader.rs | 150 +++++++++++++++++++- core/tests/regression_tests.rs | 1 + core/tests/swfs/avm1/xml_load/output.txt | 2 + core/tests/swfs/avm1/xml_load/test.fla | Bin 0 -> 40448 bytes core/tests/swfs/avm1/xml_load/test.swf | Bin 0 -> 189 bytes core/tests/swfs/avm1/xml_load/whataload.xml | 1 + 7 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 core/tests/swfs/avm1/xml_load/output.txt create mode 100644 core/tests/swfs/avm1/xml_load/test.fla create mode 100644 core/tests/swfs/avm1/xml_load/test.swf create mode 100644 core/tests/swfs/avm1/xml_load/whataload.xml diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index 8658a0540..5213aa790 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -6,6 +6,7 @@ use crate::avm1::return_value::ReturnValue; use crate::avm1::script_object::ScriptObject; use crate::avm1::xml_object::XMLObject; use crate::avm1::{Avm1, Error, Object, TObject, UpdateContext, Value}; +use crate::backend::navigator::RequestOptions; use crate::xml; use crate::xml::{XMLDocument, XMLNode}; use enumset::EnumSet; @@ -781,6 +782,71 @@ pub fn xml_parse_xml<'gc>( Ok(Value::Undefined.into()) } +pub fn xml_load<'gc>( + avm: &mut Avm1<'gc>, + ac: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let url = args.get(0).cloned().unwrap_or(Value::Undefined); + + if let Value::Null = url { + return Ok(false.into()); + } + + if let Some(node) = this.as_xml_node() { + let url = url.coerce_to_string(avm, ac)?; + + this.set("loaded", false.into(), avm, ac)?; + + let fetch = ac.navigator.fetch(url, RequestOptions::get()); + let target_clip = avm.target_clip_or_root(ac); + let process = ac.load_manager.load_xml_into_node( + ac.player.clone().unwrap(), + node, + target_clip, + fetch, + ); + + ac.navigator.spawn_future(process); + + Ok(true.into()) + } else { + Ok(false.into()) + } +} + +pub fn xml_on_data<'gc>( + avm: &mut Avm1<'gc>, + ac: &mut UpdateContext<'_, 'gc, '_>, + this: Object<'gc>, + args: &[Value<'gc>], +) -> Result, Error> { + let src = args.get(0).cloned().unwrap_or(Value::Undefined); + + if let Value::Undefined = src { + let on_load = this.get("onLoad", avm, ac)?.resolve(avm, ac)?; + on_load + .call(avm, ac, this, &[false.into()])? + .resolve(avm, ac)?; + } else { + let src = src.coerce_to_string(avm, ac)?; + let parse_xml = this.get("parseXML", avm, ac)?.resolve(avm, ac)?; + parse_xml + .call(avm, ac, this, &[src.into()])? + .resolve(avm, ac)?; + + this.set("loaded", true.into(), avm, ac)?; + + let on_load = this.get("onLoad", avm, ac)?.resolve(avm, ac)?; + on_load + .call(avm, ac, this, &[true.into()])? + .resolve(avm, ac)?; + } + + Ok(Value::Undefined.into()) +} + pub fn xml_doc_type_decl<'gc>( _avm: &mut Avm1<'gc>, _ac: &mut UpdateContext<'_, 'gc, '_>, @@ -927,6 +993,20 @@ pub fn create_xml_proto<'gc>( EnumSet::empty(), Some(fn_proto), ); + xml_proto.as_script_object().unwrap().force_set_function( + "load", + xml_load, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); + xml_proto.as_script_object().unwrap().force_set_function( + "onData", + xml_on_data, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); xml_proto } diff --git a/core/src/loader.rs b/core/src/loader.rs index 6eda0a857..8ef763306 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -5,6 +5,7 @@ use crate::context::{ActionQueue, ActionType}; use crate::display_object::{DisplayObject, MorphShape, TDisplayObject}; use crate::player::{Player, NEWEST_PLAYER_VERSION}; use crate::tag_utils::SwfMovie; +use crate::xml::XMLNode; use gc_arena::{Collect, CollectionContext}; use generational_arena::{Arena, Index}; use std::future::Future; @@ -125,6 +126,29 @@ impl<'gc> LoadManager<'gc> { loader.form_loader(player, fetch) } + + /// Kick off an XML data load into an XML node. + /// + /// Returns the loader's async process, which you will need to spawn. + pub fn load_xml_into_node( + &mut self, + player: Weak>, + target_node: XMLNode<'gc>, + active_clip: DisplayObject<'gc>, + fetch: Pin, Error>>>>, + ) -> Pin> + 'static>> { + let loader = Loader::XML { + self_handle: None, + active_clip, + target_node, + }; + let handle = self.add_loader(loader); + + let loader = self.get_loader_mut(handle).unwrap(); + loader.introduce_loader_handle(handle); + + loader.xml_loader(player, fetch) + } } impl<'gc> Default for LoadManager<'gc> { @@ -156,13 +180,38 @@ pub enum Loader<'gc> { /// The target AVM1 object to load form data into. target_object: Object<'gc>, }, + + /// Loader that is loading XML data into an XML tree. + XML { + /// The handle to refer to this loader instance. + self_handle: Option, + + /// The active movie clip at the time of load invocation. + /// + /// This property is a technicality: Under normal circumstances, it's + /// not supposed to be a load factor, and only exists so that the + /// runtime can do *something* in really contrived scenarios where we + /// actually need an active clip. + active_clip: DisplayObject<'gc>, + + /// The target node whose contents will be replaced with the parsed XML. + target_node: XMLNode<'gc>, + }, } unsafe impl<'gc> Collect for Loader<'gc> { fn trace(&self, cc: CollectionContext) { match self { - Loader::Movie { target_clip, .. } => target_clip.trace(cc), + Loader::Movie { + target_clip, + target_broadcaster, + .. + } => { + target_clip.trace(cc); + target_broadcaster.trace(cc); + } Loader::Form { target_object, .. } => target_object.trace(cc), + Loader::XML { target_node, .. } => target_node.trace(cc), } } } @@ -176,6 +225,7 @@ impl<'gc> Loader<'gc> { match self { Loader::Movie { self_handle, .. } => *self_handle = Some(handle), Loader::Form { self_handle, .. } => *self_handle = Some(handle), + Loader::XML { self_handle, .. } => *self_handle = Some(handle), } } @@ -418,4 +468,102 @@ impl<'gc> Loader<'gc> { false } } + + pub fn xml_loader( + &mut self, + player: Weak>, + fetch: Pin, Error>>>>, + ) -> Pin> + 'static>> { + let handle = match self { + Loader::XML { self_handle, .. } => self_handle.expect("Loader not self-introduced"), + _ => return Box::pin(async { Err("Non-XML loader spawned as XML loader".into()) }), + }; + + let player = player + .upgrade() + .expect("Could not upgrade weak reference to player"); + + Box::pin(async move { + let data = fetch.await; + if let Ok(data) = data { + let xmlstring = String::from_utf8(data)?; + + player.lock().expect("Could not lock player!!").update( + |avm, uc| -> Result<(), Error> { + let (mut node, active_clip) = match uc.load_manager.get_loader(handle) { + Some(Loader::XML { + target_node, + active_clip, + .. + }) => (*target_node, *active_clip), + _ => unreachable!(), + }; + + let object = + node.script_object(uc.gc_context, Some(avm.prototypes().xml_node)); + avm.insert_stack_frame_for_method( + active_clip, + object, + NEWEST_PLAYER_VERSION, + uc, + "onHTTPStatus", + &[200.into()], + ); + avm.run_stack_till_empty(uc)?; + + avm.insert_stack_frame_for_method( + active_clip, + object, + NEWEST_PLAYER_VERSION, + uc, + "onData", + &[xmlstring.into()], + ); + avm.run_stack_till_empty(uc)?; + + Ok(()) + }, + )?; + } else { + player.lock().expect("Could not lock player!!").update( + |avm, uc| -> Result<(), Error> { + let (mut node, active_clip) = match uc.load_manager.get_loader(handle) { + Some(Loader::XML { + target_node, + active_clip, + .. + }) => (*target_node, *active_clip), + _ => unreachable!(), + }; + + let object = + node.script_object(uc.gc_context, Some(avm.prototypes().xml_node)); + avm.insert_stack_frame_for_method( + active_clip, + object, + NEWEST_PLAYER_VERSION, + uc, + "onHTTPStatus", + &[404.into()], + ); + avm.run_stack_till_empty(uc)?; + + avm.insert_stack_frame_for_method( + active_clip, + object, + NEWEST_PLAYER_VERSION, + uc, + "onData", + &[], + ); + avm.run_stack_till_empty(uc)?; + + Ok(()) + }, + )?; + } + + Ok(()) + }) + } } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 5f09f9406..f9509bd75 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -172,6 +172,7 @@ swf_tests! { (loadvariables, "avm1/loadvariables", 3), (loadvariablesnum, "avm1/loadvariablesnum", 3), (loadvariables_method, "avm1/loadvariables_method", 3), + (xml_load, "avm1/xml_load", 1), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/xml_load/output.txt b/core/tests/swfs/avm1/xml_load/output.txt new file mode 100644 index 000000000..569043554 --- /dev/null +++ b/core/tests/swfs/avm1/xml_load/output.txt @@ -0,0 +1,2 @@ +XML has loaded! +What a load! diff --git a/core/tests/swfs/avm1/xml_load/test.fla b/core/tests/swfs/avm1/xml_load/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..5fdf7da359112a326e5d699a1a2ac85d14d55e36 GIT binary patch literal 40448 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 a load! \ No newline at end of file From 33d26b9149d63e02a691130983f80b2d41fac45d Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 17 Jan 2020 23:11:09 -0500 Subject: [PATCH 45/59] Shorten `Pin> + 'static>>` into `OwnedFuture`. This is technically stricter on `fetch` impls, but right now we can't support non-`'static` futures at all. --- core/src/backend/navigator.rs | 6 +----- core/src/loader.rs | 27 +++++++++++++-------------- desktop/src/navigator.rs | 17 +++++------------ web/src/navigator.rs | 14 +++++--------- 4 files changed, 24 insertions(+), 40 deletions(-) diff --git a/core/src/backend/navigator.rs b/core/src/backend/navigator.rs index 36ec31627..70ca4c2d2 100644 --- a/core/src/backend/navigator.rs +++ b/core/src/backend/navigator.rs @@ -283,11 +283,7 @@ impl NavigatorBackend for NullNavigatorBackend { ) { } - fn fetch( - &self, - url: String, - _opts: RequestOptions, - ) -> Pin, Error>>>> { + fn fetch(&self, url: String, _opts: RequestOptions) -> OwnedFuture, Error> { let mut path = self.relative_base_path.clone(); path.push(url); diff --git a/core/src/loader.rs b/core/src/loader.rs index 8ef763306..6ea0e40e0 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -1,6 +1,7 @@ //! Management of async loaders use crate::avm1::{Object, TObject, Value}; +use crate::backend::navigator::OwnedFuture; use crate::context::{ActionQueue, ActionType}; use crate::display_object::{DisplayObject, MorphShape, TDisplayObject}; use crate::player::{Player, NEWEST_PLAYER_VERSION}; @@ -8,8 +9,6 @@ use crate::tag_utils::SwfMovie; use crate::xml::XMLNode; use gc_arena::{Collect, CollectionContext}; use generational_arena::{Arena, Index}; -use std::future::Future; -use std::pin::Pin; use std::sync::{Arc, Mutex, Weak}; use url::form_urlencoded; @@ -67,9 +66,9 @@ impl<'gc> LoadManager<'gc> { &mut self, player: Weak>, target_clip: DisplayObject<'gc>, - fetch: Pin, Error>>>>, + fetch: OwnedFuture, Error>, target_broadcaster: Option>, - ) -> Pin> + 'static>> { + ) -> OwnedFuture<(), Error> { let loader = Loader::Movie { self_handle: None, target_clip, @@ -113,8 +112,8 @@ impl<'gc> LoadManager<'gc> { &mut self, player: Weak>, target_object: Object<'gc>, - fetch: Pin, Error>>>>, - ) -> Pin> + 'static>> { + fetch: OwnedFuture, Error>, + ) -> OwnedFuture<(), Error> { let loader = Loader::Form { self_handle: None, target_object, @@ -135,8 +134,8 @@ impl<'gc> LoadManager<'gc> { player: Weak>, target_node: XMLNode<'gc>, active_clip: DisplayObject<'gc>, - fetch: Pin, Error>>>>, - ) -> Pin> + 'static>> { + fetch: OwnedFuture, Error>, + ) -> OwnedFuture<(), Error> { let loader = Loader::XML { self_handle: None, active_clip, @@ -239,8 +238,8 @@ impl<'gc> Loader<'gc> { pub fn movie_loader( &mut self, player: Weak>, - fetch: Pin, Error>>>>, - ) -> Pin> + 'static>> { + fetch: OwnedFuture, Error>, + ) -> OwnedFuture<(), Error> { let handle = match self { Loader::Movie { self_handle, .. } => self_handle.expect("Loader not self-introduced"), _ => return Box::pin(async { Err("Non-movie loader spawned as movie loader".into()) }), @@ -394,8 +393,8 @@ impl<'gc> Loader<'gc> { pub fn form_loader( &mut self, player: Weak>, - fetch: Pin, Error>>>>, - ) -> Pin> + 'static>> { + fetch: OwnedFuture, Error>, + ) -> OwnedFuture<(), Error> { let handle = match self { Loader::Form { self_handle, .. } => self_handle.expect("Loader not self-introduced"), _ => return Box::pin(async { Err("Non-form loader spawned as form loader".into()) }), @@ -472,8 +471,8 @@ impl<'gc> Loader<'gc> { pub fn xml_loader( &mut self, player: Weak>, - fetch: Pin, Error>>>>, - ) -> Pin> + 'static>> { + fetch: OwnedFuture, Error>, + ) -> OwnedFuture<(), Error> { let handle = match self { Loader::XML { self_handle, .. } => self_handle.expect("Loader not self-introduced"), _ => return Box::pin(async { Err("Non-XML loader spawned as XML loader".into()) }), diff --git a/desktop/src/navigator.rs b/desktop/src/navigator.rs index 38137aaa1..9de72e381 100644 --- a/desktop/src/navigator.rs +++ b/desktop/src/navigator.rs @@ -1,10 +1,10 @@ //! Navigator backend for web use log; -use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend, RequestOptions}; +use ruffle_core::backend::navigator::{ + Error, NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions, +}; use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; use url::Url; use webbrowser; @@ -63,18 +63,11 @@ impl NavigatorBackend for ExternalNavigatorBackend { }; } - fn fetch( - &self, - _url: String, - _options: RequestOptions, - ) -> Pin, Error>>>> { + fn fetch(&self, _url: String, _options: RequestOptions) -> OwnedFuture, Error> { Box::pin(async { Err("Fetch not implemented on desktop!".into()) }) } - fn spawn_future( - &mut self, - _future: Pin> + 'static>>, - ) { + fn spawn_future(&mut self, _future: OwnedFuture<(), Error>) { unimplemented!(); } } diff --git a/web/src/navigator.rs b/web/src/navigator.rs index 5f399f085..23a1becb3 100644 --- a/web/src/navigator.rs +++ b/web/src/navigator.rs @@ -1,10 +1,10 @@ //! Navigator backend for web use js_sys::{Array, ArrayBuffer, Uint8Array}; -use ruffle_core::backend::navigator::{Error, NavigationMethod, NavigatorBackend, RequestOptions}; +use ruffle_core::backend::navigator::{ + Error, NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions, +}; use std::collections::HashMap; -use std::future::Future; -use std::pin::Pin; use wasm_bindgen::JsCast; use wasm_bindgen_futures::{spawn_local, JsFuture}; use web_sys::{window, Blob, BlobPropertyBag, Request, RequestInit, Response}; @@ -77,11 +77,7 @@ impl NavigatorBackend for WebNavigatorBackend { } } - fn fetch( - &self, - url: String, - options: RequestOptions, - ) -> Pin, Error>>>> { + fn fetch(&self, url: String, options: RequestOptions) -> OwnedFuture, Error> { Box::pin(async move { let mut init = RequestInit::new(); @@ -135,7 +131,7 @@ impl NavigatorBackend for WebNavigatorBackend { }) } - fn spawn_future(&mut self, future: Pin> + 'static>>) { + fn spawn_future(&mut self, future: OwnedFuture<(), Error>) { spawn_local(async move { if let Err(e) = future.await { log::error!("Asynchronous error occured: {}", e); From 88b10f21c4311b827b17d25e6cb07b93933100f3 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Tue, 28 Jan 2020 19:23:02 -0500 Subject: [PATCH 46/59] Implement an async executor for desktop so that these methods don't immediately panic. --- desktop/src/custom_event.rs | 7 ++ desktop/src/executor.rs | 218 ++++++++++++++++++++++++++++++++++++ desktop/src/main.rs | 17 ++- desktop/src/navigator.rs | 31 ++++- desktop/src/task.rs | 71 ++++++++++++ 5 files changed, 337 insertions(+), 7 deletions(-) create mode 100644 desktop/src/custom_event.rs create mode 100644 desktop/src/executor.rs create mode 100644 desktop/src/task.rs diff --git a/desktop/src/custom_event.rs b/desktop/src/custom_event.rs new file mode 100644 index 000000000..7eed93ef1 --- /dev/null +++ b/desktop/src/custom_event.rs @@ -0,0 +1,7 @@ +//! Custom event type for desktop ruffle + +/// User-defined events. +pub enum RuffleEvent { + /// Indicates that one or more tasks are ready to poll on our executor. + TaskPoll, +} diff --git a/desktop/src/executor.rs b/desktop/src/executor.rs new file mode 100644 index 000000000..7988342d5 --- /dev/null +++ b/desktop/src/executor.rs @@ -0,0 +1,218 @@ +//! Async executor + +use crate::custom_event::RuffleEvent; +use crate::task::Task; +use generational_arena::{Arena, Index}; +use glutin::event_loop::EventLoopProxy; +use ruffle_core::backend::navigator::{Error, OwnedFuture}; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::{Arc, Mutex, Weak}; +use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + +/// Exeuctor context passed to event sources. +/// +/// All task handles are identical and interchangeable. Cloning a `TaskHandle` +/// does not clone the underlying task. +#[derive(Clone)] +struct TaskHandle { + /// The arena handle for a given task. + handle: Index, + + /// The executor the task belongs to. + executor: Arc>, +} + +impl TaskHandle { + /// Construct a handle to a given task. + fn for_task(task: Index, executor: Arc>) -> Self { + Self { + handle: task, + executor, + } + } + + /// Construct a new `RawWaker` for this task handle. + /// + /// This function clones the underlying task handle. + fn raw_waker(&self) -> RawWaker { + let clone = Box::new(self.clone()); + RawWaker::new(Box::into_raw(clone) as *const (), &Self::VTABLE) + } + + /// Construct a new waker for this task handle. + fn waker(&self) -> Waker { + unsafe { Waker::from_raw(self.raw_waker()) } + } + + /// Wake the task this context refers to. + fn wake(&self) { + self.executor + .lock() + .expect("able to lock executor") + .wake(self.handle); + } + + /// Convert a voidptr into an `TaskHandle` reference, if non-null. + /// + /// This function is unsafe because the pointer can refer to any resource + /// in memory. It also can belong to any lifetime. Use of this function on + /// a pointer *not* ultimately derived from an TaskHandle in memory + /// constitutes undefined behavior. + unsafe fn from_const_ptr<'a>(almost_self: *const ()) -> Option<&'a Self> { + if almost_self.is_null() { + return None; + } + + Some(&*(almost_self as *const Self)) + } + + /// Convert a voidptr into a mutable `TaskHandle` reference, if + /// non-null. + /// + /// This function is unsafe because the pointer can refer to any resource + /// in memory. It also can belong to any lifetime. Use of this function on + /// a pointer *not* ultimately derived from an TaskHandle in memory + /// constitutes undefined behavior. + /// + /// It's also additionally unsound to call this function while other + /// references to the same `TaskHandle` exist. + unsafe fn box_from_const_ptr(almost_self: *const ()) -> Option> { + if almost_self.is_null() { + return None; + } + + Some(Box::from_raw(almost_self as *mut Self)) + } + + /// Construct a new `RawWaker` that wakes the same task. + /// + /// This is part of the vtable methods of our `RawWaker` impl. + unsafe fn clone_as_ptr(almost_self: *const ()) -> RawWaker { + let selfish = TaskHandle::from_const_ptr(almost_self).expect("non-null context ptr"); + + selfish.raw_waker() + } + + /// Wake the given task, then drop it. + unsafe fn wake_as_ptr(almost_self: *const ()) { + let selfish = TaskHandle::box_from_const_ptr(almost_self).expect("non-null context ptr"); + + selfish.wake(); + } + + /// Wake the given task. + unsafe fn wake_by_ref_as_ptr(almost_self: *const ()) { + let selfish = TaskHandle::from_const_ptr(almost_self).expect("non-null context ptr"); + + selfish.wake(); + } + + /// Drop the async executor. + unsafe fn drop_as_ptr(almost_self: *const ()) { + let _ = TaskHandle::box_from_const_ptr(almost_self).expect("non-null context ptr"); + } + + const VTABLE: RawWakerVTable = RawWakerVTable::new( + Self::clone_as_ptr, + Self::wake_as_ptr, + Self::wake_by_ref_as_ptr, + Self::drop_as_ptr, + ); +} + +pub struct GlutinAsyncExecutor { + /// List of all spawned tasks. + task_queue: Arena, + + /// Source of tasks sent to us by the `NavigatorBackend`. + channel: Receiver>, + + /// Weak reference to ourselves. + self_ref: Weak>, + + /// Event injector for the main thread event loop. + event_loop: EventLoopProxy, + + /// Whether or not we have already queued a `TaskPoll` event. + waiting_for_poll: bool, +} + +impl GlutinAsyncExecutor { + /// Construct a new executor for the Glutin event loop. + /// + /// This function returns the executor itself, plus the `Sender` necessary + /// to spawn new tasks. + pub fn new( + event_loop: EventLoopProxy, + ) -> (Arc>, Sender>) { + let (send, recv) = channel(); + let new_self = Arc::new(Mutex::new(Self { + task_queue: Arena::new(), + channel: recv, + self_ref: Weak::new(), + event_loop, + waiting_for_poll: false, + })); + let self_ref = Arc::downgrade(&new_self); + + new_self.lock().expect("locked self").self_ref = self_ref; + + (new_self, send) + } + + /// Poll all `Ready` futures. + pub fn poll_all(&mut self) { + self.waiting_for_poll = false; + + while let Ok(fut) = self.channel.try_recv() { + self.task_queue.insert(Task::from_future(fut)); + } + + let self_ref = self.self_ref.upgrade().expect("active self-reference"); + let mut completed_tasks = vec![]; + + for (index, task) in self.task_queue.iter_mut() { + if task.is_ready() { + let handle = TaskHandle::for_task(index, self_ref.clone()); + let waker = handle.waker(); + let mut context = Context::from_waker(&waker); + + match task.poll(&mut context) { + Poll::Pending => {} + Poll::Ready(r) => { + if let Err(e) = r { + log::error!("Async error: {}", e); + } + + completed_tasks.push(index); + } + } + } + } + + for index in completed_tasks { + self.task_queue.remove(index); + } + } + + /// Mark a task as ready to proceed. + fn wake(&mut self, task: Index) { + if let Some(task) = self.task_queue.get_mut(task) { + if !task.is_completed() { + if !self.waiting_for_poll { + self.waiting_for_poll = true; + + if self.event_loop.send_event(RuffleEvent::TaskPoll).is_err() { + log::warn!("A task was queued on an event loop that has already ended. It will not be polled."); + } + } else { + log::info!("Double polling"); + } + } else { + log::warn!("A Waker was invoked after the task it was attached to was completed."); + } + } else { + log::warn!("Attempted to wake an already-finished task"); + } + } +} diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 5f331728e..b6eaad90b 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -1,10 +1,15 @@ #![allow(clippy::unneeded_field_pattern)] mod audio; +mod custom_event; +mod executor; mod input; mod navigator; mod render; +mod task; +use crate::custom_event::RuffleEvent; +use crate::executor::GlutinAsyncExecutor; use crate::render::GliumRenderBackend; use glutin::{ dpi::{LogicalSize, PhysicalPosition}, @@ -44,7 +49,7 @@ fn main() { fn run_player(input_path: PathBuf) -> Result<(), Box> { let swf_data = std::fs::read(&input_path)?; - let event_loop = EventLoop::new(); + let event_loop: EventLoop = EventLoop::with_user_event(); let window_builder = WindowBuilder::new().with_title(format!( "Ruffle - {}", input_path.file_name().unwrap_or_default().to_string_lossy() @@ -63,7 +68,11 @@ fn run_player(input_path: PathBuf) -> Result<(), Box> { } }; let renderer = Box::new(GliumRenderBackend::new(windowed_context)?); - let navigator = Box::new(navigator::ExternalNavigatorBackend::new()); //TODO: actually implement this backend type + let (executor, chan) = GlutinAsyncExecutor::new(event_loop.create_proxy()); + let navigator = Box::new(navigator::ExternalNavigatorBackend::new( + chan, + event_loop.create_proxy(), + )); //TODO: actually implement this backend type let display = renderer.display().clone(); let input = Box::new(input::WinitInputBackend::new(display.clone())); let player = Player::new(renderer, audio, navigator, input, swf_data)?; @@ -144,6 +153,10 @@ fn run_player(input_path: PathBuf) -> Result<(), Box> { } _ => (), }, + glutin::event::Event::UserEvent(RuffleEvent::TaskPoll) => executor + .lock() + .expect("active executor reference") + .poll_all(), _ => (), } diff --git a/desktop/src/navigator.rs b/desktop/src/navigator.rs index 9de72e381..f0479e80b 100644 --- a/desktop/src/navigator.rs +++ b/desktop/src/navigator.rs @@ -1,20 +1,35 @@ //! Navigator backend for web +use crate::custom_event::RuffleEvent; +use glutin::event_loop::EventLoopProxy; use log; use ruffle_core::backend::navigator::{ Error, NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions, }; use std::collections::HashMap; +use std::sync::mpsc::Sender; use url::Url; use webbrowser; /// Implementation of `NavigatorBackend` for non-web environments that can call /// out to a web browser. -pub struct ExternalNavigatorBackend {} +pub struct ExternalNavigatorBackend { + /// Sink for tasks sent to us through `spawn_future`. + channel: Sender>, + + /// Event sink to trigger a new task poll. + event_loop: EventLoopProxy, +} impl ExternalNavigatorBackend { - pub fn new() -> Self { - ExternalNavigatorBackend {} + pub fn new( + channel: Sender>, + event_loop: EventLoopProxy, + ) -> Self { + Self { + channel, + event_loop, + } } } @@ -67,7 +82,13 @@ impl NavigatorBackend for ExternalNavigatorBackend { Box::pin(async { Err("Fetch not implemented on desktop!".into()) }) } - fn spawn_future(&mut self, _future: OwnedFuture<(), Error>) { - unimplemented!(); + fn spawn_future(&mut self, future: OwnedFuture<(), Error>) { + self.channel.send(future).expect("working channel send"); + + if self.event_loop.send_event(RuffleEvent::TaskPoll).is_err() { + log::warn!( + "A task was queued on an event loop that has already ended. It will not be polled." + ); + } } } diff --git a/desktop/src/task.rs b/desktop/src/task.rs new file mode 100644 index 000000000..5441a1562 --- /dev/null +++ b/desktop/src/task.rs @@ -0,0 +1,71 @@ +//! Task state information + +use ruffle_core::backend::navigator::{Error, OwnedFuture}; +use std::task::{Context, Poll}; + +/// Indicates the state of a given task. +#[derive(Eq, PartialEq)] +enum TaskState { + /// Indicates that a task is ready to be polled to make progress. + Ready, + + /// Indicates that a task is blocked on another event source. + Blocked, + + /// Indicates that a task is complete and should not be awoken again. + Completed, +} + +/// Wrapper type for futures in our executor. +pub struct Task { + /// The state of the task. + state: TaskState, + + /// The future to poll in order to progress the task. + future: OwnedFuture<(), Error>, +} + +impl Task { + /// Box an owned future into a task structure. + pub fn from_future(future: OwnedFuture<(), Error>) -> Self { + Self { + state: TaskState::Ready, + future, + } + } + + /// Returns `true` if the task is ready to be polled. + pub fn is_ready(&self) -> bool { + self.state == TaskState::Ready + } + + /// Returns `true` if the task is awaiting further progress. + #[allow(dead_code)] + pub fn is_blocked(&self) -> bool { + self.state == TaskState::Blocked + } + + /// Returns `true` if the task has completed and should not be polled again. + pub fn is_completed(&self) -> bool { + self.state == TaskState::Completed + } + + /// Poll the underlying future. + /// + /// This wrapper function ensures that futures cannot be polled after they + /// have completed. Future polls will return `Ok(())`. + pub fn poll(&mut self, context: &mut Context) -> Poll> { + if self.is_completed() { + return Poll::Ready(Ok(())); + } + + let poll = self.future.as_mut().poll(context); + + self.state = match poll { + Poll::Pending => TaskState::Blocked, + Poll::Ready(_) => TaskState::Completed, + }; + + poll + } +} From aa6aba13dd739df5c8dd4bdfebee1c96d4e49f6c Mon Sep 17 00:00:00 2001 From: David Wendt Date: Wed, 29 Jan 2020 23:21:06 -0500 Subject: [PATCH 47/59] Abolish `context.root` completely. `_root` is calculated dynamically based on the clip the currently executing function was called in. Other things that used `context.root` have been changed to either update all layers or just update layer 0, which is the former `context.root`. --- core/src/avm1.rs | 60 +++++++++++++-------------- core/src/avm1/globals/color.rs | 2 +- core/src/avm1/globals/movie_clip.rs | 4 +- core/src/avm1/globals/xml.rs | 2 +- core/src/avm1/script_object.rs | 1 - core/src/avm1/test_utils.rs | 1 - core/src/avm1/tests.rs | 2 +- core/src/context.rs | 9 ---- core/src/display_object.rs | 32 +++++++++++++- core/src/display_object/button.rs | 1 - core/src/display_object/movie_clip.rs | 5 --- core/src/loader.rs | 5 +-- core/src/player.rs | 32 +++++++------- 13 files changed, 82 insertions(+), 74 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 9ece32cfa..bcbdeec5a 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -149,15 +149,14 @@ impl<'gc> Avm1<'gc> { /// The current target clip of the executing code, or `root` if there is none. /// Actions that affect `root` after an invalid `tellTarget` will use this. - pub fn target_clip_or_root( - &self, - context: &mut UpdateContext<'_, 'gc, '_>, - ) -> DisplayObject<'gc> { + /// + /// The `root` is determined relative to the base clip that defined the + pub fn target_clip_or_root(&self) -> DisplayObject<'gc> { self.current_stack_frame() .unwrap() .read() .target_clip() - .unwrap_or(context.root) + .unwrap_or_else(|| self.base_clip().root()) } /// Convert the current locals pool into a set of form values. @@ -695,7 +694,7 @@ impl<'gc> Avm1<'gc> { start: DisplayObject<'gc>, path: &str, ) -> Result>, Error> { - let root = context.root; + let root = start.root(); // Empty path resolves immediately to start clip. if path.is_empty() { @@ -809,8 +808,7 @@ impl<'gc> Avm1<'gc> { path: &'s str, ) -> Result, Error> { // Resolve a variable path for a GetVariable action. - let root = context.root; - let start = self.target_clip().unwrap_or(root); + let start = self.target_clip_or_root(); // Find the right-most : or . in the path. // If we have one, we must resolve as a target path. @@ -878,8 +876,7 @@ impl<'gc> Avm1<'gc> { value: Value<'gc>, ) -> Result<(), Error> { // Resolve a variable path for a GetVariable action. - let root = context.root; - let start = self.target_clip().unwrap_or(root); + let start = self.target_clip_or_root(); // If the target clip is invalid, we default to root for the variable path. if path.is_empty() { @@ -1092,7 +1089,7 @@ impl<'gc> Avm1<'gc> { let depth = self.pop(); let target = self.pop(); let source = self.pop(); - let start_clip = self.target_clip_or_root(context); + let start_clip = self.target_clip_or_root(); let source_clip = self.resolve_target_display_object(context, start_clip, source)?; if let Some(movie_clip) = source_clip.and_then(|o| o.as_movie_clip()) { @@ -1161,7 +1158,7 @@ impl<'gc> Avm1<'gc> { fn action_call(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> { // Runs any actions on the given frame. let frame = self.pop(); - let clip = self.target_clip_or_root(context); + let clip = self.target_clip_or_root(); if let Some(clip) = clip.as_movie_clip() { // Use frame # if parameter is a number, otherwise cast to string and check for frame labels. let frame = if let Ok(frame) = frame.as_u32() { @@ -1180,7 +1177,7 @@ impl<'gc> Avm1<'gc> { // so we want to push the stack frames in reverse order. for action in clip.actions_on_frame(context, frame).rev() { self.insert_stack_frame_for_action( - self.target_clip_or_root(context), + self.target_clip_or_root(), self.current_swf_version(), action, context, @@ -1214,7 +1211,7 @@ impl<'gc> Avm1<'gc> { .read() .resolve(fn_name.as_string()?, self, context)? .resolve(self, context)?; - let this = self.target_clip_or_root(context).object().as_object()?; + let this = self.target_clip_or_root().object().as_object()?; target_fn.call(self, context, this, &args)?.push(self); Ok(()) @@ -1235,7 +1232,7 @@ impl<'gc> Avm1<'gc> { match method_name { Value::Undefined | Value::Null => { - let this = self.target_clip_or_root(context).object(); + let this = self.target_clip_or_root().object(); if let Ok(this) = this.as_object() { object.call(self, context, this, &args)?.push(self); } else { @@ -1340,7 +1337,7 @@ impl<'gc> Avm1<'gc> { params, scope, constant_pool, - self.target_clip_or_root(context), + self.target_clip_or_root(), ); let prototype = ScriptObject::object(context.gc_context, Some(self.prototypes.object)).into(); @@ -1606,8 +1603,8 @@ impl<'gc> Avm1<'gc> { } /// Obtain the value of `_root`. - pub fn root_object(&self, context: &mut UpdateContext<'_, 'gc, '_>) -> Value<'gc> { - context.root.object() + pub fn root_object(&self, _context: &mut UpdateContext<'_, 'gc, '_>) -> Value<'gc> { + self.base_clip().root().object() } /// Obtain the value of `_global`. @@ -1692,11 +1689,11 @@ impl<'gc> Avm1<'gc> { if let Value::Object(target) = target { target.as_display_object() } else { - let start = self.target_clip_or_root(context); + let start = self.target_clip_or_root(); self.resolve_target_display_object(context, start, target.clone())? } } else { - Some(self.target_clip_or_root(context)) + Some(self.target_clip_or_root()) }; if is_load_vars { @@ -2209,7 +2206,7 @@ impl<'gc> Avm1<'gc> { context: &mut UpdateContext<'_, 'gc, '_>, ) -> Result<(), Error> { let target = self.pop(); - let start_clip = self.target_clip_or_root(context); + let start_clip = self.target_clip_or_root(); let target_clip = self.resolve_target_display_object(context, start_clip, target)?; if let Some(target_clip) = target_clip.and_then(|o| o.as_movie_clip()) { @@ -2295,16 +2292,15 @@ impl<'gc> Avm1<'gc> { context: &mut UpdateContext<'_, 'gc, '_>, target: &str, ) -> Result<(), Error> { - let stack_frame = self.current_stack_frame().unwrap(); - let mut sf = stack_frame.write(context.gc_context); - let base_clip = sf.base_clip(); + let base_clip = self.base_clip(); + let new_target_clip; if target.is_empty() { - sf.set_target_clip(Some(base_clip)); + new_target_clip = Some(base_clip); } else if let Some(clip) = self .resolve_target_path(context, base_clip, target)? .and_then(|o| o.as_display_object()) { - sf.set_target_clip(Some(clip)); + new_target_clip = Some(clip); } else { log::warn!("SetTarget failed: {} not found", target); // TODO: Emulate AVM1 trace error message. @@ -2313,13 +2309,17 @@ impl<'gc> Avm1<'gc> { // When SetTarget has an invalid target, subsequent GetVariables act // as if they are targeting root, but subsequent Play/Stop/etc. // fail silenty. - sf.set_target_clip(None); + new_target_clip = None; } + let stack_frame = self.current_stack_frame().unwrap(); + let mut sf = stack_frame.write(context.gc_context); + sf.set_target_clip(new_target_clip); + let scope = sf.scope_cell(); let clip_obj = sf .target_clip() - .unwrap_or(context.root) + .unwrap_or_else(|| sf.base_clip().root()) .object() .as_object() .unwrap(); @@ -2367,7 +2367,7 @@ impl<'gc> Avm1<'gc> { let scope = sf.scope_cell(); let clip_obj = sf .target_clip() - .unwrap_or(context.root) + .unwrap_or_else(|| sf.base_clip().root()) .object() .as_object() .unwrap(); @@ -2385,7 +2385,7 @@ impl<'gc> Avm1<'gc> { fn action_start_drag(&mut self, context: &mut UpdateContext<'_, 'gc, '_>) -> Result<(), Error> { let target = self.pop(); - let start_clip = self.target_clip_or_root(context); + let start_clip = self.target_clip_or_root(); let display_object = self.resolve_target_display_object(context, start_clip, target)?; if let Some(display_object) = display_object { let lock_center = self.pop(); diff --git a/core/src/avm1/globals/color.rs b/core/src/avm1/globals/color.rs index 8693f1f7b..1685b5fb1 100644 --- a/core/src/avm1/globals/color.rs +++ b/core/src/avm1/globals/color.rs @@ -82,7 +82,7 @@ fn target<'gc>( // This means calls on the same `Color` object could set the color of different clips // depending on which timeline its called from! let target = this.get("target", avm, context)?.resolve(avm, context)?; - let start_clip = avm.target_clip_or_root(context); + let start_clip = avm.target_clip_or_root(); avm.resolve_target_display_object(context, start_clip, target) } diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index 73db6ad7f..e52ebcf37 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -102,8 +102,8 @@ pub fn hit_test<'gc>( if x.is_finite() && y.is_finite() { // The docs say the point is in "Stage coordinates", but actually they are in root coordinates. // root can be moved via _root._x etc., so we actually have to transform from root to world space. - let point = context - .root + let point = movie_clip + .root() .local_to_global((Twips::from_pixels(x), Twips::from_pixels(y))); return Ok(movie_clip.hit_test(point).into()); } diff --git a/core/src/avm1/globals/xml.rs b/core/src/avm1/globals/xml.rs index 5213aa790..2c6643f51 100644 --- a/core/src/avm1/globals/xml.rs +++ b/core/src/avm1/globals/xml.rs @@ -800,7 +800,7 @@ pub fn xml_load<'gc>( this.set("loaded", false.into(), avm, ac)?; let fetch = ac.navigator.fetch(url, RequestOptions::get()); - let target_clip = avm.target_clip_or_root(ac); + let target_clip = avm.target_clip_or_root(); let process = ac.load_manager.load_xml_into_node( ac.player.clone().unwrap(), node, diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index 0b7c1f760..418f7a327 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -597,7 +597,6 @@ mod tests { player_version: 32, swf: &swf, layers: &mut layers, - root, rng: &mut SmallRng::from_seed([0u8; 16]), action_queue: &mut crate::context::ActionQueue::new(), audio: &mut NullAudioBackend::new(), diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index a5b48fcc4..fde4f0570 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -36,7 +36,6 @@ where player_version: 32, swf: &swf, layers: &mut layers, - root, rng: &mut SmallRng::from_seed([0u8; 16]), audio: &mut NullAudioBackend::new(), input: &mut NullInputBackend::new(), diff --git a/core/src/avm1/tests.rs b/core/src/avm1/tests.rs index 58953cdfa..9cc019a04 100644 --- a/core/src/avm1/tests.rs +++ b/core/src/avm1/tests.rs @@ -10,7 +10,7 @@ fn locals_into_form_values() { 19, avm.global_object_cell(), context.gc_context, - context.root, + *context.layers.get(&0).expect("root layer in test"), ); let my_locals = my_activation.scope().locals().to_owned(); diff --git a/core/src/context.rs b/core/src/context.rs index 8f71f0d6f..b167e3938 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -68,10 +68,6 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { /// All loaded layers of the current player. pub layers: &'a mut BTreeMap>, - /// The root of the current timeline being updated. - /// This will always be one of the layers in `layers`. - pub root: DisplayObject<'gc>, - /// The current set of system-specified prototypes to use when constructing /// new built-in objects. pub system_prototypes: avm1::SystemPrototypes<'gc>, @@ -106,9 +102,6 @@ pub struct QueuedActions<'gc> { /// The movie clip this ActionScript is running on. pub clip: DisplayObject<'gc>, - /// The root timeline this action was queued in. - pub root: DisplayObject<'gc>, - /// The type of action this is, along with the corresponding bytecode/method data. pub action_type: ActionType<'gc>, @@ -145,13 +138,11 @@ impl<'gc> ActionQueue<'gc> { pub fn queue_actions( &mut self, clip: DisplayObject<'gc>, - root: DisplayObject<'gc>, action_type: ActionType<'gc>, is_unload: bool, ) { self.queue.push_back(QueuedActions { clip, - root, action_type, is_unload, }) diff --git a/core/src/display_object.rs b/core/src/display_object.rs index d8677b159..88402824a 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -1,4 +1,4 @@ -use crate::avm1::{Object, Value}; +use crate::avm1::{Object, TObject, Value}; use crate::context::{RenderContext, UpdateContext}; use crate::player::NEWEST_PLAYER_VERSION; use crate::prelude::*; @@ -366,7 +366,7 @@ impl<'gc> DisplayObjectBase<'gc> { Text(Text<'gc>), } )] -pub trait TDisplayObject<'gc>: 'gc + Collect + Debug { +pub trait TDisplayObject<'gc>: 'gc + Collect + Debug + Into> { fn id(&self) -> CharacterId; fn depth(&self) -> Depth; fn set_depth(&self, gc_context: MutationContext<'gc, '_>, depth: Depth); @@ -804,6 +804,34 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug { fn allow_as_mask(&self) -> bool { true } + + /// Obtain the top-most parent of the display tree hierarchy. + /// + /// This function can panic in the rare case that a top-level display + /// object has not been post-instantiated, or that a top-level display + /// object does not implement `object`. + fn root(&self) -> DisplayObject<'gc> { + let mut parent = self.parent(); + + while let Some(p) = parent { + let grandparent = p.parent(); + + if grandparent.is_none() { + break; + } + + parent = grandparent; + } + + parent + .or_else(|| { + self.object() + .as_object() + .ok() + .and_then(|o| o.as_display_object()) + }) + .expect("All objects must have root") + } } pub enum DisplayObjectPtr {} diff --git a/core/src/display_object/button.rs b/core/src/display_object/button.rs index 2e51e1979..094f713a6 100644 --- a/core/src/display_object/button.rs +++ b/core/src/display_object/button.rs @@ -370,7 +370,6 @@ impl<'gc> ButtonData<'gc> { handled = ButtonEventResult::Handled; context.action_queue.queue_actions( parent, - context.root, ActionType::Normal { bytecode: action.action_data.clone(), }, diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index a39044a01..37398de1b 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -923,7 +923,6 @@ impl<'gc> MovieClipData<'gc> { { context.action_queue.queue_actions( self_display_object, - context.root, ActionType::Normal { bytecode: clip_action.action_data.clone(), }, @@ -958,7 +957,6 @@ impl<'gc> MovieClipData<'gc> { if let Some(name) = name { context.action_queue.queue_actions( self_display_object, - context.root, ActionType::Method { object: self.object.unwrap(), name, @@ -991,7 +989,6 @@ impl<'gc> MovieClipData<'gc> { context.load_manager.movie_clip_on_load( self_display_object, self.object, - context.root, context.action_queue, ); } @@ -1772,7 +1769,6 @@ impl<'gc, 'a> MovieClipData<'gc> { })?; context.action_queue.queue_actions( self_display_object, - context.root, ActionType::Normal { bytecode: slice }, false, ); @@ -1806,7 +1802,6 @@ impl<'gc, 'a> MovieClipData<'gc> { })?; context.action_queue.queue_actions( self_display_object, - context.root, ActionType::Init { bytecode: slice }, true, ); diff --git a/core/src/loader.rs b/core/src/loader.rs index 6ea0e40e0..75b50796e 100644 --- a/core/src/loader.rs +++ b/core/src/loader.rs @@ -89,13 +89,12 @@ impl<'gc> LoadManager<'gc> { &mut self, loaded_clip: DisplayObject<'gc>, clip_object: Option>, - root: DisplayObject<'gc>, queue: &mut ActionQueue<'gc>, ) { let mut invalidated_loaders = vec![]; for (index, loader) in self.0.iter_mut() { - if loader.movie_clip_loaded(loaded_clip, clip_object, root, queue) { + if loader.movie_clip_loaded(loaded_clip, clip_object, queue) { invalidated_loaders.push(index); } } @@ -433,7 +432,6 @@ impl<'gc> Loader<'gc> { &mut self, loaded_clip: DisplayObject<'gc>, clip_object: Option>, - root: DisplayObject<'gc>, queue: &mut ActionQueue<'gc>, ) -> bool { let (clip, broadcaster) = match self { @@ -449,7 +447,6 @@ impl<'gc> Loader<'gc> { if let Some(broadcaster) = broadcaster { queue.queue_actions( clip, - root, ActionType::Method { object: broadcaster, name: "broadcastMessage", diff --git a/core/src/player.rs b/core/src/player.rs index c3f22816a..538dc0b58 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -6,7 +6,7 @@ use crate::backend::{ }; use crate::context::{ActionQueue, ActionType, RenderContext, UpdateContext}; use crate::display_object::{MorphShape, MovieClip}; -use crate::events::{ButtonEvent, ButtonKeyCode, ClipEvent, PlayerEvent}; +use crate::events::{ButtonEvent, ButtonEventResult, ButtonKeyCode, ClipEvent, PlayerEvent}; use crate::library::Library; use crate::loader::LoadManager; use crate::prelude::*; @@ -369,9 +369,14 @@ impl Player { if button_event.is_some() { self.mutate_with_update_context(|_avm, context| { - let root = context.root; - if let Some(button_event) = button_event { - root.propagate_button_event(context, button_event); + let layers: Vec> = context.layers.values().copied().collect(); + for layer in layers { + if let Some(button_event) = button_event { + let state = layer.propagate_button_event(context, button_event); + if state == ButtonEventResult::Handled { + return; + } + } } }); } @@ -387,16 +392,17 @@ impl Player { if clip_event.is_some() || mouse_event_name.is_some() { self.mutate_with_update_context(|_avm, context| { - let root = context.root; + let layers: Vec> = context.layers.values().copied().collect(); - if let Some(clip_event) = clip_event { - root.propagate_clip_event(context, clip_event); + for layer in layers { + if let Some(clip_event) = clip_event { + layer.propagate_clip_event(context, clip_event); + } } if let Some(mouse_event_name) = mouse_event_name { context.action_queue.queue_actions( - root, - root, + *context.layers.get(&0).expect("root layer"), ActionType::NotifyListeners { listener: SystemListener::Mouse, method: mouse_event_name, @@ -521,7 +527,7 @@ impl Player { fn preload(&mut self) { self.mutate_with_update_context(|_avm, context| { let mut morph_shapes = fnv::FnvHashMap::default(); - let root = context.root; + let root = *context.layers.get(&0).expect("root layer"); root.as_movie_clip() .unwrap() .preload(context, &mut morph_shapes); @@ -550,8 +556,6 @@ impl Player { } for mut layer in layers { - update_context.root = layer; - layer.run_frame(update_context); } }) @@ -636,8 +640,6 @@ impl Player { continue; } - context.root = actions.root; - match actions.action_type { // DoAction/clip event code ActionType::Normal { bytecode } => { @@ -772,7 +774,6 @@ impl Player { let mouse_hovered_object = root_data.mouse_hovered_object; let (layers, library, action_queue, avm, drag_object, load_manager) = root_data.update_context_params(); - let layer0 = layers.get(&0).expect("Layer 0 should always exist"); let mut update_context = UpdateContext { player_version, @@ -787,7 +788,6 @@ impl Player { input, action_queue, gc_context, - root: *layer0, layers, mouse_hovered_object, mouse_position, From 5a7e530c9113b16bd9f2b6cb79c5878e16a7c9c6 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Wed, 29 Jan 2020 23:22:31 -0500 Subject: [PATCH 48/59] Add a test for cross-movie `_root`. This test does not pass yet. Other layers need to resolve as target paths in order for this to happen. --- core/tests/regression_tests.rs | 1 + .../tests/swfs/avm1/cross_movie_root/layer1.fla | Bin 0 -> 40448 bytes .../tests/swfs/avm1/cross_movie_root/layer1.swf | Bin 0 -> 139 bytes .../tests/swfs/avm1/cross_movie_root/output.txt | 10 ++++++++++ core/tests/swfs/avm1/cross_movie_root/test.fla | Bin 0 -> 39936 bytes core/tests/swfs/avm1/cross_movie_root/test.swf | Bin 0 -> 175 bytes 6 files changed, 11 insertions(+) create mode 100644 core/tests/swfs/avm1/cross_movie_root/layer1.fla create mode 100644 core/tests/swfs/avm1/cross_movie_root/layer1.swf create mode 100644 core/tests/swfs/avm1/cross_movie_root/output.txt create mode 100644 core/tests/swfs/avm1/cross_movie_root/test.fla create mode 100644 core/tests/swfs/avm1/cross_movie_root/test.swf diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index f9509bd75..fd9b3749c 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -173,6 +173,7 @@ swf_tests! { (loadvariablesnum, "avm1/loadvariablesnum", 3), (loadvariables_method, "avm1/loadvariables_method", 3), (xml_load, "avm1/xml_load", 1), + (cross_movie_root, "avm1/cross_movie_root", 5), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/cross_movie_root/layer1.fla b/core/tests/swfs/avm1/cross_movie_root/layer1.fla new file mode 100644 index 0000000000000000000000000000000000000000..f39edaff45e07adf1eaa7d2211e4e1527b00f205 GIT binary patch literal 40448 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 TknulVqR(;v`w$AhkNy9D#>5NU literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/cross_movie_root/layer1.swf b/core/tests/swfs/avm1/cross_movie_root/layer1.swf new file mode 100644 index 0000000000000000000000000000000000000000..a10efe359ac95bc5fc6db54be667d377a7380bc4 GIT binary patch literal 139 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 literal 0 HcmV?d00001 diff --git a/core/tests/swfs/avm1/cross_movie_root/test.swf b/core/tests/swfs/avm1/cross_movie_root/test.swf new file mode 100644 index 0000000000000000000000000000000000000000..1e24868e76fe907400cc00ef83c956de000f7162 GIT binary patch literal 175 zcmV;g08sx!S5pWT0RRAaoU349jb~usU%3I;rY1}~lP{6?;GmVLXgE2@AizpLN6i6}4Kq(N(B9n&1V*v437&usA dI{a)<%we@fHJ1gfiZw`$9q1bd1^`ZoGd^mpN0a~n literal 0 HcmV?d00001 From 9adf0f43d7f7a8df96882c31b8d1ae623d8847f6 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 1 Feb 2020 14:44:25 -0500 Subject: [PATCH 49/59] Allow levels to be read as scope variables, and add a test for this. --- core/src/avm1.rs | 14 ++++++---- core/src/avm1/activation.rs | 4 +-- core/src/avm1/function.rs | 8 +++--- core/src/avm1/globals/object.rs | 4 +-- core/src/avm1/object.rs | 8 +++--- core/src/avm1/scope.rs | 22 +++++++++------ core/src/avm1/script_object.rs | 8 +++--- core/src/avm1/sound_object.rs | 8 +++--- core/src/avm1/stage_object.rs | 25 +++++++++++++----- core/src/avm1/super_object.rs | 8 +++--- core/src/avm1/value_object.rs | 8 +++--- core/src/avm1/xml_attributes_object.rs | 6 ++--- core/src/avm1/xml_idmap_object.rs | 9 ++++--- core/src/avm1/xml_object.rs | 8 +++--- core/src/display_object.rs | 12 +++++++++ core/tests/regression_tests.rs | 1 + .../swfs/avm1/roots_and_levels/output.txt | 7 +++++ .../tests/swfs/avm1/roots_and_levels/test.fla | Bin 0 -> 40448 bytes .../tests/swfs/avm1/roots_and_levels/test.swf | Bin 0 -> 106 bytes 19 files changed, 101 insertions(+), 59 deletions(-) create mode 100644 core/tests/swfs/avm1/roots_and_levels/output.txt create mode 100644 core/tests/swfs/avm1/roots_and_levels/test.fla create mode 100644 core/tests/swfs/avm1/roots_and_levels/test.swf diff --git a/core/src/avm1.rs b/core/src/avm1.rs index bcbdeec5a..c64731030 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1450,13 +1450,17 @@ impl<'gc> Avm1<'gc> { //Fun fact: This isn't in the Adobe SWF19 spec, but this opcode returns //a boolean based on if the delete actually deleted something. - let did_exist = self.current_stack_frame().unwrap().read().is_defined(name); - - self.current_stack_frame() + let did_exist = self + .current_stack_frame() .unwrap() .read() - .scope() - .delete(name, context.gc_context); + .is_defined(context, name); + + self.current_stack_frame().unwrap().read().scope().delete( + context, + name, + context.gc_context, + ); self.push(did_exist); Ok(()) diff --git a/core/src/avm1/activation.rs b/core/src/avm1/activation.rs index 34f88d0c7..5a3087679 100644 --- a/core/src/avm1/activation.rs +++ b/core/src/avm1/activation.rs @@ -331,7 +331,7 @@ impl<'gc> Activation<'gc> { } /// Check if a particular property in the scope chain is defined. - pub fn is_defined(&self, name: &str) -> bool { + pub fn is_defined(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { if name == "this" { return true; } @@ -340,7 +340,7 @@ impl<'gc> Activation<'gc> { return true; } - self.scope().is_defined(name) + self.scope().is_defined(context, name) } /// Define a named local variable within this activation. diff --git a/core/src/avm1/function.rs b/core/src/avm1/function.rs index cfc2688a6..b23b6cbe9 100644 --- a/core/src/avm1/function.rs +++ b/core/src/avm1/function.rs @@ -544,12 +544,12 @@ impl<'gc> TObject<'gc> for FunctionObject<'gc> { .add_property(gc_context, name, get, set, attributes) } - fn has_property(&self, name: &str) -> bool { - self.base.has_property(name) + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.base.has_property(context, name) } - fn has_own_property(&self, name: &str) -> bool { - self.base.has_own_property(name) + fn has_own_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.base.has_own_property(context, name) } fn is_property_overwritable(&self, name: &str) -> bool { diff --git a/core/src/avm1/globals/object.rs b/core/src/avm1/globals/object.rs index 61177bf03..1aa953cca 100644 --- a/core/src/avm1/globals/object.rs +++ b/core/src/avm1/globals/object.rs @@ -66,12 +66,12 @@ pub fn add_property<'gc>( /// Implements `Object.prototype.hasOwnProperty` pub fn has_own_property<'gc>( _avm: &mut Avm1<'gc>, - _action_context: &mut UpdateContext<'_, 'gc, '_>, + context: &mut UpdateContext<'_, 'gc, '_>, this: Object<'gc>, args: &[Value<'gc>], ) -> Result, Error> { match args.get(0) { - Some(Value::String(name)) => Ok(Value::Bool(this.has_own_property(name)).into()), + Some(Value::String(name)) => Ok(Value::Bool(this.has_own_property(context, name)).into()), _ => Ok(Value::Bool(false).into()), } } diff --git a/core/src/avm1/object.rs b/core/src/avm1/object.rs index 433fbd4be..a5e4ba84d 100644 --- a/core/src/avm1/object.rs +++ b/core/src/avm1/object.rs @@ -58,7 +58,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, ) -> Result, Error> { - if self.has_own_property(name) { + if self.has_own_property(context, name) { self.get_local(name, avm, context, (*self).into()) } else { search_prototype(self.proto(), name, avm, context, (*self).into()) @@ -172,11 +172,11 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into> + Clone + Copy ); /// Checks if the object has a given named property. - fn has_property(&self, name: &str) -> bool; + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool; /// Checks if the object has a given named property on itself (and not, /// say, the object's prototype or superclass) - fn has_own_property(&self, name: &str) -> bool; + fn has_own_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool; /// Checks if a named property can be overwritten. fn is_property_overwritable(&self, name: &str) -> bool; @@ -362,7 +362,7 @@ pub fn search_prototype<'gc>( return Err("Encountered an excessively deep prototype chain.".into()); } - if proto.unwrap().has_own_property(name) { + if proto.unwrap().has_own_property(context, name) { return proto.unwrap().get_local(name, avm, context, this); } diff --git a/core/src/avm1/scope.rs b/core/src/avm1/scope.rs index e0d59769f..c3fb9dfaf 100644 --- a/core/src/avm1/scope.rs +++ b/core/src/avm1/scope.rs @@ -232,7 +232,7 @@ impl<'gc> Scope<'gc> { context: &mut UpdateContext<'_, 'gc, '_>, this: Object<'gc>, ) -> Result, Error> { - if self.locals().has_property(name) { + if self.locals().has_property(context, name) { return self.locals().get(name, avm, context); } if let Some(scope) = self.parent() { @@ -244,13 +244,13 @@ impl<'gc> Scope<'gc> { } /// Check if a particular property in the scope chain is defined. - pub fn is_defined(&self, name: &str) -> bool { - if self.locals().has_property(name) { + pub fn is_defined(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + if self.locals().has_property(context, name) { return true; } if let Some(scope) = self.parent() { - return scope.is_defined(name); + return scope.is_defined(context, name); } false @@ -271,7 +271,8 @@ impl<'gc> Scope<'gc> { this: Object<'gc>, ) -> Result<(), Error> { if self.class == ScopeClass::Target - || (self.locals().has_property(name) && self.locals().is_property_overwritable(name)) + || (self.locals().has_property(context, name) + && self.locals().is_property_overwritable(name)) { // Value found on this object, so overwrite it. // Or we've hit the executing movie clip, so create it here. @@ -300,13 +301,18 @@ impl<'gc> Scope<'gc> { } /// Delete a value from scope - pub fn delete(&self, name: &str, mc: MutationContext<'gc, '_>) -> bool { - if self.locals().has_property(name) { + pub fn delete( + &self, + context: &mut UpdateContext<'_, 'gc, '_>, + name: &str, + mc: MutationContext<'gc, '_>, + ) -> bool { + if self.locals().has_property(context, name) { return self.locals().delete(mc, name); } if let Some(scope) = self.parent() { - return scope.delete(name, mc); + return scope.delete(context, name, mc); } false diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index 418f7a327..5bb8b6044 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -379,17 +379,17 @@ impl<'gc> TObject<'gc> for ScriptObject<'gc> { } /// Checks if the object has a given named property. - fn has_property(&self, name: &str) -> bool { - self.has_own_property(name) + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.has_own_property(context, name) || self .proto() .as_ref() - .map_or(false, |p| p.has_property(name)) + .map_or(false, |p| p.has_property(context, name)) } /// Checks if the object has a given named property on itself (and not, /// say, the object's prototype or superclass) - fn has_own_property(&self, name: &str) -> bool { + fn has_own_property(&self, _context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { if name == "__proto__" { return true; } diff --git a/core/src/avm1/sound_object.rs b/core/src/avm1/sound_object.rs index bdc75a65b..18b25c6fe 100644 --- a/core/src/avm1/sound_object.rs +++ b/core/src/avm1/sound_object.rs @@ -213,12 +213,12 @@ impl<'gc> TObject<'gc> for SoundObject<'gc> { .add_property(gc_context, name, get, set, attributes) } - fn has_property(&self, name: &str) -> bool { - self.base().has_property(name) + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.base().has_property(context, name) } - fn has_own_property(&self, name: &str) -> bool { - self.base().has_own_property(name) + fn has_own_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.base().has_own_property(context, name) } fn is_property_overwritable(&self, name: &str) -> bool { diff --git a/core/src/avm1/stage_object.rs b/core/src/avm1/stage_object.rs index 1a901280f..b74a56815 100644 --- a/core/src/avm1/stage_object.rs +++ b/core/src/avm1/stage_object.rs @@ -65,7 +65,7 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { ) -> Result, Error> { let props = avm.display_properties; // Property search order for DisplayObjects: - if self.has_own_property(name) { + if self.has_own_property(context, name) { // 1) Actual properties on the underlying object self.get_local(name, avm, context, (*self).into()) } else if let Some(property) = props.read().get_by_name(&name) { @@ -75,8 +75,11 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { } else if let Some(child) = self.display_object.get_child_by_name(name) { // 3) Child display objects with the given instance name Ok(child.object().into()) + } else if let Some(layer) = self.display_object.get_layer_by_name(name, context) { + // 4) Top-level layers + Ok(layer.object().into()) } else { - // 4) Prototype + // 5) Prototype crate::avm1::object::search_prototype(self.proto(), name, avm, context, (*self).into()) } // 4) TODO: __resolve? @@ -100,7 +103,7 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { context: &mut UpdateContext<'_, 'gc, '_>, ) -> Result<(), Error> { let props = avm.display_properties; - if self.base.has_own_property(name) { + if self.base.has_own_property(context, name) { // 1) Actual proeprties on the underlying object self.base .internal_set(name, value, avm, context, (*self).into()) @@ -175,8 +178,8 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { .add_property(gc_context, name, get, set, attributes) } - fn has_property(&self, name: &str) -> bool { - if self.base.has_property(name) { + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + if self.base.has_property(context, name) { return true; } @@ -184,12 +187,20 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { return true; } + if self + .display_object + .get_layer_by_name(name, context) + .is_some() + { + return true; + } + false } - fn has_own_property(&self, name: &str) -> bool { + fn has_own_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { // Note that `hasOwnProperty` does NOT return true for child display objects. - self.base.has_own_property(name) + self.base.has_own_property(context, name) } fn is_property_enumerable(&self, name: &str) -> bool { diff --git a/core/src/avm1/super_object.rs b/core/src/avm1/super_object.rs index 396411166..dd7c214aa 100644 --- a/core/src/avm1/super_object.rs +++ b/core/src/avm1/super_object.rs @@ -161,12 +161,12 @@ impl<'gc> TObject<'gc> for SuperObject<'gc> { //`super` cannot have properties defined on it } - fn has_property(&self, name: &str) -> bool { - self.0.read().child.has_property(name) + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.0.read().child.has_property(context, name) } - fn has_own_property(&self, name: &str) -> bool { - self.0.read().child.has_own_property(name) + fn has_own_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.0.read().child.has_own_property(context, name) } fn is_property_enumerable(&self, name: &str) -> bool { diff --git a/core/src/avm1/value_object.rs b/core/src/avm1/value_object.rs index b81467bd6..30cc64b90 100644 --- a/core/src/avm1/value_object.rs +++ b/core/src/avm1/value_object.rs @@ -210,12 +210,12 @@ impl<'gc> TObject<'gc> for ValueObject<'gc> { self.0.read().base.proto() } - fn has_property(&self, name: &str) -> bool { - self.0.read().base.has_property(name) + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.0.read().base.has_property(context, name) } - fn has_own_property(&self, name: &str) -> bool { - self.0.read().base.has_own_property(name) + fn has_own_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.0.read().base.has_own_property(context, name) } fn is_property_overwritable(&self, name: &str) -> bool { diff --git a/core/src/avm1/xml_attributes_object.rs b/core/src/avm1/xml_attributes_object.rs index 07bfb7936..78a24bd3d 100644 --- a/core/src/avm1/xml_attributes_object.rs +++ b/core/src/avm1/xml_attributes_object.rs @@ -152,11 +152,11 @@ impl<'gc> TObject<'gc> for XMLAttributesObject<'gc> { self.base().proto() } - fn has_property(&self, name: &str) -> bool { - self.base().has_property(name) + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.base().has_property(context, name) } - fn has_own_property(&self, name: &str) -> bool { + fn has_own_property(&self, _context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { self.node() .attribute_value(&XMLName::from_str(name)) .is_some() diff --git a/core/src/avm1/xml_idmap_object.rs b/core/src/avm1/xml_idmap_object.rs index 0887e7a89..00f9eb54c 100644 --- a/core/src/avm1/xml_idmap_object.rs +++ b/core/src/avm1/xml_idmap_object.rs @@ -146,12 +146,13 @@ impl<'gc> TObject<'gc> for XMLIDMapObject<'gc> { self.base().proto() } - fn has_property(&self, name: &str) -> bool { - self.base().has_property(name) + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.base().has_property(context, name) } - fn has_own_property(&self, name: &str) -> bool { - self.document().get_node_by_id(name).is_some() || self.base().has_own_property(name) + fn has_own_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.document().get_node_by_id(name).is_some() + || self.base().has_own_property(context, name) } fn is_property_overwritable(&self, name: &str) -> bool { diff --git a/core/src/avm1/xml_object.rs b/core/src/avm1/xml_object.rs index 8cfee1ca4..b54273aec 100644 --- a/core/src/avm1/xml_object.rs +++ b/core/src/avm1/xml_object.rs @@ -140,12 +140,12 @@ impl<'gc> TObject<'gc> for XMLObject<'gc> { self.base().proto() } - fn has_property(&self, name: &str) -> bool { - self.base().has_property(name) + fn has_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.base().has_property(context, name) } - fn has_own_property(&self, name: &str) -> bool { - self.base().has_own_property(name) + fn has_own_property(&self, context: &mut UpdateContext<'_, 'gc, '_>, name: &str) -> bool { + self.base().has_own_property(context, name) } fn is_property_overwritable(&self, name: &str) -> bool { diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 88402824a..f90b053af 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -634,6 +634,18 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug + Into> // TODO: Make a HashMap from name -> child? self.children().find(|child| &*child.name() == name) } + /// Get another layer by layer name. + fn get_layer_by_name( + &self, + name: &str, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> Option> { + context + .layers + .values() + .find(|layer| &*layer.name() == name) + .copied() + } fn removed(&self) -> bool; fn set_removed(&mut self, context: MutationContext<'gc, '_>, value: bool); diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index fd9b3749c..23dd304c9 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -174,6 +174,7 @@ swf_tests! { (loadvariables_method, "avm1/loadvariables_method", 3), (xml_load, "avm1/xml_load", 1), (cross_movie_root, "avm1/cross_movie_root", 5), + (roots_and_levels, "avm1/roots_and_levels", 1), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/roots_and_levels/output.txt b/core/tests/swfs/avm1/roots_and_levels/output.txt new file mode 100644 index 000000000..edb1af04a --- /dev/null +++ b/core/tests/swfs/avm1/roots_and_levels/output.txt @@ -0,0 +1,7 @@ +_level0 +_level0 +_level0 +_level0 +true +true +true diff --git a/core/tests/swfs/avm1/roots_and_levels/test.fla b/core/tests/swfs/avm1/roots_and_levels/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..6d0084b7b8286c7d29d2292bd4196db340f6674a GIT binary patch literal 40448 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 M4nPM00GVGKny5-DN&o-= literal 0 HcmV?d00001 From 8ef759d37782e4f261d8133c9397398c3d9ac3cd Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 1 Feb 2020 17:56:06 -0500 Subject: [PATCH 50/59] Top-level layers do not get an instance name of `_leveln`. --- core/src/player.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/player.rs b/core/src/player.rs index 538dc0b58..32eeefe9e 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -233,9 +233,8 @@ impl Player { let mut root_data = gc_root.0.write(gc_context); let mc_proto = root_data.avm.prototypes().movie_clip; - for (i, layer) in root_data.layers.iter_mut() { + for (_i, layer) in root_data.layers.iter_mut() { layer.post_instantiation(gc_context, *layer, mc_proto); - layer.set_name(gc_context, &format!("_level{}", i)) } }); From 00f88f9e876dd0da74cc2f00d64404ed4002eb7c Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 1 Feb 2020 18:29:21 -0500 Subject: [PATCH 51/59] Use the depth to indicate which layer a particular root clip is, and then use that to calculate it's `_leveln` path. --- core/src/avm1.rs | 1 + core/src/avm1/script_object.rs | 1 + core/src/avm1/test_utils.rs | 1 + core/src/display_object.rs | 3 +-- core/src/player.rs | 1 + 5 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index c64731030..956eabbcb 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -951,6 +951,7 @@ impl<'gc> Avm1<'gc> { MovieClip::new(NEWEST_PLAYER_VERSION, context.gc_context).into(); layer.post_instantiation(context.gc_context, layer, self.prototypes.movie_clip); + layer.set_depth(context.gc_context, level_id as i32); context.layers.insert(level_id, layer); layer diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index 5bb8b6044..19fd969ec 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -588,6 +588,7 @@ mod tests { let swf = Arc::new(SwfMovie::empty(swf_version)); let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); + root.set_depth(gc_context, 0); let mut layers = BTreeMap::new(); layers.insert(0, root); diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index fde4f0570..1ed078f9b 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -27,6 +27,7 @@ where let swf = Arc::new(SwfMovie::empty(swf_version)); let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); + root.set_depth(gc_context, 0); let mut layers = BTreeMap::new(); layers.insert(0, root); diff --git a/core/src/display_object.rs b/core/src/display_object.rs index f90b053af..5eccbdba2 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -581,8 +581,7 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug + Into> path.push_str(&*self.name()); path } else { - // TODO: Get the actual level # from somewhere. - "_level0".to_string() + format!("_level{}", self.depth()) } } diff --git a/core/src/player.rs b/core/src/player.rs index 32eeefe9e..b738e56cb 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -235,6 +235,7 @@ impl Player { for (_i, layer) in root_data.layers.iter_mut() { layer.post_instantiation(gc_context, *layer, mc_proto); + layer.set_depth(gc_context, 0); } }); From e0b4a0f1933da9a20d94cb4cde072687fffc9804 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 1 Feb 2020 20:04:01 -0500 Subject: [PATCH 52/59] Top-level layers are referenced via their path (`_leveln`) rather than name - in fact, their name is always the empty string. --- core/src/avm1/stage_object.rs | 4 ++-- core/src/display_object.rs | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/core/src/avm1/stage_object.rs b/core/src/avm1/stage_object.rs index b74a56815..cfae2036e 100644 --- a/core/src/avm1/stage_object.rs +++ b/core/src/avm1/stage_object.rs @@ -75,7 +75,7 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { } else if let Some(child) = self.display_object.get_child_by_name(name) { // 3) Child display objects with the given instance name Ok(child.object().into()) - } else if let Some(layer) = self.display_object.get_layer_by_name(name, context) { + } else if let Some(layer) = self.display_object.get_layer_by_path(name, context) { // 4) Top-level layers Ok(layer.object().into()) } else { @@ -189,7 +189,7 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { if self .display_object - .get_layer_by_name(name, context) + .get_layer_by_path(name, context) .is_some() { return true; diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 5eccbdba2..89484a71a 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -8,6 +8,7 @@ use enumset::{EnumSet, EnumSetType}; use gc_arena::{Collect, MutationContext}; use ruffle_macros::enum_trait_object; use std::cell::{Ref, RefMut}; +use std::cmp::min; use std::fmt::Debug; use std::sync::Arc; @@ -634,16 +635,21 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug + Into> self.children().find(|child| &*child.name() == name) } /// Get another layer by layer name. - fn get_layer_by_name( + /// + /// Since layers don't have instance names, this function instead parses + /// their ID and uses that to retrieve the layer. + fn get_layer_by_path( &self, name: &str, context: &mut UpdateContext<'_, 'gc, '_>, ) -> Option> { - context - .layers - .values() - .find(|layer| &*layer.name() == name) - .copied() + if name.get(0..min(name.len(), 6)) == Some("_level") { + if let Some(layer_id) = name.get(6..).and_then(|v| v.parse::().ok()) { + return context.layers.get(&layer_id).copied(); + } + } + + None } fn removed(&self) -> bool; fn set_removed(&mut self, context: MutationContext<'gc, '_>, value: bool); From 766ded6dfdf383192a9e5bb522bfec69cbe601a5 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 1 Feb 2020 20:19:44 -0500 Subject: [PATCH 53/59] When replacing a movie clip with another movie, don't wipe out the entire display object base as that disassociates us from our parent, siblings, and most importantly, *layer depth*. --- core/src/display_object.rs | 6 ++++++ core/src/display_object/movie_clip.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 89484a71a..88a6e4e4b 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -93,6 +93,12 @@ unsafe impl<'gc> Collect for DisplayObjectBase<'gc> { #[allow(dead_code)] impl<'gc> DisplayObjectBase<'gc> { + /// Reset all properties that would be adjusted by a movie load. + fn reset_for_movie_load(&mut self) { + self.first_child = None; + self.flags = DisplayObjectFlags::Visible.into(); + } + fn id(&self) -> CharacterId { 0 } diff --git a/core/src/display_object/movie_clip.rs b/core/src/display_object/movie_clip.rs index 37398de1b..278d8aa77 100644 --- a/core/src/display_object/movie_clip.rs +++ b/core/src/display_object/movie_clip.rs @@ -442,7 +442,7 @@ impl<'gc> MovieClipData<'gc> { let movie = movie.unwrap_or_else(|| Arc::new(SwfMovie::empty(self.movie().version()))); let total_frames = movie.header().num_frames; - self.base = Default::default(); + self.base.reset_for_movie_load(); self.static_data = Gc::allocate( gc_context, MovieClipStatic { From abc1d0027635772d6c62d2110b4b79e6acfa9531 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Mon, 3 Feb 2020 14:37:10 -0500 Subject: [PATCH 54/59] Fix incorrect preload of `_root` in `DefineFunction2` function calls. This is caused by the fact that `avm.root_object` references the *current* stack frame, not the one we are about to introduce. Ergo, we need to pull the base clip of the *new* stack frame and find it's root. This particular behavior only crops up in situations where there can be multiple root objects, at least until we implement `_lockroot`. --- core/src/avm1/function.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/avm1/function.rs b/core/src/avm1/function.rs index b23b6cbe9..ad7d4cfd1 100644 --- a/core/src/avm1/function.rs +++ b/core/src/avm1/function.rs @@ -319,7 +319,11 @@ impl<'gc> Executable<'gc> { } if af.preload_root { - frame.set_local_register(preload_r, avm.root_object(ac), ac.gc_context); + frame.set_local_register( + preload_r, + af.base_clip.root().object(), + ac.gc_context, + ); preload_r += 1; } From 05e5fbb69c4aafb35c6e43a36c8beff2054d4f05 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Fri, 14 Feb 2020 00:36:33 -0500 Subject: [PATCH 55/59] For some reason, this color transform code broke on the moviefetch branch --- core/src/avm1/globals/color.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/avm1/globals/color.rs b/core/src/avm1/globals/color.rs index 1685b5fb1..b1e01da69 100644 --- a/core/src/avm1/globals/color.rs +++ b/core/src/avm1/globals/color.rs @@ -169,7 +169,7 @@ fn set_transform<'gc>( out: &mut f32, ) -> Result<(), Error> { // The parameters are set only if the property exists on the object itself (prototype excluded). - if transform.has_own_property(property) { + if transform.has_own_property(context, property) { let n = transform .get(property, avm, context)? .resolve(avm, context)? @@ -187,7 +187,7 @@ fn set_transform<'gc>( out: &mut f32, ) -> Result<(), Error> { // The parameters are set only if the property exists on the object itself (prototype excluded). - if transform.has_own_property(property) { + if transform.has_own_property(context, property) { let n = transform .get(property, avm, context)? .resolve(avm, context)? From 1b08fb538da7e94df4cdd0238030b243a7c23511 Mon Sep 17 00:00:00 2001 From: Mike Welsh Date: Sun, 23 Feb 2020 23:41:55 -0800 Subject: [PATCH 56/59] chore: Rename layer -> level Unify mix of 'layer' and 'level' in the code, and it's probably better to stick with Flash nomenclature. --- core/src/avm1.rs | 24 ++++++------- core/src/avm1/globals/sound.rs | 4 +-- core/src/avm1/script_object.rs | 6 ++-- core/src/avm1/stage_object.rs | 10 +++--- core/src/avm1/test_utils.rs | 6 ++-- core/src/avm1/tests.rs | 2 +- core/src/context.rs | 4 +-- core/src/display_object.rs | 13 +++---- core/src/player.rs | 65 ++++++++++++++++++---------------- 9 files changed, 69 insertions(+), 65 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 956eabbcb..47a11c4e2 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -935,26 +935,26 @@ impl<'gc> Avm1<'gc> { clip } - /// Resolve a layer by ID. + /// Resolve a level by ID. /// - /// If the layer does not exist, then it will be created and instantiated + /// If the level does not exist, then it will be created and instantiated /// with a script object. - pub fn resolve_layer( + pub fn resolve_level( &self, level_id: u32, context: &mut UpdateContext<'_, 'gc, '_>, ) -> DisplayObject<'gc> { - if let Some(layer) = context.layers.get(&level_id) { - *layer + if let Some(level) = context.levels.get(&level_id) { + *level } else { - let mut layer: DisplayObject<'_> = + let mut level: DisplayObject<'_> = MovieClip::new(NEWEST_PLAYER_VERSION, context.gc_context).into(); - layer.post_instantiation(context.gc_context, layer, self.prototypes.movie_clip); - layer.set_depth(context.gc_context, level_id as i32); - context.layers.insert(level_id, layer); + level.post_instantiation(context.gc_context, level, self.prototypes.movie_clip); + level.set_depth(context.gc_context, level_id as i32); + context.levels.insert(level_id, level); - layer + level } } @@ -1649,11 +1649,11 @@ impl<'gc> Avm1<'gc> { let url = url.to_string(); let level_id = target[6..].parse::()?; let fetch = context.navigator.fetch(url, RequestOptions::get()); - let layer = self.resolve_layer(level_id, context); + let level = self.resolve_level(level_id, context); let process = context.load_manager.load_movie_into_clip( context.player.clone().unwrap(), - layer, + level, fetch, None, ); diff --git a/core/src/avm1/globals/sound.rs b/core/src/avm1/globals/sound.rs index 2fccc38c5..9d44237a4 100644 --- a/core/src/avm1/globals/sound.rs +++ b/core/src/avm1/globals/sound.rs @@ -170,7 +170,7 @@ fn attach_sound<'gc>( let name = name.clone().coerce_to_string(avm, context)?; let movie = sound_object .owner() - .or_else(|| context.layers.get(&0).copied()) + .or_else(|| context.levels.get(&0).copied()) .and_then(|o| o.movie()); if let Some(movie) = movie { if let Some(Character::Sound(sound)) = context @@ -413,7 +413,7 @@ fn stop<'gc>( let name = name.clone().coerce_to_string(avm, context)?; let movie = sound .owner() - .or_else(|| context.layers.get(&0).copied()) + .or_else(|| context.levels.get(&0).copied()) .and_then(|o| o.movie()); if let Some(movie) = movie { if let Some(Character::Sound(sound)) = context diff --git a/core/src/avm1/script_object.rs b/core/src/avm1/script_object.rs index 19fd969ec..bbdec05b9 100644 --- a/core/src/avm1/script_object.rs +++ b/core/src/avm1/script_object.rs @@ -589,15 +589,15 @@ mod tests { let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); root.set_depth(gc_context, 0); - let mut layers = BTreeMap::new(); - layers.insert(0, root); + let mut levels = BTreeMap::new(); + levels.insert(0, root); let mut context = UpdateContext { gc_context, global_time: 0, player_version: 32, swf: &swf, - layers: &mut layers, + levels: &mut levels, rng: &mut SmallRng::from_seed([0u8; 16]), action_queue: &mut crate::context::ActionQueue::new(), audio: &mut NullAudioBackend::new(), diff --git a/core/src/avm1/stage_object.rs b/core/src/avm1/stage_object.rs index cfae2036e..dee788fde 100644 --- a/core/src/avm1/stage_object.rs +++ b/core/src/avm1/stage_object.rs @@ -75,14 +75,14 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { } else if let Some(child) = self.display_object.get_child_by_name(name) { // 3) Child display objects with the given instance name Ok(child.object().into()) - } else if let Some(layer) = self.display_object.get_layer_by_path(name, context) { - // 4) Top-level layers - Ok(layer.object().into()) + } else if let Some(level) = self.display_object.get_level_by_path(name, context) { + // 4) _levelN + Ok(level.object().into()) } else { // 5) Prototype crate::avm1::object::search_prototype(self.proto(), name, avm, context, (*self).into()) } - // 4) TODO: __resolve? + // 6) TODO: __resolve? } fn get_local( @@ -189,7 +189,7 @@ impl<'gc> TObject<'gc> for StageObject<'gc> { if self .display_object - .get_layer_by_path(name, context) + .get_level_by_path(name, context) .is_some() { return true; diff --git a/core/src/avm1/test_utils.rs b/core/src/avm1/test_utils.rs index 1ed078f9b..412a7ab7e 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -28,15 +28,15 @@ where let mut root: DisplayObject<'_> = MovieClip::new(swf_version, gc_context).into(); root.post_instantiation(gc_context, root, avm.prototypes().movie_clip); root.set_depth(gc_context, 0); - let mut layers = BTreeMap::new(); - layers.insert(0, root); + let mut levels = BTreeMap::new(); + levels.insert(0, root); let mut context = UpdateContext { gc_context, global_time: 0, player_version: 32, swf: &swf, - layers: &mut layers, + levels: &mut levels, rng: &mut SmallRng::from_seed([0u8; 16]), audio: &mut NullAudioBackend::new(), input: &mut NullInputBackend::new(), diff --git a/core/src/avm1/tests.rs b/core/src/avm1/tests.rs index 9cc019a04..7f65dfc3e 100644 --- a/core/src/avm1/tests.rs +++ b/core/src/avm1/tests.rs @@ -10,7 +10,7 @@ fn locals_into_form_values() { 19, avm.global_object_cell(), context.gc_context, - *context.layers.get(&0).expect("root layer in test"), + *context.levels.get(&0).expect("_level0 in test"), ); let my_locals = my_activation.scope().locals().to_owned(); diff --git a/core/src/context.rs b/core/src/context.rs index b167e3938..970168ca5 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -65,8 +65,8 @@ pub struct UpdateContext<'a, 'gc, 'gc_context> { /// The RNG, used by the AVM `RandomNumber` opcode, `Math.random(),` and `random()`. pub rng: &'a mut SmallRng, - /// All loaded layers of the current player. - pub layers: &'a mut BTreeMap>, + /// All loaded levels of the current player. + pub levels: &'a mut BTreeMap>, /// The current set of system-specified prototypes to use when constructing /// new built-in objects. diff --git a/core/src/display_object.rs b/core/src/display_object.rs index 88a6e4e4b..28c4d4df1 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -640,18 +640,19 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug + Into> // TODO: Make a HashMap from name -> child? self.children().find(|child| &*child.name() == name) } - /// Get another layer by layer name. + + /// Get another level by level name. /// - /// Since layers don't have instance names, this function instead parses - /// their ID and uses that to retrieve the layer. - fn get_layer_by_path( + /// Since levels don't have instance names, this function instead parses + /// their ID and uses that to retrieve the level. + fn get_level_by_path( &self, name: &str, context: &mut UpdateContext<'_, 'gc, '_>, ) -> Option> { if name.get(0..min(name.len(), 6)) == Some("_level") { - if let Some(layer_id) = name.get(6..).and_then(|v| v.parse::().ok()) { - return context.layers.get(&layer_id).copied(); + if let Some(level_id) = name.get(6..).and_then(|v| v.parse::().ok()) { + return context.levels.get(&level_id).copied(); } } diff --git a/core/src/player.rs b/core/src/player.rs index b738e56cb..e8b8a53fe 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -35,10 +35,13 @@ struct GcRoot<'gc>(GcCell<'gc, GcRootData<'gc>>); struct GcRootData<'gc> { library: Library<'gc>, - /// The list of layers on the current stage. + /// The list of levels on the current stage. /// - /// Each layer is a `_root` MovieClip that holds a particular SWF movie. - layers: BTreeMap>, + /// Each level is a `_root` MovieClip that holds a particular SWF movie, also accessible via + /// the `_levelN` property. + /// levels[0] represents the initial SWF file that was loaded. + levels: BTreeMap>, + mouse_hovered_object: Option>, // TODO: Remove GcCell wrapped inside GcCell. /// The object being dragged via a `startDrag` action. @@ -66,7 +69,7 @@ impl<'gc> GcRootData<'gc> { &mut LoadManager<'gc>, ) { ( - &mut self.layers, + &mut self.levels, &mut self.library, &mut self.action_queue, &mut self.avm, @@ -188,8 +191,8 @@ impl Player { let mut library = Library::default(); let root = MovieClip::from_movie(gc_context, movie.clone()).into(); - let mut layers = BTreeMap::new(); - layers.insert(0, root); + let mut levels = BTreeMap::new(); + levels.insert(0, root); library .library_for_movie_mut(movie.clone()) @@ -199,7 +202,7 @@ impl Player { gc_context, GcRootData { library, - layers, + levels, mouse_hovered_object: None, drag_object: None, avm: Avm1::new(gc_context, NEWEST_PLAYER_VERSION), @@ -233,9 +236,9 @@ impl Player { let mut root_data = gc_root.0.write(gc_context); let mc_proto = root_data.avm.prototypes().movie_clip; - for (_i, layer) in root_data.layers.iter_mut() { - layer.post_instantiation(gc_context, *layer, mc_proto); - layer.set_depth(gc_context, 0); + for (_i, level) in root_data.levels.iter_mut() { + level.post_instantiation(gc_context, *level, mc_proto); + level.set_depth(gc_context, 0); } }); @@ -369,10 +372,10 @@ impl Player { if button_event.is_some() { self.mutate_with_update_context(|_avm, context| { - let layers: Vec> = context.layers.values().copied().collect(); - for layer in layers { + let levels: Vec> = context.levels.values().copied().collect(); + for level in levels { if let Some(button_event) = button_event { - let state = layer.propagate_button_event(context, button_event); + let state = level.propagate_button_event(context, button_event); if state == ButtonEventResult::Handled { return; } @@ -392,17 +395,17 @@ impl Player { if clip_event.is_some() || mouse_event_name.is_some() { self.mutate_with_update_context(|_avm, context| { - let layers: Vec> = context.layers.values().copied().collect(); + let levels: Vec> = context.levels.values().copied().collect(); - for layer in layers { + for level in levels { if let Some(clip_event) = clip_event { - layer.propagate_clip_event(context, clip_event); + level.propagate_clip_event(context, clip_event); } } if let Some(mouse_event_name) = mouse_event_name { context.action_queue.queue_actions( - *context.layers.get(&0).expect("root layer"), + *context.levels.get(&0).expect("root level"), ActionType::NotifyListeners { listener: SystemListener::Mouse, method: mouse_event_name, @@ -485,9 +488,9 @@ impl Player { self.mutate_with_update_context(|avm, context| { // Check hovered object. let mut new_hovered = None; - for (_depth, layer) in context.layers.iter().rev() { + for (_depth, level) in context.levels.iter().rev() { if new_hovered.is_none() { - new_hovered = layer.mouse_pick(*layer, (mouse_pos.0, mouse_pos.1)); + new_hovered = level.mouse_pick(*level, (mouse_pos.0, mouse_pos.1)); } else { break; } @@ -527,7 +530,7 @@ impl Player { fn preload(&mut self) { self.mutate_with_update_context(|_avm, context| { let mut morph_shapes = fnv::FnvHashMap::default(); - let root = *context.layers.get(&0).expect("root layer"); + let root = *context.levels.get(&0).expect("root level"); root.as_movie_clip() .unwrap() .preload(context, &mut morph_shapes); @@ -545,18 +548,18 @@ impl Player { pub fn run_frame(&mut self) { self.update(|_avm, update_context| { - // TODO: In what order are layers run? + // TODO: In what order are levels run? // NOTE: We have to copy all the layer pointers into a separate list - // because layer updates can create more layers, which we don't + // because level updates can create more levels, which we don't // want to run frames on - let mut layers = vec![]; + let mut levels = vec![]; - for (_depth, layer) in update_context.layers.iter() { - layers.push(*layer); + for (_depth, level) in update_context.levels.iter() { + levels.push(*level); } - for mut layer in layers { - layer.run_frame(update_context); + for mut level in levels { + level.run_frame(update_context); } }) } @@ -590,8 +593,8 @@ impl Player { clip_depth_stack: vec![], }; - for (_depth, layer) in root_data.layers.iter() { - layer.render(&mut render_context); + for (_depth, level) in root_data.levels.iter() { + level.render(&mut render_context); } }); transform_stack.pop(); @@ -772,7 +775,7 @@ impl Player { self.gc_arena.mutate(|gc_context, gc_root| { let mut root_data = gc_root.0.write(gc_context); let mouse_hovered_object = root_data.mouse_hovered_object; - let (layers, library, action_queue, avm, drag_object, load_manager) = + let (levels, library, action_queue, avm, drag_object, load_manager) = root_data.update_context_params(); let mut update_context = UpdateContext { @@ -788,7 +791,7 @@ impl Player { input, action_queue, gc_context, - layers, + levels, mouse_hovered_object, mouse_position, drag_object, From 5315d059b9f2f0cc915cfd090abd97ba0f3b0a86 Mon Sep 17 00:00:00 2001 From: Mike Welsh Date: Mon, 24 Feb 2020 01:49:28 -0800 Subject: [PATCH 57/59] desktop: Fetch from local filesystem for now --- desktop/src/main.rs | 5 ++++- desktop/src/navigator.rs | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/desktop/src/main.rs b/desktop/src/main.rs index b6eaad90b..edee38629 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -69,7 +69,10 @@ fn run_player(input_path: PathBuf) -> Result<(), Box> { }; let renderer = Box::new(GliumRenderBackend::new(windowed_context)?); let (executor, chan) = GlutinAsyncExecutor::new(event_loop.create_proxy()); - let navigator = Box::new(navigator::ExternalNavigatorBackend::new( + let navigator = Box::new(navigator::ExternalNavigatorBackend::with_base_path( + input_path + .parent() + .unwrap_or_else(|| std::path::Path::new("")), chan, event_loop.create_proxy(), )); //TODO: actually implement this backend type diff --git a/desktop/src/navigator.rs b/desktop/src/navigator.rs index f0479e80b..cc0a8655f 100644 --- a/desktop/src/navigator.rs +++ b/desktop/src/navigator.rs @@ -7,6 +7,8 @@ use ruffle_core::backend::navigator::{ Error, NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions, }; use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; use std::sync::mpsc::Sender; use url::Url; use webbrowser; @@ -19,9 +21,13 @@ pub struct ExternalNavigatorBackend { /// Event sink to trigger a new task poll. event_loop: EventLoopProxy, + + /// The base path for all relative fetches. + relative_base_path: PathBuf, } impl ExternalNavigatorBackend { + #[allow(dead_code)] pub fn new( channel: Sender>, event_loop: EventLoopProxy, @@ -29,6 +35,24 @@ impl ExternalNavigatorBackend { Self { channel, event_loop, + relative_base_path: PathBuf::new(), + } + } + + /// Construct a navigator backend with fetch and async capability. + pub fn with_base_path>( + path: P, + channel: Sender>, + event_loop: EventLoopProxy, + ) -> Self { + let mut relative_base_path = PathBuf::new(); + + relative_base_path.push(path); + + Self { + channel, + event_loop, + relative_base_path, } } } @@ -78,8 +102,13 @@ impl NavigatorBackend for ExternalNavigatorBackend { }; } - fn fetch(&self, _url: String, _options: RequestOptions) -> OwnedFuture, Error> { - Box::pin(async { Err("Fetch not implemented on desktop!".into()) }) + fn fetch(&self, url: String, _options: RequestOptions) -> OwnedFuture, Error> { + // Load from local filesystem. + // TODO: Support network loads, honor sandbox type (local-with-filesystem, local-with-network, remote, ...) + let mut path = self.relative_base_path.clone(); + path.push(url); + + Box::pin(async move { fs::read(path).map_err(|e| e.into()) }) } fn spawn_future(&mut self, future: OwnedFuture<(), Error>) { From f163afc5b43669aa74af34dea29b8a941e924eed Mon Sep 17 00:00:00 2001 From: Mike Welsh Date: Mon, 24 Feb 2020 02:28:27 -0800 Subject: [PATCH 58/59] core: Use collect to copy level pointers --- core/src/player.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/src/player.rs b/core/src/player.rs index e8b8a53fe..7c7d0488a 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -552,11 +552,7 @@ impl Player { // NOTE: We have to copy all the layer pointers into a separate list // because level updates can create more levels, which we don't // want to run frames on - let mut levels = vec![]; - - for (_depth, level) in update_context.levels.iter() { - levels.push(*level); - } + let levels: Vec<_> = update_context.levels.values().copied().collect(); for mut level in levels { level.run_frame(update_context); From 8c486b754416d1ce101746b25c84aee59e0c4ab9 Mon Sep 17 00:00:00 2001 From: Mike Welsh Date: Mon, 24 Feb 2020 11:17:40 -0800 Subject: [PATCH 59/59] chore: Use free functions for movie_clip methods Try to keep style more consistent by using functions for all MC methods. Previous was a mix of closures and functions (we're still a little bad with this elsewhere) --- core/src/avm1.rs | 4 +- core/src/avm1/globals/movie_clip.rs | 282 ++++++++++++++++++---------- 2 files changed, 184 insertions(+), 102 deletions(-) diff --git a/core/src/avm1.rs b/core/src/avm1.rs index 47a11c4e2..b33c344b0 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1094,7 +1094,7 @@ impl<'gc> Avm1<'gc> { let source_clip = self.resolve_target_display_object(context, start_clip, source)?; if let Some(movie_clip) = source_clip.and_then(|o| o.as_movie_clip()) { - let _ = globals::movie_clip::duplicate_movie_clip( + let _ = globals::movie_clip::duplicate_movie_clip_with_bias( movie_clip, self, context, @@ -2215,7 +2215,7 @@ impl<'gc> Avm1<'gc> { let target_clip = self.resolve_target_display_object(context, start_clip, target)?; if let Some(target_clip) = target_clip.and_then(|o| o.as_movie_clip()) { - let _ = globals::movie_clip::remove_movie_clip(target_clip, context, 0); + let _ = globals::movie_clip::remove_movie_clip_with_bias(target_clip, context, 0); } else { log::warn!("RemoveSprite: Source is not a movie clip"); } diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index e52ebcf37..76b8630e9 100644 --- a/core/src/avm1/globals/movie_clip.rs +++ b/core/src/avm1/globals/movie_clip.rs @@ -139,100 +139,28 @@ pub fn create_proto<'gc>( "attachMovie" => attach_movie, "createEmptyMovieClip" => create_empty_movie_clip, "createTextField" => create_text_field, - "duplicateMovieClip" => |movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, args| { - // duplicateMovieClip method uses biased depth compared to CloneSprite - duplicate_movie_clip(movie_clip, avm, context, args, AVM_DEPTH_BIAS) - }, - "stopDrag" => stop_drag, - "nextFrame" => |movie_clip: MovieClip<'gc>, _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _args| { - movie_clip.next_frame(context); - Ok(Value::Undefined.into()) - }, - "prevFrame" => |movie_clip: MovieClip<'gc>, _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _args| { - movie_clip.prev_frame(context); - Ok(Value::Undefined.into()) - }, - "play" => |movie_clip: MovieClip<'gc>, _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _args| { - movie_clip.play(context); - Ok(Value::Undefined.into()) - }, - "removeMovieClip" => |movie_clip: MovieClip<'gc>, _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _args| { - // removeMovieClip method uses biased depth compared to RemoveSprite - remove_movie_clip(movie_clip, context, AVM_DEPTH_BIAS) - }, - "stop" => |movie_clip: MovieClip<'gc>, _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _args| { - movie_clip.stop(context); - Ok(Value::Undefined.into()) - }, - "getBytesLoaded" => |_movie_clip: MovieClip<'gc>, _avm: &mut Avm1<'gc>, _context: &mut UpdateContext<'_, 'gc, '_>, _args| { - // TODO find a correct value - Ok(1.0.into()) - }, - "getBytesTotal" => |_movie_clip: MovieClip<'gc>, _avm: &mut Avm1<'gc>, _context: &mut UpdateContext<'_, 'gc, '_>, _args| { - // TODO find a correct value - Ok(1.0.into()) - }, + "duplicateMovieClip" => duplicate_movie_clip, + "getBytesLoaded" => get_bytes_loaded, + "getBytesTotal" => get_bytes_total, "getDepth" => get_depth, "getNextHighestDepth" => get_next_highest_depth, - "hitTest" => |movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, args: &[Value<'gc>]| { - hit_test(movie_clip, avm, context, args) - }, + "globalToLocal" => global_to_local, "gotoAndPlay" => goto_and_play, "gotoAndStop" => goto_and_stop, - "startDrag" => start_drag, - "swapDepths" => swap_depths, - "toString" => |movie_clip: MovieClip<'gc>, _avm: &mut Avm1<'gc>, _context: &mut UpdateContext<'_, 'gc, '_>, _args| { - Ok(movie_clip.path().into()) - }, + "hitTest" => hit_test, + "loadMovie" => load_movie, + "loadVariables" => load_variables, "localToGlobal" => local_to_global, - "globalToLocal" => global_to_local, - "loadMovie" => |target: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, args: &[Value<'gc>]| { - let url = args - .get(0) - .cloned() - .unwrap_or(Value::Undefined) - .coerce_to_string(avm, context)?; - let method = args.get(1).cloned().unwrap_or(Value::Undefined); - let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); - let (url, opts) = avm.locals_into_request_options(context, url, method); - let fetch = context.navigator.fetch(url, opts); - let process = context.load_manager.load_movie_into_clip( - context.player.clone().unwrap(), - DisplayObject::MovieClip(target), - fetch, - None - ); - - context.navigator.spawn_future(process); - - Ok(Value::Undefined.into()) - }, - "loadVariables" => |target: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, args: &[Value<'gc>]| { - let url = args - .get(0) - .cloned() - .unwrap_or(Value::Undefined) - .coerce_to_string(avm, context)?; - let method = args.get(1).cloned().unwrap_or(Value::Undefined); - let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); - let (url, opts) = avm.locals_into_request_options(context, url, method); - let fetch = context.navigator.fetch(url, opts); - let process = context.load_manager.load_form_into_object( - context.player.clone().unwrap(), - target.object().as_object()?, - fetch, - ); - - context.navigator.spawn_future(process); - - Ok(Value::Undefined.into()) - }, - "unloadMovie" => |mut target: MovieClip<'gc>, _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, _args: &[Value<'gc>]| { - target.unload(context); - target.replace_with_movie(context.gc_context, None); - - Ok(Value::Undefined.into()) - } + "nextFrame" => next_frame, + "play" => play, + "prevFrame" => prev_frame, + "removeMovieClip" => remove_movie_clip, + "startDrag" => start_drag, + "stop" => stop, + "stopDrag" => stop_drag, + "swapDepths" => swap_depths, + "toString" => to_string, + "unloadMovie" => unload_movie ); object.add_property( @@ -408,7 +336,17 @@ fn create_text_field<'gc>( } } -pub fn duplicate_movie_clip<'gc>( +fn duplicate_movie_clip<'gc>( + movie_clip: MovieClip<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], +) -> Result, Error> { + // duplicateMovieClip method uses biased depth compared to CloneSprite + duplicate_movie_clip_with_bias(movie_clip, avm, context, args, AVM_DEPTH_BIAS) +} + +pub fn duplicate_movie_clip_with_bias<'gc>( movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, @@ -472,7 +410,27 @@ pub fn duplicate_movie_clip<'gc>( } } -pub fn get_depth<'gc>( +fn get_bytes_loaded<'gc>( + _movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + // TODO find a correct value + Ok(1.0.into()) +} + +fn get_bytes_total<'gc>( + _movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + // TODO find a correct value + Ok(1.0.into()) +} + +fn get_depth<'gc>( movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, _context: &mut UpdateContext<'_, 'gc, '_>, @@ -486,7 +444,7 @@ pub fn get_depth<'gc>( } } -pub fn get_next_highest_depth<'gc>( +fn get_next_highest_depth<'gc>( movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, _context: &mut UpdateContext<'_, 'gc, '_>, @@ -506,7 +464,7 @@ pub fn get_next_highest_depth<'gc>( } } -pub fn goto_and_play<'gc>( +fn goto_and_play<'gc>( movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, @@ -515,7 +473,7 @@ pub fn goto_and_play<'gc>( goto_frame(movie_clip, avm, context, args, false, 0) } -pub fn goto_and_stop<'gc>( +fn goto_and_stop<'gc>( movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, @@ -561,7 +519,47 @@ pub fn goto_frame<'gc>( Ok(Value::Undefined.into()) } -pub fn remove_movie_clip<'gc>( +fn next_frame<'gc>( + movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + movie_clip.next_frame(context); + Ok(Value::Undefined.into()) +} + +fn play<'gc>( + movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + movie_clip.play(context); + Ok(Value::Undefined.into()) +} + +fn prev_frame<'gc>( + movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + movie_clip.prev_frame(context); + Ok(Value::Undefined.into()) +} + +fn remove_movie_clip<'gc>( + movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + // removeMovieClip method uses biased depth compared to RemoveSprite + remove_movie_clip_with_bias(movie_clip, context, AVM_DEPTH_BIAS) +} + +pub fn remove_movie_clip_with_bias<'gc>( movie_clip: MovieClip<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, depth_bias: i32, @@ -584,7 +582,7 @@ pub fn remove_movie_clip<'gc>( Ok(Value::Undefined.into()) } -pub fn start_drag<'gc>( +fn start_drag<'gc>( movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, @@ -594,7 +592,17 @@ pub fn start_drag<'gc>( Ok(Value::Undefined.into()) } -pub fn stop_drag<'gc>( +fn stop<'gc>( + movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + movie_clip.stop(context); + Ok(Value::Undefined.into()) +} + +fn stop_drag<'gc>( _movie_clip: MovieClip<'gc>, _avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, @@ -605,7 +613,7 @@ pub fn stop_drag<'gc>( Ok(Value::Undefined.into()) } -pub fn swap_depths<'gc>( +fn swap_depths<'gc>( movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, @@ -650,7 +658,16 @@ pub fn swap_depths<'gc>( Ok(Value::Undefined.into()) } -pub fn local_to_global<'gc>( +fn to_string<'gc>( + movie_clip: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + _context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + Ok(movie_clip.path().into()) +} + +fn local_to_global<'gc>( movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, @@ -682,7 +699,7 @@ pub fn local_to_global<'gc>( Ok(Value::Undefined.into()) } -pub fn global_to_local<'gc>( +fn global_to_local<'gc>( movie_clip: MovieClip<'gc>, avm: &mut Avm1<'gc>, context: &mut UpdateContext<'_, 'gc, '_>, @@ -713,3 +730,68 @@ pub fn global_to_local<'gc>( Ok(Value::Undefined.into()) } + +fn load_movie<'gc>( + target: MovieClip<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], +) -> Result, Error> { + let url = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let method = args.get(1).cloned().unwrap_or(Value::Undefined); + let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); + let (url, opts) = avm.locals_into_request_options(context, url, method); + let fetch = context.navigator.fetch(url, opts); + let process = context.load_manager.load_movie_into_clip( + context.player.clone().unwrap(), + DisplayObject::MovieClip(target), + fetch, + None, + ); + + context.navigator.spawn_future(process); + + Ok(Value::Undefined.into()) +} + +fn load_variables<'gc>( + target: MovieClip<'gc>, + avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + args: &[Value<'gc>], +) -> Result, Error> { + let url = args + .get(0) + .cloned() + .unwrap_or(Value::Undefined) + .coerce_to_string(avm, context)?; + let method = args.get(1).cloned().unwrap_or(Value::Undefined); + let method = NavigationMethod::from_method_str(&method.coerce_to_string(avm, context)?); + let (url, opts) = avm.locals_into_request_options(context, url, method); + let fetch = context.navigator.fetch(url, opts); + let process = context.load_manager.load_form_into_object( + context.player.clone().unwrap(), + target.object().as_object()?, + fetch, + ); + + context.navigator.spawn_future(process); + + Ok(Value::Undefined.into()) +} + +fn unload_movie<'gc>( + mut target: MovieClip<'gc>, + _avm: &mut Avm1<'gc>, + context: &mut UpdateContext<'_, 'gc, '_>, + _args: &[Value<'gc>], +) -> Result, Error> { + target.unload(context); + target.replace_with_movie(context.gc_context, None); + + Ok(Value::Undefined.into()) +}