Repository: ruffle-rs/ruffle (language: rust)

Issue:
ExternalInterface.call returns undefined when sending an object.
### Describe the bug

If you call ExternalInterface.call and pass an object like below:

```
ExternalInterface.call("emit",{
  "id":"sw",
  "f":"s",
  "d": "data"
});
```

It returns undefined instead of the object itself.

### Expected behavior

It should return { id: 'sw', f: 's', d: 'data' } but it returns undefined.

### Content Location

I have no idea about this, its just something I had in my flash file as attached.[game.swf.zip](https://github.com/user-attachments/files/17689053/game.swf.zip)


### Affected platform

Self-hosted version

### Operating system

OSX

### Browser

Chrome Version 130.0.6723.117 (Official Build) (arm64)

### Additional information

_No response_


Discussion / hints:
What's the source code to the `emit` js callback? Is it just returning the first parameter? 
https://github.com/ruffle-rs/ruffle/blob/5954c1b0f99616e551ff7e3c78808d3168b611a0/core/src/external.rs#L214

I suppose this should be the reason?
That certainly looks relevant! :D
> What's the source code to the `emit` js callback? Is it just returning the first parameter?

It calls my server and passes the parameters for me to then use in my website. For example id - sw is for me to know the game is swapples (sw). I have a workaround that I just pass everything as a string and parse the string in javascript, but the issue comes when there are certain things I want to pass as a map (key value pairs) from the flash game, but I cant do that because the string parsing would be crazy.

Fix patch:
diff --git a/core/src/avm2/globals/flash/external/external_interface.rs b/core/src/avm2/globals/flash/external/external_interface.rs
index dddb4f6091f44..34a3bb7deb247 100644
--- a/core/src/avm2/globals/flash/external/external_interface.rs
+++ b/core/src/avm2/globals/flash/external/external_interface.rs
@@ -15,8 +15,8 @@ pub fn call<'gc>(
     let external_args = args
         .iter()
         .skip(1)
-        .map(|arg| ExternalValue::from_avm2(arg.to_owned()))
-        .collect::<Vec<ExternalValue>>();
+        .map(|arg| ExternalValue::from_avm2(activation, arg.to_owned()))
+        .collect::<Result<Vec<ExternalValue>, Error>>()?;
 
     Ok(
         ExternalInterface::call_method(activation.context, &name.to_utf8_lossy(), &external_args)
diff --git a/core/src/external.rs b/core/src/external.rs
index f63afe98df52f..9a4c417b98d35 100644
--- a/core/src/external.rs
+++ b/core/src/external.rs
@@ -6,6 +6,7 @@ use crate::avm1::{
     ScriptObject as Avm1ScriptObject,
 };
 use crate::avm2::activation::Activation as Avm2Activation;
+use crate::avm2::error::Error as Avm2Error;
 use crate::avm2::object::TObject as _;
 use crate::avm2::Value as Avm2Value;
 use crate::avm2::{ArrayObject as Avm2ArrayObject, Object as Avm2Object};
@@ -191,31 +192,54 @@ impl Value {
         }
     }
 
-    pub fn from_avm2(value: Avm2Value) -> Value {
-        match value {
+    pub fn from_avm2<'gc>(
+        activation: &mut Avm2Activation<'_, 'gc>,
+        value: Avm2Value<'gc>,
+    ) -> Result<Value, Avm2Error<'gc>> {
+        Ok(match value {
             Avm2Value::Undefined => Value::Undefined,
             Avm2Value::Null => Value::Null,
             Avm2Value::Bool(value) => value.into(),
             Avm2Value::Number(value) => value.into(),
             Avm2Value::Integer(value) => value.into(),
             Avm2Value::String(value) => Value::String(value.to_string()),
-            Avm2Value::Object(object) => {
-                if let Some(array) = object.as_array_storage() {
+            Avm2Value::Object(obj) => {
+                if let Some(array) = obj.as_array_storage() {
                     let length = array.length();
                     let values = (0..length)
                         .map(|i| {
                             // FIXME - is this right?
                             let element = array.get(i).unwrap_or(Avm2Value::Null);
-                            Value::from_avm2(element)
+                            Value::from_avm2(activation, element)
                         })
-                        .collect();
+                        .collect::<Result<Vec<Value>, Avm2Error>>()?;
                     Value::List(values)
+                } else if matches!(obj, Avm2Object::ScriptObject(_)) {
+                    let mut values = BTreeMap::new();
+
+                    let mut last_index = obj.get_next_enumerant(0, activation)?;
+                    while let Some(index) = last_index {
+                        if index == 0 {
+                            break;
+                        }
+
+                        let name = obj
+                            .get_enumerant_name(index, activation)?
+                            .coerce_to_string(activation)?;
+                        let value = obj.get_enumerant_value(index, activation)?;
+
+                        values.insert(name.to_string(), Value::from_avm2(activation, value)?);
+
+                        last_index = obj.get_next_enumerant(index, activation)?;
+                    }
+
+                    Value::Object(values)
                 } else {
                     tracing::warn!("from_avm2 needs to be implemented for Avm2Value::Object");
                     Value::Null
                 }
             }
-        }
+        })
     }
 
     pub fn into_avm2<'gc>(self, activation: &mut Avm2Activation<'_, 'gc>) -> Avm2Value<'gc> {
@@ -306,8 +330,11 @@ impl<'gc> Callback<'gc> {
                     .into_iter()
                     .map(|v| v.into_avm2(&mut activation))
                     .collect();
-                match method.call(Avm2Value::Null, &args, &mut activation) {
-                    Ok(result) => Value::from_avm2(result),
+                match method
+                    .call(Avm2Value::Null, &args, &mut activation)
+                    .and_then(|value| Value::from_avm2(&mut activation, value))
+                {
+                    Ok(result) => result,
                     Err(e) => {
                         tracing::error!(
                             "Unhandled error in External Interface callback {name}: {:?}",


Test patch:
diff --git a/tests/tests/external_interface/tests.rs b/tests/tests/external_interface/tests.rs
index 0a691f4a689d5..a40c1261c838b 100644
--- a/tests/tests/external_interface/tests.rs
+++ b/tests/tests/external_interface/tests.rs
@@ -115,6 +115,13 @@ pub fn external_interface_avm2(
                 "After calling `parrot` with a string: {parroted:?}",
             ));
 
+            let map = BTreeMap::from([("a".into(), 100.into()), ("b".into(), "string".into())]);
+            let parroted =
+                player_locked.call_internal_interface("parrot", vec![ExternalValue::Object(map)]);
+            player_locked.log_backend().avm_trace(&format!(
+                "After calling `parrot` with an object: {parroted:?}",
+            ));
+
             player_locked.call_internal_interface("freestanding", vec!["Hello World!".into()]);
 
             let root: ExternalValue = vec![
diff --git a/tests/tests/swfs/avm2/external_interface/output.txt b/tests/tests/swfs/avm2/external_interface/output.txt
index 5974abfc55f5e..f2c2ef6e20f07 100644
--- a/tests/tests/swfs/avm2/external_interface/output.txt
+++ b/tests/tests/swfs/avm2/external_interface/output.txt
@@ -54,6 +54,21 @@ Hello World!
 
 /// parrot() end
 After calling `parrot` with a string: String("Hello World!")
+/// parrot() start
+// this
+[object Target]
+
+// this == thisTarget
+true
+
+// arguments.length
+1
+
+// arguments[0]
+[object Object]
+
+/// parrot() end
+After calling `parrot` with an object: Object({"a": Number(100.0), "b": String("string")})
 /// freestanding() start
 // this
 [object global]