AVM2 NetConnection ignores `objectEncoding` (AMF3 Remoting)
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):
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):
- Ruffle hardcodes AMF0 for remoting regardless of
objectEncoding:core/src/net_connection.rs→flush_queue()buildsPacket { version: AMFVersion::AMF0, .. }core/src/avm2/globals/flash/net/net_connection.rs→call()serializes arguments withAMFVersion::AMF0
- Real Flash Player with
objectEncoding = AMF3sends an envelope with version0x0003where the message body is still AMF0-framed — a strict-array header (0x0a+ u32 arg count) whose arguments are each written as an AMF00x11("this value is AMF3") marker wrapping the AMF3-encoded value. Ruffle instead sends envelope0x0000with 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 AMF00x11marker 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):
core/src/net_connection.rs: store the encoding onFlashRemoting(connect_to_flash_remoting(..., object_encoding: AMFVersion)) and use it forPacket::versioninflush_queue()(was hardcodedAMFVersion::AMF0).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 hardcodedAMFVersion::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 emptyclass="Object"(observed:... 0a 03 0d "Object") and PyAMF answers 400.- AVM1
connect()keeps AMF0 (AS2-era default).
- flash_lso (
packet/write.rs): forAMFVersion::AMF3, write the message body exactly like Flash/PyAMF do:
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.
Source: ruffle-rs/ruffle