From 1bf68e78e4260c31693670858a536818db4cf103 Mon Sep 17 00:00:00 2001 From: Lol3rrr Date: Fri, 20 Sep 2024 23:46:38 +0200 Subject: [PATCH] Start with entity support --- Cargo.lock | 24 + Cargo.toml | 4 + src/huf.b | Bin 0 -> 262142 bytes src/parser.rs | 361 ++++++- src/parser/decoder.rs | 370 +++++++ src/parser/decoder/quantizedfloat.rs | 208 ++++ src/parser/fieldpath.rs | 411 ++++++++ src/parser/sendtables.rs | 1449 ++++++++++++++++++++++++++ src/parser/variant.rs | 26 + testfiles/ancient_sendtables.b | Bin 0 -> 99653 bytes tests/parse.rs | 4 +- 11 files changed, 2849 insertions(+), 8 deletions(-) create mode 100644 src/huf.b create mode 100644 src/parser/decoder.rs create mode 100644 src/parser/decoder/quantizedfloat.rs create mode 100644 src/parser/fieldpath.rs create mode 100644 src/parser/sendtables.rs create mode 100644 src/parser/variant.rs create mode 100644 testfiles/ancient_sendtables.b diff --git a/Cargo.lock b/Cargo.lock index 4bc1bbe..6741c65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,12 +47,20 @@ version = "0.1.0" dependencies = [ "bitter", "phf", + "pretty_assertions", "prost", "prost-build", "prost-types", + "regex", "snap", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "either" version = "1.13.0" @@ -206,6 +214,16 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.2.22" @@ -467,3 +485,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/Cargo.toml b/Cargo.toml index 6c60385..b874784 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,10 @@ snap = "1.1.1" bitter = "0.7" phf = { version = "0.11", features = ["macros"] } +regex = "1.10.6" + +[dev-dependencies] +pretty_assertions = { version = "1.4" } [build-dependencies] prost-build = { version = "0.13.2" } diff --git a/src/huf.b b/src/huf.b new file mode 100644 index 0000000000000000000000000000000000000000..52322f7271cfb1d7122051d002dc33cc359c5dbc GIT binary patch literal 262142 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 literal 0 HcmV?d00001 diff --git a/src/parser.rs b/src/parser.rs index 89d9a4e..f19723e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,5 +1,12 @@ use crate::{packet::DemoEvent, DemoCommand, Frame, UserId}; +mod fieldpath; +pub use fieldpath::{FieldPath, Paths}; + +mod decoder; +mod sendtables; +mod variant; + #[derive(Debug)] pub enum FirstPassError { DecompressFrame, @@ -35,6 +42,11 @@ pub struct Player { pub color: i32, } +#[derive(Debug)] +pub struct Entity { + pub cls: u32, +} + #[derive(Debug)] pub struct FirstPassOutput { pub header: crate::csgo_proto::CDemoFileHeader, @@ -54,6 +66,13 @@ struct GameEventMapping { >, } +#[derive(Debug)] +pub struct Class { + class_id: i32, + name: String, + serializer: sendtables::Serializer, +} + pub fn parse<'b, FI>(frames: FI) -> Result where FI: IntoIterator>, @@ -66,6 +85,16 @@ where mapping: std::collections::HashMap::new(), }; let mut player_info = std::collections::HashMap::new(); + let mut entities = std::collections::HashMap::new(); + let mut cls_to_class = std::collections::HashMap::::new(); + let mut paths = Paths::new(); + let mut qf_mapper = decoder::QfMapper { + idx: 0, + map: std::collections::HashMap::new(), + }; + let mut serializers = std::collections::HashMap::new(); + + let mut baselines = std::collections::HashMap::new(); for mut frame in frames.into_iter() { frame @@ -83,15 +112,86 @@ where file_info = Some(raw); } DemoCommand::SignonPacket | DemoCommand::Packet => { - parse_packet(data, &mut events, &mut event_mapping, &mut player_info)?; + parse_packet( + data, + &mut events, + &mut event_mapping, + &mut player_info, + &mut entities, + &mut cls_to_class, + &mut paths, + &mut qf_mapper, + &mut baselines, + )?; } DemoCommand::FullPacket => { - parse_fullpacket(data, &mut events, &mut event_mapping, &mut player_info)?; + parse_fullpacket( + data, + &mut events, + &mut event_mapping, + &mut player_info, + &mut entities, + &mut cls_to_class, + &mut paths, + &mut qf_mapper, + &mut baselines, + )?; } // TODO DemoCommand::AnimationData => {} DemoCommand::AnimationHeader => {} - _ => {} + DemoCommand::StringTables => { + let raw: crate::csgo_proto::CDemoStringTables = prost::Message::decode(data)?; + + for table in raw.tables.iter() { + if table.table_name() == "instancebaseline" { + for item in table.items.iter() { + let k = item.str().parse::().unwrap_or(u32::MAX); + baselines.insert(k, item.data().to_vec()); + } + } + } + } + DemoCommand::SendTables => { + let tables: crate::csgo_proto::CDemoSendTables = prost::Message::decode(data)?; + + let mut bitreader = crate::bitreader::Bitreader::new(tables.data()); + + let n_bytes = bitreader.read_varint()?; + let bytes = bitreader.read_n_bytes(n_bytes as usize)?; + + let serializer_msg: crate::csgo_proto::CsvcMsgFlattenedSerializer = + prost::Message::decode(bytes.as_slice())?; + + // std::fs::write("send_table.b", bytes.as_slice()); + + assert!(serializers.is_empty()); + serializers = sendtables::get_serializers(&serializer_msg, &mut qf_mapper)?; + } + DemoCommand::ClassInfo => { + let raw: crate::csgo_proto::CDemoClassInfo = prost::Message::decode(data)?; + + cls_to_class.clear(); + + for class_t in raw.classes { + let cls_id = class_t.class_id(); + let network_name = class_t.network_name(); + + if let Some(ser) = serializers.remove(network_name) { + cls_to_class.insert( + cls_id as u32, + Class { + name: network_name.to_owned(), + class_id: cls_id, + serializer: ser, + }, + ); + } + } + } + other => { + dbg!(other); + } } } @@ -111,15 +211,33 @@ fn parse_fullpacket( events: &mut Vec, event_mapper: &mut GameEventMapping, player_info: &mut std::collections::HashMap, + entities: &mut std::collections::HashMap, + cls_to_class: &mut std::collections::HashMap, + paths: &mut Paths, + qf_mapper: &mut decoder::QfMapper, + baselines: &mut std::collections::HashMap>, ) -> Result<(), FirstPassError> { let raw: crate::csgo_proto::CDemoFullPacket = prost::Message::decode(data)?; // TODO // Handle string table stuff + for item in raw.string_table.iter().flat_map(|st| st.tables.iter()) { + // dbg!(&item.table_name); + } match raw.packet { Some(packet) => { - inner_parse_packet(&packet, events, event_mapper, player_info)?; + inner_parse_packet( + &packet, + events, + event_mapper, + player_info, + entities, + cls_to_class, + paths, + qf_mapper, + baselines, + )?; Ok(()) } @@ -132,10 +250,25 @@ fn parse_packet( events: &mut Vec, event_mapper: &mut GameEventMapping, player_info: &mut std::collections::HashMap, + entities: &mut std::collections::HashMap, + cls_to_class: &mut std::collections::HashMap, + paths: &mut Paths, + qf_mapper: &mut decoder::QfMapper, + baselines: &mut std::collections::HashMap>, ) -> Result<(), FirstPassError> { let raw: crate::csgo_proto::CDemoPacket = prost::Message::decode(data)?; - inner_parse_packet(&raw, events, event_mapper, player_info)?; + inner_parse_packet( + &raw, + events, + event_mapper, + player_info, + entities, + cls_to_class, + paths, + qf_mapper, + baselines, + )?; Ok(()) } @@ -145,6 +278,11 @@ fn inner_parse_packet( events: &mut Vec, event_mapper: &mut GameEventMapping, player_info: &mut std::collections::HashMap, + entities: &mut std::collections::HashMap, + cls_to_class: &mut std::collections::HashMap, + paths: &mut Paths, + qf_mapper: &mut decoder::QfMapper, + baselines: &mut std::collections::HashMap>, ) -> Result<(), FirstPassError> { let mut bitreader = crate::bitreader::Bitreader::new(raw.data()); @@ -153,6 +291,8 @@ fn inner_parse_packet( let size = bitreader.read_varint()?; let msg_bytes = bitreader.read_n_bytes(size as usize)?; + assert_eq!(msg_bytes.len(), size as usize); + let net_msg_type = match crate::netmessagetypes::NetmessageType::try_from(msg_type as i32) { Ok(v) => v, Err(e) => { @@ -196,7 +336,70 @@ fn inner_parse_packet( crate::netmessagetypes::NetmessageType::net_SetConVar => {} crate::netmessagetypes::NetmessageType::svc_ClassInfo => {} crate::netmessagetypes::NetmessageType::svc_VoiceInit => {} - crate::netmessagetypes::NetmessageType::svc_PacketEntities => {} + crate::netmessagetypes::NetmessageType::svc_PacketEntities => { + let raw: crate::csgo_proto::CsvcMsgPacketEntities = + prost::Message::decode(msg_bytes.as_slice())?; + + let mut bitreader = crate::bitreader::Bitreader::new(raw.entity_data()); + let mut entity_id: i32 = -1; + for _ in 0..raw.updated_entries() { + entity_id += 1 + (bitreader.read_u_bit_var()? as i32); + + match bitreader.read_nbits(2)? { + 0b01 | 0b11 => { + entities.remove(&entity_id); + } + 0b10 => { + let (id, entity) = create_entity(entity_id, &mut bitreader, baselines)?; + let cls = entity.cls; + + entities.insert(entity_id, entity); + if let Some(baseline_bytes) = baselines.get(&cls) { + let mut br = crate::bitreader::Bitreader::new(&baseline_bytes); + update_entity( + entity_id, + &mut br, + entities, + cls_to_class, + paths, + qf_mapper, + )?; + } + + + update_entity( + entity_id, + &mut bitreader, + entities, + cls_to_class, + paths, + qf_mapper, + )?; + } + 0b00 => { + if raw.has_pvs_vis_bits() > 0 { + if bitreader.read_nbits(2)? & 0x01 == 1 { + continue; + } + } + + update_entity( + entity_id, + &mut bitreader, + entities, + cls_to_class, + paths, + qf_mapper, + )?; + } + unknown => { + panic!("{:?}", unknown); + } + }; + } + + // dbg!("PacketEntities"); + } crate::netmessagetypes::NetmessageType::svc_UserCmds => {} crate::netmessagetypes::NetmessageType::GE_SosStartSoundEvent => {} crate::netmessagetypes::NetmessageType::GE_SosStopSoundEvent => {} @@ -243,7 +446,11 @@ fn inner_parse_packet( crate::netmessagetypes::NetmessageType::CS_UM_RadioText => {} crate::netmessagetypes::NetmessageType::TE_WorldDecal => {} crate::netmessagetypes::NetmessageType::TE_EffectDispatch => {} - crate::netmessagetypes::NetmessageType::CS_UM_PlayerStatsUpdate => {} + crate::netmessagetypes::NetmessageType::CS_UM_PlayerStatsUpdate => { + let raw: crate::csgo_proto::CcsUsrMsgPlayerStatsUpdate = + prost::Message::decode(msg_bytes.as_slice())?; + // dbg!(&raw); + } crate::netmessagetypes::NetmessageType::CS_UM_EndOfMatchAllPlayersData => { let raw: crate::csgo_proto::CcsUsrMsgEndOfMatchAllPlayersData = prost::Message::decode(msg_bytes.as_slice())?; @@ -274,3 +481,143 @@ fn inner_parse_packet( Ok(()) } + +fn create_entity( + entity_id: i32, + bitreader: &mut crate::bitreader::Bitreader, + baselines: &mut std::collections::HashMap>, +) -> Result<(i32, Entity), FirstPassError> { + let cls_id: u32 = bitreader.read_nbits(8)?; + let _serial = bitreader.read_nbits(17)?; + let _unknown = bitreader.read_varint()?; + + Ok((entity_id, Entity { cls: cls_id })) +} + +fn update_entity( + entity_id: i32, + bitreader: &mut crate::bitreader::Bitreader, + entities: &mut std::collections::HashMap, + cls_to_class: &mut std::collections::HashMap, + paths: &mut Paths, + qf_mapper: &mut decoder::QfMapper, +) -> Result<(), FirstPassError> { + let n_updates = fieldpath::parse_paths(bitreader, paths)?; + let n_updated_values = decode_entity_update( + entity_id, + bitreader, + n_updates, + entities, + cls_to_class, + paths, + qf_mapper, + )?; + if n_updated_values > 0 { + gather_extra_info()?; + } + + Ok(()) +} + +fn gather_extra_info() -> Result<(), FirstPassError> { + // TODO + + Ok(()) +} + +fn decode_entity_update( + entity_id: i32, + bitreader: &mut crate::bitreader::Bitreader, + n_updates: usize, + entities: &mut std::collections::HashMap, + cls_to_class: &mut std::collections::HashMap, + paths: &mut Paths, + qf_mapper: &mut decoder::QfMapper, +) -> Result { + let entity = match entities.get_mut(&entity_id) { + Some(e) => e, + None => panic!("ID: {:?} - Entities: {:?}", entity_id, entities), + }; + let class = match cls_to_class.get_mut(&entity.cls) { + Some(c) => c, + None => panic!(), + }; + + // dbg!(&class.name); + for path in paths.paths().take(n_updates) { + // dbg!(&path); + + let field = path.find(&class.serializer)?; + let field_info = field.get_propinfo(path); + let decoder = field.get_decoder()?; + let result = decoder.decode(bitreader, qf_mapper)?; + + // dbg!(&field, &field_info, &decoder, &result); + + if let Some(fi) = field_info { + // dbg!(&fi); + } + } + + Ok(n_updates) +} + +static HUFFMAN_LOOKUP_TABLE: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + let buf = include_bytes!("huf.b"); + let mut huf2 = Vec::with_capacity((1 << 17) - 1); + for chunk in buf.chunks_exact(2) { + huf2.push((chunk[0], chunk[1])); + } + huf2 +}); + +fn do_op( + symbol: u8, + bitreader: &mut crate::bitreader::Bitreader, + field_path: &mut FieldPath, +) -> Result<(), FirstPassError> { + use fieldpath::ops::*; + + match symbol { + 0 => plus_one(bitreader, field_path), + 1 => plus_two(bitreader, field_path), + 2 => plus_three(bitreader, field_path), + 3 => plus_four(bitreader, field_path), + 4 => plus_n(bitreader, field_path), + 5 => push_one_left_delta_zero_right_zero(bitreader, field_path), + 6 => push_one_left_delta_zero_right_non_zero(bitreader, field_path), + 7 => push_one_left_delta_one_right_zero(bitreader, field_path), + 8 => push_one_left_delta_one_right_non_zero(bitreader, field_path), + 9 => push_one_left_delta_n_right_zero(bitreader, field_path), + 10 => push_one_left_delta_n_right_non_zero(bitreader, field_path), + 11 => push_one_left_delta_n_right_non_zero_pack6_bits(bitreader, field_path), + 12 => push_one_left_delta_n_right_non_zero_pack8_bits(bitreader, field_path), + 13 => push_two_left_delta_zero(bitreader, field_path), + 14 => push_two_pack5_left_delta_zero(bitreader, field_path), + 15 => push_three_left_delta_zero(bitreader, field_path), + 16 => push_three_pack5_left_delta_zero(bitreader, field_path), + 17 => push_two_left_delta_one(bitreader, field_path), + 18 => push_two_pack5_left_delta_one(bitreader, field_path), + 19 => push_three_left_delta_one(bitreader, field_path), + 20 => push_three_pack5_left_delta_one(bitreader, field_path), + 21 => push_two_left_delta_n(bitreader, field_path), + 22 => push_two_pack5_left_delta_n(bitreader, field_path), + 23 => push_three_left_delta_n(bitreader, field_path), + 24 => push_three_pack5_left_delta_n(bitreader, field_path), + 25 => push_n(bitreader, field_path), + 26 => push_n_and_non_topological(bitreader, field_path), + 27 => pop_one_plus_one(bitreader, field_path), + 28 => pop_one_plus_n(bitreader, field_path), + 29 => pop_all_but_one_plus_one(bitreader, field_path), + 30 => pop_all_but_one_plus_n(bitreader, field_path), + 31 => pop_all_but_one_plus_n_pack3_bits(bitreader, field_path), + 32 => pop_all_but_one_plus_n_pack6_bits(bitreader, field_path), + 33 => pop_n_plus_one(bitreader, field_path), + 34 => pop_n_plus_n(bitreader, field_path), + 35 => pop_n_and_non_topographical(bitreader, field_path), + 36 => non_topo_complex(bitreader, field_path), + 37 => non_topo_penultimate_plus_one(bitreader, field_path), + 38 => non_topo_complex_pack4_bits(bitreader, field_path), + other => todo!("Other OP: {:?}", other), + } +} diff --git a/src/parser/decoder.rs b/src/parser/decoder.rs new file mode 100644 index 0000000..37648f0 --- /dev/null +++ b/src/parser/decoder.rs @@ -0,0 +1,370 @@ +mod quantizedfloat; +pub use quantizedfloat::{QfMapper, QuantalizedFloat}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Decoder { + QuantalizedFloatDecoder(u8), + VectorNormalDecoder, + VectorNoscaleDecoder, + VectorFloatCoordDecoder, + Unsigned64Decoder, + CentityHandleDecoder, + NoscaleDecoder, + BooleanDecoder, + StringDecoder, + SignedDecoder, + UnsignedDecoder, + ComponentDecoder, + FloatCoordDecoder, + FloatSimulationTimeDecoder, + Fixed64Decoder, + QanglePitchYawDecoder, + Qangle3Decoder, + QangleVarDecoder, + BaseDecoder, + AmmoDecoder, + QanglePresDecoder, + GameModeRulesDecoder, +} +use Decoder::*; + +pub static BASETYPE_DECODERS: phf::Map<&'static str, Decoder> = phf::phf_map! { + "bool" => BooleanDecoder, + "char" => StringDecoder, + "int16" => SignedDecoder, + "int32" => SignedDecoder, + "int64" => SignedDecoder, + "int8" => SignedDecoder, + "uint16" => UnsignedDecoder, + "uint32" => UnsignedDecoder, + "uint8" => UnsignedDecoder, + "color32" => UnsignedDecoder, + "GameTime_t" => NoscaleDecoder, + "CBodyComponent" => ComponentDecoder, + "CGameSceneNodeHandle" => UnsignedDecoder, + "Color" => UnsignedDecoder, + "CPhysicsComponent" => ComponentDecoder, + "CRenderComponent" => ComponentDecoder, + "CUtlString" => StringDecoder, + "CUtlStringToken" => UnsignedDecoder, + "CUtlSymbolLarge" => StringDecoder, + "Quaternion" => NoscaleDecoder, + "CTransform" => NoscaleDecoder, + "HSequence" => Unsigned64Decoder, + "AttachmentHandle_t"=> Unsigned64Decoder, + "CEntityIndex"=> Unsigned64Decoder, + "MoveCollide_t"=> Unsigned64Decoder, + "MoveType_t"=> Unsigned64Decoder, + "RenderMode_t"=> Unsigned64Decoder, + "RenderFx_t"=> Unsigned64Decoder, + "SolidType_t"=> Unsigned64Decoder, + "SurroundingBoundsType_t"=> Unsigned64Decoder, + "ModelConfigHandle_t"=> Unsigned64Decoder, + "NPC_STATE"=> Unsigned64Decoder, + "StanceType_t"=> Unsigned64Decoder, + "AbilityPathType_t"=> Unsigned64Decoder, + "WeaponState_t"=> Unsigned64Decoder, + "DoorState_t"=> Unsigned64Decoder, + "RagdollBlendDirection"=> Unsigned64Decoder, + "BeamType_t"=> Unsigned64Decoder, + "BeamClipStyle_t"=> Unsigned64Decoder, + "EntityDisolveType_t"=> Unsigned64Decoder, + "tablet_skin_state_t" => Unsigned64Decoder, + "CStrongHandle" => Unsigned64Decoder, + "CSWeaponMode" => Unsigned64Decoder, + "ESurvivalSpawnTileState"=> Unsigned64Decoder, + "SpawnStage_t"=> Unsigned64Decoder, + "ESurvivalGameRuleDecision_t"=> Unsigned64Decoder, + "RelativeDamagedDirection_t"=> Unsigned64Decoder, + "CSPlayerState"=> Unsigned64Decoder, + "MedalRank_t"=> Unsigned64Decoder, + "CSPlayerBlockingUseAction_t"=> Unsigned64Decoder, + "MoveMountingAmount_t"=> Unsigned64Decoder, + "QuestProgress::Reason"=> Unsigned64Decoder, +}; + +pub fn find_decoder(field: &super::sendtables::ConstructorField, qf_map: &mut QfMapper) -> Decoder { + if field.var_name.as_str() == "m_iClip1" { + return Decoder::AmmoDecoder; + } + + match BASETYPE_DECODERS.get(field.field_type.base_type.as_str()) { + Some(d) => d.clone(), + None => match field.field_type.base_type.as_str() { + "float32" => float_decoder(field, qf_map), + "Vector" => find_vector_type(3, field, qf_map), + "Vector2D" => find_vector_type(2, field, qf_map), + "Vector4D" => find_vector_type(4, field, qf_map), + "uint64" => find_uint_decoder(field), + "QAngle" => find_qangle_decoder(field), + "CHandle" => Decoder::UnsignedDecoder, + "CNetworkedQuantizedFloat" => float_decoder(field, qf_map), + "CStrongHandle" => find_uint_decoder(field), + "CEntityHandle" => find_uint_decoder(field), + _ => Decoder::UnsignedDecoder, + }, + } +} + +fn find_qangle_decoder(field: &super::sendtables::ConstructorField) -> Decoder { + match field.var_name.as_str() { + "m_angEyeAngles" => Decoder::QanglePitchYawDecoder, + _ => { + if field.bitcount != 0 { + Decoder::Qangle3Decoder + } else { + Decoder::QangleVarDecoder + } + } + } +} + +fn find_uint_decoder(field: &super::sendtables::ConstructorField) -> Decoder { + match field.encoder.as_str() { + "fixed64" => Decoder::Fixed64Decoder, + _ => Decoder::Unsigned64Decoder, + } +} + +fn float_decoder(field: &super::sendtables::ConstructorField, qf_map: &mut QfMapper) -> Decoder { + match field.var_name.as_str() { + "m_flSimulationTime" => return Decoder::FloatSimulationTimeDecoder, + "m_flAnimTime" => return Decoder::FloatSimulationTimeDecoder, + _ => {} + }; + + match field.encoder.as_str() { + "coord" => Decoder::FloatCoordDecoder, + "m_flSimulationTime" => Decoder::FloatSimulationTimeDecoder, + _ => { + if field.bitcount <= 0 || field.bitcount >= 32 { + return Decoder::NoscaleDecoder; + } else { + let qf = QuantalizedFloat::new( + field.bitcount as u32, + Some(field.encode_flags), + Some(field.low_value), + Some(field.high_value), + ); + let idx = qf_map.idx; + qf_map.map.insert(idx, qf); + qf_map.idx += 1; + Decoder::QuantalizedFloatDecoder(idx as u8) + } + } + } +} + +fn find_vector_type( + dimensions: usize, + field: &super::sendtables::ConstructorField, + qf_map: &mut QfMapper, +) -> Decoder { + if dimensions == 3 && field.encoder.as_str() == "normal" { + return Decoder::VectorNormalDecoder; + } + + let float_type = float_decoder(field, qf_map); + match float_type { + Decoder::NoscaleDecoder => Decoder::VectorNoscaleDecoder, + Decoder::FloatCoordDecoder => Decoder::VectorFloatCoordDecoder, + _ => Decoder::VectorNormalDecoder, + } +} + +impl Decoder { + pub fn decode( + &self, + bitreader: &mut crate::bitreader::Bitreader, + qf_map: &mut QfMapper, + ) -> Result { + use super::variant::Variant; + + match self { + Self::NoscaleDecoder => Ok(Variant::F32(f32::from_bits(bitreader.read_nbits(32)?))), + Self::FloatSimulationTimeDecoder => Ok(Variant::F32(bitreader.decode_simul_time()?)), + Self::UnsignedDecoder => Ok(Variant::U32(bitreader.read_varint()?)), + Self::QuantalizedFloatDecoder(qf_idx) => Ok(bitreader.decode_qfloat(*qf_idx, qf_map)?), + Self::Qangle3Decoder => Ok(Variant::VecXYZ(bitreader.decode_qangle_all_3()?)), + Self::SignedDecoder => Ok(Variant::I32(bitreader.read_varint32()?)), + Self::VectorNoscaleDecoder => Ok(Variant::VecXYZ(bitreader.decode_vector_noscale()?)), + Self::BooleanDecoder => Ok(Variant::Bool(bitreader.read_boolean()?)), + Self::BaseDecoder => Ok(Variant::U32(bitreader.read_varint()?)), + Self::CentityHandleDecoder => Ok(Variant::U32(bitreader.read_varint()?)), + Self::ComponentDecoder => Ok(Variant::Bool(bitreader.read_boolean()?)), + Self::FloatCoordDecoder => Ok(Variant::F32(bitreader.read_bit_coord()?)), + Self::StringDecoder => Ok(Variant::String(bitreader.read_string()?)), + Self::QanglePitchYawDecoder => { + Ok(Variant::VecXYZ(bitreader.decode_qangle_pitch_yaw()?)) + } + Self::QangleVarDecoder => Ok(Variant::VecXYZ(bitreader.decode_qangle_variant()?)), + Self::VectorNormalDecoder => Ok(Variant::VecXYZ(bitreader.decode_normal_vec()?)), + Self::Unsigned64Decoder => Ok(Variant::U64(bitreader.read_varint_u_64()?)), + Self::Fixed64Decoder => Ok(Variant::U64(bitreader.decode_uint64()?)), + Self::VectorFloatCoordDecoder => { + Ok(Variant::VecXYZ(bitreader.decode_vector_float_coord()?)) + } + Self::AmmoDecoder => Ok(Variant::U32(bitreader.decode_ammo()?)), + Self::QanglePresDecoder => Ok(Variant::VecXYZ(bitreader.decode_qangle_variant_pres()?)), + Self::GameModeRulesDecoder => Ok(Variant::U32(bitreader.read_nbits(7)?)), + } + } +} + +impl<'b> crate::bitreader::Bitreader<'b> { + pub fn read_bit_coord_pres(&mut self) -> Result { + return Ok(self.read_nbits(20)? as f32 * 360.0 / (1 << 20) as f32 - 180.0); + } + + pub fn decode_qfloat( + &mut self, + qf_idx: u8, + qf_map: &QfMapper, + ) -> Result { + match qf_map.map.get(&(qf_idx as u32)) { + Some(qf) => Ok(super::variant::Variant::F32(qf.decode(self)?)), + None => panic!(), + } + } + + pub fn decode_ammo(&mut self) -> Result { + let ammo = self.read_varint()?; + if ammo > 0 { + return Ok(ammo - 1); + } + return Ok(ammo); + } + + pub fn decode_uint64(&mut self) -> Result { + let bytes = self.read_n_bytes(8)?; + match bytes.try_into() { + Err(_) => panic!(), + Ok(arr) => Ok(u64::from_ne_bytes(arr)), + } + } + + pub fn decode_noscale(&mut self) -> Result { + Ok(f32::from_le_bytes(self.read_nbits(32)?.to_le_bytes())) + } + + pub fn read_string(&mut self) -> Result { + let mut s: Vec = vec![]; + loop { + let c = self.read_nbits(8)? as u8; + if c == 0 { + break; + } + s.push(c); + } + Ok(String::from_utf8_lossy(&s).to_string()) + } + pub fn decode_float_coord(&mut self) -> Result { + Ok(self.read_bit_coord()?) + } + + fn decode_simul_time(&mut self) -> Result { + Ok(self.read_varint()? as f32 * (1.0 / 30.0)) + } + + pub fn decode_vector_noscale(&mut self) -> Result<[f32; 3], super::FirstPassError> { + let mut v = [0.0; 3]; + for idx in 0..3 { + v[idx] = self.decode_noscale()?; + } + Ok(v) + } + + pub fn decode_qangle_pitch_yaw(&mut self) -> Result<[f32; 3], super::FirstPassError> { + let mut v = [0.0; 3]; + v[0] = self.read_angle(32)?; + v[1] = self.read_angle(32)?; + v[2] = self.read_angle(32)?; + Ok(v) + } + pub fn decode_qangle_all_3(&mut self) -> Result<[f32; 3], super::FirstPassError> { + // Used by aimpunch props (not exposed atm) maybe wrong format? correct number of bits anyhow. + let mut v = [0.0; 3]; + v[0] = self.decode_noscale()?; + v[1] = self.decode_noscale()?; + v[2] = self.decode_noscale()?; + Ok(v) + } + pub fn decode_qangle_variant(&mut self) -> Result<[f32; 3], super::FirstPassError> { + let mut v = [0.0; 3]; + let has_x = self.read_boolean()?; + let has_y = self.read_boolean()?; + let has_z = self.read_boolean()?; + if has_x { + v[0] = self.read_bit_coord()?; + } + if has_y { + v[1] = self.read_bit_coord()?; + } + if has_z { + v[2] = self.read_bit_coord()?; + } + Ok(v) + } + pub fn read_angle(&mut self, n: usize) -> Result { + return Ok(self.decode_noscale()? / ((1 << n) as f32)); + } + + pub fn decode_normal(&mut self) -> Result { + let is_neg = self.read_boolean()?; + let len = self.read_nbits(11)?; + let result = (len as f64 * (1.0 / ((1 << 11) as f64) - 1.0)) as f32; + match is_neg { + true => Ok(-result), + false => Ok(result), + } + } + pub fn decode_normal_vec(&mut self) -> Result<[f32; 3], super::FirstPassError> { + let mut v = [0.0; 3]; + let has_x = self.read_boolean()?; + let has_y = self.read_boolean()?; + if has_x { + v[0] = self.decode_normal()?; + } + if has_y { + v[1] = self.decode_normal()?; + } + let neg_z = self.read_boolean()?; + let prod_sum = v[0] * v[0] + v[1] * v[1]; + if prod_sum < 1.0 { + v[2] = (1.0 - prod_sum).sqrt() as f32; + } else { + v[2] = 0.0; + } + if neg_z { + v[2] = -v[2]; + } + Ok(v) + } + + pub fn decode_vector_float_coord(&mut self) -> Result<[f32; 3], super::FirstPassError> { + let mut v = [0.0; 3]; + for idx in 0..3 { + v[idx] = self.decode_float_coord()?; + } + Ok(v) + } + + pub fn decode_qangle_variant_pres(&mut self) -> Result<[f32; 3], super::FirstPassError> { + let mut v = [0.0; 3]; + + let has_x = self.read_boolean()?; + let has_y = self.read_boolean()?; + let has_z = self.read_boolean()?; + + if has_x { + v[0] = self.read_bit_coord_pres()?; + } + if has_y { + v[1] = self.read_bit_coord_pres()?; + } + if has_z { + v[2] = self.read_bit_coord_pres()?; + } + Ok(v) + } +} diff --git a/src/parser/decoder/quantizedfloat.rs b/src/parser/decoder/quantizedfloat.rs new file mode 100644 index 0000000..efabece --- /dev/null +++ b/src/parser/decoder/quantizedfloat.rs @@ -0,0 +1,208 @@ +use crate::{bitreader::Bitreader, parser::FirstPassError}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct QuantalizedFloat { + low: f32, + high: f32, + high_low_mul: f32, + dec_mul: f32, + offset: f32, + bit_count: u32, + flags: u32, + no_scale: bool, +} + +#[derive(Debug, Clone)] +pub struct QfMapper { + pub idx: u32, + pub map: std::collections::HashMap, +} + +const QFF_ROUNDDOWN: u32 = 1 << 0; +const QFF_ROUNDUP: u32 = 1 << 1; +const QFF_ENCODE_ZERO: u32 = 1 << 2; +const QFF_ENCODE_INTEGERS: u32 = 1 << 3; + +impl QuantalizedFloat { + // More or less directly translated from here: + // https://github.com/dotabuff/manta/blob/09a1d60ef77f68eef84b79e9ca519caf76a1f291/quantizedfloat.go + fn validate_flags(&mut self) { + if self.flags == 0 { + return; + } + if (self.low == 0.0 && (self.flags & QFF_ROUNDDOWN) != 0) + || (self.high == 0.0 && (self.flags & QFF_ROUNDUP) != 0) + { + self.flags &= !QFF_ENCODE_ZERO; + } + if self.low == 0.0 && (self.flags & QFF_ENCODE_ZERO) != 0 { + self.flags |= QFF_ROUNDDOWN; + self.flags &= !QFF_ENCODE_ZERO; + } + if self.high == 0.0 && (self.flags & QFF_ENCODE_ZERO) != 0 { + self.flags |= QFF_ROUNDUP; + self.flags &= !QFF_ENCODE_ZERO; + } + if self.low > 0.0 || self.high < 0.0 { + self.flags &= !QFF_ENCODE_ZERO; + } + if (self.flags & QFF_ENCODE_INTEGERS) != 0 { + self.flags &= !(QFF_ROUNDUP | QFF_ROUNDDOWN | QFF_ENCODE_ZERO); + } + } + fn assign_multipliers(&mut self, steps: u32) { + self.high_low_mul = 0.0; + let range = self.high - self.low; + + let high: u32; + if self.bit_count == 32 { + high = 0xFFFFFFFE; + } else { + high = (1 << self.bit_count) - 1; + } + + let mut high_mul: f32; + // Xd? + if range.abs() <= 0.0 { + high_mul = high as f32; + } else { + high_mul = (high as f32) / range; + } + + if (high_mul * range > (high as f32)) + || (((high_mul * range) as f64) > ((high as f32) as f64)) + { + let multipliers = vec![0.9999, 0.99, 0.9, 0.8, 0.7]; + for multiplier in multipliers { + high_mul = (high as f32) / range * multiplier; + if (high_mul * range > (high as f32)) + || (((high_mul * range) as f64) > (high as f32) as f64) + { + continue; + } + break; + } + } + self.high_low_mul = high_mul; + self.dec_mul = 1.0 / (steps - 1) as f32; + } + pub fn quantize(&mut self, val: f32) -> f32 { + if val < self.low { + return self.low; + } else if val > self.high { + return self.high; + } + let i = ((val - self.low) * self.high_low_mul) as u32; + self.low + (self.high - self.low) * ((i as f32) * self.dec_mul) + } + pub fn decode(&self, bitreader: &mut Bitreader) -> Result { + if self.flags & QFF_ROUNDDOWN != 0 && bitreader.read_boolean()? { + return Ok(self.low); + } + if self.flags & QFF_ROUNDUP != 0 && bitreader.read_boolean()? { + return Ok(self.high); + } + if self.flags & QFF_ENCODE_ZERO != 0 && bitreader.read_boolean()? { + return Ok(0.0); + } + let bits = bitreader.read_nbits(self.bit_count)?; + Ok(self.low + (self.high - self.low) * bits as f32 * self.dec_mul) + } + pub fn new( + bitcount: u32, + flags: Option, + low_value: Option, + high_value: Option, + ) -> Self { + let mut qf = QuantalizedFloat { + no_scale: false, + bit_count: 0, + dec_mul: 0.0, + low: 0.0, + high: 0.0, + high_low_mul: 0.0, + offset: 0.0, + flags: 0, + }; + + if bitcount == 0 || bitcount >= 32 { + qf.no_scale = true; + qf.bit_count = 32; + return qf; + } else { + qf.no_scale = false; + qf.bit_count = bitcount; + qf.offset = 0.0; + + if low_value.is_some() { + qf.low = low_value.unwrap_or(0.0); + } else { + qf.low = 0.0; + } + if high_value.is_some() { + qf.high = high_value.unwrap_or(0.0); + } else { + qf.high = 1.0; + } + } + if flags.is_some() { + qf.flags = flags.unwrap_or(0) as u32; + } else { + qf.flags = 0; + } + qf.validate_flags(); + let mut steps = 1 << qf.bit_count; + + if (qf.flags & QFF_ROUNDDOWN) != 0 { + let range = qf.high - qf.low; + qf.offset = range / (steps as f32); + qf.high -= qf.offset; + } else if (qf.flags & QFF_ROUNDUP) != 0 { + let range = qf.high - qf.low; + qf.offset = range / (steps as f32); + qf.low += qf.offset; + } + if (qf.flags & QFF_ENCODE_INTEGERS) != 0 { + let mut delta = qf.high - qf.low; + if delta < 1.0 { + delta = 1.0; + } + let delta_log2 = delta.log2().ceil(); + let range_2: u32 = 1 << delta_log2 as u32; + let mut bit_count = qf.bit_count; + loop { + if (1 << bit_count) > range_2 { + break; + } else { + bit_count += 1; + } + } + if bit_count > qf.bit_count { + qf.bit_count = bit_count; + steps = 1 << qf.bit_count; + } + qf.offset = range_2 as f32 / steps as f32; + qf.high = qf.low + ((range_2 as f32 - qf.offset) as f32); + } + + qf.assign_multipliers(steps); + + if (qf.flags & QFF_ROUNDDOWN) != 0 { + if qf.quantize(qf.low) == qf.low { + qf.flags &= !QFF_ROUNDDOWN; + } + } + if (qf.flags & QFF_ROUNDUP) != 0 { + if qf.quantize(qf.high) == qf.high { + qf.flags &= !QFF_ROUNDUP + } + } + if (qf.flags & QFF_ENCODE_ZERO) != 0 { + if qf.quantize(0.0) == 0.0 { + qf.flags &= !QFF_ENCODE_ZERO; + } + } + + qf + } +} diff --git a/src/parser/fieldpath.rs b/src/parser/fieldpath.rs new file mode 100644 index 0000000..0a026c3 --- /dev/null +++ b/src/parser/fieldpath.rs @@ -0,0 +1,411 @@ +#[derive(Debug)] +pub struct Paths(Vec); + +#[derive(Debug, Clone, Copy)] +pub struct FieldPath { + pub path: [i32; 7], + pub last: usize, +} + +impl Paths { + pub fn new() -> Self { + Self(Vec::new()) + } + + pub fn new_path() -> FieldPath { + FieldPath { + path: [-1, 0, 0, 0, 0, 0, 0], + last: 0, + } + } + + pub fn write(&mut self, fp: &FieldPath, idx: usize) { + match self.0.get_mut(idx) { + Some(entry) => { + *entry = *fp; + } + None => { + self.0.resize(idx + 1, Self::new_path()); + let entry = self.0.get_mut(idx).expect("We just resized the Vec"); + *entry = *fp; + } + }; + } + + pub fn paths(&self) -> impl Iterator { + self.0.iter() + } +} + +pub fn parse_paths( + bitreader: &mut crate::bitreader::Bitreader, + paths: &mut Paths, +) -> Result { + let mut path = Paths::new_path(); + let mut idx = 0; + loop { + if bitreader.bits_left < 17 { + bitreader.refill(); + } + + let peeked_bits = bitreader.peek(17); + let (symbol, code_len) = super::HUFFMAN_LOOKUP_TABLE[peeked_bits as usize]; + bitreader.consume(code_len as u32); + + if symbol == 39 { + break; + } + + super::do_op(symbol, bitreader, &mut path)?; + paths.write(&path, idx); + idx += 1; + } + + Ok(idx) +} + +impl FieldPath { + pub fn pop_special(&mut self, n: usize) -> Result<(), super::FirstPassError> { + for _ in 0..n { + *self.get_entry_mut(self.last)? = 0; + self.last -= 1; + } + Ok(()) + } + + pub fn get_entry_mut(&mut self, idx: usize) -> Result<&mut i32, super::FirstPassError> { + match self.path.get_mut(idx) { + Some(e) => Ok(e), + None => panic!(), + } + } + + pub fn find<'ser>( + &self, + ser: &'ser super::sendtables::Serializer, + ) -> Result<&'ser super::sendtables::Field, super::FirstPassError> { + let f = match ser.fields.get(self.path[0] as usize) { + Some(entry) => entry, + None => panic!("Field-Len: {:?} - Path: {:?}", ser.fields.len(), self.path), + }; + + match self.last { + 0 => Ok(f), + 1 => Ok(f.get_inner(self.path[1] as usize)?), + 2 => Ok(f.get_inner(self.path[1] as usize)?.get_inner(self.path[2] as usize)?), + 3 => Ok(f.get_inner(self.path[1] as usize)?.get_inner(self.path[2] as usize)?.get_inner(self.path[3] as usize)?), + other => panic!("{:?}", other), + } + } +} + +pub mod ops { + use super::FieldPath; + use crate::{bitreader::Bitreader, parser::FirstPassError}; + +pub fn plus_one(_bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 1; + Ok(()) +} + +pub fn plus_two(_bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 2; + Ok(()) +} + +pub fn plus_three(_bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 3; + Ok(()) +} + +pub fn plus_four(_bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 4; + Ok(()) +} + +pub fn plus_n(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32 + 5; + Ok(()) +} + +pub fn push_one_left_delta_zero_right_zero(_bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = 0; + Ok(()) +} + +pub fn push_one_left_delta_zero_right_non_zero(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + Ok(()) +} + +pub fn push_one_left_delta_one_right_zero(_bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 1; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = 0; + Ok(()) +} + +pub fn push_one_left_delta_one_right_non_zero(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 1; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = bitreader.read_ubit_var_fp()? as i32; + Ok(()) +} + +pub fn push_one_left_delta_n_right_zero(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = 0; + Ok(()) +} + +pub fn push_one_left_delta_n_right_non_zero(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32 + 2; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = bitreader.read_ubit_var_fp()? as i32 + 1; + Ok(()) +} + +pub fn push_one_left_delta_n_right_non_zero_pack6_bits( + bitreader: &mut Bitreader, + field_path: &mut FieldPath, +) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += (bitreader.read_nbits(3)? + 2) as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = (bitreader.read_nbits(3)? + 1) as i32; + Ok(()) +} + +pub fn push_one_left_delta_n_right_non_zero_pack8_bits( + bitreader: &mut Bitreader, + field_path: &mut FieldPath, +) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += (bitreader.read_nbits(4)? + 2) as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = (bitreader.read_nbits(4)? + 1) as i32; + Ok(()) +} + +pub fn push_two_left_delta_zero(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + Ok(()) +} + +pub fn push_two_pack5_left_delta_zero(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = bitreader.read_nbits(5)? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = bitreader.read_nbits(5)? as i32; + Ok(()) +} + +pub fn push_three_left_delta_zero(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + Ok(()) +} + +pub fn push_three_pack5_left_delta_zero(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = bitreader.read_nbits(5)? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = bitreader.read_nbits(5)? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = bitreader.read_nbits(5)? as i32; + Ok(()) +} + +pub fn push_two_left_delta_one(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 1; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + Ok(()) +} + +pub fn push_two_pack5_left_delta_one(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 1; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + Ok(()) +} + +pub fn push_three_left_delta_one(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 1; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + Ok(()) +} + +pub fn push_three_pack5_left_delta_one(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += 1; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + Ok(()) +} + +pub fn push_two_left_delta_n(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += (bitreader.read_u_bit_var()? + 2) as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + Ok(()) +} + +pub fn push_two_pack5_left_delta_n(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += (bitreader.read_u_bit_var()? + 2) as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + Ok(()) +} + +pub fn push_three_left_delta_n(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += (bitreader.read_u_bit_var()? + 2) as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + Ok(()) +} + +pub fn push_three_pack5_left_delta_n(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last)? += (bitreader.read_u_bit_var()? + 2) as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_nbits(5)? as i32; + Ok(()) +} + +pub fn push_n(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + let n = bitreader.read_u_bit_var()? as i32; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_u_bit_var()? as i32; + for _ in 0..n { + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32; + } + Ok(()) +} + +pub fn push_n_and_non_topological(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + for i in 0..field_path.last + 1 { + if bitreader.read_boolean()? { + *field_path.get_entry_mut(i)? += bitreader.read_varint32()? + 1; + } + } + let count = bitreader.read_u_bit_var()?; + for _ in 0..count { + field_path.last += 1; + *field_path.get_entry_mut(field_path.last)? = bitreader.read_ubit_var_fp()? as i32; + } + Ok(()) +} + +pub fn pop_one_plus_one(_bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.pop_special(1)?; + *field_path.get_entry_mut(field_path.last)? += 1; + Ok(()) +} + +pub fn pop_one_plus_n(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.pop_special(1)?; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_ubit_var_fp()? as i32 + 1; + Ok(()) +} + +pub fn pop_all_but_one_plus_one(_bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.pop_special(field_path.last)?; + *field_path.get_entry_mut(0)? += 1; + Ok(()) +} + +pub fn pop_all_but_one_plus_n(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.pop_special(field_path.last)?; + *field_path.get_entry_mut(0)? += bitreader.read_ubit_var_fp()? as i32 + 1; + Ok(()) +} + +pub fn pop_all_but_one_plus_n_pack3_bits(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.pop_special(field_path.last)?; + *field_path.get_entry_mut(0)? += bitreader.read_nbits(3)? as i32 + 1; + Ok(()) +} + +pub fn pop_all_but_one_plus_n_pack6_bits(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.pop_special(field_path.last)?; + *field_path.get_entry_mut(0)? += bitreader.read_nbits(6)? as i32 + 1; + Ok(()) +} + +pub fn pop_n_plus_one(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.pop_special(bitreader.read_ubit_var_fp()? as usize)?; + *field_path.get_entry_mut(field_path.last)? += 1; + Ok(()) +} + +pub fn pop_n_plus_n(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.pop_special(bitreader.read_ubit_var_fp()? as usize)?; + *field_path.get_entry_mut(field_path.last)? += bitreader.read_varint32()?; + Ok(()) +} + +pub fn pop_n_and_non_topographical(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + field_path.pop_special(bitreader.read_ubit_var_fp()? as usize)?; + for i in 0..field_path.last + 1 { + if bitreader.read_boolean()? { + *field_path.get_entry_mut(i)? += bitreader.read_varint32()?; + } + } + Ok(()) +} + +pub fn non_topo_complex(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + for i in 0..field_path.last + 1 { + if bitreader.read_boolean()? { + *field_path.get_entry_mut(i)? += bitreader.read_varint32()?; + } + } + Ok(()) +} + +pub fn non_topo_penultimate_plus_one(_bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + *field_path.get_entry_mut(field_path.last - 1)? += 1; + Ok(()) +} + +pub fn non_topo_complex_pack4_bits(bitreader: &mut Bitreader, field_path: &mut FieldPath) -> Result<(), FirstPassError> { + for i in 0..field_path.last + 1 { + if bitreader.read_boolean()? { + *field_path.get_entry_mut(i)? += bitreader.read_nbits(4)? as i32 - 7; + } + } + Ok(()) +} +} diff --git a/src/parser/sendtables.rs b/src/parser/sendtables.rs new file mode 100644 index 0000000..3be4fab --- /dev/null +++ b/src/parser/sendtables.rs @@ -0,0 +1,1449 @@ +use super::decoder; + +#[derive(Debug)] +pub enum ParseSendTables {} + +#[derive(Debug, Clone, PartialEq)] +pub struct Serializer { + pub name: String, + pub fields: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub struct FieldInfo { + pub decoder: decoder::Decoder, + pub should_parse: bool, + pub prop_id: u32, +} + +// Design from https://github.com/skadistats/clarity +#[derive(Debug, Clone, PartialEq)] +pub enum Field { + Array(ArrayField), + Vector(VectorField), + Serializer(SerializerField), + Pointer(PointerField), + Value(ValueField), + None, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ArrayField { + pub field_enum: Box, + pub length: usize, +} +#[derive(Debug, Clone, PartialEq)] +pub struct VectorField { + pub field_enum: Box, + pub decoder: decoder::Decoder, +} +#[derive(Debug, Clone, PartialEq)] +pub struct ValueField { + pub decoder: decoder::Decoder, + pub name: String, + pub should_parse: bool, + pub prop_id: u32, + pub full_name: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SerializerField { + pub serializer: Serializer, +} +#[derive(Debug, Clone, PartialEq)] +pub struct PointerField { + pub decoder: decoder::Decoder, + pub serializer: Serializer, +} + +impl ArrayField { + pub fn new(field_enum: Field, length: usize) -> ArrayField { + ArrayField { + field_enum: Box::new(field_enum), + length, + } + } +} +impl PointerField { + pub fn new(serializer: &Serializer) -> PointerField { + let decoder = if serializer.name == "CCSGameModeRules" { + decoder::Decoder::GameModeRulesDecoder + } else { + decoder::Decoder::BooleanDecoder + }; + PointerField { + serializer: serializer.clone(), + decoder, + } + } +} +impl SerializerField { + pub fn new(serializer: &Serializer) -> SerializerField { + SerializerField { + serializer: serializer.clone(), + } + } +} +impl ValueField { + pub fn new(decoder: decoder::Decoder, name: &str) -> ValueField { + ValueField { + decoder, + name: name.to_string(), + prop_id: 0, + should_parse: false, + full_name: "None ".to_string() + name, + } + } +} +impl VectorField { + pub fn new(field_enum: Field) -> VectorField { + VectorField { + field_enum: Box::new(field_enum), + decoder: decoder::Decoder::UnsignedDecoder, + } + } +} + +pub fn get_serializers( + msg: &crate::csgo_proto::CsvcMsgFlattenedSerializer, + qf_mapper: &mut decoder::QfMapper, +) -> Result, super::FirstPassError> { + let mut fields: Vec> = vec![None; msg.fields.len()]; + let mut field_type_map: std::collections::HashMap = + std::collections::HashMap::new(); + let mut serializers: std::collections::HashMap = + std::collections::HashMap::new(); + + for (field, msg_field) in fields.iter_mut().zip(msg.fields.iter()) { + let field_data = generate_field_data(msg_field, msg, &mut field_type_map, qf_mapper)?; + *field = Some(field_data); + } + + for serializer in msg.serializers.iter() { + let ser = generate_serializer(serializer, &mut fields, msg, &mut serializers)?; + serializers.insert(ser.name.clone(), ser); + } + + Ok(serializers) +} + +fn generate_field_data( + field: &crate::csgo_proto::ProtoFlattenedSerializerFieldT, + msg: &crate::csgo_proto::CsvcMsgFlattenedSerializer, + field_type_map: &mut std::collections::HashMap, + qf_mapper: &mut decoder::QfMapper, +) -> Result { + let name = msg.symbols.get(field.var_type_sym() as usize).unwrap(); + + let ft = find_field_type(&name, field_type_map)?; + let mut field = field_from_msg(field, msg, ft.clone())?; + + field.category = find_category(&field); + field.decoder = decoder::find_decoder(&field, qf_mapper); + + match field.var_name.as_str() { + "m_PredFloatVariables" | "m_OwnerOnlyPredNetFloatVariables" => { + field.decoder = decoder::Decoder::NoscaleDecoder + } + "m_OwnerOnlyPredNetVectorVariables" | "m_PredVectorVariables" => { + field.decoder = decoder::Decoder::VectorNoscaleDecoder + } + "m_pGameModeRules" => field.decoder = decoder::Decoder::GameModeRulesDecoder, + _ => {} + }; + + if field.encoder == "qangle_precise" { + field.decoder = decoder::Decoder::QanglePresDecoder; + } + + field.field_type = ft; + + Ok(field) +} + +fn generate_serializer( + serializer: &crate::csgo_proto::ProtoFlattenedSerializerT, + field_data: &mut Vec>, + msg: &crate::csgo_proto::CsvcMsgFlattenedSerializer, + serializers: &mut std::collections::HashMap, +) -> Result { + let symbol = match msg.symbols.get(serializer.serializer_name_sym() as usize) { + Some(s) => s, + None => panic!(), + }; + + let mut fields_this_ser: Vec = vec![Field::None; serializer.fields_index.len()]; + for (idx, field_this_ser) in fields_this_ser.iter_mut().enumerate() { + let fi: usize = match serializer.fields_index.get(idx).map(|i| *i as usize) { + Some(f) => f, + None => continue, + }; + + let f = match field_data.get_mut(fi) { + Some(Some(f)) => f, + _ => continue, + }; + + if f.field_enum_type.is_none() { + f.field_enum_type = Some(create_field(&symbol, f, serializers)?); + } + if let Some(Some(f)) = &field_data.get(fi) { + if let Some(field) = &f.field_enum_type { + *field_this_ser = field.clone(); + } + } + } + + Ok(Serializer { + name: symbol.to_owned(), + fields: fields_this_ser, + }) +} + +#[derive(Debug, Clone)] +pub struct FieldType { + pub base_type: String, + pub generic_type: Option>, + pub pointer: bool, + pub count: Option, + pub element_type: Option>, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum FieldCategory { + Pointer, + Vector, + Array, + Value, +} + +#[derive(Debug, Clone)] +pub struct ConstructorField { + pub var_name: String, + pub var_type: String, + pub send_node: String, + pub serializer_name: Option, + pub encoder: String, + pub encode_flags: i32, + pub bitcount: i32, + pub low_value: f32, + pub high_value: f32, + pub field_type: FieldType, + + pub decoder: decoder::Decoder, + pub category: FieldCategory, + pub field_enum_type: Option, + pub serializer: Option<()>, + pub base_decoder: Option<()>, + pub child_decoder: Option<()>, +} + +static RE: std::sync::LazyLock = std::sync::LazyLock::new(|| { + regex::Regex::new(r"([^<\[\*]+)(<\s(.*)\s>)?(\*)?(\[(.*)\])?").unwrap() +}); + +const POINTER_TYPES: &'static [&'static str] = &[ + "CBodyComponent", + "CLightComponent", + "CPhysicsComponent", + "CRenderComponent", + "CPlayerLocalData", +]; + +fn find_field_type( + name: &str, + field_type_map: &mut std::collections::HashMap, +) -> Result { + let captures = match RE.captures(name) { + Some(c) => c, + None => panic!("No captures found"), + }; + + let base_type = match captures.get(1) { + Some(s) => s.as_str().to_owned(), + None => String::new(), + }; + + let pointer = match captures.get(4) { + Some(s) => { + if s.as_str() == "*" { + true + } else { + POINTER_TYPES.contains(&name) + } + } + None => POINTER_TYPES.contains(&name), + }; + + let mut ft = FieldType { + base_type, + pointer, + count: None, + generic_type: None, + element_type: None, + }; + + if let Some(generic) = captures.get(3) { + ft.generic_type = Some(Box::new(find_field_type(generic.as_str(), field_type_map)?)); + } + if let Some(count) = captures.get(6) { + ft.count = Some(count.as_str().parse::().unwrap_or(0)); + } + + if ft.count.is_some() { + let ft_string = ft.to_string(true); + let for_string_res = for_string(field_type_map, ft_string)?; + ft.element_type = Some(Box::new(for_string_res)); + } + + Ok(ft) +} + +fn field_from_msg( + field: &crate::csgo_proto::ProtoFlattenedSerializerFieldT, + msg: &crate::csgo_proto::CsvcMsgFlattenedSerializer, + ft: FieldType, +) -> Result { + let ser_name = match field.field_serializer_name_sym { + Some(idx) => match msg.symbols.get(idx as usize) { + Some(entry) => Some(entry.to_owned()), + None => panic!(), + }, + None => None, + }; + + let enc_name = match field.var_encoder_sym { + Some(idx) => match msg.symbols.get(idx as usize) { + Some(enc_name) => enc_name.to_owned(), + None => panic!(), + }, + None => String::new(), + }; + + let var_name = match msg.symbols.get(field.var_name_sym() as usize) { + Some(n) => n.to_owned(), + None => panic!(), + }; + let var_type = match msg.symbols.get(field.var_type_sym() as usize) { + Some(n) => n.to_owned(), + None => panic!(), + }; + let send_node = match msg.symbols.get(field.send_node_sym() as usize) { + Some(n) => n.to_owned(), + None => panic!(), + }; + + Ok(ConstructorField { + field_enum_type: None, + bitcount: field.bit_count(), + var_name, + var_type, + send_node, + serializer_name: ser_name, + encoder: enc_name, + encode_flags: field.encode_flags(), + low_value: field.low_value(), + high_value: field.high_value(), + + field_type: ft, + serializer: None, + decoder: decoder::Decoder::BaseDecoder, + base_decoder: None, + child_decoder: None, + + category: FieldCategory::Value, + }) +} + +fn find_category(field: &ConstructorField) -> FieldCategory { + if field.is_pointer() { + return FieldCategory::Pointer; + } + + if field.is_vector() { + return FieldCategory::Vector; + } + + if field.is_array() { + return FieldCategory::Array; + } + + FieldCategory::Value +} + +impl ConstructorField { + pub fn is_pointer(&self) -> bool { + if self.field_type.pointer { + return true; + } + + matches!( + self.field_type.base_type.as_str(), + "CBodyComponent" + | "CLightComponent" + | "CPhysicsComponent" + | "CRenderComponent" + | "CPlayerLocalData" + ) + } + + pub fn is_array(&self) -> bool { + self.field_type + .count + .map(|_| self.field_type.base_type.as_str() != "char") + .unwrap_or(false) + } + + pub fn is_vector(&self) -> bool { + if self.serializer_name.is_some() { + return true; + } + + matches!( + self.field_type.base_type.as_str(), + "CUtlVector" | "CNetworkUtlVectorBase" + ) + } +} + +impl FieldType { + fn to_string(&self, omit_count: bool) -> String { + let mut s = String::new(); + + s += &self.base_type; + + if let Some(gt) = self.generic_type.as_ref() { + s += "< "; + s += &FieldType::to_string(>, true); + s += "< "; + } + if self.pointer { + s += "*"; + } + if !omit_count && self.count.is_some() { + if let Some(c) = self.count { + s += "["; + s += &c.to_string(); + s += "]"; + } + } + + s + } +} + +fn for_string( + field_type_map: &mut std::collections::HashMap, + field_type_string: String, +) -> Result { + match field_type_map.get(&field_type_string) { + Some(s) => Ok(s.clone()), + None => { + let result = find_field_type(&field_type_string, field_type_map)?; + field_type_map.insert(field_type_string, result.clone()); + Ok(result) + } + } +} + +fn create_field( + symbol: &String, + fd: &mut ConstructorField, + serializers: &mut std::collections::HashMap, +) -> Result { + let element_field = match fd.serializer_name.as_ref() { + Some(name) => { + let ser = match serializers.get(name.as_str()) { + Some(ser) => ser, + None => panic!(), + }; + if fd.category == FieldCategory::Pointer { + Field::Pointer(PointerField::new(ser)) + } else { + Field::Serializer(SerializerField::new(ser)) + } + } + None => Field::Value(ValueField::new(fd.decoder, &fd.var_name)), + }; + + match fd.category { + FieldCategory::Array => Ok(Field::Array(ArrayField::new( + element_field, + fd.field_type.count.unwrap_or(0) as usize, + ))), + FieldCategory::Vector => Ok(Field::Vector(VectorField::new(element_field))), + _ => Ok(element_field), + } +} + +impl Field { + pub fn get_inner(&self, idx: usize) -> Result<&Field, super::FirstPassError> { + match self { + Field::Array(inner) => Ok(&inner.field_enum), + Field::Vector(inner) => Ok(&inner.field_enum), + Field::Serializer(inner) => match inner.serializer.fields.get(idx) { + Some(f) => Ok(f), + None => panic!(), + }, + Field::Pointer(inner) => match inner.serializer.fields.get(idx) { + Some(f) => Ok(f), + None => panic!(), + }, + // Illegal + Field::Value(_) => panic!("Can not get inner of Field::Value"), + Field::None => panic!("Can not get inner of Field::None"), + } + } + + pub fn get_propinfo(&self, path: &super::FieldPath) -> Option { + const MY_WEAPONS_OFFSET: u32 = 500000; + const WEAPON_SKIN_ID: u32 = 10000000; + const ITEM_PURCHASE_COUNT: u32 = 200000000; + const FLATTENED_VEC_MAX_LEN: u32 = 100000; + const ITEM_PURCHASE_DEF_IDX: u32 = 300000000; + const ITEM_PURCHASE_NEW_DEF_IDX: u32 = 600000000; + const ITEM_PURCHASE_COST: u32 = 400000000; + const ITEM_PURCHASE_HANDLE: u32 = 500000000; + + let mut fi = match self { + Self::Value(v) => FieldInfo { + decoder: v.decoder, + should_parse: v.should_parse, + prop_id: v.prop_id, + }, + Self::Vector(v) => match self.get_inner(0) { + Ok(Field::Value(inner)) => FieldInfo { + decoder: v.decoder, + prop_id: inner.prop_id, + should_parse: inner.should_parse, + }, + _ => return None, + }, + _ => return None, + }; + + if fi.prop_id == MY_WEAPONS_OFFSET { + if path.last == 1 { + } else { + fi.prop_id = MY_WEAPONS_OFFSET + path.path[2] as u32 + 1; + } + } + if fi.prop_id == WEAPON_SKIN_ID { + fi.prop_id = WEAPON_SKIN_ID + path.path[1] as u32; + } + if path.path[1] != 1 { + if fi.prop_id >= ITEM_PURCHASE_COUNT + && fi.prop_id < ITEM_PURCHASE_COUNT + FLATTENED_VEC_MAX_LEN + { + fi.prop_id = ITEM_PURCHASE_COUNT + path.path[2] as u32; + } + if fi.prop_id >= ITEM_PURCHASE_DEF_IDX + && fi.prop_id < ITEM_PURCHASE_DEF_IDX + FLATTENED_VEC_MAX_LEN + { + fi.prop_id = ITEM_PURCHASE_DEF_IDX + path.path[2] as u32; + } + if fi.prop_id >= ITEM_PURCHASE_COST + && fi.prop_id < ITEM_PURCHASE_COST + FLATTENED_VEC_MAX_LEN + { + fi.prop_id = ITEM_PURCHASE_COST + path.path[2] as u32; + } + if fi.prop_id >= ITEM_PURCHASE_HANDLE + && fi.prop_id < ITEM_PURCHASE_HANDLE + FLATTENED_VEC_MAX_LEN + { + fi.prop_id = ITEM_PURCHASE_HANDLE + path.path[2] as u32; + } + if fi.prop_id >= ITEM_PURCHASE_NEW_DEF_IDX + && fi.prop_id < ITEM_PURCHASE_NEW_DEF_IDX + FLATTENED_VEC_MAX_LEN + { + fi.prop_id = ITEM_PURCHASE_NEW_DEF_IDX + path.path[2] as u32; + } + } + return Some(fi); + } + + pub fn get_decoder(&self) -> Result { + match self { + Self::Value(inner) => Ok(inner.decoder), + Self::Pointer(inner) => Ok(inner.decoder), + Self::Vector(_) => Ok(decoder::Decoder::UnsignedDecoder), + _ => panic!(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn parse_ancient_example_msg() { + use Field::*; + use decoder::Decoder::*; + + let data: &[u8] = include_bytes!("../../testfiles/ancient_sendtables.b"); + + let mut qf_mapper = crate::parser::decoder::QfMapper { + idx: 0, + map: std::collections::HashMap::new(), + }; + + let serializer_msg: crate::csgo_proto::CsvcMsgFlattenedSerializer = + prost::Message::decode(data).unwrap(); + + let result = get_serializers(&serializer_msg, &mut qf_mapper).unwrap(); + + let cworld_parser = result.get("CWorld").unwrap(); + dbg!(&cworld_parser); + + let expected_parser = super::Serializer { + name: "CWorld".to_string(), + fields: [ + Value( + ValueField { + decoder: FloatSimulationTimeDecoder, + name: "m_flAnimTime".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flAnimTime".to_string(), + }, + ), + Value( + ValueField { + decoder: FloatSimulationTimeDecoder, + name: "m_flSimulationTime".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flSimulationTime".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_hOwnerEntity".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_hOwnerEntity".to_string(), + }, + ), + Pointer( + PointerField { + decoder: BooleanDecoder, + serializer: super::Serializer { + name: "CBodyComponentBaseModelEntity".to_string(), + fields: [ + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_cellX".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_cellX".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_cellY".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_cellY".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_cellZ".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_cellZ".to_string(), + }, + ), + Value( + ValueField { + decoder: QuantalizedFloatDecoder( + 0, + ), + name: "m_vecX".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vecX".to_string(), + }, + ), + Value( + ValueField { + decoder: QuantalizedFloatDecoder( + 1, + ), + name: "m_vecY".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vecY".to_string(), + }, + ), + Value( + ValueField { + decoder: QuantalizedFloatDecoder( + 2, + ), + name: "m_vecZ".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vecZ".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_hParent".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_hParent".to_string(), + }, + ), + Value( + ValueField { + decoder: QanglePresDecoder, + name: "m_angRotation".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_angRotation".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flScale".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flScale".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_name".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_name".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_hierarchyAttachName".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_hierarchyAttachName".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_hModel".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_hModel".to_string(), + }, + ), + Value( + ValueField { + decoder: BooleanDecoder, + name: "m_bClientClothCreationSuppressed".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bClientClothCreationSuppressed".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_MeshGroupMask".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_MeshGroupMask".to_string(), + }, + ), + Value( + ValueField { + decoder: SignedDecoder, + name: "m_nIdealMotionType".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nIdealMotionType".to_string(), + }, + ), + Value( + ValueField { + decoder: BooleanDecoder, + name: "m_bIsAnimationEnabled".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bIsAnimationEnabled".to_string(), + }, + ), + Value( + ValueField { + decoder: BooleanDecoder, + name: "m_bUseParentRenderBounds".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bUseParentRenderBounds".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_materialGroup".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_materialGroup".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nHitboxSet".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nHitboxSet".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nOutsideWorld".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nOutsideWorld".to_string(), + }, + ), + ].to_vec(), + }, + }, + ), + Pointer( + PointerField { + decoder: BooleanDecoder, + serializer: super::Serializer { + name: "CEntityIdentity".to_string(), + fields: [ + Value( + ValueField { + decoder: SignedDecoder, + name: "m_nameStringableIndex".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nameStringableIndex".to_string(), + }, + ), + ].to_vec(), + }, + }, + ), + Value( + ValueField { + decoder: BooleanDecoder, + name: "m_bVisibleinPVS".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bVisibleinPVS".to_string(), + }, + ), + Value( + ValueField { + decoder: BooleanDecoder, + name: "m_bIsPlatform".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bIsPlatform".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_MoveCollide".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_MoveCollide".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_MoveType".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_MoveType".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nSubclassID".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nSubclassID".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flCreateTime".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flCreateTime".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_ubInterpolationFrame".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_ubInterpolationFrame".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_iTeamNum".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_iTeamNum".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_hEffectEntity".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_hEffectEntity".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_fEffects".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_fEffects".to_string(), + }, + ), + Value( + ValueField { + decoder: FloatCoordDecoder, + name: "m_flElasticity".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flElasticity".to_string(), + }, + ), + Value( + ValueField { + decoder: BooleanDecoder, + name: "m_bAnimatedEveryTick".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bAnimatedEveryTick".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flNavIgnoreUntilTime".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flNavIgnoreUntilTime".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nBloodType".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nBloodType".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_nRenderMode".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nRenderMode".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_nRenderFX".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nRenderFX".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_clrRender".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_clrRender".to_string(), + }, + ), + Vector( + VectorField { + field_enum: Box::new(Serializer( + SerializerField { + serializer: super::Serializer { + name: "EntityRenderAttribute_t".to_string(), + fields: [ + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_ID".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_ID".to_string(), + }, + ), + Value( + ValueField { + decoder: VectorNoscaleDecoder, + name: "m_Values".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_Values".to_string(), + }, + ), + ].to_vec(), + }, + }, + )), + decoder: UnsignedDecoder, + }, + ), + Value( + ValueField { + decoder: BooleanDecoder, + name: "m_bRenderToCubemaps".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bRenderToCubemaps".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_nInteractsAs".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nInteractsAs".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_nInteractsWith".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nInteractsWith".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_nInteractsExclude".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nInteractsExclude".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nEntityId".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nEntityId".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nOwnerId".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nOwnerId".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nHierarchyId".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nHierarchyId".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nCollisionGroup".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nCollisionGroup".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nCollisionFunctionMask".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nCollisionFunctionMask".to_string(), + }, + ), + Value( + ValueField { + decoder: VectorNoscaleDecoder, + name: "m_vecMins".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vecMins".to_string(), + }, + ), + Value( + ValueField { + decoder: VectorNoscaleDecoder, + name: "m_vecMaxs".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vecMaxs".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_usSolidFlags".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_usSolidFlags".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_nSolidType".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nSolidType".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_triggerBloat".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_triggerBloat".to_string(), + }, + ), + Value( + ValueField { + decoder: Unsigned64Decoder, + name: "m_nSurroundType".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nSurroundType".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_CollisionGroup".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_CollisionGroup".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nEnablePhysics".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nEnablePhysics".to_string(), + }, + ), + Value( + ValueField { + decoder: VectorNoscaleDecoder, + name: "m_vecSpecifiedSurroundingMins".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vecSpecifiedSurroundingMins".to_string(), + }, + ), + Value( + ValueField { + decoder: VectorNoscaleDecoder, + name: "m_vecSpecifiedSurroundingMaxs".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vecSpecifiedSurroundingMaxs".to_string(), + }, + ), + Value( + ValueField { + decoder: VectorNoscaleDecoder, + name: "m_vCapsuleCenter1".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vCapsuleCenter1".to_string(), + }, + ), + Value( + ValueField { + decoder: VectorNoscaleDecoder, + name: "m_vCapsuleCenter2".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vCapsuleCenter2".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flCapsuleRadius".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flCapsuleRadius".to_string(), + }, + ), + Value( + ValueField { + decoder: SignedDecoder, + name: "m_iGlowType".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_iGlowType".to_string(), + }, + ), + Value( + ValueField { + decoder: SignedDecoder, + name: "m_iGlowTeam".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_iGlowTeam".to_string(), + }, + ), + Value( + ValueField { + decoder: SignedDecoder, + name: "m_nGlowRange".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nGlowRange".to_string(), + }, + ), + Value( + ValueField { + decoder: SignedDecoder, + name: "m_nGlowRangeMin".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nGlowRangeMin".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_glowColorOverride".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_glowColorOverride".to_string(), + }, + ), + Value( + ValueField { + decoder: BooleanDecoder, + name: "m_bFlashing".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bFlashing".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flGlowTime".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flGlowTime".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flGlowStartTime".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flGlowStartTime".to_string(), + }, + ), + Value( + ValueField { + decoder: BooleanDecoder, + name: "m_bEligibleForScreenHighlight".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bEligibleForScreenHighlight".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flGlowBackfaceMult".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flGlowBackfaceMult".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_fadeMinDist".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_fadeMinDist".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_fadeMaxDist".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_fadeMaxDist".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flFadeScale".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flFadeScale".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flShadowStrength".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flShadowStrength".to_string(), + }, + ), + Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_nObjectCulling".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nObjectCulling".to_string(), + }, + ), + Value( + ValueField { + decoder: SignedDecoder, + name: "m_nAddDecal".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_nAddDecal".to_string(), + }, + ), + Value( + ValueField { + decoder: VectorNoscaleDecoder, + name: "m_vDecalPosition".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vDecalPosition".to_string(), + }, + ), + Value( + ValueField { + decoder: VectorNoscaleDecoder, + name: "m_vDecalForwardAxis".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_vDecalForwardAxis".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flDecalHealBloodRate".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flDecalHealBloodRate".to_string(), + }, + ), + Value( + ValueField { + decoder: NoscaleDecoder, + name: "m_flDecalHealHeightRate".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_flDecalHealHeightRate".to_string(), + }, + ), + Vector( + VectorField { + field_enum: Box::new(Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_ConfigEntitiesToPropagateMaterialDecalsTo".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_ConfigEntitiesToPropagateMaterialDecalsTo".to_string(), + }, + )), + decoder: UnsignedDecoder, + }, + ), + Array( + ArrayField { + field_enum: Box::new(Value( + ValueField { + decoder: UnsignedDecoder, + name: "m_bvDisabledHitGroups".to_string(), + should_parse: false, + prop_id: 0, + full_name: "None m_bvDisabledHitGroups".to_string(), + }, + )), + length: 1, + }, + ), + Pointer( + PointerField { + decoder: BooleanDecoder, + serializer: super::Serializer { + name: "CRenderComponent".to_string(), + fields: [].to_vec(), + }, + }, + ), + ].to_vec() + }; + + assert_eq!(&expected_parser, cworld_parser); + } +} diff --git a/src/parser/variant.rs b/src/parser/variant.rs new file mode 100644 index 0000000..ec4aba9 --- /dev/null +++ b/src/parser/variant.rs @@ -0,0 +1,26 @@ +#[derive(Debug, Clone, PartialEq)] +pub enum Variant { + Bool(bool), + U32(u32), + I32(i32), + I16(i16), + F32(f32), + U64(u64), + U8(u8), + String(String), + VecXY([f32; 2]), + VecXYZ([f32; 3]), + // Todo change to Vec + StringVec(Vec), + U32Vec(Vec), + U64Vec(Vec), + Stickers(Vec), +} +#[derive(Debug, Clone, PartialEq)] +pub struct Sticker { + pub name: String, + pub wear: f32, + pub id: u32, + pub x: f32, + pub y: f32, +} diff --git a/testfiles/ancient_sendtables.b b/testfiles/ancient_sendtables.b new file mode 100644 index 0000000000000000000000000000000000000000..2142484274d02ab4c2d7e478005ecb4a3b27bad6 GIT binary patch literal 99653 zcmeFabzofAl|SD1hTgn!5@nh;rzxfFLN|qt9cT*Mtt{KIzO`f5QXJ^F>yD(6H0#lf zIx~{3d@0P#%*@;{Gcz+YGyXoGbMFIzAzRmR_NRZOchBW>&pmX{z2mR!eXZ$h@-=b9 zm4qbywC5n5^1D43|I?BYPqN}mPA14ova+lqO|q)w6407rCZ0X2Wj%@A7HjZrT$aaow@5m00 z?C8i&j_mBnE{^Q#$Zn48?#LdF?CHo}j_mEoK920`$bOFO@5lj;9O%eFjvVaBA&wmC z$YG8g?#L029O=kWjvVdCF^(MT$Z?Jw@5l*`oao3&j-2esDUO`#$Z3w8?#LOAoax9} zj-2htIgXs`$a#*O@5o;rxxkSN9l6MniygVdkxL!9%#q6-xx(>(>Se}U)v?xceg8Pu z^THLl(iyP;%NPAuIdZil*En*mBiA`{y(2d`a-$WaEBeywnyCZiva;GDA zIdZon_c(H|BlkISzatMg@}MIRIr6Y0k2vzEBab=qxFb(E@}whAIr6k4&p7g|BhNYV zydy6-@}eUzIr6e2uQ>9mBdfmBU`$%l`C7jvW+X-y0V=s+q<%ZYx&WRuI%K>&aS_=ObY(AB_I70-SN3&fKUem52puefw=_`_w6w<-qv6{0mAw}h0nhvVf#Y=6m=Wl;JX@9T=ugIsRk-$7?- zm4jV5#Fax`In0&AT{*&~s1D{JN4j#9D@VI>jO&-cVqD8&j&)+~n-we_I zS6433$zJHnMXp?IcE}~}ilnxU;OCdRa+xca2aM_pSFUvBDp#&{%u&u6N}I zS8jCWCRc8D<(5S*n%wHjZLZwz${nuU>B?QM-0jLeuH5U&eXiW^$^))E=*mN`JnYIN zt~~0>W3D{z$`h_U>B>{C|Gwj`9^&)ApVn(O?O5umj}0v0%vXpF44vs)iyzU`?sBw1 zo^j>bKxF+2+#vxY?3*yZzMx{^uj#E3&gwZA2Gksw2J*ZsFSzoeD=)eJYA_Qm(;V{QH(c;}VOWZ_1{7P@4hg|PRLTx^qHy3Ajj$_mAP+ zmSk|tt_j&KA-gAJkA&=*ki8PJcS80__}_wk57+D{`yK}UkG+3F`}-zjzl7|ckOL9| zN8^DBIVd3qC*+WX9GZ~B5^{LLKf&?-IULHEg5XO~Suofm5^`ihj!MYU0nU=%kKt0! zc>jXo9+QBIVU0KCgi+?oR1C?azR2aOvptExi}%0B;?YBT$Yf_6LLjDt_1cHa&?39$9ld?-vc1;4UKLg%&OZta7-p}!6jwhB# zn!6`ukEHCGl)aL&cT)CA%Dze2FDd&c<$$Cdn3RK(a&S@(Ny?!~IV>rMC*_Ew9GR4( zl5%uXj!DX~NjWYl$0y~4q@0+Plag|BQcg+AsYy94DW@mpjHH~Il(Uj@c2dsK@%`%x zKEAbA@bOJt!N)gg1s~r!EAsgM$y+xZ-k4UfoST&Ml5qL17nWPnHurmAmM0hQv|Bbu z1HQLDMsz~<_tT$7Y*lX4xVr=Gs+ zlX62+ZcNHeNx3;Gwq zEY)uTxj!inB;~=R{~hn&k$fmA4=3f3q&%9G$CC1RQl3c4lce3>A@`}IJe`zhlJaa) zo=eK}NqHeDFDB)sq`aJzSCaB-QeI2?)4XOBemyB~B<0Pdyp@!vkob)(-eAwEA|*$rBpN-jvrg(n`=^XZAz|7$@M9@Atg7aC2PRT7Pxiy8I zwjp*}(wnY3xfNOewVnJQ-v2<|+b|y8%(tiHj+ES)lDkrJcS`O_$-ODLFD3V<=x3uh@mOaw4XIl13%id|(CoTJ?WxurSpOypC za$s5xO3T4%IV3HIrsc4-9G;dV(sE>4j!MhXX*nh>$EM}Dv>cz76Vh^GT24yK$!R$y zEvKgCw6vU_mNU|FW?Ifl%h_o;CoSiu<-D|MQOP>EtjO_QcNl> zm#5{5v|O2%tI~3HTCPdUwQ0F7E!U^z2Fx@qH>KrfH1g%~)?3nYYg%qg4?A*6dhOVC zue;vH5y5@ch%7wDf^fTiNmTVEE-*kZApDNB+?kfU(sFlN?n%qNX}K>g_owB7 zw7<5u2@15zgK2pPLAYLYIJ~!9Z}V}SE+tN%OUv_Vc_A$?rsbuyyquO-((-CrUQ5gCX?Y_p zZ>Hs~w7eZima$Hkc@}lIQCV?S6nZ(ibnrA$NP^Jcd+jV5s6FhQr~w}_;EPVdH=td z=xL7k=WwE5@-F!#EuW_4v$TAkmMt@~RYtbX$Tk_-HY3|*YzV?cuMiG*NnR(}J|jD1 z{JPh%^i_7u$W9sAIU~DdWY>)BmXX~vvPVYt%*b9D**hcqWMtor?3a=KGjc#i4$R0w z896v3hh*f?j2xDc!!vS3#xHoCOW&#K?#PTBm64-|_hPy@M!P#UBgbXr_>7!jyXg9R zckv%yap}7_F(W5s=WbZpp~4 z8M!SZw`b&zjNF-#yE1ZjM()YTy&1VL`gMm|#9e4LR_ zGV*CgKFi4G86MIa0k)&#^@XEeB86`0$yT0h?a4NtfD3Qy$#$M>@5v6H?C8l(p6u*# z5AWj1uAc1X$?l%);mMw!?B&Vcp6ugsd++PXexB^_$pM}m=*dBz9PG&&Z2qT(#@i$0Lvy zAAxkECpXz8S!ARMtk%XjLYK4Eic3HPK>AnOYSo<{UUTqgc!QYH&7R!i$*rE;=E?1z z+~LWcp4{ch-Jaaz$-SQ3=gIw^JmATLo;>8q!=60i$)lb;=E>upJmJZco;>CGhdbVa zkSfRIc3(nZ&eNVe6L2^Lwl7@#0GFSIZg}q6P`eVNkEx&aaKU%1^Xc{JWf%(Q@SHcs z%_m%OS$@-a-jf$RdC`-XJbBrZR{~Vydf67?Fvp~Y4m{(l!g zy5&cGW~1-vc>hmF^pVHtjgdDbPFVOUMS=m18zwLV4 zMU}=?>cy+n%j(?XPRHAxSpV;&0>0;XJ4BQep0=BVffXy5?ES#q{-HhqU_$bD$jXjc zL8SUl_?wkova)MdcFW4{S=l2iduC;?tn8hYeX=&%AvpK;k9NErqlvF@2O)+>ALMvD zMS~oZM}G-AwHX_D(9Y4=#(B{4;X$W3-YyZ=$E1kmr`s&ir}R^A*J!Xy5}SAwXSXj< zdWyIE(w7$8+1)oQ`(v?;8fmRO5%Ar{(!u|%{o?oEaVOcpm zD@SDI$gKZ>>+Ka)9p5&K-!?~O<>;&&la*t$0x_F5OFu3v$6GPv z-{^Y#L_-~?g2hw8;+Oq;$J>{<#G6znWbK<&CuaG^)Ja)6IV-1R<+GOQ|tX!Cti?VWYRxZiPrCGTwE0<^GimY6Tou8Gf zvvN(A&sE{gt9HJ9buIeL%Jo^fAuBhMImk`fFz*Kp%>gciOx~i_kZXe$id!+ zWp?p$!~I!#02G>)hqCf;RvrPtxLo~%R=gQuR0pqkquR_nBpTF`6bpGYE01B%X61>j zJeifJunJjuCM(ZEsK6M{$_rU}F)J@+<>f3{^Py4OpYPVRBMBZ&2{wCm)Rz=;sXeE!rsGNPp=k+*+_>m3yp|B}T!^pq9w z9nV)G3A6HgpeFy)JDLdlFGL@~m^>!J^%9q6ws-8(mzFoO@@7`v$}+O)?JQ%U-pR_l zS$Quj?`P$MtbCaD!TOGin*5R%+ps=_qa4p6aT6}$5zFQ3O&ax*FW#*AC|G(7_Jps@ zVBt(WFI-F?}^mpy&i z%a^@<*~gcCec8{K{e3yWmjit{$d`kCJNe&wr+$IqAL7fQz8nTb2Io%mz0)SNoaW2vzMSF9nJDJ_M>yW; zQEy)|H-zXr+m~~EIoFr-0(3s`tp?TpXGSxet{nTzPu9a{4^*XkHq`(tS`^$Zh-Ue zd0$@e<;4JnH#**V(U`|%Im=bsF|UTc%P3<%bUKu<;&Y(@pvN1 zmv?=6&zJXo`5>4J&H>L~@#bRG3MjgNUBcLw--(nN(hs}~q7jYxri_1y<6X!yE3z(Z z-fR2Nmydk;*q2Xy`7|I1&%21D|IGJKcf5%2IoU2J+XsU`$@MOc20tcKS$@R6Vw&)w@lIcO$_BRvzHr_s+Q7SvDBK|@JLY7k zoa~&FU2?K(PIk-5?m5{bCwt~(ubk|ilYMfsZ%+2h$^JPxAm=xGmq*B7l2nufb8=8l z4$jFTIXN^Zhvnq(93!W$h}sx;|1BT)A0nr&B-X#u?mv|+QSf{Ro){ZR-tix1V*K{s z`lC#@Io?$P-j2wP2=+EVJF;ALm_>MOj?BqXIXOBf$K>SLoE(>v<8yLCPEO3pNjW(= zC#MAbb9=|TI@*3q+`^~k%E{R|IVUIQ=H$GboR3y>azRcm%*jPL zxi}}6vM8LPHt2_a7{FjaXzqo_`uVS zcWpq=U)f*MxhW?%=j4{0+?tcya&o(+?T9n{+Pf|yttHYp?1DRTa%WEN%E{e1NXP5{ zE0y13An^@fpyoX}xi<$d`Hf3o2R+=h^rgl0u$p%>>)e-<`*ZR@P9Dt3LplFny<5IO zOAoWctzV!5u<>i}wl7d2e6jeEoIILC<=g+3%6I%NmGAspD&O_DRKELfseI3Wz4HE! zcW+etOIE5M%kkY5V@ICrc=xR^1A9CtPvqpuoII7|FMm9plV@`BY)+ob$@4iALJ|%8 z{;%M$CwdQX)N|y8oV=KmmoUwoypoewbMjh_*Xv);$s0L&6Pyw^@9`@qImT){7|nEC ziZ5P@8wGYzuQGkzdx)qU_R5=&kjjT61b@j}^X;6xlaqII@*ehRPCm%VhdKEuP%Wo; zk3=I~qSXC3C!e69z|Hn)PCm=Q1Lq&=c#lTSj5*=us&N~y;pf35;o5#|#T(w16J)Ci zvh@VnW`b-xLAIM9+fR@kCdiHxWTy$T^90#tg6ulMawqsQA7AmNU>H7pnNKWXY|GER zV}=BW#goyH##!9rS=_RUSGwHso+8%2LJkYV-NVqNUwcnSWV1wexZ4ETeS+*ULH3*= zdrjcwsArCgK<9!Ki3N9rfzNL|KA1uOk@9H~o- z)Frag&&F}Nw76V)R{HrkQkOAO?{&Q`T?Ar{|B~<*`X%ALZM5Yg-%5otwbeLYmp!p= zJ&x2bjMROWf>^g1$Lkly>k@T=eYd7#m# zR`RQ90;B0DG+KHKwL)j3Sj&rnwryA|RtjCk*|q9_7CRfIaxtIPoGI06E&ui5LzrDH zRT_EIxE+NlJgr<@n4h5aYv0`d9-PYA@7!ijDkAEt6}-LT4}0)ldruh2i43s|TP<-Y^itd6=;@4Vn6qVIU_#&xA~CHytw86!5uuqJ0#2aA1RKVg%g zi#EHNW@Dxy{JZfM!!d{1tHiQ(Lva;H?$dd#6+>E;;!s%g`}``_ng<%aSl&jVQ5r0o z9oSZ`Hn3O!aqN}`G3sh9YG+mSSFFt;;@Z$k9W}XPqsGvz^xqB^Ha|YUm0G40`U*Y8 zImOOutteYRCSwd41%@hx zzEY==sS|>va`T+31y<;-qzUv>Q_df#2UAM*dbK=Afny3zD}xz>4NR>JQv5;`bZOgY zv7=~tHX26mKt~ar12QB^Os*~*mY!DaflNU@#$6~ws_SQ?P%6iSsX|Bf(ZsT|3$;e6 zvs~P;TwP#l+qoCJ+KUSt12tk6<=Q%H#bRX^wlyR*&_~exA&3inm@z{w>cnVb-L5_= z)6ULg2Dok)+MZUb6ii<4@U&9BH=pLeMx9wG(%S)%kFkc| zvDI3+W%v-9kghiBjbgtY7uy)9bWW}fpxu@98##l;A?P*V)Z|o3m0GF0RO|{DgH>h} zx}d&c6`7b>t(3s4pmmtkU~vZ4u~4(kbFbEH^K+`?fEckUt85B?N)=mn6LJV-l8>qc z#dIJuC$vCs>?(mJa*1;-(+icZaL8KdS4!Pf%9YMzTDAd90ZplxCKnICDHY0~cNq>C%4-fO z$>avDRr|PvAY?_A?a^ZU>!yXdh4KJ&12EWr-GAC_*c>(vw9*E!0;{8r$yr{oatlOP zXQ^M+8YtZ;7EezuSINns+lLY^lwL#+_8v{B7*DW9-poP;l9=ozu===7SBs}$BEtA6 zb^-rTX&PqbS8Cyc{=PWWjV(nP(}pcfxs8nkLD5#M)T_0CtYmmPMqJ-&Y3nUCz=$>k zgRSGc)@6-Q^ae(67i*;!<;y{Ps}D=Yn)`N(+AGa4eauj1)wsOHE|T0gM`pJ5_ZA^% zzG2`~sRlMTPo8Wi0WFXkpn@%EwHOGtkcIw8%N1Fj#1G4;!tGM3ZB%S5sMh8eyP#v( zk!fpDDcb_(6Lw_=Wix;{W(4+EY;@llku%V!&Z$=WhBp}0i^gh)5zC-Bw6(rByv;=o zGpMYDt2z)?F+NPqux(=Hz@p0Z3rvOF%^(TYocz*!1Phc;wzR2!)J|DJuY+n(tK*+) z1MH`ZUX4OSwVbcFv_p1a6GfbE5}36$t0xashYFR>A#`IQ0ab-X0Po)f%o)ULi`bpE z64RPReE`;WJQ<_4Ra)j&8^x%5>;V<2M(qXp8qKeHpeOp?WHw8wQ($sw3vA;c)w207 z)+>amsSropQ>NrJ$gDc%W>YX~+DL3+{)X$cZ8#CrF!>u6%B6DioG6!^AT;d<+>A7%EIish~-vu(q*HwaVhAgfw9g!W=*G}Mh}&R%ytC?}?ijoW62 z*a*7abmqjhC$1Y}jLOWMV*fxHoDX_|vh;j<03v7N`uT77%`1hzwkgGK80Yjnw8AyF zF#olDrn_7%G}f7vUkT;r6c(tEK(eWCUU#{bWea76zh}f4-Em%Icizh zHpQR!%^NIsVr%w5TTHRo!n`fNnMp$xH}y9+=`A2RLw5Iv0Z8f67R9bYPmkA)?VtP&rnwtRpDKzH3-YEsy!6{GOe@_ z>dg=gqx{O`b~E7TqS}T^t6;_BAkzvJA6m|Hpv_~0Y}_cDg>Hu#Qz3d`Lqf!1s3o{F zVOs4AGLrI4sGLBa+s*zqB;v^1njaJYDINoO|3I20xxa| z>#sX}2*+}VkwZoaH<)rz_1nn!RQu7 zE$-+d!xnN8O~e;<5k-cr-{@XOtkf8dj9ip|9NoxRxc}$T#m2@jXtXj6q3ZXe{Hla_ zA0r*A|#o=V|*eo+($ZLv_PcUNnDAQ!NjFj#5#@0;&w3xN|Fy=*vwOkLPn9)RzNvkd5qt6(;6 zYZq4mLfL94NJAfj8fCjN;v14a0@3?P)j*Yow7~}{6;s5ySDhV&)dP58q0Vfe-c|*H z!ix*{T~5C(QF7_RG=~J0nTNDkPN=b~w?WCKnF}@As2H{yxkiwuBmiWDotv=}Jw{^* z?9lJ(Sljxcjnd;A)JNMo{>5Z-8bHrrOQFAxQ`MFt7i!{Y&?KW&+G2Cy(}ghryp-rt z)Nw*PwtyZQ-T5qZPR%8r^7>_UIN3KSz|KP+HRZ;(;rA)(v91 zZL`$rhbRUoR1*=3M->JrN*LBS9k{~DCRLvfax+y;H}3C3fk1)AF2tt6 z9AVLL?ezKzUYsElvCQYQToZImEFL2nQuZELZo6Kr8)3_^ixHu z4k{Oc?oP&wMGN^!;FVO-h?o}yh!g@|)6aU~hz&>*9?fDr+7v)a)mWmEdgw5DI`=P$|h%E|1j0u*$Ljzfc%2IDz)Nd zN6(bQiKD?_k!Ts?tTN=|zgcs1kdILw!tvJg|FX!@V-jJx8QdG|#{d?QcB1$f+9mdM zF|1n8XL0me`{Ts*C;o}kuW!*Z0OILWLlHPN-he_`HI+>VN?n{e$bJ-D5k&4ktzZvz zapu(ceaTp`0g$?wv0wuT{4DTa$Li6Tg!(j})Z1Ow!#QhuN4$i}-QhpF9WeNQxbQJgB3F@NC;H(A`(B#s5@ z@Y57I6G&wMl67b{LY>N%*5FvLmdeI61D@Mmjsws{$K-)py`fE5M?&X(bPCK=Kw=FB zesMO~T3xECAl8PhjLL(O)T!*c71{>GT9>j0^9juk)L+&>GPr}RQ(_AEPCs0)#!rE< zLE)QTfZ*vOe^w1JaE#5WP#N!5l_-FtB(#uHKrvQL>%i^?9hGKc9+)-u zO)LZDJM0KH0(~F|K#vS1Ql*2_cOJ-_UDFHWYtSgvWbv%7${X=YmA?(Acq*H??OWmh zXd|}{mZt>GacWC^C%61)Q7@2_3r8xI}v3)z%n7=%4yfZAFgLLIW?}B8|eZJy0k%<{t)%12*^~)~?b+ z)R}T7#XtSyeWwFFn!K(HgKsym=i2fePaS4@sJLoK}%0TNg!DzYFZ1sFm?mF_asNrZXz z^eVlf-NEKEl#L2Fx*=5fd%A~%V)RMpDq|$HX?%pLCe1vil_Kku01#hDs{>gHQB_sU zRt1@DfZ8iSJS9`gG}VWf1Rv2=17?B@mIt;w!1Ox7WuYu}P2+(_9gd2U`bP(2S;la| zwCnVoj*&y_0^P@RaC&H1_81w)Rh~~9@A?=8rY>xNqvQB<(fTeCM4Q5BU=*YS$|$Wn zTYYacH1ZXgu5ST0+7G8U$=jf|xNsBIw^bHFv!CBEfwmKusCrKYTZE|AxGB}Uxl3ja z1t9eyQ}X~iXLXa!ad6$nas=5!63)PB9Odbhz8-5~DP2wBX$q)F#SFa-WfN~ko42>U zw^m&Mp$FD7X!Z6`O^m_jLO!cU0(Ic`A}QDN|F-0f-->=|HJT0?$%*oN|(t+p8k+Zik-tUEisS{6g$IF0tvv!#%AR}L3K#-B2RICuH|ec zlnOJ{0Mnk}anG+YoI;F-W`#Ng2Tp(xTTNfk?<>^>6{-j`5$B`f!K<0VU;q`!1&9JY zg#cBRLQtUGPTJs4+-{}%7N(ABw^&L9uN3;Ayl1+30E3eWD*RK5b;ysdX|v{1d|EX=DxSPy2SqXnx9>AnZ*(Lzg|WHcL#*{fTJJEk%csInbw5^k^& z#hGJ-b8JH?tBT^WvCPm?pmIU%M$nc^WRnXY$2>Kf|GYN^R31k!yg$}?l2!D33StXV4brX$ua z|H~!rUMjb+{D#HJ=q?6M$I4^;dB}ypk0~%r)Ia7*5>p(3LJeu4M1Ccl$LTuZs0iwI z${atBJRJ49U@z$|TNN&&N>>ACGM)2#6nA+al*RxeYT>IW zrIji00mT{W61=6{3wC+qcRD~0V0&<%!@m_4F$N`KLAluQKTO|vik`(!TtA;OogpVc zS3V8BgzWR%bY1taGo@`l!wacCwO93MVH%XAA7XsZxV zX=M}>h`161O(B3n0C=?H1=0NnI zTbJBAhfvlWh?tXP&Q$6AJ7dwgvA(g2*qFU0!GQ+kZX))O2hpx)2D*3j6TH=$3d2!_ zSyf;c@@fwF7S4Z{LJS9fIvygkckaUgM#ZHv9TA6S+xu+Tx*foODnU|U2 zdrD!)@fylusu>o2sX?bci-O118U~we5Nbc%JF9T$q;NHIKt7yxm}aV@eE`97Jd|Va zpd|DU)~&S23sB%&jNY5hFg=n*b;DjTebBHuf1uRu(5~U2sKMSd-e-IwikzH-Bxc*` z48q4Ktzk8qb1R(SAjGm%KnMYMb_avi&>=-15Jy>V0O$mi5m@vvgtcdrH+e`;Az*AX z62aK>#X9_`=3E8)p6k!N&;?5l{-3yng<LK3BaNj^N#1cV4L6@ELRX;&tq-Uw2 zT&*d{6wydLO=Gp))?Lbl8X8V#yG-ocpy{T4CXkZA|QrsgyB8F8FcuW z8d0SLr{tyPn1vExHw&uQE!1)!4zC^Y_~qG!0mQ#hjnhM%il%LY)|5f?%?RG_nW$va@%S0?sxn zJ-VUlyjTdLD8X#6m_3f&(XHj8bsv;xLO2}1RGppZJlT0L*OIy-Ngai?xr}|84u9dI17_`VUQ(zw;|?R z1QdriL*bATeRbv6*oBUO7NIP86XfnDgBTVkb zErO;S?eH34$nmmP^MG8aGZ#J|?A4%$Smx}`hVgH=%$%tQOmG$=HWlZ95w)OcD0L(A zu`5Z{I?vX+`m=#OqmXQv=JV7Z+C` ztk@1@x!%?cB-FZp%Ku10u4`3Q5hn<0+*A*-9DBy<>JIBFce~22Ps+Dk}8@grf<>L6RqTw6{R2!m% zjhA^HGey`z*!yg#K2O^KMdKj2o(Cg}=jleP)tk#;T=QDmxwwnWZqMgz1lk|hfzZG< zpdSGZQ$yXPfanyg5`qNC0r&}(iRG}XjH^6WQYE7ya_krwu^PBC2+_q$w<>*spfF8Y z>IZ$paz-eaEjpJhfL658y3Nrsys8EnRP8V$1P^p6KCnirjWhI|3L`=b7m+&)wi75? z_jIf_D`GXEAuxxB9WVmxaMF_Tx3tf|A#a_d8<7*Ta8rN>3p$?W4#Z9&LZ(nF57`wC z?qQQ}=)JPFh^v0$Gz4+wO#@0=UT-G9If+03$6#@w82FFb|@LNRzUeZ zfU2Legi|llXD7}#K(>bKlgLUW)i!JoT__zZuwb~4$|SCQE6raG;6o4ejX@@j1amMU z&`Mo$H609^G{|I$h};3T$e>afl*Eyd#zfTMk}DcH9kxPht0|8lhYM;T%7xaniRBG$ zJdukkm5(T8HiG7&jj(0g#uVI}m`L_HR>q`7WvoM%6%GfXx^;3??3=I^)2qQNQ56ho zqw357yd@EF1>j^xu?NSVx*$O@Q<0RY5M`wL1;XUPAeuDtQ**5F!1Y>ElO-;Yk`kAp z9Xd;c6@|)aBmXF|Dzj<`-N9W|P+Q&TWhyz~#$SlzY!VG7ha-GO1V9LciJ!;L)AfM; z$HOxhbEJ;3!LvPBtZSp#$WdxktN$`0ck#_G&@<(r*(fzuv%_*1(=6k*L8JraE)(Bo zO#0?ZS8!rY!9<5S{0Y`B2cQ(ZC7_A|SblE+XgAMS0Kp~tP`NAdUssX|m=Uw)D$N5; zy_@5VH9WM{EH%cFg<~49PqipR77F5L~Qj-lDQ!i9lBOyv*ikiG~s(fYuP7TDa z;#NaN+3$n_eX$2@zO`#1DSe=#%#b?5%wiWzghB<;8-HB?PrRT}Lqg9>AzYv#709V) z4|J4q^B>A(IsZct^QbJu3xJZk>NAYT0J`3y%Rn~a`erTv4~r`_jTa@m%0n765bJK! zeuUtZ%4R74-Vz2ajTL-K;#|fkQ1t6ZA*3mPI#4$S&yC9>;k3+v}+*Xq{AV^XPp5q zq{{*0xzO?kZCDaSWMan{)?t7(?id3=6b~*cbH_mavHUjLk3n(pno)UCO>EiVjLue2 zVNLDG9^HjyH3N%c%P}^MgA4N&K-K{Z2mwy!a0MO;GAAkqoIAXB>V_yWK*-$SG+~l# z36}4y26m9*JwIV8v8Kv5T$-pq-zL?ofs4;ww&u^4KnZ5N7$)M)K?PWJ12H;AC8efc+YjU5ixGKXxAO zNsQ*mcAimvP)@&@djR8kTuEg#$b_PV3~zw`A&#KAV*tK%xGq&a-J+#dt=D@I>(c@e zs8(kQv2{@PdZ?CTb*Vi#Km23&oK4Yeae2y_*iopgM`^rZ1fTxzThk51A1WMhq(HL}Nv zIMHPMnw5_+rHe{H>=YwoKouUE+Ie(iVSc&hK?|++gz>1&3=pqgivb3b}iy1DXT|^ z0?}L>jPnM}2iyVj&Tv_vp=0J?e_$A4yBCqPb|Php>IjIFg_=M*3z9SE&aUfr&<|jL zpu)X2STueI(3^e)deN_bWt-&s4a>1`s8<7)KHKpSkS>`yUjhmlUEgWqD7e$1R1@VL zaM;+ARtM$^tK=hI>~S?_q$_~q7o7i+Of<7l#~NY3;w_a3G`(2v!!@k+7Jk$CVAg_%jmlyAJIrfOb&dUA?YC6=7ufG$-&cBEt6X?Gt42?QW)5R zFcpjsNYBnp+$-2pFg47bJHSqh6V+5S(cQuey4nN*>i0I*8j~0)eGfeEmvtWyK@%v~Ac8YVoO^2nd-g35aJ;Wn6%Qdx+)-vJqAf z0Wg4P0x>W9i3;FUCWJ=wAhQ@-0GdXYJv-)!6z6ZW0R03JaO!B18JsW;oG=ls9@mrE z!yx8HA#3WiaQ{)oGNlrzL5(3J7n-|)ElM=p_QS(6VNi<37X_vq4tCf16SV>j*-`5* zJY%4`68hLdG`;%D5)MjnI!X@>4W8j3QVZG9K*gFR*cjN;xGsPm;8H6N8bamlnX;6O zx5e}r)C^IhWYp)dguseLyyu3k3*u1=VDx)$(80;~Leoh}8ypv-q&=p~f>RM3YjBm; zTfA~*ofP&tIc-62!2wqbjQ_%B)k&EnKoni-F_ zG2I?mw*tJJd@YN&E^O&AkjS7VP_j%Q(Qp_a7BW5!u5(lpE7Ko`;5Mr*r@sTEVw}h%AVc5N0RKajLla6s*xv;W^7inT}Cg;u#~rf915krSR5mkW*C5i zA_n6=A$Q}@gK~r+^g%a(>ISWK;BscMGJsGAjrBC~le6^JMr-9i2XK>(?AYEmGkeHf zRRiS@gQ{DQs>^V27-wtNb$z%I;pwe!F|4lH5W2!t4~FG&d(krHJc6c4pSbmJ0=_^K zQ=vfN{vy1-yDlM`>@L$o*hj~q;S`&yKW`*-voWEY8*v=AKo_s>i25dlvYIx* z!;?l!0X0B@sjHKwSTisc-*u?eh=frN5B`o5e)@tf!&e21Y!JhRriKXHjt(RPL5KIO z%%;}!B4ohvYPL7X97kiNQSjLL5zyCNp4wMx=%KYD1t4tVjjBek<2z!UWn~jQi`*M- zVc@vLjZr5jH}MvsMgkGQ3%we|gH2Rw1ZSie}@BQiqJSNk+J-Kx_lC!;%rXIMc&vk_MABpoGE@8c`rPI$lrr1cbCf zEM5WY>?P@8#nkBy${*nb)|f)>2z#L@e1$Jy6@h$&?ge#o-NlP;Xdk1VXocbcm1cAf zoP`~DnB|WQ0Rl_vuw-FuMeOVnRD0EkromXPE&zSBRr`u?VRW9> zDe6n;0OyA4XfIkftF}B0G*1#SXj=`}s7rc&H=6lAkekj<%P;L4Oa;kBkV!YOEN zcP&L6H2JDoD{G@6a8~1>q3#t-Mm@jI7qGNwwrK;Q&L&G zEolpZYvb-zX#IWNY;yQJ__&-6zQxe!0SQ6m(u}KAKupBrG_eOa?iu}xC1c2~38@9K zAl=c@3*?#$HcXqI?A=F9&8Fj*8>y;TaIbxa!`|Pc6tg)U=0^62d@O2EQx^%AUI*Nq z<_+bajg=X@KWUlz(iUgf+`GTu>Z~Vn;fNpjeHO83AIL3{W1-mnO4@sDn3K z7=n{6_-^s&N$>^)WD22jq4A=EoM+8*h{s?WSIDP!?OYydL!M&N*Oj}_*hsf;Faps znlWq2wg-Ho_ zA<~Q?pFdQn1b=3 z0AxJR*ZScY(co#tZu|lz=FgA?T;Mf_OTisEXoR4&L1Q6W?Mz?Ch2lyl^#*@L354C0 zMwj4WcaJ^@4i{A<(J9HzlEObo`xO)$UTmqNNn{$D+NjFoYW0wAR5i0!THB~P2UpZM z1>C4%z=heZx@!y@FaV`zIP$t%%lJ7L0|Sm!*>6WjB=}y-+C$eeTIp@qupdizP!>qi z@Wvvmqq)4GFjTkaqtFWAF(oO{bR_MY4Hw*-=3WFs;U~6q14kJuDXIpvB4SnVn$fJ_AS$?-1rZM4gB~za9Cw%T7z`A|MyWHN zgZdrjrC>Y;pz&?6da8I*G*tE!aT-og=Hyh7!(4fR2nLW|+$+z_z_ZAG~;@yO|f%Hm8C|bqZfP zl?eNK{a%+Xd{>n_9nMd<Z918@Lv3y!~~{EFs6@98Z@R8#TJYL*;}8Ks7m13TkAH{J5O0my1K zd^vdbzyQeK!~DWR!`zjUm|s}PeBDskCc$Myv%#WQB}{R$Fg@7N2~UCj5z3^nKr7E( zjn9SpSq(BcBrYtqTH*xSrJhpi+ms|Q27I$cz&F>E$v_DM7E~d#+Lx7;`XGkV8W^jE z+36`%Z5qRlW3$jK?A&-G2(rbi8WAvtB;9_a8Zo=liuKsT99lY186r&UCb8gQ7$tEU zo)X|u`A3j|uV}{Xxf)Xg*OI-Qggz)JgEE2cN~x<(MO=`?85vlvJ(TD+n?GFB;k*EA zI%pu!oRt+3F|d~TBpE0wxab6)2dSed8mW@#9twuq(EV z_tl338Sfcz?y;*uQwvLss*c{m%;B@f#Ox2PU|#eJfLoOioY1Aw-XOOUHW||-Ltc2+ zZ){q4Xu2ZXlBG6;^&6DZZ{0UGV@eUs#IO!D!t5AClfex&)G8?eK_corWt+S$%+nVt z9C)%zUr^&nU>^m$FNB#*>(9lu!`*iZUh)I%&A_Fs4{a+zpQ?_)#zVy#7(@qMhV*me zQCj0T;HN$6B~M4TAuX^EY-k)1RI19JxWQT3s2#M=%>xZeM1Tiz4%P#eEYtbVzb-ZARy21aEY5&whchxANl0^!rR+JK+Snb%*f z;~M}xp%Vgz2-qS9*n+2s@LyvTqW^HMlPBD;oY;Z}$B=>d!?71R)fZ4RG*O69sz>KxpA zHzNk(I;O!0H$71H!LfqhDzv}Xf$_l}fY)Sz`)exPe@gtuLu~>f@FyU8L zpW{B^?J&hw*SzLH#LHvH-Xk@pK>R4L$?kdawl`g z5km0uMlt5DrkfE&f!S#EIGTZxDH*`Dq_u;TBSB=KoQ0@B0%Wee+Y_3XU^*Z`qm`&0 zMf*i}AX1P)fyD_5H9kDmZ5dN&lc3;1oUDoRpAck^u zhtR)FX>CA;+Z&ep<3(+=;rc2ZrN9PuL`z$n26m%CMoW+!iCgMQstE&u3;0V>N}@IlRCiggW$$Kruhf=kBnm>4}j!gUDnIO~$eka#o{ z0WfRhEXSL0hN6@>qX(+=-Bb*Vy1cnft&|!wX4W>cGX|MB3NndCBc0bchb9_e=k}IJ zJA9?96Xa{23pShDs3D{!la6+XePq~rZOBrr!vOq5UkIbl7raDF9@z)4De?Ty*pVH1pqe;0hP5jWX zyg*miZ1DOKRfFucDTNl1H*qm}lNOV=j*8)7qltFT+KbDaxVX$oi_6qL$)oEk>-pp( zqNM^0ilh?S911jYlE{IAMr6bQ_N88Gt`^_%awEaEG z{NXIVd{*Z(RN(foFfh&t+D@_s-tg1OV}LO#&NM1+$nv|35MDh0hQu1y5)KLjO#-_n zYANpC1GDUCUchq{%s?-4Q%mJUb#^W5B632X21lHfXAPJD_wQ$5W3LJ} zga}s7;op6ep=fTmhw!%kIg@C|)I$&z$}Z^q!TJU*>;$1P%IIX~@)_=&I(s<1l$ z7KEITVzgOi(v4+$Ba7gx`zF450|YJLw3{P@m$U~K41Y5fWWb=F;3q9Z`(K?k;D>>P zqHa*a13pmTU>d0MOO|4sqOnZqn>MZ|5;io>I?$FOG2%fPEqs*Bi4;h#Yj)|#Z4a6i8 zf@gZvB{C4oBm=_aP*ZP} z()h5j5LVv{9{Sg-23RtT)chy_v<}|*0dhzy_D3Yx15Y-1BBN$+hngaKP7Lzzpaa-w zdhOM~z{8}2cgJRy`ZvT`5K8Kkm;BKWjR-MT2B`(j@|rvA5;t&LGgKR_0*W@R5M?V2 zdkeXH~o-;-R?WKlsy6{Y{zX-02lP`2tqL{$B0I#}ZjIktkqf#z~El}Bo z=lF;)b96F`uV|5Xi3Wx7wH^MLsL~DD8)Dx;ThPrwN6PL{wO32=q{FZ&^2QKEf3vDg z=oYqfpRt+{hivp4>yO1D2Yx}W2hNeYIapPLW9o32Syo)2tQGMZD1RX||E*!8gnYBZ z!Z3jYDm3wNnxwKvnsRFlt317cXDvGGc9N96F-ZQ~!^+M?s8^*i5In4g3B!boKxV@P z5NM4iZVw}Zf{If_ZEHmL;7_`;-NBgGz1Tbrys8P`8-9ibw2z6y zZV1#k4pE~W=ChJ_My_!K8iYZE`F63KX8XBgq8@nlo_pB9@VlDQJ@mNy9PTN(Nfs0SZ{F1 zhIM$s0XH(o2)Ws?C2$W~&0Sj)W(hrMO4QR9J={_y=r?kbogbglz?c)p^mG^p$(?yka; zvQDHfcbg^EN!deW$VuQq>6W~-S9!Y@;P4QbhGoYH@_BrXzAxa=EpKuL$y7U}5H!gLfvvF5A(AU{=;^x({hEY)j&wg_8hI3Z3e z7Z<9x8;GEuRIRDyi54rWUO>6UkPH^VY1KbhhFXP*!~Cw{hkfe$TNfZkA;PY8kfbJ0H(7@^)huIfYXeEd2{vbXA&N9P_*_h@a}!**D5Y=xLW~;-U$ih))coti?}`TJO2H8nMgEi$u0Dl% zD%Zgi?T_v;IjGsjmW4HJlJ=c+=#69_8aQAOVSa#n9YTSYj7WknWMTNT= z){-hXP?iVT1pk9Wn*UItu*OWQ0~g_C2vP0=pd~Eyb{xkIygWQ!`CT2w`aYgA+2CFDFb;B`0 zH{y9gSO$w>Jl)D+O)=0Gf@T?^Kx-8#{EFSIL5=(%iDL0$CsmjiSFrp8L3? zuYjLMp=Aj+iLP-*$iX87t$-g-(1Qz2Y8(?`MujrTE;R+ISJXB)WkC%Pi#YMZ2jH{Y zI2>vsDj1=vDp&0df?E_oTD6hhRegrPt4;zN3k-VGRZ}Kx;%pTCT-HD>VBOI`)P!t` zl#%8x=((z-1S$!xZ!9uf6%*_eOsR~=FRT^MvGk01kRHbeFnec|aOZ_nH%Z*1>IWlb zc{rx{YaONlTD0k)1aNJtnCn%FdPJ7Nvg2=0>j-R7C_H!y9vlN3fHp_LJk+GZs9A$* zAuY>gI7ac(khW&z6lD9DHB?i!8!FabL}gH+D1Sj^;8s(L(6Z~ij*PW6XD`4x-~Ygg zLTkM~U^EI^R3(iggZHz&s;?W^pSr;%7*Mu4JOh-1NK*&9Ykb2y#BDm%l~sQBwXbbC zWo?I54zu9Hhl7u>;3J2Fk7AQYyG_Tq(~ouke4M++s^0N#(+RG84e8k@x_>^&PUU2d z{#cei#cev(oqn48=hK;WhTC)|vre<+&a#QDcedMf4$G~vI!Ae~E8ah1lviM=ziVDQ z#f+?Xp4)Ujdp4hcwVxNbO&7Y;FCrB9x!8VQ;x=7sKQD8eE{_UaVXL9+l~H3?+0U!p zrfclywQkdO?)2-~?hS6!jrQ{-DeY7?|!%GfvBAa?I-$t$bO>FhwUf&d?YISsQpBrkLjoO`FPk3`g|fx zpwB1ceLiI?gOZ+(KA(v`pS4YbG@lDAg4muXL76pp!BX#wZqrLFqNMz?+w_V}Oz>WH zn_gp~Kfg|DKvQpM>P?$^OH*$%g_7??B{8aZH3hm^$$QUjde7a!{oU8t!%E)!Zqo?we@ZFR?)~HpBgG6%h6TiRlNjFMJ*peI9H-F|lF zcO<6YnK1kBE`AyUzdO-%k7lTpx;HWXK8yVO?I+0Wf#~zW=<}iI^Wo_85&Ma;KdPTA zd5Pc6wk}%SDf-+w`rO5SqVZkr=WfZS-ILSzNMb7`ni3l%0!l+Sd)gxS+)F(A^ao< z(7q3~eQ4sac;fJIRH%PM)YOsD=TZ8pRgaEWJtnMrY_jP%Tj&h8>G%+SLbB;Z+YUZY z(ofyiC-c)R%PGmGQ*B*vg45z1oE}e{5l@^MPn;Ddu(i()6KL(6FoB$NZ9=K#JliGU z=f~lHRk#+pAo{$}egfMUMbL}&Q}J?1*b#cYG)$oN%fbY5E{}J0MO6LDIQ*&zes#R^ zHSxr?@x*oU#Pv3zO#KGi76x)-SO|%m;)$DWVrA;Gw-_GHr? zY|bnjP;sX=p7riZHr=hCc`AJOBxM5i>wkxS9cU!?YW4*0zGTz=K@*C%2W;!g*B;bT z0g1c6i|RjQn+FOXPKqC>?<%4|aF5tl@>H)LwFO#C|H=g(i#o-W9=FAHeourW0R2Cy z{ebl*o1V6xfIg$2TKZXAT6J_-5_#(A*K#~l*G^7q9?{KKw#5 zR8m(CcoVqROLkr_C!1d3XGlA*YDU(3E!p(C{d^1vPwN6DwQOZ%u3SX&URKX`o2cSf2l{j5B+VV#&_;Q8jUkMIVZ|!;E12sh={K58u3X|0e)8eB3Z$8!n#(LrNfmqOcjw!rj@zrW&W(N zyviEQuwth5m_?6Vg1h8uamI>PW9?^0d)#f$5qq?HuHrmFbhL9Q4N}i?4;%x5V@?dVr7hu#8#=t>S%-% z)nL8I-GEnBu$CewdlnY$c4u9Uf} zQUmasAmUtCyrFng@fPuqliOu(sI2s-yfgUk?~*@r-{9|`^Y34{Z?F;~*l^jDk+Sq6 zd`I!FAPnv)-WSAO->BC1K=GjkpMAG0B94K^|h?l1n83{P@@SD(*yz{fq+OLAQA|O1c+$@K%2nq zujL#~Ao$Fcvw>RY3F7?EC-Q-3fyeaH4x5E)zKF<`X>m5(+u?y@NpRrmUn_@ruTz)h z?!h;|4G(lnv#wqYxZl5wFgTbC{UZ7T6TAnP`0D>ueW(bp|fjw{qA zTfSM#w@5krVXGC}q6g;Ac6#KK)~ws%G3{#{*vSK2fp#hGR%{c*INGDQS8<=>e!&A- zcTf;jv@0G`Jgj&`v4dDhj%HnFw)A(Qj}WCxjk*<&DIQlmp?FfUNAZ;6X~kZ}KE-~; zGsHr2HtWu1OW%K;-GEFnWET+G1w?iMkzK&^iWd|wDh?`MQoO7wT@l2zxvHgTb*KWZ z4v1CM1;@N2#bKX$0DtHQXt@s+w6R4&2Fe9J2=jKJH1Sfqy( zhXrw>M-=ZU-W9~GxF>i&>qgb!0kM!g%(_R}(&RAN9%tQ?Z0XlQKFzvk+0t)`36wEmqHYe%jik-Pd9kwcN6GSVv z6S;`DietK01KoZ^5WGCNPqCl?ZT(W9@RkPIg7 zQd0W2uxenWUKSVN74^PKEF{+wcReZn4)!+^xU0PBPct@&Z_$wLyKOsr$ckYRymJ{z z;++fK?pVvaQQ#Tpo`^znKXIc;>9=9+dyu$?N$Kx{e3ZDyN$DSgOz(H51exCN{9};m z{mwrHncm?1bC6FG_cST}OOVeJ_dF^6YmhGz_cAH{dyuaZzasxfDmiyvOAo$D+*|F; zcZ%;7KL}zlj46(*!AHeUYVcX`OX4PooWYg3bOy7Br>4Tc)S{lIngeEf4&y;KY{ZY* z&yaGA_nEOA>CKXIoRVrO=dM*vG@6}rb7;gkbJc!cG{N}7)6M)`x;o(LhKN~)r<;Jd zw-1PW`+&H&4~TnvBE~nKZh-dQe!lD|EHPYQ+ zliCDClj_kUh^?tpg6P51ioJ?`iv5BJc1956`>f!(oEuO)FNpjuC|*7sjLWSW zXPe@7#a0cu!$#R@#V!$HuPFPyyy(Ufw_9ViX{vn(PzAM$xQ7S$ zq91jf;sKuC(?r(M8&iczpPKY5o>4rjc#g;f20X@Z&(7OKFIaKW)-_0f=5WaxU5-Zn z)<})6TBB=LT({yzbjOLkNq4Rrw*(RNHj&$pL*PiG;w;S}GKsl1 z;Jhf%r1?=`ZZ3#b!F{2+FS169typ43trc}5nCnu>3InEIahcSB&g7N7p#m+#z+PT4 zg@_y2M#UyUq}HsZD->4}+1^#Ya>SrmZN(Z9eD+>T5AJ!a^O$~cfNIxks14D3V})z+ zf|ChilZafhxx#IcwxE1#EJv!_q`AQDimhUeKHm|o(K$P{oWH~g^LAIP7aqH#2l}c_ zgYHq>ODy^?F7~C?zu#YhXGrJ+q7RC07k#J#zca$|!xfXVnC)%pQu6yF71(ygIO*^a zegCjakH!>`bf?sdFS5Icbik)X-Rw{<2>6t!yCN?GB48~81y5(f?Q=%aFDNzvo zlqd*(N(8~tg-?kB;!~o4_>>69+VL@$sU074ncDd=7Yx&J<3E%Lf**4Q!H>Cu;Ky7+ z@MA6tjAMSx6^`&@t|0g^R}lP|D+qqf1;H;;k4b0Z9^^PN{8G#REK9y`)N8+dUXJr~ zLe2=AaWbABJQDOsM2x^wM4FrylYG)!;re`edOb(I{j}mqJmWF_9t?Gy{Ysq+itz?w zl^7A{8G$?R7X(rJML`%0Dqd3C%S2xCuXv1i7+2*0hG+V_qiepmu-aYc0VZ(6cEC+5 zZdq~L3S1)f2O~prgd=8{^@j(95jn)sc1M!IzTRD0g&UK5k_>w4zI7O7Ij=Df)Hi-u z#Oxn>hev)CVh}x!0u6p5BA4KsxTh7~h-o~NP*r$s?$LJ^yGAe2VoT;%6}dei6g~m{6?DBX~Of*b7th zrWYteFF=G|0P3$%2hmM$Xl^e?utm)O{~Vvmv z?f+g8?8`ni>bFK`tT-DzQ13bMz^_~fq7lS-D=t`ZF}e@t-6e4!z&npvj%=^6oH<<; zlUDo|H=3a4>tX`Dq1HDQZ;2z8oZHb6v4^yLSjxG+j##5RG~!Bh*JE6l?y166<9<+_ zaier%)(-^n(&3>X5`83yLHAhkiQ-ctSKg8Mp(idv&tjqw&#ib7vqt=v5()T9@wEg+ zN4&AtZ^gvFg%*6DcOTSxELH^9aVtJr@yUwMBG?OG7>iST!egA;l?AhcPqkv22>MSi zz$v|+RN3+wT0YY{&$6Q0deqQ^L1(M;9BV(FQd~^r&Ce2# z>E#wfuU2e;b&5-goSOBvre#(%#A^Lhl{f<%6`K^B1yRuo#g&Sy6jv**QCzFIPI0~B z20@Iqje^+TXc0t*Y$CE3Hhc8Fz(#I~O@Y-`b=W3|h}#ug1<{lpiaP~&72Iwj>uK|t zPE}0KJwdV7y|y>^S+PIngE|gK;DZI%E`f1ZeJF&=C5H>{h}d)#+)*Mc=(I7rV$AM> zJElg*V>w>Wosc-F_N1Eh*f^)6{po`175lz|>lZvzaAyV272JSEId7v}u;QW>gH~L! z;<6Q2thj2$H4&Uz*R}o|*65}cx2(7w-G>TpSXzSq7?DU=?(T>QUlHCd#8-s*-#nCDE{OJO3W``oAeLBB8%15wEsX+~t$Gn0pUWgH8rC3)UlcAEOn+t7Si}&)$;Gd% z8jI;J63p={D;!`m`75hng{_e$TJa)>t&ygp-5N<1-MBTaDvu| z$R%rwZk^cUin3nqH(2|PR`NI_$E zNuC%|yCqKqYZDhvgFP(geBJ9YokWP=n=v5eoAAVj^)@L z>Xqt%ePV*F`(qr4Gf|*N!|P>cb=Dp^XT^XO=dHM4#YHOyt+-^xWh<^&an*`zR$RB@ zMigkzO%d#uTiWQ`)@aBY4NE6soQ){n5xiS;_q6oBAWr2dkv;Vw#>RYpn2P@ccV?}S literal 0 HcmV?d00001 diff --git a/tests/parse.rs b/tests/parse.rs index 3c0c855..11d54d6 100644 --- a/tests/parse.rs +++ b/tests/parse.rs @@ -27,13 +27,15 @@ fn mirage_1() { .player_info .get(death.userid.as_ref().unwrap()) .unwrap(); - dbg!(died_user); + // dbg!(died_user); } _ => {} }, _ => {} }; } + + todo!() } #[test]