diff --git a/Cargo.lock b/Cargo.lock index cb88f1a8b..e2b3a5f41 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)", @@ -1575,6 +1576,8 @@ 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.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]] @@ -1640,6 +1643,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)", ] @@ -2239,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" @@ -2639,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 cebbc985e..674e9b635 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,6 +20,9 @@ enumset = "0.4.2" smallvec = "1.2.0" 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 d7b463dda..b33c344b0 100644 --- a/core/src/avm1.rs +++ b/core/src/avm1.rs @@ -1,17 +1,20 @@ 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}; +use crate::display_object::{DisplayObject, MovieClip}; +use crate::player::NEWEST_PLAYER_VERSION; use crate::tag_utils::SwfSlice; #[cfg(test)] @@ -146,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. @@ -194,6 +196,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, @@ -257,38 +294,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); } } @@ -662,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() { @@ -776,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. @@ -845,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() { @@ -882,6 +912,52 @@ 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 + } + + /// Resolve a level by ID. + /// + /// If the level does not exist, then it will be created and instantiated + /// with a script object. + pub fn resolve_level( + &self, + level_id: u32, + context: &mut UpdateContext<'_, 'gc, '_>, + ) -> DisplayObject<'gc> { + if let Some(level) = context.levels.get(&level_id) { + *level + } else { + let mut level: DisplayObject<'_> = + MovieClip::new(NEWEST_PLAYER_VERSION, context.gc_context).into(); + + 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); + + level + } + } + fn push(&mut self, value: impl Into>) { let value = value.into(); avm_debug!("Stack push {}: {:?}", self.stack.len(), value); @@ -1014,11 +1090,11 @@ 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()) { - let _ = globals::movie_clip::duplicate_movie_clip( + let _ = globals::movie_clip::duplicate_movie_clip_with_bias( movie_clip, self, context, @@ -1083,7 +1159,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() { @@ -1102,7 +1178,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, @@ -1136,7 +1212,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(()) @@ -1157,7 +1233,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 { @@ -1262,7 +1338,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(); @@ -1375,13 +1451,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(()) @@ -1528,8 +1608,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`. @@ -1561,16 +1641,24 @@ 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 url = url.to_string(); + let level_id = target[6..].parse::()?; + let fetch = context.navigator.fetch(url, RequestOptions::get()); + let level = self.resolve_level(level_id, context); + + let process = context.load_manager.load_movie_into_clip( + context.player.clone().unwrap(), + level, + fetch, + None, ); + context.navigator.spawn_future(process); + return Ok(()); } @@ -1594,29 +1682,77 @@ 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) { return fscommand::handle(fscommand, self, context); } - if is_target_sprite { - log::warn!("GetURL into target sprite is not yet implemented"); - return Ok(()); //maybe error? - } - - 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, + let window_target = target.clone().into_string(self.current_swf_version()); + let clip_target: Option> = if is_target_sprite { + if let Value::Object(target) = target { + target.as_display_object() + } else { + let start = self.target_clip_or_root(); + self.resolve_target_display_object(context, start, target.clone())? + } + } else { + Some(self.target_clip_or_root()) }; - context.navigator.navigate_to_url(url, Some(target), vars); + if is_load_vars { + if let Some(clip_target) = clip_target { + let target_obj = clip_target + .as_movie_clip() + .unwrap() + .object() + .as_object() + .unwrap(); + 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, + fetch, + ); + + context.navigator.spawn_future(process); + } + + return Ok(()); + } else if is_target_sprite { + if let Some(clip_target) = clip_target { + 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, + fetch, + None, + ); + context.navigator.spawn_future(process); + } + + return Ok(()); + } 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(window_target), vars); + } Ok(()) } @@ -2075,11 +2211,11 @@ 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()) { - 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"); } @@ -2161,16 +2297,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. @@ -2179,13 +2314,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(); @@ -2233,7 +2372,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(); @@ -2251,7 +2390,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(); @@ -2534,7 +2673,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..5a3087679 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. @@ -329,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; } @@ -338,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..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; } @@ -544,12 +548,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.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/color.rs b/core/src/avm1/globals/color.rs index 8693f1f7b..b1e01da69 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) } @@ -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)? diff --git a/core/src/avm1/globals/movie_clip.rs b/core/src/avm1/globals/movie_clip.rs index 9e8c10a07..76b8630e9 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; @@ -101,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()); } @@ -138,53 +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 + "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( @@ -246,11 +222,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 +290,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 +322,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); @@ -354,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, '_>, @@ -385,10 +377,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); @@ -416,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, '_>, @@ -430,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, '_>, @@ -450,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, '_>, @@ -459,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, '_>, @@ -505,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, @@ -528,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, '_>, @@ -538,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, '_>, @@ -549,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, '_>, @@ -594,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, '_>, @@ -626,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, '_>, @@ -657,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()) +} 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..3742a82d1 --- /dev/null +++ b/core/src/avm1/globals/movie_clip_loader.rs @@ -0,0 +1,283 @@ +//! `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 crate::backend::navigator::RequestOptions; +use crate::display_object::{DisplayObject, TDisplayObject}; +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 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 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 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>, + 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.as_script_object().unwrap().force_set_function( + "loadClip", + load_clip, + gc_context, + 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.as_script_object().unwrap().force_set_function( + "getProgress", + get_progress, + gc_context, + EnumSet::empty(), + Some(fn_proto), + ); + + mcl_proto.into() +} 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/globals/sound.rs b/core/src/avm1/globals/sound.rs index c5b760c62..9d44237a4 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,30 @@ 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() + .or_else(|| context.levels.get(&0).copied()) + .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 +411,26 @@ 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() + .or_else(|| context.levels.get(&0).copied()) + .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/globals/xml.rs b/core/src/avm1/globals/xml.rs index 8658a0540..2c6643f51 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(); + 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/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 6f2d16a80..bbdec05b9 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; } @@ -571,9 +571,12 @@ 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; use rand::{rngs::SmallRng, SeedableRng}; + use std::collections::BTreeMap; use std::sync::Arc; fn with_object(swf_version: u8, test: F) -> R @@ -582,15 +585,19 @@ 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); + root.set_depth(gc_context, 0); + let mut levels = BTreeMap::new(); + levels.insert(0, root); let mut context = UpdateContext { gc_context, global_time: 0, player_version: 32, - swf_version, - root, + swf: &swf, + levels: &mut levels, rng: &mut SmallRng::from_seed([0u8; 16]), action_queue: &mut crate::context::ActionQueue::new(), audio: &mut NullAudioBackend::new(), @@ -601,15 +608,16 @@ mod tests { b: 0, a: 0, }, - library: &mut Library::new(), + library: &mut Library::default(), 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)), 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/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..dee788fde 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,11 +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(level) = self.display_object.get_level_by_path(name, context) { + // 4) _levelN + Ok(level.object().into()) } else { - // 4) Prototype + // 5) Prototype crate::avm1::object::search_prototype(self.proto(), name, avm, context, (*self).into()) } - // 4) TODO: __resolve? + // 6) TODO: __resolve? } fn get_local( @@ -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_level_by_path(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/test_utils.rs b/core/src/avm1/test_utils.rs index 6c77a3db3..412a7ab7e 100644 --- a/core/src/avm1/test_utils.rs +++ b/core/src/avm1/test_utils.rs @@ -7,9 +7,12 @@ 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}; use rand::{rngs::SmallRng, SeedableRng}; +use std::collections::BTreeMap; use std::sync::Arc; pub fn with_avm(swf_version: u8, test: F) -> R @@ -21,15 +24,19 @@ 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); + root.set_depth(gc_context, 0); + let mut levels = BTreeMap::new(); + levels.insert(0, root); let mut context = UpdateContext { gc_context, global_time: 0, player_version: 32, - swf_version, - root, + swf: &swf, + levels: &mut levels, rng: &mut SmallRng::from_seed([0u8; 16]), audio: &mut NullAudioBackend::new(), input: &mut NullInputBackend::new(), @@ -40,15 +47,16 @@ where b: 0, a: 0, }, - library: &mut Library::new(), + library: &mut Library::default(), 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)), 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/avm1/tests.rs b/core/src/avm1/tests.rs index 58953cdfa..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.root, + *context.levels.get(&0).expect("_level0 in test"), ); let my_locals = my_activation.scope().locals().to_owned(); 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/backend/audio.rs b/core/src/backend/audio.rs index 7b94b8dc6..6d6ccb868 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/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/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/backend/navigator.rs b/core/src/backend/navigator.rs index 47093af0d..70ca4c2d2 100644 --- a/core/src/backend/navigator.rs +++ b/core/src/backend/navigator.rs @@ -1,9 +1,19 @@ //! 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; + /// Enumerates all possible navigation methods. +#[derive(Copy, Clone)] pub enum NavigationMethod { /// Indicates that navigation should generate a GET request. GET, @@ -21,8 +31,60 @@ 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 + } +} + +/// 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. @@ -53,14 +115,156 @@ 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, request_options: RequestOptions) -> OwnedFuture, 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: 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, + } } } @@ -78,4 +282,19 @@ impl NavigatorBackend for NullNavigatorBackend { _vars_method: Option<(NavigationMethod, HashMap)>, ) { } + + fn fetch(&self, url: String, _opts: RequestOptions) -> OwnedFuture, Error> { + 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>) { + self.channel + .as_ref() + .expect("Expected ability to execute futures") + .send(future) + .unwrap(); + } } diff --git a/core/src/context.rs b/core/src/context.rs index ec6197fd4..970168ca5 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -2,17 +2,20 @@ 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; +use crate::loader::LoadManager; +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}; use rand::rngs::SmallRng; -use std::sync::Arc; +use std::collections::BTreeMap; +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 @@ -44,20 +47,17 @@ 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, + 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, @@ -65,9 +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, - /// The root of the current timeline. - /// This will generally be `_level0`, except for loadMovie/loadMovieNum. - pub root: DisplayObject<'gc>, + /// 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. @@ -84,6 +83,18 @@ 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>>, + + /// 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. @@ -184,7 +195,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 { @@ -205,9 +220,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.rs b/core/src/display_object.rs index 9a193e21d..28c4d4df1 100644 --- a/core/src/display_object.rs +++ b/core/src/display_object.rs @@ -1,13 +1,16 @@ -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::*; +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::cmp::min; use std::fmt::Debug; +use std::sync::Arc; mod bitmap; mod button; @@ -90,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 } @@ -345,6 +354,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( @@ -360,7 +373,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); @@ -575,8 +588,7 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug { path.push_str(&*self.name()); path } else { - // TODO: Get the actual level # from somewhere. - "_level0".to_string() + format!("_level{}", self.depth()) } } @@ -628,6 +640,24 @@ pub trait TDisplayObject<'gc>: 'gc + Collect + Debug { // TODO: Make a HashMap from name -> child? self.children().find(|child| &*child.name() == name) } + + /// Get another level by level name. + /// + /// 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(level_id) = name.get(6..).and_then(|v| v.parse::().ok()) { + return context.levels.get(&level_id).copied(); + } + } + + None + } fn removed(&self) -> bool; fn set_removed(&mut self, context: MutationContext<'gc, '_>, value: bool); @@ -725,7 +755,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().unwrap())) .collect(), ); } @@ -784,6 +814,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; @@ -793,6 +828,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 e30ce0d7b..094f713a6 100644 --- a/core/src/display_object/button.rs +++ b/core/src/display_object/button.rs @@ -3,9 +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::{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)] @@ -26,16 +28,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(), @@ -49,6 +48,7 @@ impl<'gc> Button<'gc> { } let static_data = ButtonStatic { + swf: source_movie.movie.clone(), id: button.id, records: button.records.clone(), actions, @@ -122,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, '_>, @@ -225,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( @@ -255,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()); @@ -337,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); } } @@ -369,6 +380,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> { @@ -411,6 +426,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 0ba7a3fcc..278d8aa77 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,31 @@ 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, + ) + } + + /// 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: Option>, + ) { + self.0 + .write(gc_context) + .replace_with_movie(gc_context, movie) + } + pub fn preload( self, context: &mut UpdateContext<'_, 'gc, '_>, @@ -249,7 +269,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 +277,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 +287,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) + } } _ => (), } @@ -299,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() { @@ -307,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 { @@ -318,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>) { @@ -406,6 +426,40 @@ 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 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: Option>, + ) { + let movie = movie.unwrap_or_else(|| Arc::new(SwfMovie::empty(self.movie().version()))); + let total_frames = movie.header().num_frames; + + self.base.reset_for_movie_load(); + self.static_data = Gc::allocate( + gc_context, + MovieClipStatic { + id: 0, + swf: movie.into(), + total_frames, + audio_stream_info: None, + frame_labels: HashMap::new(), + }, + ); + self.tag_stream_pos = 0; + self.flags = MovieClipFlags::Playing.into(); + self.current_frame = 0; + self.audio_stream = None; + self.children = BTreeMap::new(); + } + fn id(&self) -> CharacterId { self.static_data.id } @@ -453,12 +507,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 +537,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 +558,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; @@ -567,10 +606,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. @@ -693,10 +732,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 +745,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 +915,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 +932,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"), @@ -917,7 +957,11 @@ impl<'gc> MovieClipData<'gc> { if let Some(name) = name { context.action_queue.queue_actions( self_display_object, - ActionType::Method { name }, + ActionType::Method { + object: self.object.unwrap(), + name, + args: vec![], + }, event == ClipEvent::Unload, ); } @@ -925,6 +969,31 @@ impl<'gc> MovieClipData<'gc> { } } + /// 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.action_queue, + ); + } + } + pub fn clip_actions(&self) -> &[ClipAction] { &self.clip_actions } @@ -951,6 +1020,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 +1038,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 { @@ -1059,6 +1133,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(define_bits_lossless.id, Character::Bitmap(bitmap)); Ok(()) } @@ -1089,6 +1164,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(()) } @@ -1196,10 +1272,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, @@ -1209,6 +1289,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::Bitmap(bitmap)); Ok(()) } @@ -1238,6 +1319,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::Bitmap(bitmap)); Ok(()) } @@ -1275,6 +1357,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::Bitmap(bitmap)); Ok(()) } @@ -1313,6 +1396,7 @@ impl<'gc, 'a> MovieClipData<'gc> { ); context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::Bitmap(bitmap)); Ok(()) } @@ -1324,9 +1408,15 @@ 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 + .library_for_movie_mut(self.movie()) .register_character(swf_button.id, Character::Button(button)); Ok(()) } @@ -1338,9 +1428,15 @@ 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 + .library_for_movie_mut(self.movie()) .register_character(swf_button.id, Character::Button(button)); Ok(()) } @@ -1353,7 +1449,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 { @@ -1378,7 +1478,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 { @@ -1404,9 +1508,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(()) } @@ -1445,6 +1550,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(()) } @@ -1459,6 +1565,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(()) } @@ -1473,6 +1580,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(()) @@ -1487,12 +1595,15 @@ 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 .library + .library_for_movie_mut(self.movie()) .register_character(sound.id, Character::Sound(handle)); Ok(()) } @@ -1507,11 +1618,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, ); @@ -1519,6 +1636,7 @@ impl<'gc, 'a> MovieClipData<'gc> { context .library + .library_for_movie_mut(self.movie()) .register_character(id, Character::MovieClip(movie_clip)); Ok(()) @@ -1532,9 +1650,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(()) } @@ -1547,7 +1666,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(()) } @@ -1588,7 +1710,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(()) } @@ -1632,15 +1757,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 +1790,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 +1892,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(()) @@ -1789,7 +1921,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 { @@ -1818,19 +1954,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 +2109,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 +2152,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/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/lib.rs b/core/src/lib.rs index b23c5faaa..148601271 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; @@ -14,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/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/loader.rs b/core/src/loader.rs new file mode 100644 index 000000000..75b50796e --- /dev/null +++ b/core/src/loader.rs @@ -0,0 +1,565 @@ +//! 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}; +use crate::tag_utils::SwfMovie; +use crate::xml::XMLNode; +use gc_arena::{Collect, CollectionContext}; +use generational_arena::{Arena, Index}; +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: OwnedFuture, Error>, + target_broadcaster: Option>, + ) -> OwnedFuture<(), Error> { + let loader = Loader::Movie { + self_handle: None, + target_clip, + target_broadcaster, + }; + let handle = self.add_loader(loader); + + let loader = self.get_loader_mut(handle).unwrap(); + loader.introduce_loader_handle(handle); + + 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>, + clip_object: Option>, + 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, 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. + pub fn load_form_into_object( + &mut self, + player: Weak>, + target_object: Object<'gc>, + fetch: OwnedFuture, Error>, + ) -> OwnedFuture<(), Error> { + 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) + } + + /// 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: OwnedFuture, Error>, + ) -> OwnedFuture<(), Error> { + 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> { + 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>, + + /// Event broadcaster (typically a `MovieClipLoader`) to fire events + /// into. + target_broadcaster: Option>, + }, + + /// 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>, + }, + + /// 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_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), + } + } +} + +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), + Loader::XML { 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: 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()) }), + }; + + let player = player + .upgrade() + .expect("Could not upgrade weak reference to player"); + + Box::pin(async move { + 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!(), + }; + + 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, + broadcaster, + NEWEST_PLAYER_VERSION, + uc, + "broadcastMessage", + &["onLoadStart".into(), 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, + "broadcastMessage", + &[ + "onLoadProgress".into(), + 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, Some(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, + "broadcastMessage", + &["onLoadComplete".into(), 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, + "broadcastMessage", + &[ + "onLoadError".into(), + Value::Object(broadcaster), + "LoadNeverCompleted".into(), + ], + ); + avm.run_stack_till_empty(uc)?; + } + + Ok(()) + }, + ) + } + }) + } + + pub fn form_loader( + &mut self, + player: Weak>, + 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()) }), + }; + + 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(()) + }) + }) + } + + /// 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>, + clip_object: Option>, + 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, + ActionType::Method { + object: broadcaster, + name: "broadcastMessage", + args: vec![ + "onLoadInit".into(), + clip_object.map(|co| co.into()).unwrap_or(Value::Undefined), + ], + }, + false, + ); + } + + true + } else { + false + } + } + + pub fn xml_loader( + &mut self, + player: Weak>, + 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()) }), + }; + + 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/src/player.rs b/core/src/player.rs index 73851f56a..7c7d0488a 100644 --- a/core/src/player.rs +++ b/core/src/player.rs @@ -6,15 +6,19 @@ 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::*; +use crate::tag_utils::SwfMovie; 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::sync::Arc; +use std::ops::DerefMut; +use std::sync::{Arc, Mutex, Weak}; static DEVICE_FONT_TAG: &[u8] = include_bytes!("../assets/noto-sans-definefont3.bin"); @@ -30,7 +34,14 @@ struct GcRoot<'gc>(GcCell<'gc, GcRootData<'gc>>); #[collect(no_drop)] struct GcRootData<'gc> { library: Library<'gc>, - root: DisplayObject<'gc>, + + /// The list of levels on the current stage. + /// + /// 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. @@ -38,6 +49,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> { @@ -46,18 +61,20 @@ impl<'gc> GcRootData<'gc> { fn update_context_params( &mut self, ) -> ( - DisplayObject<'gc>, + &mut BTreeMap>, &mut Library<'gc>, &mut ActionQueue<'gc>, &mut Avm1<'gc>, &mut Option>, + &mut LoadManager<'gc>, ) { ( - self.root, + &mut self.levels, &mut self.library, &mut self.action_queue, &mut self.avm, &mut self.drag_object, + &mut self.load_manager, ) } } @@ -65,12 +82,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: @@ -83,14 +100,13 @@ 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, audio: Audio, renderer: Renderer, - navigator: Navigator, + pub navigator: Navigator, input: Input, transform_stack: TransformStack, view_matrix: Matrix, @@ -113,58 +129,40 @@ 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 { - let swf_stream = swf::read::read_swf_header(&swf_data[..]).unwrap(); - let header = swf_stream.header; - let mut reader = swf_stream.reader; + ) -> Result>, Error> { + 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, @@ -191,30 +189,30 @@ impl< } }; - let mut library = Library::new(); - library.set_device_font(device_font); + let mut library = Library::default(); + let root = MovieClip::from_movie(gc_context, movie.clone()).into(); + let mut levels = BTreeMap::new(); + levels.insert(0, root); + + library + .library_for_movie_mut(movie.clone()) + .set_device_font(device_font); + GcRoot(GcCell::allocate( gc_context, GcRootData { library, - root: MovieClip::new_with_data( - header.version, - gc_context, - 0, - 0, - swf_len, - header.num_frames, - ) - .into(), + levels, mouse_hovered_object: None, drag_object: None, avm: Avm1::new(gc_context, NEWEST_PLAYER_VERSION), action_queue: ActionQueue::new(), + load_manager: LoadManager::new(), }, )) }), - frame_rate: header.frame_rate.into(), + frame_rate: movie.header().frame_rate.into(), frame_accumulator: 0.0, global_time: 0, @@ -231,18 +229,28 @@ impl< audio, navigator, input, + self_reference: None, }; 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, level) in root_data.levels.iter_mut() { + level.post_instantiation(gc_context, *level, mc_proto); + level.set_depth(gc_context, 0); + } }); 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) { @@ -364,9 +372,14 @@ impl< 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 levels: Vec> = context.levels.values().copied().collect(); + for level in levels { + if let Some(button_event) = button_event { + let state = level.propagate_button_event(context, button_event); + if state == ButtonEventResult::Handled { + return; + } + } } }); } @@ -382,15 +395,17 @@ impl< if clip_event.is_some() || mouse_event_name.is_some() { self.mutate_with_update_context(|_avm, context| { - let root = context.root; + let levels: Vec> = context.levels.values().copied().collect(); - if let Some(clip_event) = clip_event { - root.propagate_clip_event(context, clip_event); + for level in levels { + if let Some(clip_event) = clip_event { + level.propagate_clip_event(context, clip_event); + } } if let Some(mouse_event_name) = mouse_event_name { context.action_queue.queue_actions( - root, + *context.levels.get(&0).expect("root level"), ActionType::NotifyListeners { listener: SystemListener::Mouse, method: mouse_event_name, @@ -461,17 +476,28 @@ 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 { 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 (_depth, level) in context.levels.iter().rev() { + if new_hovered.is_none() { + new_hovered = level.mouse_pick(*level, (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 { @@ -497,10 +523,14 @@ impl< }) } + /// 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(); - let root = context.root; + let root = *context.levels.get(&0).expect("root level"); root.as_movie_clip() .unwrap() .preload(context, &mut morph_shapes); @@ -510,24 +540,24 @@ impl< 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)); } }); } 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 levels run? + // 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 levels: Vec<_> = update_context.levels.values().copied().collect(); - // Update mouse state (check for new hovered button, etc.) - self.update_drag(); - self.update_roll_over(); - - // GC - self.gc_arena.collect_debt(); + for mut level in levels { + level.run_frame(update_context); + } + }) } pub fn render(&mut self) { @@ -552,13 +582,16 @@ 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, clip_depth_stack: vec![], }; - root_data.root.render(&mut render_context); + + for (_depth, level) in root_data.levels.iter() { + level.render(&mut render_context); + } }); transform_stack.pop(); @@ -595,8 +628,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, '_>) { @@ -605,12 +638,13 @@ impl< if !actions.is_unload && actions.clip.removed() { continue; } + match actions.action_type { // DoAction/clip event code ActionType::Normal { bytecode } => { avm.insert_stack_frame_for_action( actions.clip, - context.swf_version, + context.swf.header().version, bytecode, context, ); @@ -619,19 +653,20 @@ impl< ActionType::Init { bytecode } => { avm.insert_stack_frame_for_init_action( actions.clip, - context.swf_version, + context.swf.header().version, bytecode, context, ); } - // Event handler method call (e.g. onEnterFrame) - ActionType::Method { name } => { - avm.insert_stack_frame_for_avm_function( + ActionType::Method { object, name, args } => { + avm.insert_stack_frame_for_method( actions.clip, - context.swf_version, + object, + context.swf.header().version, context, name, + &args, ); } @@ -645,7 +680,7 @@ impl< // so this doesn't require any further execution. avm.notify_system_listeners( actions.clip, - context.swf_version, + context.swf.version(), context, listener, method, @@ -706,8 +741,7 @@ impl< let ( player_version, global_time, - swf_data, - swf_version, + swf, background_color, renderer, audio, @@ -717,31 +751,33 @@ impl< mouse_position, stage_width, stage_height, + player, ) = ( self.player_version, self.global_time, - &mut self.swf_data, - self.swf_version, + &self.swf, &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| { 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 (levels, library, action_queue, avm, drag_object, load_manager) = + root_data.update_context_params(); + let mut update_context = UpdateContext { player_version, global_time, - swf_data, - swf_version, + swf, library, background_color, rng, @@ -751,12 +787,14 @@ impl< input, action_queue, gc_context, - root, - system_prototypes: avm.prototypes().clone(), + levels, mouse_hovered_object, mouse_position, drag_object, stage_size: (stage_width, stage_height), + system_prototypes: avm.prototypes().clone(), + player, + load_manager, }; let ret = f(avm, &mut update_context); @@ -776,10 +814,43 @@ 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) } + + /// 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_drag(); + self.update_roll_over(); + + // GC + self.gc_arena.collect_debt(); + + rval + } } pub struct DragObject<'gc> { 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/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index 2cb57389b..23dd304c9 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; @@ -158,6 +159,22 @@ 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), + (loadmovie, "avm1/loadmovie", 2), + (loadmovienum, "avm1/loadmovienum", 2), + (loadmovie_method, "avm1/loadmovie_method", 2), + (unloadmovie, "avm1/unloadmovie", 11), + (unloadmovienum, "avm1/unloadmovienum", 11), + (unloadmovie_method, "avm1/unloadmovie_method", 11), + (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), + (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. @@ -270,19 +287,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 player = Player::new( - NullRenderer, - NullAudioBackend::new(), - NullNavigatorBackend::new(), - NullInputBackend::new(), + let (mut executor, channel) = NullExecutor::new(); + let player = Player::new( + Box::new(NullRenderer), + Box::new(NullAudioBackend::new()), + Box::new(NullNavigatorBackend::with_base_path(base_path, channel)), + Box::new(NullInputBackend::new()), swf_data, )?; for _ in 0..num_frames { - player.run_frame(); + player.lock().unwrap().run_frame(); + executor.poll_all().unwrap(); } + executor.block_all().unwrap(); + Ok(trace_log()) } 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 000000000..f39edaff4 Binary files /dev/null and b/core/tests/swfs/avm1/cross_movie_root/layer1.fla differ 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 000000000..a10efe359 Binary files /dev/null and b/core/tests/swfs/avm1/cross_movie_root/layer1.swf differ diff --git a/core/tests/swfs/avm1/cross_movie_root/output.txt b/core/tests/swfs/avm1/cross_movie_root/output.txt new file mode 100644 index 000000000..9bed5305c --- /dev/null +++ b/core/tests/swfs/avm1/cross_movie_root/output.txt @@ -0,0 +1,10 @@ +_level1 +false +true +_level1 +_level1 +_level0 +true +false +_level1 +_level1 diff --git a/core/tests/swfs/avm1/cross_movie_root/test.fla b/core/tests/swfs/avm1/cross_movie_root/test.fla new file mode 100644 index 000000000..ee681a325 Binary files /dev/null and b/core/tests/swfs/avm1/cross_movie_root/test.fla differ 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 000000000..1e24868e7 Binary files /dev/null and b/core/tests/swfs/avm1/cross_movie_root/test.swf differ 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 000000000..3b639cffc Binary files /dev/null and b/core/tests/swfs/avm1/loadmovie/target.fla differ diff --git a/core/tests/swfs/avm1/loadmovie/target.swf b/core/tests/swfs/avm1/loadmovie/target.swf new file mode 100644 index 000000000..1a1a83455 Binary files /dev/null and b/core/tests/swfs/avm1/loadmovie/target.swf differ diff --git a/core/tests/swfs/avm1/loadmovie/test.fla b/core/tests/swfs/avm1/loadmovie/test.fla new file mode 100644 index 000000000..d6cae71a0 Binary files /dev/null and b/core/tests/swfs/avm1/loadmovie/test.fla differ diff --git a/core/tests/swfs/avm1/loadmovie/test.swf b/core/tests/swfs/avm1/loadmovie/test.swf new file mode 100644 index 000000000..37051c93a Binary files /dev/null and b/core/tests/swfs/avm1/loadmovie/test.swf differ 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 000000000..3b639cffc Binary files /dev/null and b/core/tests/swfs/avm1/loadmovie_method/target.fla differ 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 000000000..1a1a83455 Binary files /dev/null and b/core/tests/swfs/avm1/loadmovie_method/target.swf differ 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 000000000..21c33acbe Binary files /dev/null and b/core/tests/swfs/avm1/loadmovie_method/test.fla differ diff --git a/core/tests/swfs/avm1/loadmovie_method/test.swf b/core/tests/swfs/avm1/loadmovie_method/test.swf new file mode 100644 index 000000000..582fae1af Binary files /dev/null and b/core/tests/swfs/avm1/loadmovie_method/test.swf differ 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 000000000..3b639cffc Binary files /dev/null and b/core/tests/swfs/avm1/loadmovienum/target.fla differ diff --git a/core/tests/swfs/avm1/loadmovienum/target.swf b/core/tests/swfs/avm1/loadmovienum/target.swf new file mode 100644 index 000000000..1a1a83455 Binary files /dev/null and b/core/tests/swfs/avm1/loadmovienum/target.swf differ diff --git a/core/tests/swfs/avm1/loadmovienum/test.fla b/core/tests/swfs/avm1/loadmovienum/test.fla new file mode 100644 index 000000000..d10618bf1 Binary files /dev/null and b/core/tests/swfs/avm1/loadmovienum/test.fla differ diff --git a/core/tests/swfs/avm1/loadmovienum/test.swf b/core/tests/swfs/avm1/loadmovienum/test.swf new file mode 100644 index 000000000..ca376356f Binary files /dev/null and b/core/tests/swfs/avm1/loadmovienum/test.swf differ 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 000000000..2e0900127 Binary files /dev/null and b/core/tests/swfs/avm1/loadvariables/test.fla differ diff --git a/core/tests/swfs/avm1/loadvariables/test.swf b/core/tests/swfs/avm1/loadvariables/test.swf new file mode 100644 index 000000000..25cf54285 Binary files /dev/null and b/core/tests/swfs/avm1/loadvariables/test.swf differ 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 000000000..c245e19c7 Binary files /dev/null and b/core/tests/swfs/avm1/loadvariables_method/test.fla differ 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 000000000..4a768f727 Binary files /dev/null and b/core/tests/swfs/avm1/loadvariables_method/test.swf differ 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 000000000..2c254693a Binary files /dev/null and b/core/tests/swfs/avm1/loadvariablesnum/test.fla differ diff --git a/core/tests/swfs/avm1/loadvariablesnum/test.swf b/core/tests/swfs/avm1/loadvariablesnum/test.swf new file mode 100644 index 000000000..17d2b3001 Binary files /dev/null and b/core/tests/swfs/avm1/loadvariablesnum/test.swf differ 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 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 000000000..324f8c0ba Binary files /dev/null and b/core/tests/swfs/avm1/mcl_as_broadcaster/test.fla differ 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 000000000..7c1b21100 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_as_broadcaster/test.swf differ 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 000000000..3ec29ba19 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_getprogress/target.fla differ 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 000000000..1a1a83455 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_getprogress/target.swf differ 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 000000000..c2499cee0 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_getprogress/test.fla differ 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 000000000..8a53aecf9 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_getprogress/test.swf differ 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..d21cd4a56 --- /dev/null +++ b/core/tests/swfs/avm1/mcl_loadclip/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_loadclip/target.fla b/core/tests/swfs/avm1/mcl_loadclip/target.fla new file mode 100644 index 000000000..3b639cffc Binary files /dev/null and b/core/tests/swfs/avm1/mcl_loadclip/target.fla differ 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 000000000..1a1a83455 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_loadclip/target.swf differ 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 000000000..c66b96222 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_loadclip/test.fla differ diff --git a/core/tests/swfs/avm1/mcl_loadclip/test.swf b/core/tests/swfs/avm1/mcl_loadclip/test.swf new file mode 100644 index 000000000..74e69843a Binary files /dev/null and b/core/tests/swfs/avm1/mcl_loadclip/test.swf differ 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 000000000..856b2e1ea Binary files /dev/null and b/core/tests/swfs/avm1/mcl_unloadclip/target.fla differ diff --git a/core/tests/swfs/avm1/mcl_unloadclip/target.swf b/core/tests/swfs/avm1/mcl_unloadclip/target.swf new file mode 100644 index 000000000..2a3887827 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_unloadclip/target.swf differ diff --git a/core/tests/swfs/avm1/mcl_unloadclip/test.fla b/core/tests/swfs/avm1/mcl_unloadclip/test.fla new file mode 100644 index 000000000..9b6ad9480 Binary files /dev/null and b/core/tests/swfs/avm1/mcl_unloadclip/test.fla differ 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 000000000..3db34d03a Binary files /dev/null and b/core/tests/swfs/avm1/mcl_unloadclip/test.swf differ 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 000000000..6d0084b7b Binary files /dev/null and b/core/tests/swfs/avm1/roots_and_levels/test.fla differ diff --git a/core/tests/swfs/avm1/roots_and_levels/test.swf b/core/tests/swfs/avm1/roots_and_levels/test.swf new file mode 100644 index 000000000..b069dddc2 Binary files /dev/null and b/core/tests/swfs/avm1/roots_and_levels/test.swf differ 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 000000000..8e3968575 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovie/target.fla differ diff --git a/core/tests/swfs/avm1/unloadmovie/target.swf b/core/tests/swfs/avm1/unloadmovie/target.swf new file mode 100644 index 000000000..989920a74 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovie/target.swf differ diff --git a/core/tests/swfs/avm1/unloadmovie/test.fla b/core/tests/swfs/avm1/unloadmovie/test.fla new file mode 100644 index 000000000..8bdc3b486 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovie/test.fla differ diff --git a/core/tests/swfs/avm1/unloadmovie/test.swf b/core/tests/swfs/avm1/unloadmovie/test.swf new file mode 100644 index 000000000..6a91c9944 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovie/test.swf differ diff --git a/core/tests/swfs/avm1/unloadmovie_method/output.txt b/core/tests/swfs/avm1/unloadmovie_method/output.txt new file mode 100644 index 000000000..e65d6331c --- /dev/null +++ b/core/tests/swfs/avm1/unloadmovie_method/output.txt @@ -0,0 +1,3 @@ +Loading movie +Child movie loaded! +Unloading movie diff --git a/core/tests/swfs/avm1/unloadmovie_method/target.fla b/core/tests/swfs/avm1/unloadmovie_method/target.fla new file mode 100644 index 000000000..8e3968575 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovie_method/target.fla differ 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 000000000..989920a74 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovie_method/target.swf differ 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 000000000..521cbad98 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovie_method/test.fla differ 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 000000000..18417deae Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovie_method/test.swf differ 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 000000000..8e3968575 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovienum/target.fla differ diff --git a/core/tests/swfs/avm1/unloadmovienum/target.swf b/core/tests/swfs/avm1/unloadmovienum/target.swf new file mode 100644 index 000000000..989920a74 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovienum/target.swf differ diff --git a/core/tests/swfs/avm1/unloadmovienum/test.fla b/core/tests/swfs/avm1/unloadmovienum/test.fla new file mode 100644 index 000000000..371fdfbdc Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovienum/test.fla differ diff --git a/core/tests/swfs/avm1/unloadmovienum/test.swf b/core/tests/swfs/avm1/unloadmovienum/test.swf new file mode 100644 index 000000000..6570a73e3 Binary files /dev/null and b/core/tests/swfs/avm1/unloadmovienum/test.swf differ 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 000000000..5fdf7da35 Binary files /dev/null and b/core/tests/swfs/avm1/xml_load/test.fla differ diff --git a/core/tests/swfs/avm1/xml_load/test.swf b/core/tests/swfs/avm1/xml_load/test.swf new file mode 100644 index 000000000..8eb3c3ed3 Binary files /dev/null and b/core/tests/swfs/avm1/xml_load/test.swf differ diff --git a/core/tests/swfs/avm1/xml_load/whataload.xml b/core/tests/swfs/avm1/xml_load/whataload.xml new file mode 100644 index 000000000..2be0a59e3 --- /dev/null +++ b/core/tests/swfs/avm1/xml_load/whataload.xml @@ -0,0 +1 @@ +What a load! \ No newline at end of file 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 d14c0a82b..edee38629 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}, @@ -15,7 +20,6 @@ use glutin::{ }; use ruffle_core::{ backend::audio::{AudioBackend, NullAudioBackend}, - backend::render::RenderBackend, Player, }; use std::path::PathBuf; @@ -45,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,14 +67,25 @@ 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 (executor, chan) = GlutinAsyncExecutor::new(event_loop.create_proxy()); + 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 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 +104,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,19 +136,30 @@ 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); } } _ => (), }, + glutin::event::Event::UserEvent(RuffleEvent::TaskPoll) => executor + .lock() + .expect("active executor reference") + .poll_all(), _ => (), } @@ -140,10 +169,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/desktop/src/navigator.rs b/desktop/src/navigator.rs index 062e73114..cc0a8655f 100644 --- a/desktop/src/navigator.rs +++ b/desktop/src/navigator.rs @@ -1,18 +1,59 @@ //! Navigator backend for web +use crate::custom_event::RuffleEvent; +use glutin::event_loop::EventLoopProxy; use log; -use ruffle_core::backend::navigator::{NavigationMethod, NavigatorBackend}; +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; /// 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, + + /// The base path for all relative fetches. + relative_base_path: PathBuf, +} impl ExternalNavigatorBackend { - pub fn new() -> Self { - ExternalNavigatorBackend {} + #[allow(dead_code)] + pub fn new( + channel: Sender>, + event_loop: EventLoopProxy, + ) -> Self { + 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, + } } } @@ -60,4 +101,23 @@ impl NavigatorBackend for ExternalNavigatorBackend { Err(e) => log::error!("Could not open URL {}: {}", modified_url, e), }; } + + 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>) { + 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 + } +} 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, diff --git a/web/Cargo.toml b/web/Cargo.toml index 4f365a4e5..e888540a6 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" @@ -41,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"] + "KeyboardEvent", "Path2d", "CanvasGradient", "CanvasPattern", "SvgMatrix", "SvgsvgElement", "Response", "Request", "RequestInit", + "Blob", "BlobPropertyBag"] [dev-dependencies] wasm-bindgen-test = "0.3.7" 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..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::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. diff --git a/web/src/navigator.rs b/web/src/navigator.rs index 5e866b53e..23a1becb3 100644 --- a/web/src/navigator.rs +++ b/web/src/navigator.rs @@ -1,9 +1,13 @@ //! Navigator backend for web -use ruffle_core::backend::navigator::{NavigationMethod, NavigatorBackend}; +use js_sys::{Array, ArrayBuffer, Uint8Array}; +use ruffle_core::backend::navigator::{ + Error, NavigationMethod, NavigatorBackend, OwnedFuture, RequestOptions, +}; use std::collections::HashMap; use wasm_bindgen::JsCast; -use web_sys::window; +use wasm_bindgen_futures::{spawn_local, JsFuture}; +use web_sys::{window, Blob, BlobPropertyBag, Request, RequestInit, Response}; pub struct WebNavigatorBackend {} @@ -72,4 +76,66 @@ impl NavigatorBackend for WebNavigatorBackend { }; } } + + fn fetch(&self, url: String, options: RequestOptions) -> OwnedFuture, 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_request(&request)).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: OwnedFuture<(), Error>) { + spawn_local(async move { + if let Err(e) = future.await { + log::error!("Asynchronous error occured: {}", e); + } + }) + } }