core: Implement LocalConnection

This commit is contained in:
Nathan Adams 2024-05-31 22:08:53 +02:00
parent 0a2528b4f9
commit 4d12e0e5b4
36 changed files with 2792 additions and 165 deletions

View File

@ -36,7 +36,7 @@ pub(crate) mod glow_filter;
pub(crate) mod gradient_filter;
mod key;
mod load_vars;
mod local_connection;
pub(crate) mod local_connection;
mod math;
mod matrix;
pub(crate) mod mouse;

View File

@ -2,14 +2,139 @@
use crate::avm1::activation::Activation;
use crate::avm1::error::Error;
use crate::avm1::globals::shared_object::{deserialize_value, serialize};
use crate::avm1::object::TObject;
use crate::avm1::property_decl::{define_properties_on, Declaration};
use crate::avm1::{Object, ScriptObject, Value};
use crate::context::GcContext;
use crate::avm1::{
ActivationIdentifier, ExecutionReason, NativeObject, Object, ScriptObject, Value,
};
use crate::context::{GcContext, UpdateContext};
use crate::display_object::TDisplayObject;
use crate::local_connection::{LocalConnectionHandle, LocalConnections};
use crate::string::AvmString;
use flash_lso::types::Value as AmfValue;
use gc_arena::{Collect, Gc};
use std::cell::RefCell;
#[derive(Debug, Collect)]
#[collect(require_static)]
struct LocalConnectionData {
handle: RefCell<Option<LocalConnectionHandle>>,
}
#[derive(Copy, Clone, Debug, Collect)]
#[collect(no_drop)]
pub struct LocalConnection<'gc>(Gc<'gc, LocalConnectionData>);
impl<'gc> LocalConnection<'gc> {
pub fn cast(value: Value<'gc>) -> Option<Self> {
if let Value::Object(object) = value {
if let NativeObject::LocalConnection(local_connection) = object.native() {
return Some(local_connection);
}
}
None
}
pub fn is_connected(&self) -> bool {
self.0.handle.borrow().is_some()
}
pub fn connect(
&self,
activation: &mut Activation<'_, 'gc>,
name: AvmString<'gc>,
this: Object<'gc>,
) -> bool {
if self.is_connected() {
return false;
}
let connection_handle = activation.context.local_connections.connect(
&LocalConnections::get_domain(activation.context.swf.url()),
this,
&name,
);
let result = connection_handle.is_some();
*self.0.handle.borrow_mut() = connection_handle;
result
}
pub fn disconnect(&self, activation: &mut Activation<'_, 'gc>) {
if let Some(conn_handle) = self.0.handle.take() {
activation.context.local_connections.close(conn_handle);
}
}
pub fn send_status(
context: &mut UpdateContext<'_, 'gc>,
this: Object<'gc>,
status: &'static str,
) -> Result<(), Error<'gc>> {
let Some(root_clip) = context.stage.root_clip() else {
tracing::warn!("Ignored LocalConnection callback as there's no root movie");
return Ok(());
};
let mut activation = Activation::from_nothing(
context.reborrow(),
ActivationIdentifier::root("[LocalConnection onStatus]"),
root_clip,
);
let constructor = activation.context.avm1.prototypes().object_constructor;
let event = constructor
.construct(&mut activation, &[])?
.coerce_to_object(&mut activation);
event.set("level", status.into(), &mut activation)?;
this.call_method(
"onStatus".into(),
&[event.into()],
&mut activation,
ExecutionReason::Special,
)?;
Ok(())
}
pub fn run_method(
context: &mut UpdateContext<'_, 'gc>,
this: Object<'gc>,
method_name: AvmString<'gc>,
amf_arguments: Vec<AmfValue>,
) -> Result<(), Error<'gc>> {
let Some(root_clip) = context.stage.root_clip() else {
tracing::warn!("Ignored LocalConnection callback as there's no root movie");
return Ok(());
};
let mut activation = Activation::from_nothing(
context.reborrow(),
ActivationIdentifier::root("[LocalConnection call]"),
root_clip,
);
let mut args = Vec::with_capacity(amf_arguments.len());
for arg in amf_arguments {
let reader = flash_lso::read::Reader::default();
let value = deserialize_value(
&mut activation,
&arg,
&reader.amf0_decoder,
&mut Default::default(),
);
args.push(value);
}
this.call_method(
method_name,
&args,
&mut activation,
ExecutionReason::Special,
)?;
Ok(())
}
}
const PROTO_DECLS: &[Declaration] = declare_properties! {
"domain" => method(domain; DONT_DELETE | READ_ONLY);
"domain" => method(domain; DONT_DELETE | DONT_ENUM);
"connect" => method(connect; DONT_DELETE | DONT_ENUM);
"close" => method(close; DONT_DELETE | DONT_ENUM);
"send" => method(send; DONT_DELETE | DONT_ENUM);
};
pub fn domain<'gc>(
@ -18,29 +143,104 @@ pub fn domain<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let movie = activation.base_clip().movie();
let domain = LocalConnections::get_domain(movie.url());
let domain = if let Ok(url) = url::Url::parse(movie.url()) {
if url.scheme() == "file" {
"localhost".into()
} else if let Some(domain) = url.domain() {
AvmString::new_utf8(activation.context.gc_context, domain)
} else {
// no domain?
"localhost".into()
}
} else {
tracing::error!("LocalConnection::domain: Unable to parse movie URL");
return Ok(Value::Null);
};
Ok(Value::String(domain))
Ok(Value::String(AvmString::new_utf8(
activation.context.gc_context,
domain,
)))
}
pub fn constructor<'gc>(
_activation: &mut Activation<'_, 'gc>,
pub fn connect<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let Some(Value::String(connection_name)) = args.get(0) else {
// This is deliberately not a coercion, Flash tests the type
return Ok(false.into());
};
if connection_name.is_empty() || connection_name.contains(b':') {
return Ok(false.into());
}
if let Some(local_connection) = LocalConnection::cast(this.into()) {
return Ok(local_connection
.connect(activation, *connection_name, this)
.into());
}
Ok(Value::Undefined)
}
pub fn send<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let Some(Value::String(connection_name)) = args.get(0) else {
// This is deliberately not a coercion, Flash tests the type
return Ok(false.into());
};
let Some(Value::String(method_name)) = args.get(1) else {
// This is deliberately not a coercion, Flash tests the type
return Ok(false.into());
};
if connection_name.is_empty() || method_name.is_empty() {
return Ok(false.into());
}
if method_name == b"send"
|| method_name == b"connect"
|| method_name == b"close"
|| method_name == b"allowDomain"
|| method_name == b"allowInsecureDomain"
|| method_name == b"domain"
{
return Ok(false.into());
}
let mut amf_arguments = Vec::with_capacity(args.len() - 2);
for arg in &args[2..] {
amf_arguments.push(serialize(activation, *arg));
}
activation.context.local_connections.send(
&LocalConnections::get_domain(activation.context.swf.url()),
this,
*connection_name,
*method_name,
amf_arguments,
);
Ok(true.into())
}
pub fn close<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
if let Some(local_connection) = LocalConnection::cast(this.into()) {
local_connection.disconnect(activation);
}
Ok(Value::Undefined)
}
pub fn constructor<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
this.set_native(
activation.gc(),
NativeObject::LocalConnection(LocalConnection(Gc::new(
activation.gc(),
LocalConnectionData {
handle: RefCell::new(None),
},
))),
);
Ok(this.into())
}

View File

@ -12,6 +12,7 @@ use crate::avm1::globals::drop_shadow_filter::DropShadowFilter;
use crate::avm1::globals::file_reference::FileReferenceObject;
use crate::avm1::globals::glow_filter::GlowFilter;
use crate::avm1::globals::gradient_filter::GradientFilter;
use crate::avm1::globals::local_connection::LocalConnection;
use crate::avm1::globals::netconnection::NetConnection;
use crate::avm1::globals::shared_object::SharedObject;
use crate::avm1::globals::transform::TransformObject;
@ -66,6 +67,7 @@ pub enum NativeObject<'gc> {
XmlSocket(XmlSocket<'gc>),
FileReference(FileReferenceObject<'gc>),
NetConnection(NetConnection<'gc>),
LocalConnection(LocalConnection<'gc>),
}
/// Represents an object that can be directly interacted with by the AVM

View File

@ -55,22 +55,27 @@ pub fn serialize_value<'gc>(
// Don't serialize properties from the vtable (we don't want a 'length' field)
recursive_serialize(activation, o, &mut values, None, amf_version, object_table)
.unwrap();
let mut dense = vec![];
let mut sparse = vec![];
// ActionScript `Array`s can have non-number properties, and these properties
// are confirmed and tested to also be serialized, so do not limit the values
// iterated over by the length of the internal array data.
for (i, elem) in values.into_iter().enumerate() {
if elem.name == i.to_string() {
dense.push(elem.value.clone());
} else {
sparse.push(elem);
}
}
let len = o.as_array_storage().unwrap().length() as u32;
Some(AmfValue::ECMAArray(dense, sparse, len))
if amf_version == AMFVersion::AMF3 {
let mut dense = vec![];
let mut sparse = vec![];
// ActionScript `Array`s can have non-number properties, and these properties
// are confirmed and tested to also be serialized, so do not limit the values
// iterated over by the length of the internal array data.
for (i, elem) in values.into_iter().enumerate() {
if elem.name == i.to_string() {
dense.push(elem.value.clone());
} else {
sparse.push(elem);
}
}
Some(AmfValue::ECMAArray(dense, sparse, len))
} else {
// TODO: is this right?
Some(AmfValue::ECMAArray(vec![], values, len))
}
} else if let Some(vec) = o.as_vector_storage() {
let val_type = vec.value_type();
if val_type == Some(activation.avm2().classes().int.inner_class_definition()) {

View File

@ -524,6 +524,23 @@ pub fn make_error_2037<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc>
}
}
#[inline(never)]
#[cold]
pub fn make_error_2085<'gc>(activation: &mut Activation<'_, 'gc>, param_name: &str) -> Error<'gc> {
let err = argument_error(
activation,
&format!(
"Error #2085: Parameter {} must be non-empty string.",
param_name
),
2007,
);
match err {
Ok(err) => Error::AvmError(err),
Err(err) => err,
}
}
#[inline(never)]
#[cold]
pub fn make_error_2097<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> {

View File

@ -165,6 +165,7 @@ pub struct SystemClasses<'gc> {
pub netstatusevent: ClassObject<'gc>,
pub shaderfilter: ClassObject<'gc>,
pub statusevent: ClassObject<'gc>,
pub asyncerrorevent: ClassObject<'gc>,
pub contextmenuevent: ClassObject<'gc>,
pub filereference: ClassObject<'gc>,
pub filefilter: ClassObject<'gc>,
@ -293,6 +294,7 @@ impl<'gc> SystemClasses<'gc> {
netstatusevent: object,
shaderfilter: object,
statusevent: object,
asyncerrorevent: object,
contextmenuevent: object,
filereference: object,
filefilter: object,
@ -804,6 +806,7 @@ fn load_playerglobal<'gc>(
("flash.events", "UncaughtErrorEvents", uncaughterrorevents),
("flash.events", "NetStatusEvent", netstatusevent),
("flash.events", "StatusEvent", statusevent),
("flash.events", "AsyncErrorEvent", asyncerrorevent),
("flash.events", "ContextMenuEvent", contextmenuevent),
("flash.events", "FocusEvent", focusevent),
("flash.geom", "Matrix", matrix),

View File

@ -25,21 +25,7 @@ package flash.net {
public native function connect(connectionName:String):void;
public function send(connectionName: String, methodName: String, ... arguments):void {
if (connectionName === null) {
throw new TypeError("Error #2007: Parameter connectionName must be non-null.", 2007);
}
if (methodName === null) {
throw new TypeError("Error #2007: Parameter methodName must be non-null.", 2007);
}
var self = this;
setTimeout(function() {
self.send_internal(connectionName, methodName, arguments);
}, 0);
}
private native function send_internal(connectionName: String, methodName: String, args: Array):void;
public native function send(connectionName: String, methodName: String, ... arguments):void;
public function allowDomain(... domains): void {
stub_method("flash.net.LocalConnection", "allowDomain");

View File

@ -1,12 +1,13 @@
use crate::avm2::error::{argument_error, make_error_2007};
use crate::avm2::amf::serialize_value;
use crate::avm2::error::{argument_error, make_error_2004, make_error_2085, Error2004Type};
use crate::avm2::object::TObject;
use crate::avm2::parameters::ParametersExt;
use crate::avm2::{Activation, Avm2, Error, Object, Value};
use crate::avm2::{Activation, Error, Object, Value};
use crate::string::AvmString;
use crate::avm2_stub_method;
use flash_lso::types::{AMFVersion, Value as AmfValue};
pub use crate::avm2::object::local_connection_allocator;
use crate::local_connection::LocalConnections;
/// Implements `domain` getter
pub fn get_domain<'gc>(
@ -15,60 +16,56 @@ pub fn get_domain<'gc>(
_args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let movie = &activation.context.swf;
let domain = LocalConnections::get_domain(movie.url());
let domain = if let Ok(url) = url::Url::parse(movie.url()) {
if url.scheme() == "file" {
"localhost".into()
} else if let Some(domain) = url.domain() {
AvmString::new_utf8(activation.context.gc_context, domain)
} else {
// no domain?
"localhost".into()
}
} else {
tracing::error!("LocalConnection::domain: Unable to parse movie URL");
return Ok(Value::Null);
};
Ok(Value::String(domain))
Ok(Value::String(AvmString::new_utf8(
activation.context.gc_context,
domain,
)))
}
/// Implements `LocalConnection.send`
pub fn send_internal<'gc>(
pub fn send<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
// Already null-checked by the AS wrapper `LocalConnection.send`
let connection_name = args.get_value(0);
let connection_name = args.get_string_non_null(activation, 0, "connectionName")?;
let method_name = args.get_string_non_null(activation, 1, "methodName")?;
let connection_name = connection_name.coerce_to_string(activation)?;
let event_name = if activation
.context
.local_connections
.all_by_name(connection_name)
.is_empty()
if connection_name.is_empty() {
return Err(make_error_2085(activation, "connectionName"));
}
if method_name.is_empty() {
return Err(make_error_2085(activation, "methodName"));
}
if &method_name == b"send"
|| &method_name == b"connect"
|| &method_name == b"close"
|| &method_name == b"allowDomain"
|| &method_name == b"allowInsecureDomain"
|| &method_name == b"domain"
{
"error"
} else {
avm2_stub_method!(activation, "flash.net.LocalConnection", "send");
return Err(make_error_2004(activation, Error2004Type::ArgumentError));
}
"status"
};
let mut amf_arguments = Vec::with_capacity(args.len() - 2);
for arg in &args[2..] {
amf_arguments.push(
serialize_value(activation, *arg, AMFVersion::AMF0, &mut Default::default())
.unwrap_or(AmfValue::Undefined),
);
}
let event = activation.avm2().classes().statusevent.construct(
activation,
&[
"status".into(),
false.into(),
false.into(),
Value::Null,
event_name.into(),
],
)?;
Avm2::dispatch_event(&mut activation.context, event, this);
if let Some(local_connection) = this.as_local_connection_object() {
activation.context.local_connections.send(
&LocalConnections::get_domain(activation.context.swf.url()),
(activation.domain(), local_connection),
connection_name,
method_name,
amf_arguments,
);
}
Ok(Value::Undefined)
}
@ -79,22 +76,24 @@ pub fn connect<'gc>(
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let connection_name = args.get_value(0);
if matches!(connection_name, Value::Null) {
return Err(make_error_2007(activation, "connectionName"));
};
let connection_name = args.get_string_non_null(activation, 0, "connectionName")?;
if connection_name.is_empty() {
return Err(make_error_2085(activation, "connectionName"));
}
if connection_name.contains(b':') {
return Err(make_error_2004(activation, Error2004Type::ArgumentError));
}
if let Some(local_connection) = this.as_local_connection_object() {
if local_connection.is_connected() {
if !local_connection.connect(activation, connection_name) {
// This triggers both if this object is already connected, OR there's something else taking the name
// (The error message is misleading, in that case!)
return Err(Error::AvmError(argument_error(
activation,
"Error #2082: Connect failed because the object is already connected.",
2082,
)?));
}
let connection_name = connection_name.coerce_to_string(activation)?;
local_connection.connect(activation, connection_name);
}
Ok(Value::Undefined)

View File

@ -1,11 +1,14 @@
use crate::avm2::activation::Activation;
use crate::avm2::amf::deserialize_value;
use crate::avm2::object::script_object::ScriptObjectData;
use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
use crate::avm2::value::Value;
use crate::avm2::Error;
use crate::local_connection::{LocalConnection, LocalConnectionHandle};
use crate::avm2::{Avm2, Domain, Error};
use crate::context::UpdateContext;
use crate::local_connection::{LocalConnectionHandle, LocalConnections};
use crate::string::AvmString;
use core::fmt;
use flash_lso::types::Value as AmfValue;
use gc_arena::{Collect, GcCell, GcWeakCell, Mutation};
use std::cell::{Ref, RefMut};
@ -42,7 +45,7 @@ impl fmt::Debug for LocalConnectionObject<'_> {
}
}
#[derive(Clone, Collect)]
#[derive(Collect)]
#[collect(no_drop)]
pub struct LocalConnectionObjectData<'gc> {
/// Base script object
@ -57,30 +60,91 @@ impl<'gc> LocalConnectionObject<'gc> {
self.0.read().connection_handle.is_some()
}
pub fn connection_handle(&self) -> Option<LocalConnectionHandle> {
self.0.read().connection_handle
}
pub fn connect(&self, activation: &mut Activation<'_, 'gc>, name: AvmString<'gc>) -> bool {
if self.is_connected() {
return false;
}
pub fn connect(&self, activation: &mut Activation<'_, 'gc>, name: AvmString<'gc>) {
assert!(!self.is_connected());
let connection_handle = activation
.context
.local_connections
.insert(LocalConnection::new(*self, name));
let connection_handle = activation.context.local_connections.connect(
&LocalConnections::get_domain(activation.context.swf.url()),
(activation.domain(), *self),
&name,
);
let result = connection_handle.is_some();
self.0
.write(activation.context.gc_context)
.connection_handle = Some(connection_handle);
.connection_handle = connection_handle;
result
}
pub fn disconnect(&self, activation: &mut Activation<'_, 'gc>) {
if let Some(conn_handle) = self.0.read().connection_handle {
activation.context.local_connections.remove(conn_handle);
if let Some(conn_handle) = self
.0
.write(activation.context.gc_context)
.connection_handle
.take()
{
activation.context.local_connections.close(conn_handle);
}
}
pub fn send_status(&self, context: &mut UpdateContext<'_, 'gc>, status: &'static str) {
let mut activation = Activation::from_nothing(context.reborrow());
if let Ok(event) = activation.avm2().classes().statusevent.construct(
&mut activation,
&[
"status".into(),
false.into(),
false.into(),
Value::Null,
status.into(),
],
) {
Avm2::dispatch_event(&mut activation.context, event, (*self).into());
}
}
pub fn run_method(
&self,
context: &mut UpdateContext<'_, 'gc>,
domain: Domain<'gc>,
method_name: AvmString<'gc>,
amf_arguments: Vec<AmfValue>,
) {
let mut activation = Activation::from_domain(context.reborrow(), domain);
let mut arguments = Vec::with_capacity(amf_arguments.len());
for argument in amf_arguments {
arguments
.push(deserialize_value(&mut activation, &argument).unwrap_or(Value::Undefined));
}
self.0
.write(activation.context.gc_context)
.connection_handle = None;
if let Ok(client) = self
.get_public_property("client", &mut activation)
.and_then(|v| v.coerce_to_object(&mut activation))
{
if let Err(e) = client.call_public_property(method_name, &arguments, &mut activation) {
match e {
Error::AvmError(error) => {
if let Ok(event) = activation.avm2().classes().asyncerrorevent.construct(
&mut activation,
&[
"asyncError".into(),
false.into(),
false.into(),
error,
error,
],
) {
Avm2::dispatch_event(&mut activation.context, event, (*self).into());
}
}
_ => {
tracing::error!("Unhandled error dispatching AVM2 LocalConnection method call to '{method_name}': {e}");
}
}
}
}
}
}

View File

@ -1,55 +1,130 @@
use crate::avm1::globals::local_connection::LocalConnection as Avm1LocalConnectionObject;
use crate::avm1::Object as Avm1Object;
use crate::avm2::object::LocalConnectionObject;
use crate::avm2::Domain as Avm2Domain;
use crate::context::UpdateContext;
use crate::string::AvmString;
use flash_lso::types::Value as AmfValue;
use fnv::FnvHashMap;
use gc_arena::Collect;
use slotmap::{new_key_type, SlotMap};
use ruffle_wstr::{WStr, WString};
use std::borrow::Cow;
new_key_type! {
pub struct LocalConnectionHandle;
}
#[derive(Collect)]
#[derive(Clone, Collect)]
#[collect(no_drop)]
pub enum LocalConnectionKind<'gc> {
Avm2(LocalConnectionObject<'gc>),
Avm2(Avm2Domain<'gc>, LocalConnectionObject<'gc>),
Avm1(Avm1Object<'gc>),
}
impl<'gc> From<LocalConnectionObject<'gc>> for LocalConnectionKind<'gc> {
fn from(obj: LocalConnectionObject<'gc>) -> Self {
Self::Avm2(obj)
impl<'gc> From<(Avm2Domain<'gc>, LocalConnectionObject<'gc>)> for LocalConnectionKind<'gc> {
fn from(obj: (Avm2Domain<'gc>, LocalConnectionObject<'gc>)) -> Self {
Self::Avm2(obj.0, obj.1)
}
}
#[derive(Collect)]
#[collect(no_drop)]
pub struct LocalConnection<'gc> {
object: LocalConnectionKind<'gc>,
connection_name: AvmString<'gc>,
impl<'gc> From<Avm1Object<'gc>> for LocalConnectionKind<'gc> {
fn from(obj: Avm1Object<'gc>) -> Self {
Self::Avm1(obj)
}
}
impl<'gc> LocalConnection<'gc> {
pub fn new(
object: impl Into<LocalConnectionKind<'gc>>,
connection_name: AvmString<'gc>,
) -> Self {
Self {
object: object.into(),
connection_name,
impl<'gc> LocalConnectionKind<'gc> {
pub fn send_status(&self, context: &mut UpdateContext<'_, 'gc>, status: &'static str) {
match self {
LocalConnectionKind::Avm2(_domain, object) => {
object.send_status(context, status);
}
LocalConnectionKind::Avm1(object) => {
if let Err(e) = Avm1LocalConnectionObject::send_status(context, *object, status) {
tracing::error!("Unhandled AVM1 error during LocalConnection onStatus: {e}");
}
}
}
}
pub fn run_method(
&self,
context: &mut UpdateContext<'_, 'gc>,
method_name: AvmString<'gc>,
arguments: Vec<AmfValue>,
) {
match self {
LocalConnectionKind::Avm2(domain, object) => {
object.run_method(context, *domain, method_name, arguments);
}
LocalConnectionKind::Avm1(object) => {
if let Err(e) =
Avm1LocalConnectionObject::run_method(context, *object, method_name, arguments)
{
tracing::error!("Unhandled AVM1 error during LocalConnection onStatus: {e}");
}
}
}
}
}
/// Manages the collection of local connections.
pub struct LocalConnections<'gc> {
connections: SlotMap<LocalConnectionHandle, LocalConnection<'gc>>,
#[derive(Collect)]
#[collect(no_drop)]
pub struct QueuedMessage<'gc> {
source: LocalConnectionKind<'gc>,
kind: QueuedMessageKind<'gc>,
}
unsafe impl<'gc> Collect for LocalConnections<'gc> {
#[derive(Collect)]
#[collect(no_drop)]
pub enum QueuedMessageKind<'gc> {
Failure,
Message {
#[collect(require_static)]
connection_name: WString,
method_name: AvmString<'gc>,
#[collect(require_static)]
arguments: Vec<AmfValue>,
},
}
impl<'gc> QueuedMessageKind<'gc> {
pub fn deliver(self, source: LocalConnectionKind<'gc>, context: &mut UpdateContext<'_, 'gc>) {
match self {
QueuedMessageKind::Failure => {
source.send_status(context, "error");
}
QueuedMessageKind::Message {
connection_name,
method_name,
arguments,
} => {
if let Some(receiver) = context.local_connections.find_listener(&connection_name) {
source.send_status(context, "status");
receiver.run_method(context, method_name, arguments);
} else {
source.send_status(context, "error");
}
}
}
}
}
/// An opaque handle to an actively listening LocalConnection.
/// Owning this handle represents ownership of a LocalConnection;
/// However, a LocalConnection must be manually closed, you can't just Drop this handle.
#[derive(Debug)]
pub struct LocalConnectionHandle(WString);
/// Manages the collection of local connections.
pub struct LocalConnections<'gc> {
connections: FnvHashMap<WString, LocalConnectionKind<'gc>>,
messages: Vec<QueuedMessage<'gc>>,
}
unsafe impl Collect for LocalConnections<'_> {
fn trace(&self, cc: &gc_arena::Collection) {
for (_, connection) in self.connections.iter() {
connection.trace(cc)
for (_, v) in self.connections.iter() {
v.trace(cc);
}
for m in self.messages.iter() {
m.trace(cc);
}
}
}
@ -57,26 +132,108 @@ unsafe impl<'gc> Collect for LocalConnections<'gc> {
impl<'gc> LocalConnections<'gc> {
pub fn empty() -> Self {
Self {
connections: SlotMap::with_key(),
connections: Default::default(),
messages: Default::default(),
}
}
pub fn insert(&mut self, connection: LocalConnection<'gc>) -> LocalConnectionHandle {
self.connections.insert(connection)
pub fn connect<C: Into<LocalConnectionKind<'gc>>>(
&mut self,
domain: &str,
connection: C,
name: &WStr,
) -> Option<LocalConnectionHandle> {
let key = if name.starts_with(b'_') {
name.to_ascii_lowercase()
} else {
let mut key = WString::from_utf8(Self::get_superdomain(domain));
key.push_char(':');
key.push_str(name);
key.make_ascii_lowercase();
key
};
if self.connections.contains_key(&key) {
None
} else {
self.connections.insert(key.to_owned(), connection.into());
Some(LocalConnectionHandle(key.to_owned()))
}
}
pub fn remove(&mut self, handle: LocalConnectionHandle) {
self.connections.remove(handle);
pub fn close(&mut self, handle: LocalConnectionHandle) {
self.connections.remove(&handle.0);
}
pub fn all_by_name(&self, requested_name: AvmString<'gc>) -> Vec<&LocalConnection<'gc>> {
let mut conns = Vec::new();
for (_, connection) in self.connections.iter() {
if connection.connection_name == requested_name {
conns.push(connection);
pub fn send<C: Into<LocalConnectionKind<'gc>>>(
&mut self,
domain: &str,
source: C,
connection_name: AvmString<'gc>,
method_name: AvmString<'gc>,
arguments: Vec<AmfValue>,
) {
// There's two checks for "is connected":
// 1 - At `send()` time, if there's no connections, just immediately queue up a failure
// 2 - At `update_connections()` time, if the connection couldn't be found, queue up a failure
// Even if one becomes available between send and update, it won't be used
// Similarly, if one becomes unavailable between send and update, it'll error
// If something *else* takes its place between send and update, it'll use that instead
let mut connection_name = connection_name.to_ascii_lowercase();
if !connection_name.contains(b':') && !connection_name.starts_with(b'_') {
let mut result = WString::from_utf8(Self::get_superdomain(domain));
result.push_char(':');
result.push_str(&connection_name);
connection_name = result;
}
let kind = if self.find_listener(&connection_name).is_some() {
QueuedMessageKind::Message {
connection_name,
method_name,
arguments,
}
} else {
QueuedMessageKind::Failure
};
self.messages.push(QueuedMessage {
source: source.into(),
kind,
});
}
fn find_listener(&self, name: &WStr) -> Option<LocalConnectionKind<'gc>> {
self.connections.get(name).cloned()
}
pub fn update_connections(context: &mut UpdateContext<'_, 'gc>) {
if context.local_connections.messages.is_empty() {
return;
}
conns
for message in std::mem::take(&mut context.local_connections.messages) {
message.kind.deliver(message.source, context);
}
}
pub fn get_domain(url: &str) -> Cow<'static, str> {
if let Ok(url) = url::Url::parse(url) {
if url.scheme() == "file" {
Cow::Borrowed("localhost")
} else if let Some(domain) = url.domain() {
Cow::Owned(domain.to_owned())
} else {
// no domain?
Cow::Borrowed("localhost")
}
} else {
tracing::error!("LocalConnection: Unable to parse movie URL: {url}");
return Cow::Borrowed("unknown"); // this is surely an error but it'll hopefully highlight this case in issues for us
}
}
pub fn get_superdomain(domain: &str) -> &str {
domain.rsplit_once('.').map(|(_, b)| b).unwrap_or(domain)
}
}

View File

@ -1715,6 +1715,7 @@ impl Player {
run_all_phases_avm2(context);
Avm1::run_frame(context);
AudioManager::update_sounds(context);
LocalConnections::update_connections(context);
// Only run the current list of callbacks - any callbacks added during callback execution
// will be run at the end of the *next* frame.

View File

@ -0,0 +1,15 @@
class CustomLocalConnection extends LocalConnection {
function test() {
trace("custom.test was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s"));
if (arguments.length > 0) {
trace(" " + repr(arguments));
}
}
function throwAnError() {
trace("custom.throwAnError was called");
//throw "aah!"; // [NA] this crashes every Flash Player I've tried
//throw {}; // [NA] this causes an error when constructing the AsyncErrorEvent
//throw new Error("aaah!");
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,111 @@
package {
import flash.display.MovieClip;
import flash.net.LocalConnection;
import flash.utils.getQualifiedClassName;
public class Child extends MovieClip {
var lc: LocalConnection = new LocalConnection();
public function Child() {
lc.connect("avm2_child");
lc.client = {};
lc.client.test = function() {
trace("avm2_child.test was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s"));
if (arguments.length > 0) {
trace(" " + repr(arguments));
}
}
}
private function getObjectId(needle: Object, haystack: Array): String {
for (var i = 0; i < haystack.length; i++) {
if (haystack[i] === needle) {
return i;
}
}
return null;
}
public function repr(value: *, indent: String = " ", seenObjects: Array = null) {
if (seenObjects == null) {
seenObjects = [];
}
if (value === lc) {
return "lc";
}
if (value === undefined || value === null || value === true || value === false || value is Number) {
return String(value);
} else if (value is String) {
return escapeString(value);
} else {
var existingId = getObjectId(value, seenObjects);
if (existingId != null) {
return "*" + existingId;
}
existingId = seenObjects.length;
seenObjects.push(value);
if (value is Array) {
if (value.length == 0) {
return "*" + existingId + " []";
} else {
var result = "*" + existingId + " [\n";
var nextIndent = indent + " ";
for (var i = 0; i < value.length; i++) {
result += nextIndent + repr(value[i], nextIndent, seenObjects) + "\n";
}
return result + indent + "]";
}
} else {
var keys = [];
for (var key in value) {
keys.push(key);
}
keys.sort();
var result = "*" + existingId + " " + getQualifiedClassName(value) + " {";
if (keys.length == 0) {
return result + "}";
} else {
result += "\n";
var nextIndent = indent + " ";
for (var i = 0; i < keys.length; i++) {
result += nextIndent + keys[i] + " = " + repr(value[keys[i]], nextIndent, seenObjects) + "\n";
}
return result + indent + "}";
}
}
}
}
public function escapeString(input: String): String {
var output:String = "\"";
for (var i:int = 0; i < input.length; i++) {
var char:String = input.charAt(i);
switch (char) {
case "\\":
output += "\\\\";
break;
case "\"":
output += "\\\"";
break;
case "\n":
output += "\\n";
break;
case "\r":
output += "\\r";
break;
case "\t":
output += "\\t";
break;
default:
output += char;
}
}
return output + "\"";
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,580 @@
-- start test: A message to nowhere! --
sender.send("nowhere", "test", *0 []): true
-- end frame: A message to nowhere! --
sender.onStatus was called
*0 Object {
level = "error"
}
-- end test: A message to nowhere! --
-- start test: Both receivers try to connect to the same channel --
receiver.connect("channel"): true
custom.connect("channel"): false
-- end frame: Both receivers try to connect to the same channel --
-- end test: Both receivers try to connect to the same channel --
-- start test: A message to an unimplemented function --
sender.send("channel", "unimplemented", *0 []): true
-- end frame: A message to an unimplemented function --
sender.onStatus was called
*0 Object {
level = "status"
}
-- end test: A message to an unimplemented function --
-- start test: Receiver tries to connect elsewhere, but can't --
receiver.connect("elsewhere"): false
-- end frame: Receiver tries to connect elsewhere, but can't --
-- end test: Receiver tries to connect elsewhere, but can't --
-- start test: Receiver actually connects elsewhere, and custom is allowed to connect to channel --
receiver.close()
receiver.connect("elsewhere"): true
custom.connect("channel"): true
-- end frame: Receiver actually connects elsewhere, and custom is allowed to connect to channel --
-- end test: Receiver actually connects elsewhere, and custom is allowed to connect to channel --
-- start test: Sender calls test() on 'channel' --
sender.send("channel", "test", *0 []): true
-- end frame: Sender calls test() on 'channel' --
sender.onStatus was called
*0 Object {
level = "status"
}
custom.test was called with 0 argument
-- end test: Sender calls test() on 'channel' --
-- start test: Sender calls test() on 'channel'... after the listener is gone --
custom.close()
sender.send("channel", "test", *0 []): true
-- end frame: Sender calls test() on 'channel'... after the listener is gone --
sender.onStatus was called
*0 Object {
level = "error"
}
-- end test: Sender calls test() on 'channel'... after the listener is gone --
-- start test: Sender calls test() on 'elsewhere'... immediately before the listener is gone --
sender.send("elsewhere", "test", *0 []): true
receiver.close()
-- end frame: Sender calls test() on 'elsewhere'... immediately before the listener is gone --
sender.onStatus was called
*0 Object {
level = "error"
}
-- end test: Sender calls test() on 'elsewhere'... immediately before the listener is gone --
-- start test: Sender calls test() on 'channel'... before the listener connects --
sender.send("channel", "test", *0 []): true
custom.connect("channel"): true
-- end frame: Sender calls test() on 'channel'... before the listener connects --
sender.onStatus was called
*0 Object {
level = "error"
}
-- end test: Sender calls test() on 'channel'... before the listener connects --
-- start test: Sending to a channel that gets reassigned before end-of-frame --
sender.send("channel", "test", *0 []): true
custom.close()
receiver.connect("channel"): true
-- end frame: Sending to a channel that gets reassigned before end-of-frame --
sender.onStatus was called
*0 Object {
level = "status"
}
receiver.test was called with 0 argument
-- end test: Sending to a channel that gets reassigned before end-of-frame --
-- start test: Channels reconnect and receive --
custom.close()
receiver.close()
receiver.connect("elsewhere"): true
sender.send("channel", "test", *0 []): true
sender.send("elsewhere", "test", *0 []): true
custom.connect("channel"): true
-- end frame: Channels reconnect and receive --
sender.onStatus was called
*0 Object {
level = "error"
}
sender.onStatus was called
*0 Object {
level = "status"
}
receiver.test was called with 0 argument
-- end test: Channels reconnect and receive --
-- start test: A connected listener can also send --
receiver.send("channel", "test", *0 []): true
receiver.send("elsewhere", "test", *0 []): true
-- end frame: A connected listener can also send --
receiver.onStatus was called
*0 Object {
level = "status"
}
custom.test was called with 0 argument
receiver.onStatus was called
*0 Object {
level = "status"
}
receiver.test was called with 0 argument
-- end test: A connected listener can also send --
-- start test: A listener throws an error --
sender.send("channel", "throwAnError", *0 []): true
-- end frame: A listener throws an error --
sender.onStatus was called
*0 Object {
level = "status"
}
custom.throwAnError was called
-- end test: A listener throws an error --
-- start test: Close something's that's already closed --
-- end frame: Close something's that's already closed --
-- end test: Close something's that's already closed --
-- start test: Send to funky channel names --
sender.send(null, "test", *0 []): false
sender.send(0, "test", *0 []): false
sender.send("", "test", *0 []): false
sender.send(" ??? ", "test", *0 []): true
-- end frame: Send to funky channel names --
sender.onStatus was called
*0 Object {
level = "error"
}
-- end test: Send to funky channel names --
-- start test: Send to funky methods --
sender.send("channel", null, *0 []): false
sender.send("channel", 0, *0 []): false
sender.send("channel", "", *0 []): false
sender.send("channel", " ??? ", *0 []): true
-- end frame: Send to funky methods --
sender.onStatus was called
*0 Object {
level = "status"
}
-- end test: Send to funky methods --
-- start test: Connect to funky names --
sender.connect(null): false
sender.close()
sender.connect(0): false
sender.close()
sender.connect(""): false
sender.close()
sender.connect(" ??? "): true
sender.close()
-- end frame: Connect to funky names --
-- end test: Connect to funky names --
-- start test: Connect to something with a prefix --
sender.connect("localhost:something"): false
sender.close()
-- end frame: Connect to something with a prefix --
-- end test: Connect to something with a prefix --
-- start test: Send to protected methods --
sender.send("channel", "send", *0 []): false
sender.send("channel", "connect", *0 []): false
sender.send("channel", "close", *0 []): false
sender.send("channel", "allowDomain", *0 []): false
sender.send("channel", "allowInsecureDomain", *0 []): false
sender.send("channel", "domain", *0 []): false
-- end frame: Send to protected methods --
-- end test: Send to protected methods --
-- start test: Arguments are sent --
sender.send("elsewhere", "test", *0 [
1
"two"
*1 Object {
value = 3
}
]): true
-- end frame: Arguments are sent --
sender.onStatus was called
*0 Object {
level = "status"
}
receiver.test was called with 3 arguments
*0 [
1
"two"
*1 Object {
value = 3
}
]
-- end test: Arguments are sent --
-- start test: Explicit host prefix --
sender.send("localhost:channel", "test", *0 []): true
sender.send("notlocalhost:elsewhere", "test", *0 []): true
-- end frame: Explicit host prefix --
sender.onStatus was called
*0 Object {
level = "status"
}
custom.test was called with 0 argument
sender.onStatus was called
*0 Object {
level = "error"
}
-- end test: Explicit host prefix --
-- start test: Underscores in names --
custom.close()
custom.connect("_channel"): true
sender.send("_channel", "test", *0 []): true
-- end frame: Underscores in names --
sender.onStatus was called
*0 Object {
level = "status"
}
custom.test was called with 0 argument
-- end test: Underscores in names --
-- start test: Underscores in name doesn't allow a prefix --
sender.send("localhost:channel", "test", *0 []): true
sender.send("localhost:_channel", "test", *0 []): true
-- end frame: Underscores in name doesn't allow a prefix --
sender.onStatus was called
*0 Object {
level = "error"
}
sender.onStatus was called
*0 Object {
level = "error"
}
-- end test: Underscores in name doesn't allow a prefix --
-- start test: Case sensitivity --
sender.send("ELSEWhere", "test", *0 []): true
sender.send("LOCalHOST:ElseWhere", "test", *0 []): true
-- end frame: Case sensitivity --
sender.onStatus was called
*0 Object {
level = "status"
}
receiver.test was called with 0 argument
sender.onStatus was called
*0 Object {
level = "status"
}
receiver.test was called with 0 argument
-- end test: Case sensitivity --
-- start test: Calling an AVM2 movie --
sender.send("avm2_child", "test", *0 []): true
-- end frame: Calling an AVM2 movie --
sender.onStatus was called
*0 Object {
level = "error"
}
-- end test: Calling an AVM2 movie --
-- start test: Calling an AVM1 movie --
sender.send("avm1_child", "test", *0 []): true
-- end frame: Calling an AVM1 movie --
sender.onStatus was called
*0 Object {
level = "status"
}
avm1_child.test was called with 0 argument
-- end test: Calling an AVM1 movie --
-- start test: Argument translations: primitives --
sender.send("avm1_child", "test", *0 [
1
1.2
true
false
"string"
null
undefined
]): true
sender.send("avm2_child", "test", *0 [
1
1.2
true
false
"string"
null
undefined
]): true
sender.send("_channel", "test", *0 [
1
1.2
true
false
"string"
null
undefined
]): true
-- end frame: Argument translations: primitives --
sender.onStatus was called
*0 Object {
level = "status"
}
avm1_child.test was called with 7 arguments
*0 [
1
1.2
true
false
"string"
null
undefined
]
sender.onStatus was called
*0 Object {
level = "error"
}
sender.onStatus was called
*0 Object {
level = "status"
}
custom.test was called with 7 arguments
*0 [
1
1.2
true
false
"string"
null
undefined
]
-- end test: Argument translations: primitives --
-- start test: Argument translations: simple object --
sender.send("avm1_child", "test", *0 [
*1 Object {
nested = *2 Object {
numbers = *3 [
1
2
]
string = "hello"
}
}
]): true
sender.send("avm2_child", "test", *0 [
*1 Object {
nested = *2 Object {
numbers = *3 [
1
2
]
string = "hello"
}
}
]): true
sender.send("_channel", "test", *0 [
*1 Object {
nested = *2 Object {
numbers = *3 [
1
2
]
string = "hello"
}
}
]): true
-- end frame: Argument translations: simple object --
sender.onStatus was called
*0 Object {
level = "status"
}
avm1_child.test was called with 1 arguments
*0 [
*1 object {
nested = *2 object {
numbers = *3 [
1
2
]
string = "hello"
}
}
]
sender.onStatus was called
*0 Object {
level = "error"
}
sender.onStatus was called
*0 Object {
level = "status"
}
custom.test was called with 1 arguments
*0 [
*1 Object {
nested = *2 Object {
numbers = *3 [
1
2
]
string = "hello"
}
}
]
-- end test: Argument translations: simple object --
-- start test: AVM1 movie throws an error --
sender.send("avm1_child", "throwAnError", *0 []): true
-- end frame: AVM1 movie throws an error --
sender.onStatus was called
*0 Object {
level = "status"
}
avm1_child.throwAnError was called
-- end test: AVM1 movie throws an error --
Finished after 117 frames

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_ticks = 300 # Test may finish in less, but it'll `fscommand:exit` when it's done.

View File

@ -0,0 +1,8 @@
/// Enumeration
/// Known Properties
domain: DONT_ENUM | DONT_DELETE
connect: DONT_ENUM | DONT_DELETE
close: DONT_ENUM | DONT_DELETE
isPerUser NOT FOUND
send: DONT_ENUM | DONT_DELETE

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_frames = 1

View File

@ -0,0 +1,26 @@
package {
import flash.net.LocalConnection;
public class CustomLocalConnection extends LocalConnection {
private var main: Test;
public function CustomLocalConnection(main: Test) {
super();
this.main = main;
}
public function test() {
trace("custom.test was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s"));
if (arguments.length > 0) {
trace(" " + main.repr(arguments));
}
}
public function throwAnError() {
trace("custom.throwAnError was called");
//throw "aah!"; // [NA] this crashes every Flash Player I've tried
//throw {}; // [NA] this causes an error when constructing the AsyncErrorEvent
//throw new Error("aaah!");
}
}
}

View File

@ -0,0 +1,449 @@
package {
import flash.display.MovieClip;
import flash.net.LocalConnection;
import flash.utils.getQualifiedClassName;
import flash.events.AsyncErrorEvent;
import flash.events.Event;
import flash.events.SecurityErrorEvent;
import flash.events.StatusEvent;
import flash.system.fscommand;
import flash.net.URLRequest;
import flash.display.Loader;
public class Test extends MovieClip {
// Just a safe value, to make things easier...
// Sometimes Flash isn't super consistant about things being done on the same frame
const TICKS_PER_TEST: uint = 3;
var receiver: LocalConnection = new LocalConnection();
var sender: LocalConnection = new LocalConnection();
var custom: LocalConnection;
var recvObject: Object = {};
var tests: Array = [];
var currentTest = null;
var frameNum: uint = 0;
var totalFrameNum: uint = 0;
var funkyNames = [null, 0, "", " ??? "];
var protectedFunctions = ["send", "connect", "close", "allowDomain", "allowInsecureDomain", "domain"];
public function Test() {
custom = new CustomLocalConnection(this);
loadMovie("avm2child/child.swf");
loadMovie("avm1child/child.swf");
trace("LocalConnection.isSupported: " + repr(LocalConnection.isSupported));
trace("");
recvObject.test = createTestFunction("recvObject.test");
setupEvents(receiver);
setupEvents(sender);
setupEvents(custom);
addTest("A message to nowhere!", function() {
send(sender, "nowhere", "test");
});
addTest("Both receivers try to connect to the same channel", function() {
connect(receiver, "channel");
connect(custom, "channel");
});
addTest("A message to an unimplemented function", function() {
send(sender, "channel", "unimplemented");
});
addTest("Receiver tries to connect elsewhere, but can't", function() {
connect(receiver, "elsewhere");
});
addTest("Receiver actually connects elsewhere, and custom is allowed to connect to channel", function() {
close(receiver);
connect(receiver, "elsewhere");
connect(custom, "channel");
});
addTest("Sender calls test() on 'channel'", function() {
send(sender, "channel", "test");
});
addTest("Client is used", function() {
receiver.client = recvObject;
send(sender, "elsewhere", "test");
});
addTest("Sender calls test() on 'channel'... after the listener is gone", function() {
close(custom);
send(sender, "channel", "test");
});
addTest("Sender calls test() on 'elsewhere'... immediately before the listener is gone", function() {
send(sender, "elsewhere", "test");
close(receiver);
});
addTest("Sender calls test() on 'channel'... before the listener connects", function() {
send(sender, "channel", "test");
connect(custom, "channel");
});
addTest("Sending to a channel that gets reassigned before end-of-frame", function() {
send(sender, "channel", "test");
close(custom);
connect(receiver, "channel");
});
addTest("Channels reconnect and receive", function() {
close(custom);
close(receiver);
connect(receiver, "elsewhere");
send(sender, "channel", "test");
send(sender, "elsewhere", "test");
connect(custom, "channel");
});
addTest("A connected listener can also send", function() {
send(receiver, "channel", "test");
send(receiver, "elsewhere", "test");
});
addTest("A listener throws an error", function() {
// Fun fact: you can crash Flash Player if the thing thrown isn't an object
send(sender, "channel", "throwAnError");
});
addTest("Close something's that's already closed", function() {
sender.close();
});
addTest("Send to funky channel names", function() {
for (var i = 0; i < funkyNames.length; i++) {
send(sender, funkyNames[i], "test");
}
});
addTest("Send to funky methods", function() {
for (var i = 0; i < funkyNames.length; i++) {
send(sender, "channel", funkyNames[i]);
}
});
addTest("Connect to funky names", function() {
for (var i = 0; i < funkyNames.length; i++) {
connect(sender, funkyNames[i]);
close(sender);
}
});
addTest("Connect to something with a prefix", function() {
connect(sender, "localhost:something");
close(sender);
});
addTest("Send to protected methods", function() {
for (var i = 0; i < protectedFunctions.length; i++) {
send(sender, "channel", protectedFunctions[i]);
}
});
addTest("Arguments are sent", function() {
send(sender, "elsewhere", "test", 1, "two", {value: 3}, [4, 5]);
});
addTest("Explicit host prefix", function() {
send(sender, "localhost:channel", "test");
send(sender, "notlocalhost:elsewhere", "test");
});
addTest("Underscores in names", function() {
close(custom);
connect(custom, "_channel");
send(sender, "_channel", "test");
});
addTest("Underscores in name doesn't allow a prefix", function() {
send(sender, "localhost:channel", "test");
send(sender, "localhost:_channel", "test");
});
addTest("Case sensitivity", function() {
send(sender, "ELSEWhere", "test");
send(sender, "LOCalHOST:ElseWhere", "test");
});
addTest("Calling an AVM2 movie", function() {
send(sender, "avm2_child", "test");
});
addTest("Calling an AVM1 movie", function() {
send(sender, "avm1_child", "test");
});
addTest("Argument translations: primitives", function() {
sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", 1, 1.2, true, false, "string", null, undefined);
});
addTest("Argument translations: simple array", function() {
sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", [1,2, "three", 4.5, NaN, Infinity]);
});
addTest("Argument translations: simple object", function() {
sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", {"nested": {"numbers": [1,2], "string": "hello"}});
});
// [NA] broken in ruffle at time of writing
//addTest("Argument translations: self referential object", function() {
// var obj = {};
// obj.self = obj;
// obj.nested = {root: obj};
// sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", obj);
//});
//addTest("Argument translations: self referential array", function() {
// var array = [];
// array.push(array);
// sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", array);
//});
//addTest("Argument translations: vector", function() {
//var vector = new Vector.<String>();
//vector.push("hello");
//vector.push("world");
//sendToMany(sender, ["avm1_child", "avm2_child", "_channel"], "test", vector);
//});
addTest("AVM1 movie throws an error", function() {
send(sender, "avm1_child", "throwAnError");
});
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}
function loadMovie(path: String) {
var loader = new Loader();
loader.load(new URLRequest(path));
addChild(loader);
}
function onEnterFrame(event: Event) {
totalFrameNum++;
if (frameNum == TICKS_PER_TEST) {
trace("");
trace("-- end test: " + currentTest[0] + " --");
trace("");
frameNum = 0;
return; // Allow any end-of-frame cleanup before next test
}
if (frameNum == 0) {
currentTest = tests.shift();
if (currentTest != null) {
trace("");
trace("-- start test: " + currentTest[0] + " --");
trace("");
try {
currentTest[1]();
} catch (e) {
trace("! test stopped with error: " + e);
}
trace("");
trace("-- end frame: " + currentTest[0] + " --");
trace("");
}
}
if (currentTest == null) {
trace("Finished after " + totalFrameNum + " frames");
fscommand("exit");
removeEventListener(Event.ENTER_FRAME, onEnterFrame);
return;
}
frameNum++;
}
function connect(lc: LocalConnection, name: String) {
var doing = repr(lc) + ".connect(" + repr(name) + ")";
try {
lc.connect(name);
trace(doing);
} catch (e) {
trace(doing + ": ! " + e);
}
}
function send(lc: LocalConnection, connectionName: String, methodName: String, ...args) {
var doing = repr(lc) + ".send(" + repr(connectionName) + ", " + repr(methodName) + ", " + repr(args) + ")";
try {
args.unshift(methodName);
args.unshift(connectionName);
lc.send.apply(lc, args);
trace(doing);
} catch (e) {
trace(doing + ": ! " + e);
}
}
function sendToMany(lc: LocalConnection, connectionNames: Array, methodName: String, ...args) {
args.unshift(methodName);
args.unshift("");
args.unshift(lc);
for (var i = 0; i < connectionNames.length; i++) {
args[1] = connectionNames[i];
send.apply(null, args);
}
}
function close(lc: LocalConnection) {
var doing = repr(lc) + ".close()";
try {
lc.close();
trace(doing);
} catch (e) {
trace(doing + ": ! " + e);
}
}
function addTest(name: String, fn: Function) {
tests.push([name, fn]);
}
function createTestFunction(name: String) {
return function() {
trace(name + " was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s"));
if (arguments.length > 0) {
trace(" " + repr(arguments));
}
}
}
function setupEvents(lc: LocalConnection) {
var name: String = repr(lc);
lc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, function(event: AsyncErrorEvent) {
trace(name + " received event AsyncErrorEvent.ASYNC_ERROR");
trace(" bubbles: " + repr(event.bubbles));
trace(" cancelable: " + repr(event.cancelable));
trace(" error: " + event.error);
trace(" currentTarget: " + repr(event.currentTarget));
trace(" target: " + repr(event.target));
trace("");
});
lc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, function(event: SecurityErrorEvent) {
trace(name + " received event SecurityErrorEvent.SECURITY_ERROR");
trace(" bubbles: " + repr(event.bubbles));
trace(" cancelable: " + repr(event.cancelable));
trace(" text: " + repr(event.text));
trace(" currentTarget: " + repr(event.currentTarget));
trace(" target: " + repr(event.target));
trace("");
});
lc.addEventListener(StatusEvent.STATUS, function(event: StatusEvent) {
trace(name + " received event StatusEvent.STATUS");
trace(" bubbles: " + repr(event.bubbles));
trace(" cancelable: " + repr(event.cancelable));
trace(" code: " + repr(event.code));
trace(" currentTarget: " + repr(event.currentTarget));
trace(" level: " + repr(event.level));
trace(" target: " + repr(event.target));
trace("");
});
}
private function getObjectId(needle: Object, haystack: Array): String {
for (var i = 0; i < haystack.length; i++) {
if (haystack[i] === needle) {
return i;
}
}
return null;
}
public function repr(value: *, indent: String = " ", seenObjects: Array = null) {
if (seenObjects == null) {
seenObjects = [];
}
if (value === receiver) {
return "receiver";
} else if (value === sender) {
return "sender";
} else if (value === recvObject) {
return "recvObject";
} else if (value === custom) {
return "custom";
}
if (value === undefined || value === null || value === true || value === false || value is Number) {
return String(value);
} else if (value is String) {
return escapeString(value);
} else {
var existingId = getObjectId(value, seenObjects);
if (existingId != null) {
return "*" + existingId;
}
existingId = seenObjects.length;
seenObjects.push(value);
if (value is Array) {
if (value.length == 0) {
return "*" + existingId + " []";
} else {
var result = "*" + existingId + " [\n";
var nextIndent = indent + " ";
for (var i = 0; i < value.length; i++) {
result += nextIndent + repr(value[i], nextIndent, seenObjects) + "\n";
}
return result + indent + "]";
}
} else {
var keys = [];
for (var key in value) {
keys.push(key);
}
keys.sort();
var result = "*" + existingId + " " + getQualifiedClassName(value) + " {";
if (keys.length == 0) {
return result + "}";
} else {
result += "\n";
var nextIndent = indent + " ";
for (var i = 0; i < keys.length; i++) {
result += nextIndent + keys[i] + " = " + repr(value[keys[i]], nextIndent, seenObjects) + "\n";
}
return result + indent + "}";
}
}
}
}
public function escapeString(input: String): String {
var output:String = "\"";
for (var i:int = 0; i < input.length; i++) {
var char:String = input.charAt(i);
switch (char) {
case "\\":
output += "\\\\";
break;
case "\"":
output += "\\\"";
break;
case "\n":
output += "\\n";
break;
case "\r":
output += "\\r";
break;
case "\t":
output += "\\t";
break;
default:
output += char;
}
}
return output + "\"";
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,111 @@
package {
import flash.display.MovieClip;
import flash.net.LocalConnection;
import flash.utils.getQualifiedClassName;
public class Child extends MovieClip {
var lc: LocalConnection = new LocalConnection();
public function Child() {
lc.connect("avm2_child");
lc.client = {};
lc.client.test = function() {
trace("avm2_child.test was called with " + arguments.length + " argument" + (arguments.length == 0 ? "" : "s"));
if (arguments.length > 0) {
trace(" " + repr(arguments));
}
}
}
private function getObjectId(needle: Object, haystack: Array): String {
for (var i = 0; i < haystack.length; i++) {
if (haystack[i] === needle) {
return i;
}
}
return null;
}
public function repr(value: *, indent: String = " ", seenObjects: Array = null) {
if (seenObjects == null) {
seenObjects = [];
}
if (value === lc) {
return "lc";
}
if (value === undefined || value === null || value === true || value === false || value is Number) {
return String(value);
} else if (value is String) {
return escapeString(value);
} else {
var existingId = getObjectId(value, seenObjects);
if (existingId != null) {
return "*" + existingId;
}
existingId = seenObjects.length;
seenObjects.push(value);
if (value is Array) {
if (value.length == 0) {
return "*" + existingId + " []";
} else {
var result = "*" + existingId + " [\n";
var nextIndent = indent + " ";
for (var i = 0; i < value.length; i++) {
result += nextIndent + repr(value[i], nextIndent, seenObjects) + "\n";
}
return result + indent + "]";
}
} else {
var keys = [];
for (var key in value) {
keys.push(key);
}
keys.sort();
var result = "*" + existingId + " " + getQualifiedClassName(value) + " {";
if (keys.length == 0) {
return result + "}";
} else {
result += "\n";
var nextIndent = indent + " ";
for (var i = 0; i < keys.length; i++) {
result += nextIndent + keys[i] + " = " + repr(value[keys[i]], nextIndent, seenObjects) + "\n";
}
return result + indent + "}";
}
}
}
}
public function escapeString(input: String): String {
var output:String = "\"";
for (var i:int = 0; i < input.length; i++) {
var char:String = input.charAt(i);
switch (char) {
case "\\":
output += "\\\\";
break;
case "\"":
output += "\\\"";
break;
case "\n":
output += "\\n";
break;
case "\r":
output += "\\r";
break;
case "\t":
output += "\\t";
break;
default:
output += char;
}
}
return output + "\"";
}
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,890 @@
LocalConnection.isSupported: true
-- start test: A message to nowhere! --
sender.send("nowhere", "test", *0 [])
-- end frame: A message to nowhere! --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
-- end test: A message to nowhere! --
-- start test: Both receivers try to connect to the same channel --
receiver.connect("channel")
custom.connect("channel"): ! ArgumentError: Error #2082: Connect failed because the object is already connected.
-- end frame: Both receivers try to connect to the same channel --
-- end test: Both receivers try to connect to the same channel --
-- start test: A message to an unimplemented function --
sender.send("channel", "unimplemented", *0 [])
-- end frame: A message to an unimplemented function --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
receiver received event AsyncErrorEvent.ASYNC_ERROR
bubbles: false
cancelable: false
error: ReferenceError: Error #1069: Property unimplemented not found on flash.net.LocalConnection and there is no default value.
currentTarget: receiver
target: receiver
-- end test: A message to an unimplemented function --
-- start test: Receiver tries to connect elsewhere, but can't --
receiver.connect("elsewhere"): ! ArgumentError: Error #2082: Connect failed because the object is already connected.
-- end frame: Receiver tries to connect elsewhere, but can't --
-- end test: Receiver tries to connect elsewhere, but can't --
-- start test: Receiver actually connects elsewhere, and custom is allowed to connect to channel --
receiver.close()
receiver.connect("elsewhere")
custom.connect("channel")
-- end frame: Receiver actually connects elsewhere, and custom is allowed to connect to channel --
-- end test: Receiver actually connects elsewhere, and custom is allowed to connect to channel --
-- start test: Sender calls test() on 'channel' --
sender.send("channel", "test", *0 [])
-- end frame: Sender calls test() on 'channel' --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
custom.test was called with 0 argument
-- end test: Sender calls test() on 'channel' --
-- start test: Client is used --
sender.send("elsewhere", "test", *0 [])
-- end frame: Client is used --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
recvObject.test was called with 0 argument
-- end test: Client is used --
-- start test: Sender calls test() on 'channel'... after the listener is gone --
custom.close()
sender.send("channel", "test", *0 [])
-- end frame: Sender calls test() on 'channel'... after the listener is gone --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
-- end test: Sender calls test() on 'channel'... after the listener is gone --
-- start test: Sender calls test() on 'elsewhere'... immediately before the listener is gone --
sender.send("elsewhere", "test", *0 [])
receiver.close()
-- end frame: Sender calls test() on 'elsewhere'... immediately before the listener is gone --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
-- end test: Sender calls test() on 'elsewhere'... immediately before the listener is gone --
-- start test: Sender calls test() on 'channel'... before the listener connects --
sender.send("channel", "test", *0 [])
custom.connect("channel")
-- end frame: Sender calls test() on 'channel'... before the listener connects --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
-- end test: Sender calls test() on 'channel'... before the listener connects --
-- start test: Sending to a channel that gets reassigned before end-of-frame --
sender.send("channel", "test", *0 [])
custom.close()
receiver.connect("channel")
-- end frame: Sending to a channel that gets reassigned before end-of-frame --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
recvObject.test was called with 0 argument
-- end test: Sending to a channel that gets reassigned before end-of-frame --
-- start test: Channels reconnect and receive --
custom.close(): ! ArgumentError: Error #2083: Close failed because the object is not connected.
receiver.close()
receiver.connect("elsewhere")
sender.send("channel", "test", *0 [])
sender.send("elsewhere", "test", *0 [])
custom.connect("channel")
-- end frame: Channels reconnect and receive --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
recvObject.test was called with 0 argument
-- end test: Channels reconnect and receive --
-- start test: A connected listener can also send --
receiver.send("channel", "test", *0 [])
receiver.send("elsewhere", "test", *0 [])
-- end frame: A connected listener can also send --
receiver received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: receiver
level: "status"
target: receiver
custom.test was called with 0 argument
receiver received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: receiver
level: "status"
target: receiver
recvObject.test was called with 0 argument
-- end test: A connected listener can also send --
-- start test: A listener throws an error --
sender.send("channel", "throwAnError", *0 [])
-- end frame: A listener throws an error --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
custom.throwAnError was called
-- end test: A listener throws an error --
-- start test: Close something's that's already closed --
! test stopped with error: ArgumentError: Error #2083: Close failed because the object is not connected.
-- end frame: Close something's that's already closed --
-- end test: Close something's that's already closed --
-- start test: Send to funky channel names --
sender.send(null, "test", *0 []): ! TypeError: Error #2007: Parameter connectionName must be non-null.
sender.send("0", "test", *0 [])
sender.send("", "test", *0 []): ! ArgumentError: Error #2085: Parameter connectionName must be non-empty string.
sender.send(" ??? ", "test", *0 [])
-- end frame: Send to funky channel names --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
-- end test: Send to funky channel names --
-- start test: Send to funky methods --
sender.send("channel", null, *0 []): ! TypeError: Error #2007: Parameter methodName must be non-null.
sender.send("channel", "0", *0 [])
sender.send("channel", "", *0 []): ! ArgumentError: Error #2085: Parameter methodName must be non-empty string.
sender.send("channel", " ??? ", *0 [])
-- end frame: Send to funky methods --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
custom received event AsyncErrorEvent.ASYNC_ERROR
bubbles: false
cancelable: false
error: ReferenceError: Error #1069: Property 0 not found on CustomLocalConnection and there is no default value.
currentTarget: custom
target: custom
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
custom received event AsyncErrorEvent.ASYNC_ERROR
bubbles: false
cancelable: false
error: ReferenceError: Error #1069: Property ??? not found on CustomLocalConnection and there is no default value.
currentTarget: custom
target: custom
-- end test: Send to funky methods --
-- start test: Connect to funky names --
sender.connect(null): ! TypeError: Error #2007: Parameter connectionName must be non-null.
sender.close(): ! ArgumentError: Error #2083: Close failed because the object is not connected.
sender.connect("0")
sender.close()
sender.connect(""): ! ArgumentError: Error #2085: Parameter connectionName must be non-empty string.
sender.close(): ! ArgumentError: Error #2083: Close failed because the object is not connected.
sender.connect(" ??? ")
sender.close()
-- end frame: Connect to funky names --
-- end test: Connect to funky names --
-- start test: Connect to something with a prefix --
sender.connect("localhost:something"): ! ArgumentError: Error #2004: One of the parameters is invalid.
sender.close(): ! ArgumentError: Error #2083: Close failed because the object is not connected.
-- end frame: Connect to something with a prefix --
-- end test: Connect to something with a prefix --
-- start test: Send to protected methods --
sender.send("channel", "send", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid.
sender.send("channel", "connect", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid.
sender.send("channel", "close", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid.
sender.send("channel", "allowDomain", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid.
sender.send("channel", "allowInsecureDomain", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid.
sender.send("channel", "domain", *0 []): ! ArgumentError: Error #2004: One of the parameters is invalid.
-- end frame: Send to protected methods --
-- end test: Send to protected methods --
-- start test: Arguments are sent --
sender.send("elsewhere", "test", *0 [
1
"two"
*1 Object {
value = 3
}
*2 [
4
5
]
])
-- end frame: Arguments are sent --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
recvObject.test was called with 4 arguments
*0 [
1
"two"
*1 Object {
value = 3
}
*2 [
4
5
]
]
-- end test: Arguments are sent --
-- start test: Explicit host prefix --
sender.send("localhost:channel", "test", *0 [])
sender.send("notlocalhost:elsewhere", "test", *0 [])
-- end frame: Explicit host prefix --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
custom.test was called with 0 argument
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
-- end test: Explicit host prefix --
-- start test: Underscores in names --
custom.close()
custom.connect("_channel")
sender.send("_channel", "test", *0 [])
-- end frame: Underscores in names --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
custom.test was called with 0 argument
-- end test: Underscores in names --
-- start test: Underscores in name doesn't allow a prefix --
sender.send("localhost:channel", "test", *0 [])
sender.send("localhost:_channel", "test", *0 [])
-- end frame: Underscores in name doesn't allow a prefix --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "error"
target: sender
-- end test: Underscores in name doesn't allow a prefix --
-- start test: Case sensitivity --
sender.send("ELSEWhere", "test", *0 [])
sender.send("LOCalHOST:ElseWhere", "test", *0 [])
-- end frame: Case sensitivity --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
recvObject.test was called with 0 argument
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
recvObject.test was called with 0 argument
-- end test: Case sensitivity --
-- start test: Calling an AVM2 movie --
sender.send("avm2_child", "test", *0 [])
-- end frame: Calling an AVM2 movie --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
avm2_child.test was called with 0 argument
-- end test: Calling an AVM2 movie --
-- start test: Calling an AVM1 movie --
sender.send("avm1_child", "test", *0 [])
-- end frame: Calling an AVM1 movie --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
avm1_child.test was called with 0 argument
-- end test: Calling an AVM1 movie --
-- start test: Argument translations: primitives --
sender.send("avm1_child", "test", *0 [
1
1.2
true
false
"string"
null
undefined
])
sender.send("avm2_child", "test", *0 [
1
1.2
true
false
"string"
null
undefined
])
sender.send("_channel", "test", *0 [
1
1.2
true
false
"string"
null
undefined
])
-- end frame: Argument translations: primitives --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
avm1_child.test was called with 7 arguments
*0 [
1
1.2
true
false
"string"
null
undefined
]
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
avm2_child.test was called with 7 arguments
*0 [
1
1.2
true
false
"string"
null
undefined
]
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
custom.test was called with 7 arguments
*0 [
1
1.2
true
false
"string"
null
undefined
]
-- end test: Argument translations: primitives --
-- start test: Argument translations: simple array --
sender.send("avm1_child", "test", *0 [
*1 [
1
2
"three"
4.5
NaN
Infinity
]
])
sender.send("avm2_child", "test", *0 [
*1 [
1
2
"three"
4.5
NaN
Infinity
]
])
sender.send("_channel", "test", *0 [
*1 [
1
2
"three"
4.5
NaN
Infinity
]
])
-- end frame: Argument translations: simple array --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
avm1_child.test was called with 1 arguments
*0 [
*1 [
1
2
"three"
4.5
NaN
Infinity
]
]
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
avm2_child.test was called with 1 arguments
*0 [
*1 [
1
2
"three"
4.5
NaN
Infinity
]
]
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
custom.test was called with 1 arguments
*0 [
*1 [
1
2
"three"
4.5
NaN
Infinity
]
]
-- end test: Argument translations: simple array --
-- start test: Argument translations: simple object --
sender.send("avm1_child", "test", *0 [
*1 Object {
nested = *2 Object {
numbers = *3 [
1
2
]
string = "hello"
}
}
])
sender.send("avm2_child", "test", *0 [
*1 Object {
nested = *2 Object {
numbers = *3 [
1
2
]
string = "hello"
}
}
])
sender.send("_channel", "test", *0 [
*1 Object {
nested = *2 Object {
numbers = *3 [
1
2
]
string = "hello"
}
}
])
-- end frame: Argument translations: simple object --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
avm1_child.test was called with 1 arguments
*0 [
*1 object {
nested = *2 object {
numbers = *3 [
1
2
]
string = "hello"
}
}
]
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
avm2_child.test was called with 1 arguments
*0 [
*1 Object {
nested = *2 Object {
numbers = *3 [
1
2
]
string = "hello"
}
}
]
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
custom.test was called with 1 arguments
*0 [
*1 Object {
nested = *2 Object {
numbers = *3 [
1
2
]
string = "hello"
}
}
]
-- end test: Argument translations: simple object --
-- start test: AVM1 movie throws an error --
sender.send("avm1_child", "throwAnError", *0 [])
-- end frame: AVM1 movie throws an error --
sender received event StatusEvent.STATUS
bubbles: false
cancelable: false
code: null
currentTarget: sender
level: "status"
target: sender
avm1_child.throwAnError was called
-- end test: AVM1 movie throws an error --
Finished after 125 frames

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
num_ticks = 300 # Test may finish in less, but it'll `fscommand:exit` when it's done.