a9a816e0ed
First Rust component of the Python -> Rust migration. Chosen first because
it is the lowest genuinely game-independent layer, it has an executable
oracle, and both existing Rust implementations of it are wrong.
Contents:
* fire2 -- the proven 16-byte frame header, frame/stream splitting
* heat2 -- tag packing, varints, all 11 TDF value types
* message -- frame + decoded body, routed by NUMERIC component/command
* diagnostics -- dumps for capture review
No FIFA 17 command tables, response schemas or notification IDs: this layer
knows 0x0009/0x0007 is component 9, command 7, not that it means
Util::preAuth. That mapping belongs to a game adapter, which is what lets a
future FIFA 18/23 adapter reuse this.
Parity is tested, not asserted. fixtures/generate.py drives the proven
Python responders (heat2.py, blaze_responder_v3b.py) and freezes 56 vectors
-- 31 of them real payloads from the responder's own builders, including
the 11.8 KB preAuth reply. tests/oracle_parity.rs replays every one
byte-for-byte. 54 tests green; clippy clean.
Supersedes two wrong framings, neither of which is removed yet:
* fifa-blaze/crates/blaze-proto/frame.rs -- a 12-byte header with a u16
length, nibble-packed type/options, an error field and a JUMBO flag.
A documented guess at FIFA 23 predating the FIFA 17 recon.
* heat2.py::build_fire2_frame -- packs >IHHHHB3s, msgId at [10:12] and
msgType at [12]. Dead code, but its docstring still states that layout.
Confidence is carried in the types: TypeId::is_verified() reports which
layouts are capture-backed (int/string/blob/struct) and which the oracle
marks UNVERIFIED (list/map/union/varlist/objtype/objid/float), with a test
asserting the unverified ones stay flagged.
Cargo.lock is deliberately NOT included: it re-resolves ~240 lines against
the current registry even without this crate, so that churn is pre-existing
and does not belong in a foundation commit.
The Python backend remains the live runtime and is untouched. Nothing
consumes this crate yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.5 KiB
Rust
69 lines
2.5 KiB
Rust
//! # openfut-protocol-blaze
|
|
//!
|
|
//! Game-independent EA Blaze wire protocol: Fire2 framing and the Heat2/TDF
|
|
//! codec.
|
|
//!
|
|
//! ## What belongs here
|
|
//!
|
|
//! Only things that are true of Blaze itself:
|
|
//!
|
|
//! * [`fire2`] — the 16-byte frame header and frame/stream splitting
|
|
//! * [`heat2`] — tag packing, varints, and the eleven TDF value types
|
|
//! * [`message`] — a frame plus its decoded body, routed by numeric
|
|
//! component/command
|
|
//! * [`diagnostics`] — dumps for capture review
|
|
//!
|
|
//! ## What does not belong here
|
|
//!
|
|
//! Anything that would have to change for a different title: command and
|
|
//! component name tables, notification IDs, response schemas, login sequencing,
|
|
//! session identity. FIFA 17 says `0x0009/0x0007` is `Util::preAuth`; this
|
|
//! crate only knows it is component 9, command 7. A game adapter owns the rest.
|
|
//!
|
|
//! The test for a change landing in the right place: *could FIFA 18 or FIFA 23
|
|
//! use this without importing FIFA 17's command tables?* If not, it belongs in
|
|
//! an adapter.
|
|
//!
|
|
//! ## Provenance and confidence
|
|
//!
|
|
//! Ported from the Python implementation in `fifa17-recon/tools/` that drove a
|
|
//! retail FIFA 17 client from Origin login to an opened FUT pack — the
|
|
//! project's behavioural oracle. Parity is not asserted, it is tested: the
|
|
//! vectors in `fixtures/` are generated from that Python code and replayed
|
|
//! byte-for-byte by `tests/oracle_parity.rs`.
|
|
//!
|
|
//! Confidence is not uniform, and the code says so rather than leaving it in a
|
|
//! comment. Int, string, blob and struct layouts are proven against captured
|
|
//! traffic; list, map, union, varlist, objtype, objid and float are not, and
|
|
//! [`heat2::TypeId::is_verified`] reports which is which.
|
|
//!
|
|
//! ```
|
|
//! use openfut_protocol_blaze::fire2::{Frame, MsgType};
|
|
//! use openfut_protocol_blaze::heat2::{self, Struct, Value};
|
|
//!
|
|
//! let body = Struct::new()
|
|
//! .with("PID", Value::Int(33068179))
|
|
//! .with("NAME", Value::String("CAGE".into()));
|
|
//!
|
|
//! let frame = Frame::new(0x0001, 0x000A, 7, MsgType::Reply, heat2::encode(&body));
|
|
//! let wire = frame.encode();
|
|
//!
|
|
//! let (parsed, used) = Frame::parse(&wire).unwrap();
|
|
//! assert_eq!(used, wire.len());
|
|
//! assert_eq!(parsed.header.component, 0x0001);
|
|
//! assert_eq!(wire[13], 0x20); // msgType REPLY in the top 3 bits, userIndex 0
|
|
//! ```
|
|
|
|
#![forbid(unsafe_code)]
|
|
|
|
pub mod diagnostics;
|
|
pub mod error;
|
|
pub mod fire2;
|
|
pub mod heat2;
|
|
pub mod message;
|
|
|
|
pub use error::{Error, Result};
|
|
pub use fire2::{Frame, Header, MsgType};
|
|
pub use heat2::{Struct, Tag, TypeId, Value};
|
|
pub use message::Message;
|