core: Merge #120, initial multiple movie support
Initial support for loading multiple movies: * `loadMovie`/`loadMovieNum` * `loadVariables`/`loadVariablesNum` * XML loading Desktop currently only loads from the local file system.
This commit is contained in:
commit
8018c88870
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
265
core/src/avm1.rs
265
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<NavigationMethod>,
|
||||
) -> (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,28 +294,24 @@ 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,
|
||||
),
|
||||
Activation::from_nothing(swf_version, self.globals, context.gc_context, active_clip),
|
||||
));
|
||||
let callback = clip
|
||||
let callback = obj
|
||||
.get(name, self, context)
|
||||
.and_then(|prop| prop.resolve(self, context));
|
||||
self.stack_frames.pop();
|
||||
|
@ -287,8 +320,7 @@ impl<'gc> Avm1<'gc> {
|
|||
// 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, &[]);
|
||||
}
|
||||
let _ = callback.call(self, context, obj, args);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -662,7 +694,7 @@ impl<'gc> Avm1<'gc> {
|
|||
start: DisplayObject<'gc>,
|
||||
path: &str,
|
||||
) -> Result<Option<Object<'gc>>, 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<ReturnValue<'gc>, 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<DisplayObject<'gc>>,
|
||||
root: DisplayObject<'gc>,
|
||||
path: &'s str,
|
||||
) -> Option<DisplayObject<'gc>> {
|
||||
// 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<Value<'gc>>) {
|
||||
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::<u32>()?;
|
||||
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?
|
||||
let window_target = target.clone().into_string(self.current_swf_version());
|
||||
let clip_target: Option<DisplayObject<'gc>> = 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())
|
||||
};
|
||||
|
||||
if is_load_vars {
|
||||
log::warn!("Reading AVM locals from forms is not yet implemented");
|
||||
return Ok(()); //maybe error?
|
||||
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(target), vars);
|
||||
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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)?
|
||||
|
|
|
@ -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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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
|
||||
|
||||
if let Ok(mut new_clip) = context
|
||||
.library
|
||||
.instantiate_by_id(movie_clip.id(), context.gc_context, &avm.prototypes)
|
||||
.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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, Error> {
|
||||
target.unload(context);
|
||||
target.replace_with_movie(context.gc_context, None);
|
||||
|
||||
Ok(Value::Undefined.into())
|
||||
}
|
||||
|
|
|
@ -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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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()
|
||||
}
|
|
@ -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<ReturnValue<'gc>, 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()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 +168,16 @@ 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) {
|
||||
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,
|
||||
|
@ -177,6 +187,12 @@ fn attach_sound<'gc>(
|
|||
} else {
|
||||
log::warn!("Sound.attachSound: Sound '{}' not found", name);
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Sound.attachSound: Cannot attach Sound '{}' without a library to reference",
|
||||
name
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::warn!("Sound.attachSound: this is not a Sound");
|
||||
}
|
||||
|
@ -395,14 +411,27 @@ 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)
|
||||
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: 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.
|
||||
// TODO: We just stop the last played sound for now.
|
||||
|
|
|
@ -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<ReturnValue<'gc>, 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<ReturnValue<'gc>, 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
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ pub trait TObject<'gc>: 'gc + Collect + Debug + Into<Object<'gc>> + Clone + Copy
|
|||
avm: &mut Avm1<'gc>,
|
||||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
) -> Result<ReturnValue<'gc>, 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<Object<'gc>> + 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -232,7 +232,7 @@ impl<'gc> Scope<'gc> {
|
|||
context: &mut UpdateContext<'_, 'gc, '_>,
|
||||
this: Object<'gc>,
|
||||
) -> Result<ReturnValue<'gc>, 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
|
||||
|
|
|
@ -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<F, R>(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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -65,7 +65,7 @@ impl<'gc> TObject<'gc> for StageObject<'gc> {
|
|||
) -> Result<ReturnValue<'gc>, 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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<F, R>(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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<Trait> or Deref<Target=Trait>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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<dyn std::error::Error>;
|
||||
|
||||
/// Enumerates all possible navigation methods.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum NavigationMethod {
|
||||
/// Indicates that navigation should generate a GET request.
|
||||
GET,
|
||||
|
@ -21,7 +31,59 @@ impl NavigationMethod {
|
|||
SendVarsMethod::Post => Some(Self::POST),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_method_str(method: &str) -> Option<Self> {
|
||||
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<u8>, 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<u8>, 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<u8>, String)> {
|
||||
&self.body
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for pinned, boxed, and owned futures that output a falliable
|
||||
/// result of type `Result<T, E>`.
|
||||
pub type OwnedFuture<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + 'static>>;
|
||||
|
||||
/// A backend interacting with a browser environment.
|
||||
pub trait NavigatorBackend {
|
||||
|
@ -53,14 +115,156 @@ pub trait NavigatorBackend {
|
|||
window: Option<String>,
|
||||
vars_method: Option<(NavigationMethod, HashMap<String, String>)>,
|
||||
);
|
||||
|
||||
/// Fetch data at a given URL and return it some time in the future.
|
||||
fn fetch(&self, url: String, request_options: RequestOptions) -> OwnedFuture<Vec<u8>, 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<OwnedFuture<(), Error>>,
|
||||
|
||||
/// The source of any additional futures.
|
||||
channel: Receiver<OwnedFuture<(), Error>>,
|
||||
}
|
||||
|
||||
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<OwnedFuture<(), Error>>) {
|
||||
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<Sender<OwnedFuture<(), Error>>>,
|
||||
|
||||
/// 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<P: AsRef<Path>>(
|
||||
path: P,
|
||||
channel: Sender<OwnedFuture<(), Error>>,
|
||||
) -> 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<String, String>)>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn fetch(&self, url: String, _opts: RequestOptions) -> OwnedFuture<Vec<u8>, 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Vec<u8>>,
|
||||
/// The root SWF file.
|
||||
pub swf: &'a Arc<SwfMovie>,
|
||||
|
||||
/// 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<u32, DisplayObject<'gc>>,
|
||||
|
||||
/// 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<Weak<Mutex<Player>>>,
|
||||
|
||||
/// 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<Value<'gc>>,
|
||||
},
|
||||
|
||||
/// 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,
|
||||
|
|
|
@ -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<Arc<SwfMovie>> {
|
||||
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<DisplayObject<'gc>> {
|
||||
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<DisplayObject<'gc>> {
|
||||
if name.get(0..min(name.len(), 6)) == Some("_level") {
|
||||
if let Some(level_id) = name.get(6..).and_then(|v| v.parse::<u32>().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<Arc<SwfMovie>> {
|
||||
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 {}
|
||||
|
|
|
@ -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<Arc<SwfMovie>> {
|
||||
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,7 +259,10 @@ 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(
|
||||
match context
|
||||
.library
|
||||
.library_for_movie_mut(self.static_data.read().swf.clone())
|
||||
.instantiate_by_id(
|
||||
record.id,
|
||||
context.gc_context,
|
||||
&context.system_prototypes,
|
||||
|
@ -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<SwfMovie> {
|
||||
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<SwfMovie>,
|
||||
id: CharacterId,
|
||||
records: Vec<swf::ButtonRecord>,
|
||||
actions: Vec<ButtonAction>,
|
||||
|
|
|
@ -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<SwfMovie>,
|
||||
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<SwfMovie>,
|
||||
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,11 +204,15 @@ 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 {
|
||||
let color = static_data
|
||||
.text
|
||||
.color
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| &swf::Color {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 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<usize> {
|
||||
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<Arc<SwfMovie>> {
|
||||
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<SwfMovie>,
|
||||
text: swf::EditText,
|
||||
}
|
||||
|
||||
unsafe impl<'gc> gc_arena::Collect for EditTextStatic {
|
||||
#[inline]
|
||||
|
|
|
@ -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<SwfMovie>) -> 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<Arc<SwfMovie>>,
|
||||
) {
|
||||
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<Item = SwfSlice> {
|
||||
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,16 +287,10 @@ 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,
|
||||
};
|
||||
if let Some(code) = clip.static_data.swf.resize_to_reader(reader, tag_len) {
|
||||
actions.push(code)
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
|
@ -299,6 +310,10 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
|
|||
self.0.read().id()
|
||||
}
|
||||
|
||||
fn movie(&self) -> Option<Arc<SwfMovie>> {
|
||||
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<Arc<SwfMovie>>,
|
||||
) {
|
||||
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<std::io::Cursor<&'a [u8]>> {
|
||||
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,9 +606,9 @@ impl<'gc> MovieClipData<'gc> {
|
|||
place_object: &swf::PlaceObject,
|
||||
copy_previous_properties: bool,
|
||||
) -> Option<DisplayObject<'gc>> {
|
||||
if let Ok(mut child) =
|
||||
context
|
||||
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,
|
||||
|
@ -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<SwfMovie> {
|
||||
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<String, FrameNumber>,
|
||||
audio_stream_info: Option<swf::SoundStreamHead>,
|
||||
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<swf::ClipAction> 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<SwfMovie>) -> Self {
|
||||
use swf::ClipEventFlag;
|
||||
|
||||
let len = other.action_data.len();
|
||||
Self {
|
||||
events: other
|
||||
.events
|
||||
|
@ -2010,9 +2152,9 @@ impl From<swf::ClipAction> 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SwfMovie>,
|
||||
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<Arc<SwfMovie>> {
|
||||
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<SwfMovie>,
|
||||
id: CharacterId,
|
||||
text_transform: Matrix,
|
||||
text_blocks: Vec<swf::TextRecord>,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<CharacterId, Character<'gc>>,
|
||||
export_characters: HashMap<String, Character<'gc>>,
|
||||
jpeg_tables: Option<Vec<u8>>,
|
||||
device_font: Option<Font<'gc>>,
|
||||
}
|
||||
|
||||
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<Weak<SwfMovie>, 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<SwfMovie>) -> Option<&MovieLibrary<'gc>> {
|
||||
self.movie_libraries.get(&movie)
|
||||
}
|
||||
|
||||
pub fn library_for_movie_mut(&mut self, movie: Arc<SwfMovie>) -> &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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<dyn std::error::Error>;
|
||||
|
||||
/// Holds all in-progress loads for the player.
|
||||
pub struct LoadManager<'gc>(Arena<Loader<'gc>>);
|
||||
|
||||
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<Mutex<Player>>,
|
||||
target_clip: DisplayObject<'gc>,
|
||||
fetch: OwnedFuture<Vec<u8>, Error>,
|
||||
target_broadcaster: Option<Object<'gc>>,
|
||||
) -> 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<Object<'gc>>,
|
||||
queue: &mut ActionQueue<'gc>,
|
||||
) {
|
||||
let mut invalidated_loaders = vec![];
|
||||
|
||||
for (index, loader) in self.0.iter_mut() {
|
||||
if loader.movie_clip_loaded(loaded_clip, clip_object, 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<Mutex<Player>>,
|
||||
target_object: Object<'gc>,
|
||||
fetch: OwnedFuture<Vec<u8>, 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<Mutex<Player>>,
|
||||
target_node: XMLNode<'gc>,
|
||||
active_clip: DisplayObject<'gc>,
|
||||
fetch: OwnedFuture<Vec<u8>, 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<Handle>,
|
||||
|
||||
/// 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<Object<'gc>>,
|
||||
},
|
||||
|
||||
/// Loader that is loading form data into an AVM1 object scope.
|
||||
Form {
|
||||
/// The handle to refer to this loader instance.
|
||||
self_handle: Option<Handle>,
|
||||
|
||||
/// 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<Handle>,
|
||||
|
||||
/// 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<Mutex<Player>>,
|
||||
fetch: OwnedFuture<Vec<u8>, 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<Mutex<Player>>,
|
||||
fetch: OwnedFuture<Vec<u8>, 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<Object<'gc>>,
|
||||
queue: &mut ActionQueue<'gc>,
|
||||
) -> bool {
|
||||
let (clip, broadcaster) = match self {
|
||||
Loader::Movie {
|
||||
target_clip,
|
||||
target_broadcaster,
|
||||
..
|
||||
} => (*target_clip, *target_broadcaster),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
if DisplayObject::ptr_eq(loaded_clip, clip) {
|
||||
if let Some(broadcaster) = broadcaster {
|
||||
queue.queue_actions(
|
||||
clip,
|
||||
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<Mutex<Player>>,
|
||||
fetch: OwnedFuture<Vec<u8>, 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(())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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<u32, DisplayObject<'gc>>,
|
||||
|
||||
mouse_hovered_object: Option<DisplayObject<'gc>>, // 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<u32, DisplayObject<'gc>>,
|
||||
&mut Library<'gc>,
|
||||
&mut ActionQueue<'gc>,
|
||||
&mut Avm1<'gc>,
|
||||
&mut Option<DragObject<'gc>>,
|
||||
&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<dyn std::error::Error>;
|
|||
|
||||
make_arena!(GcArena, GcRoot);
|
||||
|
||||
pub struct Player<
|
||||
Audio: AudioBackend,
|
||||
Renderer: RenderBackend,
|
||||
Navigator: NavigatorBackend,
|
||||
Input: InputBackend,
|
||||
> {
|
||||
type Audio = Box<dyn AudioBackend>;
|
||||
type Navigator = Box<dyn NavigatorBackend>;
|
||||
type Renderer = Box<dyn RenderBackend>;
|
||||
type Input = Box<dyn InputBackend>;
|
||||
|
||||
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<Vec<u8>>,
|
||||
swf_version: u8,
|
||||
swf: Arc<SwfMovie>,
|
||||
|
||||
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<Weak<Mutex<Self>>>,
|
||||
}
|
||||
|
||||
impl<
|
||||
Audio: AudioBackend,
|
||||
Renderer: RenderBackend,
|
||||
Navigator: NavigatorBackend,
|
||||
Input: InputBackend,
|
||||
> Player<Audio, Renderer, Navigator, Input>
|
||||
{
|
||||
impl Player {
|
||||
pub fn new(
|
||||
mut renderer: Renderer,
|
||||
audio: Audio,
|
||||
navigator: Navigator,
|
||||
input: Input,
|
||||
swf_data: Vec<u8>,
|
||||
) -> Result<Self, Error> {
|
||||
let swf_stream = swf::read::read_swf_header(&swf_data[..]).unwrap();
|
||||
let header = swf_stream.header;
|
||||
let mut reader = swf_stream.reader;
|
||||
) -> Result<Arc<Mutex<Self>>, 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;
|
||||
let levels: Vec<DisplayObject<'_>> = context.levels.values().copied().collect();
|
||||
for level in levels {
|
||||
if let Some(button_event) = button_event {
|
||||
root.propagate_button_event(context, 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<DisplayObject<'_>> = context.levels.values().copied().collect();
|
||||
|
||||
for level in levels {
|
||||
if let Some(clip_event) = clip_event {
|
||||
root.propagate_clip_event(context, clip_event);
|
||||
level.propagate_clip_event(context, clip_event);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mouse_event_name) = mouse_event_name {
|
||||
context.action_queue.queue_actions(
|
||||
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<crate::font::Font<'gc>, 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<F, R>(&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> {
|
||||
|
|
|
@ -1,47 +1,152 @@
|
|||
use gc_arena::Collect;
|
||||
use std::sync::Arc;
|
||||
use swf::TagCode;
|
||||
use swf::{Header, TagCode};
|
||||
|
||||
pub type DecodeResult = Result<(), Box<dyn std::error::Error>>;
|
||||
pub type SwfStream<R> = swf::read::Reader<std::io::Cursor<R>>;
|
||||
|
||||
/// 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<u8>,
|
||||
}
|
||||
|
||||
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<u8>) -> 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<Vec<u8>>,
|
||||
pub movie: Arc<SwfMovie>,
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
impl From<Arc<SwfMovie>> for SwfSlice {
|
||||
fn from(movie: Arc<SwfMovie>) -> 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<u8>) -> 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<SwfSlice> {
|
||||
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<SwfSlice> {
|
||||
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<SwfSlice> {
|
||||
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<std::io::Cursor<&[u8]>> {
|
||||
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 {
|
||||
|
|
|
@ -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<dyn std::error::Error>;
|
||||
|
||||
|
@ -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<String, Error> {
|
||||
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())
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,10 @@
|
|||
_level1
|
||||
false
|
||||
true
|
||||
_level1
|
||||
_level1
|
||||
_level0
|
||||
true
|
||||
false
|
||||
_level1
|
||||
_level1
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
Loading movie
|
||||
Child movie loaded!
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
Loading movie
|
||||
Child movie loaded!
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
Loading movie
|
||||
Child movie loaded!
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,2 @@
|
|||
Hurray
|
||||
The test passed
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
loaded=Hurray&also=The%20test%20passed
|
|
@ -0,0 +1,2 @@
|
|||
Hurray
|
||||
The test passed
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
loaded=Hurray&also=The%20test%20passed
|
|
@ -0,0 +1,2 @@
|
|||
Hurray
|
||||
The test passed
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
loaded=Hurray&also=The%20test%20passed
|
|
@ -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
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
Child movie loaded!
|
||||
68
|
||||
68
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,5 @@
|
|||
Event: onLoadStart
|
||||
Event: onLoadProgress
|
||||
Event: onLoadComplete
|
||||
Child movie loaded!
|
||||
Event: onLoadInit
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,5 @@
|
|||
Event: onLoadStart
|
||||
Event: onLoadProgress
|
||||
Event: onLoadComplete
|
||||
Child movie loaded!
|
||||
Event: onLoadInit
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,7 @@
|
|||
_level0
|
||||
_level0
|
||||
_level0
|
||||
_level0
|
||||
true
|
||||
true
|
||||
true
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
Loading movie
|
||||
Child movie loaded!
|
||||
Unloading movie
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,3 @@
|
|||
Loading movie
|
||||
Child movie loaded!
|
||||
Unloading movie
|
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue