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:
Mike Welsh 2020-02-24 12:47:18 -08:00 committed by GitHub
commit 8018c88870
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
122 changed files with 3303 additions and 611 deletions

10
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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 {

View File

@ -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.

View File

@ -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 {

View File

@ -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(

View File

@ -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)?

View File

@ -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())
}

View File

@ -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()
}

View File

@ -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()),
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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);
}

View File

@ -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

View File

@ -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();

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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();

View File

@ -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();

View File

@ -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 {

View File

@ -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()

View File

@ -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 {

View File

@ -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 {

View File

@ -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>

View File

@ -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,
};

View File

@ -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 {}

View File

@ -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();
}
}

View File

@ -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,

View File

@ -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 {}

View File

@ -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>,

View File

@ -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]

View File

@ -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,
},
}
}

View File

@ -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>,

View File

@ -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;

View File

@ -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(),
}
}
}

565
core/src/loader.rs Normal file
View File

@ -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(())
})
}
}

View File

@ -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> {

View File

@ -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 {

View File

@ -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.

View File

@ -0,0 +1,10 @@
_level1
false
true
_level1
_level1
_level0
true
false
_level1
_level1

Binary file not shown.

Binary file not shown.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1,2 @@
Hurray
The test passed

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
loaded=Hurray&also=The%20test%20passed

View File

@ -0,0 +1,2 @@
Hurray
The test passed

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
loaded=Hurray&also=The%20test%20passed

View File

@ -0,0 +1,2 @@
Hurray
The test passed

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
loaded=Hurray&also=The%20test%20passed

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1,7 @@
_level0
_level0
_level0
_level0
true
true
true

Binary file not shown.

Binary file not shown.

View File

@ -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.

View File

@ -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