Full rewrite
- Removed csflow, as its basically not getting used when high optimization is needed - Fully rewrote radarflow dma logic. - Speed increase from 20hz to over 130 hz over pcileech, thanks to scatter reads and improved caching - Removed docs, because those were for csflow, which is now removed
This commit is contained in:
		
							
								
								
									
										95
									
								
								src/cli.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										95
									
								
								src/cli.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| use std::path::PathBuf; | ||||
|  | ||||
| use clap::{Parser, ValueEnum}; | ||||
| use memflow::plugins::Inventory; | ||||
|  | ||||
| use crate::dma::Connector; | ||||
| const PORT_RANGE: std::ops::RangeInclusive<usize> = 8000..=65535; | ||||
|  | ||||
| #[derive(Parser)] | ||||
| #[command(author, version = version(), about, long_about = None)] | ||||
| pub struct Cli { | ||||
|     /// Specifies the connector type for DMA | ||||
|     #[clap(value_enum, short, long, ignore_case = true, default_value_t = Connector::Qemu)] | ||||
|     pub connector: Connector, | ||||
|  | ||||
|     /// Name of the Pcileech device | ||||
|     #[clap(long, default_value_t = String::from("FPGA"))] | ||||
|     pub pcileech_device: String, | ||||
|  | ||||
|     /// Port number for the Webserver to run on | ||||
|     #[arg(short, long, default_value_t = 8000, value_parser = port_in_range)] | ||||
|     pub port: u16, | ||||
|  | ||||
|     /// Path to the directory served by the Webserver | ||||
|     #[arg(short, long, default_value = "./webradar", value_parser = valid_path)] | ||||
|     pub web_path: PathBuf, | ||||
|  | ||||
|     /// Verbosity level for logging to the console | ||||
|     #[arg(value_enum, long, short,  ignore_case = true, default_value_t = Loglevel::Warn)] | ||||
|     pub loglevel: Loglevel, | ||||
| } | ||||
|  | ||||
| fn version() -> String { | ||||
|     let pkg_ver = env!("CARGO_PKG_VERSION"); | ||||
|     let git_hash = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"); | ||||
|     let commit_date = option_env!("VERGEN_GIT_COMMIT_DATE").unwrap_or("unknown"); | ||||
|     let avail_cons = { | ||||
|         let inventory = Inventory::scan(); | ||||
|         inventory.available_connectors().join(", ") | ||||
|     }; | ||||
|  | ||||
|     format!(" {pkg_ver} (rev {git_hash})\nCommit Date: {commit_date}\nAvailable Connectors: {avail_cons}") | ||||
| } | ||||
|  | ||||
| fn port_in_range(s: &str) -> Result<u16, String> { | ||||
|     let port: usize = s | ||||
|         .parse() | ||||
|         .map_err(|_| format!("`{s}` isn't a port number"))?; | ||||
|     if PORT_RANGE.contains(&port) { | ||||
|         Ok(port as u16) | ||||
|     } else { | ||||
|         Err(format!( | ||||
|             "port not in range {}-{}", | ||||
|             PORT_RANGE.start(), | ||||
|             PORT_RANGE.end() | ||||
|         )) | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn valid_path(s: &str) -> Result<PathBuf, String> { | ||||
|     let path = PathBuf::from(s); | ||||
|  | ||||
|     if !path.exists() { | ||||
|         return Err("Path does not exist".to_string()) | ||||
|     } | ||||
|  | ||||
|     if !path.is_dir() { | ||||
|         return Err("Path is not a directory".to_string()) | ||||
|     } | ||||
|  | ||||
|     Ok(path) | ||||
| } | ||||
|  | ||||
| /// Wrapper because log::LevelFilter doesn't implement ValueEnum | ||||
| #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)] | ||||
| pub enum Loglevel { | ||||
|     Error, | ||||
|     #[default] | ||||
|     Warn, | ||||
|     Info, | ||||
|     Debug, | ||||
|     Trace, | ||||
| } | ||||
|  | ||||
| impl From<Loglevel> for log::LevelFilter { | ||||
|     fn from(val: Loglevel) -> Self { | ||||
|         match val { | ||||
|             Loglevel::Error => log::LevelFilter::Error, | ||||
|             Loglevel::Warn => log::LevelFilter::Warn, | ||||
|             Loglevel::Info => log::LevelFilter::Info, | ||||
|             Loglevel::Debug => log::LevelFilter::Debug, | ||||
|             Loglevel::Trace => log::LevelFilter::Trace, | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										75
									
								
								src/comms.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										75
									
								
								src/comms.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| use serde::{Serialize, Deserialize}; | ||||
|  | ||||
| use crate::{structs::Vec3, enums::PlayerType}; | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct PlayerData { | ||||
|     pos: Vec3, | ||||
|     yaw: f32, | ||||
|     #[serde(rename = "playerType")] | ||||
|     player_type: PlayerType, | ||||
|  | ||||
|     #[serde(rename = "hasBomb")] | ||||
|     has_bomb: bool | ||||
| } | ||||
|  | ||||
| impl PlayerData { | ||||
|     pub fn new(pos: Vec3, yaw: f32, player_type: PlayerType, has_bomb: bool) -> PlayerData { | ||||
|         PlayerData { pos, yaw, player_type, has_bomb } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct BombData { | ||||
|     pos: Vec3, | ||||
|     #[serde(rename = "isPlanted")] | ||||
|     is_planted: bool | ||||
| } | ||||
|  | ||||
| #[allow(dead_code)] | ||||
| impl BombData { | ||||
|     pub fn new(pos: Vec3, is_planted: bool) -> BombData { | ||||
|         BombData { pos, is_planted } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub enum EntityData { | ||||
|     Player(PlayerData), | ||||
|     Bomb(BombData) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct RadarData { | ||||
|     freq: usize, | ||||
|     ingame: bool, | ||||
|  | ||||
|     #[serde(rename = "mapName")] | ||||
|     map_name: String, | ||||
|  | ||||
|     #[serde(rename(serialize = "entityData"))] | ||||
|     player_data: Vec<EntityData>, | ||||
|  | ||||
|     //#[serde(rename(serialize = "localYaw"))] | ||||
|     //local_yaw: f32, | ||||
| } | ||||
|  | ||||
| impl RadarData { | ||||
|     pub fn new(ingame: bool, map_name: String, player_data: Vec<EntityData>, freq: usize) -> RadarData { | ||||
|         RadarData { ingame, map_name, player_data, freq } | ||||
|     } | ||||
|  | ||||
|     /// Returns empty RadarData, it's also the same data that gets sent to clients when not ingame | ||||
|     pub fn empty(freq: usize) -> RadarData { | ||||
|         RadarData {  | ||||
|             ingame: false, | ||||
|             map_name: String::new(), | ||||
|             player_data: Vec::new(), | ||||
|             freq | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| unsafe impl Send for RadarData {} | ||||
|  | ||||
| pub type ArcRwlockRadarData = std::sync::Arc<tokio::sync::RwLock<RadarData>>; | ||||
							
								
								
									
										17
									
								
								src/dma/context/connector.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										17
									
								
								src/dma/context/connector.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum, Default)] | ||||
| pub enum Connector { | ||||
|     #[default] | ||||
|     Qemu, | ||||
|     Kvm, | ||||
|     Pcileech | ||||
| } | ||||
|  | ||||
| impl ToString for Connector { | ||||
|     fn to_string(&self) -> String { | ||||
|         match self { | ||||
|             Connector::Qemu => String::from("qemu"), | ||||
|             Connector::Kvm => String::from("kvm"), | ||||
|             Connector::Pcileech => String::from("pcileech"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										182
									
								
								src/dma/context/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										182
									
								
								src/dma/context/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| use memflow::prelude::v1::*; | ||||
|  | ||||
| mod connector; | ||||
|  | ||||
| pub use connector::Connector; | ||||
| use num_traits::FromPrimitive; | ||||
|  | ||||
| use crate::{structs::Vec3, enums::TeamID}; | ||||
|  | ||||
| use super::cs2dumper; | ||||
|  | ||||
| pub struct DmaCtx { | ||||
|     pub process: IntoProcessInstanceArcBox<'static>, | ||||
|     pub client_module: ModuleInfo, | ||||
|     pub engine_module: ModuleInfo, | ||||
| } | ||||
|  | ||||
| impl DmaCtx { | ||||
|     fn check_version(&mut self) -> anyhow::Result<()> { | ||||
|         let game_build_number: u32 = self.process.read(self.engine_module.base + cs2dumper::offsets::engine2_dll::dwBuildNumber)?; | ||||
|         let offset_build_number = cs2dumper::offsets::game_info::buildNumber; | ||||
|  | ||||
|         if game_build_number as usize != offset_build_number { | ||||
|             return Err(anyhow::anyhow!( | ||||
|                 "game build is {}, but offsets are for {}", | ||||
|                 game_build_number, | ||||
|                 offset_build_number | ||||
|             )); | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn setup(connector: Connector, pcileech_device: String) -> anyhow::Result<DmaCtx> { | ||||
|         let inventory = Inventory::scan(); | ||||
|  | ||||
|         let os = {  | ||||
|             if connector == Connector::Pcileech { | ||||
|                 let args = Args::new() | ||||
|                     .insert("device", &pcileech_device); | ||||
|  | ||||
|                 let connector_args = ConnectorArgs::new(None, args, None);                 | ||||
|  | ||||
|                 inventory.builder() | ||||
|                     .connector(&connector.to_string()) | ||||
|                     .args(connector_args) | ||||
|                     .os("win32") | ||||
|                     .build()? | ||||
|             } else { | ||||
|                 inventory.builder() | ||||
|                 .connector(&connector.to_string()) | ||||
|                 .os("win32") | ||||
|                 .build()? | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let mut process = os.into_process_by_name("cs2.exe")?; | ||||
|  | ||||
|         let client_module = process.module_by_name("client.dll")?; | ||||
|  | ||||
|         let engine_module = process.module_by_name("engine2.dll")?; | ||||
|  | ||||
|         let mut ctx = Self { | ||||
|             process, | ||||
|             client_module, | ||||
|             engine_module, | ||||
|         }; | ||||
|  | ||||
|         ctx.check_version()?; | ||||
|  | ||||
|         Ok(ctx) | ||||
|     } | ||||
|  | ||||
|     pub fn pawn_from_controller(&mut self, controller: Address, entity_list: Address) -> anyhow::Result<Option<Address>> { | ||||
|         let uhandle: u32 = self.process.read(controller + cs2dumper::client::CCSPlayerController::m_hPlayerPawn)?; | ||||
|  | ||||
|         let list_entry = self.process.read_addr64(entity_list + 0x8 * ((uhandle & 0x7FFF) >> 9) + 16)?; | ||||
|          | ||||
|         if list_entry.is_null() || !list_entry.is_valid() { | ||||
|             Ok(None) | ||||
|         } else { | ||||
|             let ptr = self.process.read_addr64(list_entry + 120 * (uhandle & 0x1FF))?; | ||||
|             Ok(Some(ptr)) | ||||
|         } | ||||
|  | ||||
|         //super::CPlayerPawn::from_uhandle(ctx, entity_list, uhandle) | ||||
|     } | ||||
|  | ||||
|     pub fn batched_player_read(&mut self, controller: Address, pawn: Address) -> anyhow::Result<BatchedPlayerData> { | ||||
|         let mut pos = Vec3::default(); | ||||
|         let mut yaw = 0f32; | ||||
|         let mut health = 0u32; | ||||
|         let mut team = 0i32; | ||||
|  | ||||
|         { | ||||
|             let mut batcher = MemoryViewBatcher::new(&mut self.process); | ||||
|             batcher.read_into(pawn + cs2dumper::client::C_BasePlayerPawn::m_vOldOrigin, &mut pos); | ||||
|             batcher.read_into(pawn + cs2dumper::client::C_CSPlayerPawnBase::m_angEyeAngles + 4, &mut yaw); | ||||
|             batcher.read_into(pawn + cs2dumper::client::C_BaseEntity::m_iHealth, &mut health); | ||||
|             batcher.read_into(controller + cs2dumper::client::C_BaseEntity::m_iTeamNum, &mut team); | ||||
|         } | ||||
|      | ||||
|         let team = TeamID::from_i32(team); | ||||
|      | ||||
|         Ok(BatchedPlayerData { | ||||
|             pos, | ||||
|             yaw, | ||||
|             team, | ||||
|             health | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn get_plantedc4(&mut self) -> anyhow::Result<Address> { | ||||
|         let ptr = self.process.read_addr64(self.client_module.base + cs2dumper::offsets::client_dll::dwPlantedC4)?; | ||||
|         let ptr2 = self.process.read_addr64(ptr)?; | ||||
|         Ok(ptr2) | ||||
|     } | ||||
|  | ||||
|     /// Professionally engineered function to quickly check if the entity has class name "weapon_c4" | ||||
|     pub fn is_dropped_c4(&mut self, entity_ptr: Address) -> anyhow::Result<bool> { | ||||
|         let entity_identity_ptr = self.process.read_addr64(entity_ptr + cs2dumper::client::CEntityInstance::m_pEntity)?; | ||||
|         let class_name_ptr = self.process.read_addr64(entity_identity_ptr + cs2dumper::client::CEntityIdentity::m_designerName)?; | ||||
|  | ||||
|         let data = self.process.read_raw(class_name_ptr + 7, 2)?; | ||||
|         let is_c4 = data == "c4".as_bytes(); | ||||
|         Ok(is_c4) | ||||
|     } | ||||
|  | ||||
|     /// Professionally engineered function to quickly check if the entity has class name "cs_player_controller" | ||||
|     pub fn is_cs_player_controller(&mut self, entity_ptr: Address) -> anyhow::Result<bool> { | ||||
|         let entity_identity_ptr = self.process.read_addr64(entity_ptr + cs2dumper::client::CEntityInstance::m_pEntity)?; | ||||
|         let class_name_ptr = self.process.read_addr64(entity_identity_ptr + cs2dumper::client::CEntityIdentity::m_designerName)?; | ||||
|  | ||||
|         let data = self.process.read_raw(class_name_ptr, 20)?; | ||||
|         let is_controller = data == "cs_player_controller".as_bytes(); | ||||
|         Ok(is_controller) | ||||
|     } | ||||
|  | ||||
|     // Todo: Optimize this function: find another way to do this | ||||
|     pub fn has_c4(&mut self, pawn: Address, entity_list: Address) -> anyhow::Result<bool> { | ||||
|         let mut has_c4 = false; | ||||
|         let wep_services = self.process.read_addr64(pawn + cs2dumper::client::C_BasePlayerPawn::m_pWeaponServices)?; | ||||
|         let wep_count: i32  = self.process.read(wep_services + cs2dumper::client::CPlayer_WeaponServices::m_hMyWeapons)?; | ||||
|         let wep_base = self.process.read_addr64(wep_services + cs2dumper::client::CPlayer_WeaponServices::m_hMyWeapons + 0x8)?; | ||||
|  | ||||
|         for wep_idx in 0..wep_count { | ||||
|             let handle: i32 = self.process.read(wep_base + wep_idx * 0x4)?; | ||||
|             if handle == -1 { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             let list_entry = self.process.read_addr64(entity_list + 0x8 * ((handle & 0x7FFF) >> 9) + 16)?; | ||||
|             if let Some(wep_ptr) = { | ||||
|                 if list_entry.is_null() || !list_entry.is_valid() { | ||||
|                     None | ||||
|                 } else { | ||||
|                     let ptr = self.process.read_addr64(list_entry + 120 * (handle & 0x1FF))?; | ||||
|                     Some(ptr) | ||||
|                 } | ||||
|             } { | ||||
|                 let wep_data = self.process.read_addr64(wep_ptr + cs2dumper::client::C_BaseEntity::m_nSubclassID + 0x8)?; | ||||
|                 let id: i32 = self.process.read(wep_data + cs2dumper::client::CCSWeaponBaseVData::m_WeaponType)?; | ||||
|  | ||||
|                 if id == 7 { | ||||
|                     has_c4 = true; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(has_c4) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|  | ||||
| pub struct BatchedPlayerData { | ||||
|     pub pos: Vec3, | ||||
|     pub yaw: f32, | ||||
|     pub team: Option<TeamID>, | ||||
|     pub health: u32, | ||||
| } | ||||
							
								
								
									
										4
									
								
								src/dma/cs2dumper/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								src/dma/cs2dumper/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| #![allow(dead_code)] | ||||
| pub mod client; | ||||
| pub mod engine2; | ||||
| pub mod offsets; | ||||
							
								
								
									
										190
									
								
								src/dma/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										190
									
								
								src/dma/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,190 @@ | ||||
| use std::{thread, time::{Duration, Instant}}; | ||||
|  | ||||
| use memflow::{os::Process, types::Address, mem::MemoryView}; | ||||
|  | ||||
| use crate::{enums::{TeamID, PlayerType}, comms::{EntityData, PlayerData, RadarData, ArcRwlockRadarData, BombData}}; | ||||
|  | ||||
| use self::{context::DmaCtx, threaddata::CsData}; | ||||
|  | ||||
| mod context; | ||||
| mod threaddata; | ||||
| mod cs2dumper; | ||||
|  | ||||
| pub use context::Connector; | ||||
|  | ||||
| pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_device: String) -> anyhow::Result<()> { | ||||
|     let mut ctx = DmaCtx::setup(connector, pcileech_device)?; | ||||
|     let mut data = CsData::default(); | ||||
|      | ||||
|     // For read timing | ||||
|     let mut last_bomb_dropped = false; | ||||
|     let mut last_bomb_planted = false; | ||||
|     let mut last_tick_count = 0; | ||||
|     let mut last_big_read = Instant::now(); | ||||
|  | ||||
|     // For frequency info | ||||
|     let mut start_stamp = Instant::now(); | ||||
|     let mut iters = 0; | ||||
|     let mut freq = 0; | ||||
|  | ||||
|     data.update_pointers(&mut ctx); | ||||
|     data.update_common(&mut ctx); | ||||
|     data.update_players(&mut ctx); | ||||
|     data.update_bomb(&mut ctx); | ||||
|  | ||||
|     loop { | ||||
|         if ctx.process.state().is_dead() { | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         if last_big_read.elapsed().as_millis() > 10000 { | ||||
|             data.update_pointers(&mut ctx); | ||||
|             data.update_players(&mut ctx); | ||||
|             last_big_read = Instant::now(); | ||||
|         } | ||||
|  | ||||
|         data.update_common(&mut ctx); | ||||
|  | ||||
|         // Bomb update | ||||
|         if (data.bomb_dropped && !last_bomb_dropped) || (data.bomb_planted && !last_bomb_planted) { | ||||
|             data.update_bomb(&mut ctx); | ||||
|         } | ||||
|  | ||||
|         if (!data.bomb_dropped && last_bomb_dropped) || !data.bomb_planted { | ||||
|             data.recheck_bomb_holder = true; | ||||
|         } | ||||
|  | ||||
|         last_bomb_dropped = data.bomb_dropped; | ||||
|         last_bomb_planted = data.bomb_planted; | ||||
|  | ||||
|         // Poll entity data | ||||
|         let ingame = !data.map.is_empty() && data.map != "<empty>"; | ||||
|         let update_data = data.tick_count != last_tick_count; | ||||
|      | ||||
|         if ingame { | ||||
|             if !update_data { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             let mut entity_data = Vec::new(); | ||||
|  | ||||
|             // Bomb | ||||
|             if data.bomb_dropped || data.bomb_planted { | ||||
|                 let node = ctx.process.read_addr64( | ||||
|                     data.bomb + cs2dumper::client::C_BaseEntity::m_pGameSceneNode as u64 | ||||
|                 ).unwrap(); | ||||
|                 let pos = ctx.process.read(node + cs2dumper::client::CGameSceneNode::m_vecAbsOrigin).unwrap(); | ||||
|      | ||||
|                 entity_data.push(EntityData::Bomb(BombData::new(pos, data.bomb_planted))); | ||||
|             } | ||||
|  | ||||
|             // Local player | ||||
|             let local_data = ctx.batched_player_read( | ||||
|                 data.local.into(), data.local_pawn.into() | ||||
|             ).unwrap(); | ||||
|  | ||||
|             if local_data.health > 0 { | ||||
|                 let has_bomb = { | ||||
|                     if data.bomb_planted { | ||||
|                         false | ||||
|                     } else if data.recheck_bomb_holder { | ||||
|                         if local_data.team == Some(TeamID::T) && !data.bomb_dropped && !data.bomb_planted { | ||||
|                             let is_holder = ctx.has_c4( | ||||
|                                 data.local_pawn.into(), data.entity_list.into() | ||||
|                             ).unwrap_or(false); | ||||
|  | ||||
|                             if is_holder { | ||||
|                                 data.bomb_holder = data.local.into(); | ||||
|                                 data.recheck_bomb_holder = false; | ||||
|                             } | ||||
|  | ||||
|                             is_holder | ||||
|                         } else { false } | ||||
|                     } else { Address::from(data.local) == data.bomb_holder } | ||||
|                 }; | ||||
|  | ||||
|                 entity_data.push( | ||||
|                     EntityData::Player( | ||||
|                         PlayerData::new( | ||||
|                             local_data.pos,  | ||||
|                             local_data.yaw, | ||||
|                             PlayerType::Local, | ||||
|                             has_bomb | ||||
|                         ) | ||||
|                     ) | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             // Other players | ||||
|             for (controller, pawn) in &data.players { | ||||
|                 let player_data = ctx.batched_player_read(*controller, *pawn).unwrap(); | ||||
|  | ||||
|                 if player_data.health < 1 { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 let has_bomb = { | ||||
|                     if data.bomb_planted { | ||||
|                         false | ||||
|                     } else if data.recheck_bomb_holder { | ||||
|                         if player_data.team == Some(TeamID::T) && !data.bomb_dropped && !data.bomb_planted { | ||||
|                             let is_holder = ctx.has_c4(*pawn, data.entity_list.into()).unwrap_or(false); | ||||
|  | ||||
|                             if is_holder { | ||||
|                                 data.bomb_holder = *controller; | ||||
|                                 data.recheck_bomb_holder = false; | ||||
|                             } | ||||
|  | ||||
|                             is_holder | ||||
|                         } else { false } | ||||
|                     } else { *controller == data.bomb_holder } | ||||
|                 }; | ||||
|  | ||||
|                 let player_type = { | ||||
|                     if local_data.team != player_data.team { | ||||
|                         PlayerType::Enemy | ||||
|                     } else if local_data.team == player_data.team { | ||||
|                         PlayerType::Team | ||||
|                     } else { | ||||
|                         PlayerType::Unknown | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 entity_data.push( | ||||
|                     EntityData::Player( | ||||
|                         PlayerData::new( | ||||
|                             player_data.pos,  | ||||
|                             player_data.yaw, | ||||
|                             player_type, | ||||
|                             has_bomb | ||||
|                         ) | ||||
|                     ) | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             let mut radar = radar_data.write().await; | ||||
|             *radar = RadarData::new( | ||||
|                 true, | ||||
|                 data.map.clone(), | ||||
|                 entity_data, | ||||
|                 freq | ||||
|             ); | ||||
|         } else { | ||||
|             let mut radar = radar_data.write().await; | ||||
|             *radar = RadarData::empty(freq); | ||||
|         } | ||||
|  | ||||
|         last_tick_count = data.tick_count; | ||||
|         iters += 1; | ||||
|      | ||||
|         if start_stamp.elapsed().as_secs() > 1 { | ||||
|             freq = iters; | ||||
|             iters = 0; | ||||
|             start_stamp = Instant::now(); | ||||
|         } | ||||
|      | ||||
|         thread::sleep(Duration::from_millis(1)); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										175
									
								
								src/dma/threaddata/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										175
									
								
								src/dma/threaddata/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,175 @@ | ||||
| use itertools::Itertools; | ||||
| use memflow::{mem::MemoryView, types::Address}; | ||||
|  | ||||
| use super::{context::DmaCtx, cs2dumper}; | ||||
|  | ||||
| #[derive(Clone, Debug, Default)] | ||||
| pub struct CsData { | ||||
|     // Entities | ||||
|     pub players: Vec<(Address, Address)>, | ||||
|     pub bomb: Address, | ||||
|     pub bomb_holder: Address, | ||||
|     pub recheck_bomb_holder: bool, | ||||
|  | ||||
|     // Pointers | ||||
|     pub globals: u64, | ||||
|     pub gamerules: u64, | ||||
|     pub entity_list: u64, | ||||
|     pub game_ent_sys: u64, | ||||
|  | ||||
|     // Common | ||||
|     pub local: u64, | ||||
|     pub local_pawn: u64, | ||||
|     pub is_dead: bool, | ||||
|     pub tick_count: i32, | ||||
|     pub bomb_dropped: bool, | ||||
|     pub bomb_planted: bool, | ||||
|     pub highest_index: i32, | ||||
|     pub map: String | ||||
| } | ||||
|  | ||||
|  | ||||
| impl CsData { | ||||
|     pub fn update_bomb(&mut self, ctx: &mut DmaCtx) { | ||||
|         // If the bomb is dropped, do a reverse entity list loop with early exit when we found the bomb. ( Now with BATCHING!!! :O ) | ||||
|         if self.bomb_dropped { | ||||
|  | ||||
|             // We search 16 entities at a time. | ||||
|             for chunk in &(0..=self.highest_index).rev().chunks(16) { | ||||
|  | ||||
|                 let indexes: Vec<i32> = chunk.collect(); | ||||
|  | ||||
|                 let mut data_array = [(0u64, 0i32); 16]; | ||||
|                 { | ||||
|                     let mut batcher = ctx.process.batcher(); | ||||
|                     let ent_list: Address = self.entity_list.into(); | ||||
|  | ||||
|                     data_array.iter_mut().zip(indexes).for_each(|((data_ptr, data_idx), index)| { | ||||
|                         batcher.read_into(ent_list + 8 * (index >> 9) + 16, data_ptr); | ||||
|                         *data_idx = index; | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 { | ||||
|                     let mut batcher = ctx.process.batcher(); | ||||
|      | ||||
|                     data_array.iter_mut().for_each(|(ptr, index)| { | ||||
|                         let handle: Address = (*ptr).into(); | ||||
|                         batcher.read_into(handle + 120 * (*index & 0x1FF), ptr); | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 // You can actually optimize this EVEN more | ||||
|                 let bomb = data_array.into_iter().find(|(ptr, _)| { | ||||
|                     // By doing this with a batcher too... | ||||
|                     ctx.is_dropped_c4((*ptr).into()).unwrap_or(false) | ||||
|                 }); | ||||
|  | ||||
|                 if let Some(bomb) = bomb { | ||||
|                     self.bomb = bomb.0.into(); | ||||
|                 } | ||||
|             } | ||||
|         } else if self.bomb_planted { | ||||
|             let bomb = ctx.get_plantedc4() | ||||
|                 .expect("Failed to get planted bomb"); | ||||
|  | ||||
|             self.bomb = bomb; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn update_players(&mut self, ctx: &mut DmaCtx) { | ||||
|         let mut list_entries = [0u64; 64]; | ||||
|         { | ||||
|             let mut batcher = ctx.process.batcher(); | ||||
|             let ent_list: Address = self.entity_list.into(); | ||||
|  | ||||
|             list_entries.iter_mut().enumerate().for_each(|(idx, data)| { | ||||
|                 let index = idx as i32; | ||||
|                 batcher.read_into(ent_list + 8 * (index >> 9) + 16, data); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         let mut player_ptrs = [0u64; 64]; | ||||
|         { | ||||
|             let mut batcher = ctx.process.batcher(); | ||||
|  | ||||
|             player_ptrs.iter_mut().enumerate().for_each(|(idx, data)| { | ||||
|                 let list_entry: Address = list_entries[idx].into(); | ||||
|                 batcher.read_into(list_entry + 120 * (idx & 0x1FF), data); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         let mut new_players: Vec<u64> = Vec::new(); | ||||
|         player_ptrs | ||||
|             .into_iter() | ||||
|             .for_each(|ptr| { | ||||
|                 if ctx.is_cs_player_controller(ptr.into()).unwrap_or(false) { | ||||
|                     new_players.push(ptr) | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|         let new_players: Vec<(Address, Address)> = new_players | ||||
|             .into_iter() | ||||
|             .map(Address::from) | ||||
|             .filter(|ptr| !ptr.is_null()) | ||||
|             .filter(|ptr| *ptr != self.local.into()) | ||||
|             .map(|ptr| { | ||||
|                 let pawn = ctx.pawn_from_controller(ptr, self.entity_list.into()).unwrap(); | ||||
|                 (ptr, pawn) | ||||
|             }) | ||||
|             .filter(|(_, pawn)| pawn.is_some()) | ||||
|             .map(|(controller, pawn)| (controller, pawn.unwrap())) | ||||
|             .collect(); | ||||
|  | ||||
|         self.players = new_players; | ||||
|     } | ||||
|  | ||||
|     pub fn update_common(&mut self, ctx: &mut DmaCtx) { | ||||
|         let mut bomb_dropped = 0u8; | ||||
|         let mut bomb_planted = 0u8; | ||||
|         let mut map_ptr = 0u64; | ||||
|  | ||||
|         { | ||||
|             // Globals | ||||
|             let tick_count_addr = (self.globals + 0x40).into(); | ||||
|             let map_addr = (self.globals + 0x188).into(); | ||||
|  | ||||
|             // Gamerules | ||||
|             let bomb_dropped_addr = (self.gamerules + cs2dumper::client::C_CSGameRules::m_bBombDropped as u64).into(); | ||||
|             let bomb_planted_addr = (self.gamerules + cs2dumper::client::C_CSGameRules::m_bBombPlanted as u64).into(); | ||||
|  | ||||
|             // Game Entity System | ||||
|             let highest_index_addr = (self.game_ent_sys + cs2dumper::offsets::client_dll::dwGameEntitySystem_getHighestEntityIndex as u64).into(); | ||||
|  | ||||
|             let mut batcher = ctx.process.batcher(); | ||||
|             batcher.read_into( | ||||
|                 ctx.client_module.base + cs2dumper::offsets::client_dll::dwLocalPlayerController,  | ||||
|                 &mut self.local | ||||
|             ); | ||||
|             batcher.read_into( | ||||
|                 ctx.client_module.base + cs2dumper::offsets::client_dll::dwLocalPlayerPawn,  | ||||
|                 &mut self.local_pawn | ||||
|             ); | ||||
|  | ||||
|             batcher.read_into(tick_count_addr, &mut self.tick_count); | ||||
|             batcher.read_into(bomb_dropped_addr, &mut bomb_dropped); | ||||
|             batcher.read_into(bomb_planted_addr, &mut bomb_planted); | ||||
|             batcher.read_into(highest_index_addr, &mut self.highest_index); | ||||
|             batcher.read_into(map_addr, &mut map_ptr); | ||||
|         } | ||||
|  | ||||
|         let map_string = ctx.process.read_char_string_n(map_ptr.into(), 32).unwrap_or(String::from("<empty>")); | ||||
|  | ||||
|         self.map = map_string; | ||||
|         self.bomb_dropped = bomb_dropped != 0; | ||||
|         self.bomb_planted = bomb_planted != 0; | ||||
|     } | ||||
|  | ||||
|     pub fn update_pointers(&mut self, ctx: &mut DmaCtx) { | ||||
|         let mut batcher = ctx.process.batcher(); | ||||
|         batcher.read_into(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGlobalVars, &mut self.globals); | ||||
|         batcher.read_into(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGameRules, &mut self.gamerules); | ||||
|         batcher.read_into(ctx.client_module.base + cs2dumper::offsets::client_dll::dwEntityList, &mut self.entity_list); | ||||
|         batcher.read_into(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGameEntitySystem, &mut self.game_ent_sys); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										5
									
								
								src/enums/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								src/enums/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| mod teamid; | ||||
| mod player_type; | ||||
|  | ||||
| pub use teamid::TeamID; | ||||
| pub use player_type::PlayerType; | ||||
							
								
								
									
										9
									
								
								src/enums/player_type.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								src/enums/player_type.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| #[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default, PartialEq)] | ||||
| pub enum PlayerType { | ||||
|     #[default] | ||||
|     Unknown, | ||||
|     Spectator, | ||||
|     Local, | ||||
|     Enemy, | ||||
|     Team | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/enums/teamid.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								src/enums/teamid.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| #[repr(i32)] | ||||
| #[derive(Debug, Eq, PartialEq, enum_primitive_derive::Primitive)] | ||||
| pub enum TeamID { | ||||
|     Spectator = 1, | ||||
|     T = 2, | ||||
|     CT = 3 | ||||
| } | ||||
							
								
								
									
										56
									
								
								src/main.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										56
									
								
								src/main.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| use std::sync::Arc; | ||||
|  | ||||
| use clap::Parser; | ||||
| use cli::Cli; | ||||
| use comms::RadarData; | ||||
| use tokio::sync::RwLock; | ||||
|  | ||||
| mod cli; | ||||
| mod structs; | ||||
| mod enums; | ||||
| mod comms; | ||||
|  | ||||
| mod dma; | ||||
| mod websocket; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> anyhow::Result<()> { | ||||
|     let cli: Cli = Cli::parse(); | ||||
|  | ||||
|     simple_logger::SimpleLogger::new() | ||||
|         .with_level(cli.loglevel.into()) | ||||
|         .init() | ||||
|         .expect("Initializing logger"); | ||||
|  | ||||
|     let radar_data = Arc::new( | ||||
|         RwLock::new( | ||||
|             RadarData::empty(0) | ||||
|         ) | ||||
|     ); | ||||
|  | ||||
|     let radar_clone = radar_data.clone(); | ||||
|     let dma_handle = tokio::spawn(async move { | ||||
|         if let Err(err) = dma::run(radar_clone, cli.connector, cli.pcileech_device).await { | ||||
|             log::error!("Error in dma thread: [{}]", err.to_string()); | ||||
|         } else { | ||||
|             println!("CS2 Process exited, exiting program...") | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     let _websocket_handle = tokio::spawn(async move { | ||||
|         if let Ok(my_local_ip) = local_ip_address::local_ip() { | ||||
|             let address = format!("http://{}:{}", my_local_ip, cli.port); | ||||
|             println!("Launched webserver at {}", address); | ||||
|         } else { | ||||
|             let address = format!("http://0.0.0.0:{}", cli.port); | ||||
|             println!("launched webserver at {}", address); | ||||
|         } | ||||
|  | ||||
|         if let Err(err) = websocket::run(cli.web_path, cli.port, radar_data).await { | ||||
|             log::error!("Error in ws server: [{}]", err.to_string()); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     dma_handle.await?; | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/structs/mod.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								src/structs/mod.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| mod vec3; | ||||
|  | ||||
| pub use vec3::Vec3; | ||||
							
								
								
									
										11
									
								
								src/structs/vec3.rs
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								src/structs/vec3.rs
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| use serde::{Serialize, Deserialize}; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] | ||||
| #[repr(C)] | ||||
| pub struct Vec3 { | ||||
|     pub x: f32,  | ||||
|     pub y: f32, | ||||
|     pub z: f32 | ||||
| } | ||||
|  | ||||
| unsafe impl dataview::Pod for Vec3 {} | ||||
							
								
								
									
										69
									
								
								src/websocket.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/websocket.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| use std::{sync::Arc, path::PathBuf}; | ||||
|  | ||||
| use axum::{ | ||||
|     extract::{ws::{WebSocketUpgrade, WebSocket, Message}, State}, | ||||
|     response::Response, | ||||
|     routing::get, | ||||
|     Router, | ||||
| }; | ||||
|  | ||||
| use tokio::sync::RwLock; | ||||
| use tower_http::services::ServeDir; | ||||
|  | ||||
| use crate::comms::RadarData; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| struct AppState { | ||||
|     data_lock: Arc<RwLock<RadarData>> | ||||
| } | ||||
|  | ||||
| async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response { | ||||
|     let clone = state.clone(); | ||||
|     ws.on_upgrade(|socket| handle_socket(socket, clone)) | ||||
| } | ||||
|  | ||||
| async fn handle_socket(mut socket: WebSocket, state: AppState) { | ||||
|     while let Some(msg) = socket.recv().await { | ||||
|         if let Ok(msg) = msg { | ||||
|             if msg == Message::Text("requestInfo".to_string()) { | ||||
|                 let str = { | ||||
|                     let data = state.data_lock.read().await; | ||||
|  | ||||
|                     match serde_json::to_string(&*data) { | ||||
|                         Ok(json) => json, | ||||
|                         Err(e) => { | ||||
|                             log::error!("Could not serialize data into json: {}", e.to_string()); | ||||
|                             log::error!("Sending \"error\" instead"); | ||||
|                             "error".to_string() | ||||
|                         }, | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 //println!("{str}"); | ||||
|  | ||||
|                 if socket.send(Message::Text(str)).await.is_err() { | ||||
|                     // client disconnected | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             // client disconnected | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn run(path: PathBuf, port: u16, data_lock: Arc<RwLock<RadarData>>) -> anyhow::Result<()> { | ||||
|     let app = Router::new() | ||||
|         .nest_service("/", ServeDir::new(path)) | ||||
|         .route("/ws", get(ws_handler)) | ||||
|         .with_state(AppState { data_lock }); | ||||
|  | ||||
|     let address = format!("0.0.0.0:{}", port); | ||||
|  | ||||
|     axum::Server::bind(&address.parse()?) | ||||
|         .serve(app.into_make_service()) | ||||
|         .await?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user