//! The Heat2 value model. //! //! Deliberately a *generic* Blaze value tree: it knows the eleven wire types //! and nothing about any game. No FIFA 17 command IDs, field names, or response //! schemas appear here or anywhere else in this crate — those belong to a game //! adapter one layer up. use super::tag::Tag; /// The eleven Heat2 type bytes. /// /// `Int` through `Struct` are validated against captured FIFA 17 traffic. The /// rest are marked UNVERIFIED in the oracle (`heat2.py`): they are absent from /// every capture we hold, and their layouts are consistent-with rather than /// proven-against EA. `TypeId::is_verified` carries that distinction into code /// so it cannot be lost by a reader who skips the comments. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(u8)] pub enum TypeId { Int = 0x00, String = 0x01, Blob = 0x02, Struct = 0x03, List = 0x04, Map = 0x05, Union = 0x06, VarList = 0x07, ObjType = 0x08, ObjId = 0x09, Float = 0x0A, } /// Union discriminator meaning "no member set". UNVERIFIED. pub const UNION_UNSET: u8 = 0x7F; impl TypeId { pub fn from_byte(b: u8) -> Option { Some(match b { 0x00 => TypeId::Int, 0x01 => TypeId::String, 0x02 => TypeId::Blob, 0x03 => TypeId::Struct, 0x04 => TypeId::List, 0x05 => TypeId::Map, 0x06 => TypeId::Union, 0x07 => TypeId::VarList, 0x08 => TypeId::ObjType, 0x09 => TypeId::ObjId, 0x0A => TypeId::Float, _ => return None, }) } pub fn as_byte(self) -> u8 { self as u8 } pub fn name(self) -> &'static str { match self { TypeId::Int => "int", TypeId::String => "string", TypeId::Blob => "blob", TypeId::Struct => "struct", TypeId::List => "list", TypeId::Map => "map", TypeId::Union => "union", TypeId::VarList => "varlist", TypeId::ObjType => "objtype", TypeId::ObjId => "objid", TypeId::Float => "float", } } pub fn from_name(name: &str) -> Option { Some(match name { "int" => TypeId::Int, "string" => TypeId::String, "blob" => TypeId::Blob, "struct" => TypeId::Struct, "list" => TypeId::List, "map" => TypeId::Map, "union" => TypeId::Union, "varlist" => TypeId::VarList, "objtype" => TypeId::ObjType, "objid" => TypeId::ObjId, "float" => TypeId::Float, _ => return None, }) } /// True when the layout is proven against captured FIFA 17 traffic. /// /// A `false` here is a standing warning: the codec will round-trip such a /// value against itself and against the Python oracle, and that still says /// nothing about what a real Blaze server emits. pub fn is_verified(self) -> bool { matches!( self, TypeId::Int | TypeId::String | TypeId::Blob | TypeId::Struct ) } } /// A decoded Heat2 value. #[derive(Debug, Clone, PartialEq)] pub enum Value { Int(i64), String(String), Blob(Vec), Struct(Struct), List { elem: TypeId, items: Vec, }, Map { key: TypeId, val: TypeId, entries: Vec<(Value, Value)>, }, Union { key: u8, member: Option>, }, VarList(Vec), ObjType { component: i64, ty: i64, }, ObjId { component: i64, ty: i64, id: i64, }, Float(f32), } impl Value { pub fn type_id(&self) -> TypeId { match self { Value::Int(_) => TypeId::Int, Value::String(_) => TypeId::String, Value::Blob(_) => TypeId::Blob, Value::Struct(_) => TypeId::Struct, Value::List { .. } => TypeId::List, Value::Map { .. } => TypeId::Map, Value::Union { .. } => TypeId::Union, Value::VarList(_) => TypeId::VarList, Value::ObjType { .. } => TypeId::ObjType, Value::ObjId { .. } => TypeId::ObjId, Value::Float(_) => TypeId::Float, } } pub fn as_int(&self) -> Option { match self { Value::Int(v) => Some(*v), _ => None, } } pub fn as_str(&self) -> Option<&str> { match self { Value::String(s) => Some(s), _ => None, } } pub fn as_struct(&self) -> Option<&Struct> { match self { Value::Struct(s) => Some(s), _ => None, } } } /// An ordered set of tagged members. /// /// A `Vec`, not a map: the wire format is a sequence, duplicate tags are /// physically representable, and encoding has to sort by packed tag anyway. /// Keeping the sequence means a decoded frame can be re-encoded byte-for-byte /// even when it contains something a map would silently drop. #[derive(Debug, Clone, Default, PartialEq)] pub struct Struct { pub fields: Vec<(Tag, Value)>, } impl Struct { pub fn new() -> Struct { Struct { fields: Vec::new() } } pub fn with(mut self, tag: &str, value: Value) -> Struct { self.fields.push((Tag::from_label(tag), value)); self } pub fn push(&mut self, tag: &str, value: Value) { self.fields.push((Tag::from_label(tag), value)); } /// First member with this tag, if any. pub fn get(&self, tag: &str) -> Option<&Value> { let t = Tag::from_label(tag); self.fields.iter().find(|(k, _)| *k == t).map(|(_, v)| v) } pub fn len(&self) -> usize { self.fields.len() } pub fn is_empty(&self) -> bool { self.fields.is_empty() } pub fn iter(&self) -> impl Iterator { self.fields.iter() } } impl FromIterator<(Tag, Value)> for Struct { fn from_iter>(iter: I) -> Struct { Struct { fields: iter.into_iter().collect(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn type_bytes_round_trip() { for b in 0x00u8..=0x0A { let t = TypeId::from_byte(b).expect("known type"); assert_eq!(t.as_byte(), b); assert_eq!(TypeId::from_name(t.name()), Some(t)); } assert_eq!(TypeId::from_byte(0x0B), None); } #[test] fn only_capture_backed_types_claim_verification() { assert!(TypeId::Int.is_verified()); assert!(TypeId::Struct.is_verified()); assert!(!TypeId::List.is_verified()); assert!(!TypeId::Float.is_verified()); } #[test] fn struct_lookup_finds_members() { let s = Struct::new() .with("PID", Value::Int(33068179)) .with("NAME", Value::String("x".into())); assert_eq!(s.get("PID").and_then(Value::as_int), Some(33068179)); assert_eq!(s.get("NOPE"), None); } }