//! A Blaze message: a Fire2 frame whose payload is decoded Heat2/TDF. //! //! This is the highest level this crate goes. It routes by *numbers*, never by //! names: mapping `0x0009/0x0007` to `Util::preAuth`, or knowing which fields a //! login reply must carry, is a per-title concern that belongs in a game //! adapter. Keeping that out is what lets a second title reuse this layer //! without inheriting FIFA 17's command tables. use crate::error::Result; use crate::fire2::{Frame, Header, MsgType}; use crate::heat2::{self, Struct}; /// A frame plus its decoded body. #[derive(Debug, Clone, PartialEq)] pub struct Message { pub header: Header, pub body: Struct, } impl Message { pub fn new( component: u16, command: u16, msg_num: u32, msg_type: MsgType, body: Struct, ) -> Message { Message { header: Header::new(component, command, msg_num, msg_type), body, } } /// Decode a frame's payload as TDF. pub fn from_frame(frame: &Frame) -> Result { Ok(Message { header: frame.header, body: heat2::decode(&frame.payload)?, }) } /// Re-frame this message. /// /// Note this is not guaranteed byte-identical to the frame a `Message` was /// decoded from: encoding sorts members by packed tag, so a peer that sent /// them out of order would see its ordering normalised. For byte-exact /// round trips (capture replay, differential tests) keep the [`Frame`] and /// its raw payload. pub fn to_frame(&self) -> Frame { let payload = heat2::encode(&self.body); let mut frame = Frame::new( self.header.component, self.header.command, self.header.msg_num, self.header.msg_type, payload, ); frame.header.user_index = self.header.user_index; frame.header.options = self.header.options; frame } pub fn encode(&self) -> Vec { self.to_frame().encode() } /// Component and command as a pair — the key an adapter dispatches on. pub fn route(&self) -> (u16, u16) { (self.header.component, self.header.command) } } #[cfg(test)] mod tests { use super::*; use crate::heat2::Value; #[test] fn decodes_a_frame_body_and_reframes_it() { let body = Struct::new() .with("PID", Value::Int(33068179)) .with("NAME", Value::String("CAGE".into())); let msg = Message::new(0x0001, 0x000A, 7, MsgType::Reply, body.clone()); let bytes = msg.encode(); let (frame, used) = Frame::parse(&bytes).unwrap(); assert_eq!(used, bytes.len()); let back = Message::from_frame(&frame).unwrap(); assert_eq!(back.route(), (0x0001, 0x000A)); assert_eq!(back.header.msg_type, MsgType::Reply); assert_eq!(back.body.get("NAME").and_then(Value::as_str), Some("CAGE")); assert_eq!(back.encode(), bytes); } #[test] fn an_empty_body_is_a_valid_message() { let msg = Message::new(0x0009, 0x0002, 1, MsgType::Reply, Struct::new()); let (frame, _) = Frame::parse(&msg.encode()).unwrap(); assert_eq!(frame.header.payload_len, 0); assert!(Message::from_frame(&frame).unwrap().body.is_empty()); } }