#24697·ruffle

AVM2 NetConnection ignores `objectEncoding` (AMF3 Remoting)

Author: kaidegitCreated Sep 15, 2026Updated Sep 15, 2026
LabelsbugunimplementedA-avm2amf

Describe the bug

AVM2 NetConnection ignores objectEncoding: Flash Remoting always sends AMF0 even when the SWF sets objectEncoding = AMF3, breaking AMF3 remoting servers (e.g. PyAMF)

A Flash 10 (AVM2) game I'm archiving communicates entirely over Flash Remoting. Its bootstrap SWF explicitly sets AMF3 before connecting (decompiled com/kingdowin/TDSheep/rpc/RemotingConnect.as):

actionscript
this.netConnect = new NetConnection();
this.netConnect.objectEncoding = ObjectEncoding.AMF3;
this.netConnect.connect(_rpcURL);

In Ruffle, every remoting call works for scalar parameters, but any call that passes an AS3 Array argument fails on the server. Concretely, dispatching a worker to the mine sends camp.create_mine with slave_ids = ["<16-hex-user-id>"], and the server answers return_msg_config_35^该苦工不存在 ("this slave does not exist"). The exact same flow works in real Flash Player on Windows.

Root cause (confirmed at byte level):

  1. Ruffle hardcodes AMF0 for remoting regardless of objectEncoding:
    • core/src/net_connection.rsflush_queue() builds Packet { version: AMFVersion::AMF0, .. }
    • core/src/avm2/globals/flash/net/net_connection.rscall() serializes arguments with AMFVersion::AMF0
  2. Real Flash Player with objectEncoding = AMF3 sends an envelope with version 0x0003 where the message body is still AMF0-framed — a strict-array header (0x0a + u32 arg count) whose arguments are each written as an AMF0 0x11 ("this value is AMF3") marker wrapping the AMF3-encoded value. Ruffle instead sends envelope 0x0000 with fully-AMF0 bodies.

Why the server breaks: this game's backend maybe Python/PyAMF. PyAMF decodes an AMF3 array as a Python list, but an AMF0 ECMAArray as a MixedArray dict keyed "0", "1", ... — so the server iterates slave_ids and gets the keys ("0") instead of the ids, and the business layer reports the slave as non-existent.

Byte-level A/B proof (same session, same gateway key, same parameter):

request form server-side slave_ids result
AMF0 envelope, ECMAArray {0: "a9164f2d9a3ffce3"} (what Ruffle sends today) {0: 'a9164f2d9a3ffce3'} (dict) return_msg_config_35^该苦工不存在
0x0003 envelope, 0a 00 00 00 01 11 + AMF3 (what Flash sends) ['a9164f2d9a3ffce3'] (list) return_code 0, mine starts

Steps to reproduce: load the game, log in, enter your village, click the mine (矿山), select a worker, pick a shift and press 工作. The gate camp.create_mine always fails with the error popup while real Flash accepts it.

Expected behavior

When a SWF sets NetConnection.objectEncoding = AMF3 (which is also Flash's AS3 default via NetConnection.defaultObjectEncoding = 3), Ruffle should send Flash Remoting requests the same way Flash Player does:

  • envelope version 0x0003;
  • message body = AMF0 strict-array frame (0x0a + u32 argument count) with each argument written as an AMF0 0x11 marker wrapping the AMF3-encoded value;
  • responses (0x11-wrapped AMF3) decoded back into AVM2 values.

With this, camp.create_mine succeeds against the real server (verified end-to-end: dispatching a worker returns return_code 0 and the mine state updates, where unpatched Ruffle always returned the "slave does not exist" error).

Content Location

https://tdsheep.tdsheepvillage.com/.

The failing remoting call is camp.create_mine (params include an Array slave_ids), sent to https://tdsheep.tdsheepvillage.com/gateway/?sessionid=....

A minimal, self-hostable reproduction of the same defect is described in #22965 (CityVille archive + Py3AMF: Flash receives [{}], Ruffle sends {0: {}}).

Affected platform

Browser's extension

Operating system

macOS 15

Browser

Chrome

Additional information

Proposed fix (implemented and verified locally, ruffle side only):

  1. core/src/net_connection.rs: store the encoding on FlashRemoting (connect_to_flash_remoting(..., object_encoding: AMFVersion)) and use it for Packet::version in flush_queue() (was hardcoded AMFVersion::AMF0).
  2. core/src/avm2/globals/flash/net/net_connection.rs:
    • connect(): read the AS3 property and pass it down — let enc = this.get_public_property("objectEncoding", activation)?.coerce_to_u32(activation)?;
    • call() / add_header(): serialize values with the connection's encoding (currently hardcoded AMFVersion::AMF0). This part is essential: serializing AMF0-style while the packet claims AMF3 produces a truncated body — e.g. the rpc object collapses to an empty class="Object" (observed: ... 0a 03 0d "Object") and PyAMF answers 400.
    • AVM1 connect() keeps AMF0 (AS2-era default).
  3. flash_lso (packet/write.rs): for AMFVersion::AMF3, write the message body exactly like Flash/PyAMF do:
rust
match value.as_ref() {
    Value::StrictArray(_, children) => {
        out.push(0x0a);                                  // AMF0 strict-array frame
        out.extend((children.len() as u32).to_be_bytes());
        let encoder = AMF3Encoder::default();
        for child in children {
            out.push(0x11);                              // AMF0 "value is AMF3" marker
            encoder.write_value_element(out, child)?;
        }
    }
    _ => amf0::write::write_value(out, value)?,
}

Read side needs no change: flash_lso's AMF0Decoder already parses 0x11 into Value::AMF3, which ruffle already unwraps (core/src/avm2/amf.rs).

Verification: all 122 ruffle_core lib tests pass; flash-lso tests pass plus a round-trip test whose output bytes I decoded successfully with PyAMF; end-to-end against the real game server the previously-failing UI flow now returns return_code 0 (mine.power 118, done_time set), while unpatched Ruffle fails 100% of the time.

A note about rust-flash-lso: item 3 is the one piece that lives in the flash-lso repository. My analysis and patch were authored with LLM assistance, and that repository does not accept LLM-generated contributions, so I'd rather not file the PR there myself. The change is small (~40 lines, local patch against rev 61b7172), so I'd appreciate it if a maintainer could make it (Open PR #125 fixes value-level reference/property emission in the AMF3 writer; this request is orthogonal — it's about remoting packet framing in packet/write.rs.)

Related reports/PRs: #22965 is very likely the same root cause (PyAMF-style server receives {0: {...}} instead of a list). #16381 covered the AVM1 side and was fixed by #23959 (AVM1-only); the broader AVM2 attempts #23772/#23775 were closed without merging. The objectEncoding gap described here appears to be unreported so far.