Big update
This repository is no longer meant for just radarflow, thus it will be renamed. I have split the SDK from radarflow, allowing for simpler use with new projects. Other than that, radarflow is functionally the same. - Fixed bug in radarflow where the entity loop didn't include the last entity.
This commit is contained in:
		
							
								
								
									
										93
									
								
								src/cli.rs
									
									
									
									
									
								
							
							
						
						
									
										93
									
								
								src/cli.rs
									
									
									
									
									
								
							| @@ -1,93 +0,0 @@ | ||||
| use std::path::PathBuf; | ||||
|  | ||||
| use clap::Parser; | ||||
| use memflow::prelude::Inventory; | ||||
|  | ||||
| use crate::structs::{Connector, Loglevel}; | ||||
|  | ||||
| const PORT_RANGE: std::ops::RangeInclusive<usize> = 8000..=65535; | ||||
| const POLL_RANGE: std::ops::RangeInclusive<usize> = 1..=1000; | ||||
|  | ||||
| #[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 = "./web", value_parser = valid_path)] | ||||
|     pub web_path: PathBuf, | ||||
|  | ||||
|     /// Polling frequency in times per second for the DMA thread | ||||
|     #[arg(short = 'r', long, default_value_t = 60, value_parser = poll_in_range)] | ||||
|     pub poll_rate: u16, | ||||
|  | ||||
|     /// 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) | ||||
| } | ||||
|  | ||||
| fn poll_in_range(s: &str) -> Result<u16, String> { | ||||
|     let port: usize = s | ||||
|         .parse() | ||||
|         .map_err(|_| format!("`{s}` isn't a valid number"))?; | ||||
|     if POLL_RANGE.contains(&port) { | ||||
|         Ok(port as u16) | ||||
|     } else { | ||||
|         Err(format!( | ||||
|             "not in range {}-{}", | ||||
|             POLL_RANGE.start(), | ||||
|             POLL_RANGE.end() | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| @@ -1,87 +0,0 @@ | ||||
| use memflow::types::Address; | ||||
|  | ||||
| use crate::structs::communication::PlayerType; | ||||
|  | ||||
| #[derive(Clone, Copy)] | ||||
| pub enum CachedEntity { | ||||
|     Bomb {ptr: Address}, | ||||
|     Player {ptr: Address, player_type: PlayerType}, | ||||
| } | ||||
|  | ||||
| pub struct Cache { | ||||
|     timestamp: std::time::Instant, | ||||
|     entity_cache: Vec<CachedEntity>, | ||||
|     map_name: String, | ||||
|     entity_list: Address, | ||||
| } | ||||
|  | ||||
| impl Cache { | ||||
|     pub fn is_valid(&self) -> bool { | ||||
|         if self.timestamp.elapsed() > std::time::Duration::from_millis(250) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         true | ||||
|     } | ||||
|  | ||||
|     pub fn new_invalid() -> Cache { | ||||
|         Cache { | ||||
|             timestamp: std::time::Instant::now().checked_sub(std::time::Duration::from_millis(500)).unwrap(), | ||||
|             entity_cache: Vec::new(), | ||||
|             map_name: String::new(), | ||||
|             entity_list: Address::null(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn entity_cache(&mut self) -> Vec<CachedEntity> { | ||||
|         self.entity_cache.clone() | ||||
|     } | ||||
|  | ||||
|     pub fn map_name(&self) -> String { | ||||
|         self.map_name.clone() | ||||
|     } | ||||
|  | ||||
|     pub fn entity_list(&self) -> Address { | ||||
|         self.entity_list | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct CacheBuilder { | ||||
|     entity_cache: Option<Vec<CachedEntity>>, | ||||
|     map_name: Option<String>, | ||||
|     entity_list: Option<Address> | ||||
| } | ||||
|  | ||||
| impl CacheBuilder { | ||||
|     pub fn new() -> CacheBuilder { | ||||
|         CacheBuilder { | ||||
|             entity_cache: None, | ||||
|             map_name: None, | ||||
|             entity_list: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn entity_cache(mut self, entity_cache: Vec<CachedEntity>) -> CacheBuilder { | ||||
|         self.entity_cache = Some(entity_cache); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn map_name(mut self, map_name: String) -> CacheBuilder { | ||||
|         self.map_name = Some(map_name); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn entity_list(mut self, entity_list: Address) -> CacheBuilder { | ||||
|         self.entity_list = Some(entity_list); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn build(self) -> anyhow::Result<Cache> { | ||||
|         Ok(Cache { | ||||
|             timestamp: std::time::Instant::now(), | ||||
|             entity_cache: self.entity_cache.ok_or(anyhow::anyhow!("entity_cache not set on builder"))?, | ||||
|             map_name: self.map_name.ok_or(anyhow::anyhow!("map_name not set on builder"))?, | ||||
|             entity_list: self.entity_list.ok_or(anyhow::anyhow!("entity_list not set on builder"))?, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										243
									
								
								src/dma/mod.rs
									
									
									
									
									
								
							
							
						
						
									
										243
									
								
								src/dma/mod.rs
									
									
									
									
									
								
							| @@ -1,243 +0,0 @@ | ||||
| mod cache; | ||||
|  | ||||
| use ::std::sync::Arc; | ||||
|  | ||||
| use memflow::prelude::v1::*; | ||||
| use tokio::{sync::RwLock, time::{Duration, Instant}}; | ||||
|  | ||||
| use crate::{structs::{Connector, communication::{RadarData, PlayerType, EntityData, PlayerData, BombData}}, sdk::{self, structs::{MemoryClass, BaseEntity, CBaseEntity, CPlayerController}}, dma::cache::CacheBuilder}; | ||||
|  | ||||
| use self::cache::Cache; | ||||
|  | ||||
|  | ||||
| pub struct CheatCtx { | ||||
|     pub process: IntoProcessInstanceArcBox<'static>, | ||||
|     pub client_module: ModuleInfo, | ||||
|     pub engine_module: ModuleInfo, | ||||
| } | ||||
|  | ||||
| impl CheatCtx { | ||||
|     pub fn setup(connector: Connector, pcileech_device: String) -> anyhow::Result<CheatCtx> { | ||||
|         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 ctx = Self { | ||||
|             process, | ||||
|             client_module, | ||||
|             engine_module, | ||||
|         }; | ||||
|  | ||||
|         Ok(ctx) | ||||
|     } | ||||
| } | ||||
|  | ||||
| const SECOND_AS_NANO: u64 = 1000*1000*1000; | ||||
| static ONCE: std::sync::Once = std::sync::Once::new(); | ||||
|  | ||||
| pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, data_lock: Arc<RwLock<RadarData>>) -> anyhow::Result<()> { | ||||
|     let mut ctx = CheatCtx::setup(connector, pcileech_device)?; | ||||
|  | ||||
|     // Avoid printing warnings and other stuff before the initial prints are complete | ||||
|     tokio::time::sleep(Duration::from_millis(500)).await; | ||||
|  | ||||
|     // For poll rate timing | ||||
|     let should_time = poll_rate != 0; | ||||
|  | ||||
|     let target_interval = Duration::from_nanos(SECOND_AS_NANO / poll_rate as u64); | ||||
|     let mut last_iteration_time = Instant::now(); | ||||
|     let mut missmatch_count = 0; | ||||
|  | ||||
|     let mut cache = Cache::new_invalid(); | ||||
|  | ||||
|     loop { | ||||
|         if ctx.process.state().is_dead() { | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         if !cache.is_valid() { | ||||
|             let mut cached_entities = Vec::new(); | ||||
|  | ||||
|             let globals = sdk::get_globals(&mut ctx)?; | ||||
|             let highest_index = sdk::highest_entity_index(&mut ctx)?; | ||||
|             let map_name = sdk::map_name(globals, &mut ctx)?; | ||||
|             let entity_list = sdk::get_entity_list(&mut ctx)?; | ||||
|  | ||||
|             let local = sdk::get_local(&mut ctx)?; | ||||
|              | ||||
|             if local.get_pawn(&mut ctx, entity_list)?.is_some() { | ||||
|                 cached_entities.push(cache::CachedEntity::Player { | ||||
|                     ptr: local.ptr(), | ||||
|                     player_type: PlayerType::Local  | ||||
|                 }); | ||||
|  | ||||
|                 for idx in 1..highest_index { | ||||
|                     if let Some(entity) = CBaseEntity::from_index(&mut ctx, entity_list, idx)? { | ||||
|  | ||||
|                         let class_name = entity.class_name(&mut ctx)?; | ||||
|  | ||||
|                         match class_name.as_str() { | ||||
|                             "weapon_c4" => { | ||||
|                                 cached_entities.push(cache::CachedEntity::Bomb { | ||||
|                                     ptr: entity.ptr() | ||||
|                                 }) | ||||
|                             }, | ||||
|                             "cs_player_controller" => { | ||||
|                                 let controller = entity.to_player_controller(); | ||||
|  | ||||
|                                 let player_type = { | ||||
|                                     match controller.get_player_type(&mut ctx, &local)? { | ||||
|                                         Some(t) => { | ||||
|                                             if t == PlayerType::Spectator { continue } else { t } | ||||
|                                         }, | ||||
|                                         None => { continue }, | ||||
|                                     } | ||||
|                                 }; | ||||
|  | ||||
|                                 cached_entities.push(cache::CachedEntity::Player { | ||||
|                                     ptr: entity.ptr(), | ||||
|                                     player_type, | ||||
|                                 }) | ||||
|                             } | ||||
|                             _ => {} | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             cache = CacheBuilder::new() | ||||
|                 .entity_cache(cached_entities) | ||||
|                 .entity_list(entity_list) | ||||
|                 .map_name(map_name) | ||||
|                 .build()?; | ||||
|  | ||||
|             log::debug!("Rebuilt cache."); | ||||
|         } | ||||
|  | ||||
|         if sdk::network_is_ingame(&mut ctx)? { | ||||
|             let mut radar_data = Vec::with_capacity(64); | ||||
|  | ||||
|             if sdk::is_bomb_planted(&mut ctx)? { | ||||
|                 let bomb = sdk::get_plantedc4(&mut ctx)?; | ||||
|                 let bomb_pos = bomb.pos(&mut ctx)?; | ||||
|                 radar_data.push( | ||||
|                     EntityData::Bomb(BombData::new( | ||||
|                         bomb_pos, | ||||
|                         true | ||||
|                     )) | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             for cached_data in cache.entity_cache() { | ||||
|                 match cached_data { | ||||
|                     cache::CachedEntity::Bomb { ptr } => { | ||||
|                         if sdk::is_bomb_dropped(&mut ctx)?  { | ||||
|                             let bomb_entity = CBaseEntity::new(ptr); | ||||
|                             let pos = bomb_entity.pos(&mut ctx)?; | ||||
|      | ||||
|                             radar_data.push( | ||||
|                                 EntityData::Bomb( | ||||
|                                     BombData::new( | ||||
|                                         pos, | ||||
|                                         false | ||||
|                                     ) | ||||
|                                 ) | ||||
|                             ); | ||||
|                         } | ||||
|                     }, | ||||
|                     cache::CachedEntity::Player { ptr, player_type } => { | ||||
|                         let controller = CPlayerController::new(ptr); | ||||
|                         if let Some(pawn) = controller.get_pawn(&mut ctx, cache.entity_list())? { | ||||
|                             if pawn.is_alive(&mut ctx)? { | ||||
|                                 let pos = pawn.pos(&mut ctx)?; | ||||
|                                 let yaw = pawn.angles(&mut ctx)?.y; | ||||
|                                 let has_bomb = pawn.has_c4(&mut ctx, cache.entity_list())?; | ||||
|                      | ||||
|                                 radar_data.push( | ||||
|                                     EntityData::Player( | ||||
|                                         PlayerData::new( | ||||
|                                             pos,  | ||||
|                                             yaw, | ||||
|                                             player_type, | ||||
|                                             has_bomb | ||||
|                                         ) | ||||
|                                     ) | ||||
|                                 ); | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             let mut data = data_lock.write().await; | ||||
|             if cache.map_name() == "<empty>" || cache.map_name().is_empty() { | ||||
|                 *data = RadarData::empty() | ||||
|             } else { | ||||
|                 *data = RadarData::new(true, cache.map_name(), radar_data) | ||||
|             } | ||||
|         } else { | ||||
|             let mut data = data_lock.write().await; | ||||
|             *data = RadarData::empty() | ||||
|         } | ||||
|  | ||||
|         if should_time { | ||||
|             let elapsed = last_iteration_time.elapsed(); | ||||
|      | ||||
|             let remaining = match target_interval.checked_sub(elapsed) { | ||||
|                 Some(t) => t, | ||||
|                 None => { | ||||
|                     if missmatch_count >= 25 { | ||||
|                         ONCE.call_once(|| { | ||||
|                             log::warn!("Remaining time till target interval was negative more than 25 times"); | ||||
|                             log::warn!("You should decrease your poll rate."); | ||||
|                             log::warn!("elapsed: {}ns", elapsed.as_nanos()); | ||||
|                             log::warn!("target: {}ns", target_interval.as_nanos()); | ||||
|                         }); | ||||
|                     } else { | ||||
|                         missmatch_count += 1; | ||||
|                     } | ||||
|                     Duration::from_nanos(0) | ||||
|                 }, | ||||
|             }; | ||||
|      | ||||
|             #[cfg(target_os = "linux")] | ||||
|             tokio_timerfd::sleep(remaining).await?; | ||||
|      | ||||
|             #[cfg(not(target_os = "linux"))] | ||||
|             tokio::time::sleep(remaining).await; | ||||
|      | ||||
|             log::info!("poll rate: {:.2}Hz", SECOND_AS_NANO as f64 / last_iteration_time.elapsed().as_nanos() as f64); | ||||
|             log::trace!("elapsed: {}ns", elapsed.as_nanos()); | ||||
|             log::trace!("target: {}ns", target_interval.as_nanos()); | ||||
|             log::trace!("missmatch count: {}", missmatch_count); | ||||
|      | ||||
|             last_iteration_time = Instant::now(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										57
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,57 +0,0 @@ | ||||
| use ::std::sync::Arc; | ||||
| use clap::Parser; | ||||
|  | ||||
| use cli::Cli; | ||||
| use structs::communication::RadarData; | ||||
| use tokio::sync::RwLock; | ||||
|  | ||||
| mod dma; | ||||
| mod sdk; | ||||
| mod structs; | ||||
| mod cli; | ||||
| mod webserver; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> anyhow::Result<()> { | ||||
|     let cli = Cli::parse(); | ||||
|  | ||||
|     simple_logger::SimpleLogger::new() | ||||
|         .with_level(cli.loglevel.into()) | ||||
|         .init() | ||||
|         .expect("Initializing logger"); | ||||
|  | ||||
|     let rwlock = Arc::new( | ||||
|         RwLock::new( | ||||
|             RadarData::empty() | ||||
|         ) | ||||
|     ); | ||||
|  | ||||
|     let rwlock_clone = rwlock.clone(); | ||||
|     let dma_handle = tokio::spawn(async move { | ||||
|         if let Err(err) = dma::run(cli.connector, cli.pcileech_device, cli.poll_rate, rwlock_clone).await { | ||||
|             log::error!("Error in dma thread: {}", err.to_string()); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     tokio::spawn(async move { | ||||
|         let future = webserver::run(cli.web_path, cli.port, rwlock); | ||||
|  | ||||
|         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) = future.await { | ||||
|             log::error!("Error in websocket server: {}", err.to_string()); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     if let Err(err) = dma_handle.await { | ||||
|         log::error!("Error when waiting for dma thread: {}", err.to_string()); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| @@ -1,4 +0,0 @@ | ||||
| #![allow(dead_code)] | ||||
| pub mod client; | ||||
| pub mod engine2; | ||||
| pub mod offsets; | ||||
| @@ -1,74 +0,0 @@ | ||||
| pub mod structs; | ||||
| pub mod cs2dumper; | ||||
| use crate::dma::CheatCtx; | ||||
|  | ||||
| use memflow::prelude::v1::*; | ||||
| use anyhow::Result; | ||||
|  | ||||
| use self::structs::{CPlayerController, CBaseEntity, MemoryClass, CPlayerPawn}; | ||||
|  | ||||
| pub fn get_local(ctx: &mut CheatCtx) -> Result<CPlayerController> { | ||||
|     let ptr = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwLocalPlayerController)?; | ||||
|     Ok(CPlayerController::new(ptr)) | ||||
| } | ||||
|  | ||||
| pub fn get_plantedc4(ctx: &mut CheatCtx) -> Result<CBaseEntity> { | ||||
|     let ptr = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwPlantedC4)?; | ||||
|     let ptr2 = ctx.process.read_addr64(ptr)?; | ||||
|     Ok(CBaseEntity::new(ptr2)) | ||||
| } | ||||
|  | ||||
| pub fn is_bomb_planted(ctx: &mut CheatCtx) -> Result<bool> { | ||||
|     let game_rules = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGameRules)?; | ||||
|     let data: u8 = ctx.process.read(game_rules + cs2dumper::client::C_CSGameRules::m_bBombPlanted)?; | ||||
|     Ok(data != 0) | ||||
| } | ||||
|  | ||||
| pub fn is_bomb_dropped(ctx: &mut CheatCtx) -> Result<bool> { | ||||
|     let game_rules = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGameRules)?; | ||||
|     let data: u8 = ctx.process.read(game_rules + cs2dumper::client::C_CSGameRules::m_bBombDropped)?; | ||||
|     Ok(data != 0) | ||||
| } | ||||
|  | ||||
| /* | ||||
| pub fn is_ingame(ctx: &mut CheatCtx) -> Result<bool> { | ||||
|     let game_rules = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGameRules)?; | ||||
|     let data: i32 = ctx.process.read(game_rules + cs2dumper::client::C_CSGameRules::m_gamePhase)?; | ||||
|     println!("m_gamePhase: {}", data); | ||||
|     Ok(data != 1) | ||||
| } | ||||
| */ | ||||
|  | ||||
| #[allow(dead_code)] | ||||
| pub fn get_local_pawn(ctx: &mut CheatCtx) -> Result<CPlayerPawn> { | ||||
|     let ptr = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwLocalPlayerPawn)?; | ||||
|     Ok(CPlayerPawn::new(ptr)) | ||||
| } | ||||
|  | ||||
| pub fn get_entity_list(ctx: &mut CheatCtx) -> Result<Address> { | ||||
|     let ptr = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwEntityList)?; | ||||
|     Ok(ptr) | ||||
| } | ||||
|  | ||||
| pub fn get_globals(ctx: &mut CheatCtx) -> Result<Address> { | ||||
|     let ptr = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGlobalVars)?; | ||||
|     Ok(ptr) | ||||
| } | ||||
|  | ||||
| pub fn map_name(global_vars: Address, ctx: &mut CheatCtx) -> Result<String> { | ||||
|     let ptr = ctx.process.read_addr64(global_vars + 0x188)?; | ||||
|     Ok(ctx.process.read_char_string_n(ptr, 32)?) | ||||
| } | ||||
|  | ||||
| pub fn highest_entity_index(ctx: &mut CheatCtx) -> Result<i32> { | ||||
|     let game_entity_system = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGameEntitySystem)?; | ||||
|     let highest_index = ctx.process.read(game_entity_system + cs2dumper::offsets::client_dll::dwGameEntitySystem_getHighestEntityIndex)?; | ||||
|     Ok(highest_index) | ||||
| } | ||||
|  | ||||
|  | ||||
| pub fn network_is_ingame(ctx: &mut CheatCtx) -> Result<bool> { | ||||
|     let ptr = ctx.process.read_addr64(ctx.engine_module.base + cs2dumper::offsets::engine2_dll::dwNetworkGameClient)?; | ||||
|     let signonstate: u64 = ctx.process.read(ptr + cs2dumper::offsets::engine2_dll::dwNetworkGameClient_signOnState)?; | ||||
|     Ok(signonstate == 6) | ||||
| } | ||||
| @@ -1,50 +0,0 @@ | ||||
| use memflow::{types::Address, mem::MemoryView}; | ||||
| use anyhow::Result; | ||||
|  | ||||
| use crate::{dma::CheatCtx, structs::Vec3, sdk::{cs2dumper, structs::{BaseEntity, MemoryClass}}}; | ||||
| use super::CPlayerController; | ||||
|  | ||||
| pub struct CBaseEntity(Address); | ||||
|  | ||||
| impl MemoryClass for CBaseEntity { | ||||
|     fn ptr(&self) -> Address { | ||||
|         self.0 | ||||
|     } | ||||
|  | ||||
|     fn new(ptr: Address) -> Self { | ||||
|         Self(ptr) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl BaseEntity for CBaseEntity { | ||||
|     fn from_index(ctx: &mut CheatCtx, entity_list: Address, index: i32) -> Result<Option<CBaseEntity>> { | ||||
|         let list_entry = ctx.process.read_addr64(entity_list + 8 * (index >> 9) + 16)?; | ||||
|         if list_entry.is_null() && !list_entry.is_valid() { | ||||
|             return Ok(None); | ||||
|         } | ||||
|  | ||||
|         let player_ptr = ctx.process.read_addr64(list_entry + 120 * (index & 0x1FF))?; | ||||
|         if player_ptr.is_null() && !player_ptr.is_valid() { | ||||
|             return Ok(None); | ||||
|         } | ||||
|  | ||||
|         Ok(Some(Self::new(player_ptr))) | ||||
|     } | ||||
|  | ||||
|     fn pos(&self, ctx: &mut CheatCtx) -> anyhow::Result<Vec3> { | ||||
|         let node = ctx.process.read_addr64(self.0 + cs2dumper::client::C_BaseEntity::m_pGameSceneNode)?; | ||||
|         Ok(ctx.process.read(node + cs2dumper::client::CGameSceneNode::m_vecAbsOrigin)?) | ||||
|     } | ||||
|  | ||||
|     fn class_name(&self, ctx: &mut CheatCtx) -> anyhow::Result<String> { | ||||
|         let entity_identity_ptr = ctx.process.read_addr64(self.0 + cs2dumper::client::CEntityInstance::m_pEntity)?; | ||||
|         let class_name_ptr = ctx.process.read_addr64(entity_identity_ptr + cs2dumper::client::CEntityIdentity::m_designerName)?; | ||||
|         Ok(ctx.process.read_char_string_n(class_name_ptr, 32)?) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl CBaseEntity { | ||||
|     pub fn to_player_controller(&self) -> CPlayerController { | ||||
|         CPlayerController::new(self.0) | ||||
|     } | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| mod base_entity; | ||||
| mod player_controller; | ||||
| mod player_pawn; | ||||
|  | ||||
| pub use base_entity::CBaseEntity; | ||||
| pub use player_controller::CPlayerController; | ||||
| pub use player_pawn::CPlayerPawn; | ||||
|  | ||||
| use crate::{dma::CheatCtx, structs::Vec3}; | ||||
|  | ||||
| use memflow::types::Address; | ||||
| use anyhow::Result; | ||||
|  | ||||
| /// A trait that implements basic functions from C_BaseEntity | ||||
| /// CCSPlayerController inherits C_BaseEntity, which is why this trait exists. | ||||
| pub trait BaseEntity { | ||||
|     fn from_index(ctx: &mut CheatCtx, entity_list: Address, index: i32) -> Result<Option<Self>> where Self: std::marker::Sized; | ||||
|     fn pos(&self, ctx: &mut CheatCtx) -> Result<Vec3>; | ||||
|     fn class_name(&self, ctx: &mut CheatCtx) -> Result<String>; | ||||
| } | ||||
|  | ||||
| /// A trait that implements basic functions for an class represented by a single pointer | ||||
| pub trait MemoryClass { | ||||
|     fn ptr(&self) -> Address; | ||||
|     fn new(ptr: Address) -> Self; | ||||
| } | ||||
| @@ -1,91 +0,0 @@ | ||||
| use memflow::{types::Address, mem::MemoryView}; | ||||
| use anyhow::Result; | ||||
| use num_traits::FromPrimitive; | ||||
|  | ||||
| use crate::{dma::CheatCtx, structs::{Vec3, communication::PlayerType}, sdk::{cs2dumper, structs::{BaseEntity, MemoryClass, TeamID}}}; | ||||
|  | ||||
| use super::CPlayerPawn; | ||||
|  | ||||
| pub struct CPlayerController(Address); | ||||
|  | ||||
| impl MemoryClass for CPlayerController { | ||||
|     fn ptr(&self) -> Address { | ||||
|         self.0 | ||||
|     } | ||||
|  | ||||
|     fn new(ptr: Address) -> Self { | ||||
|         Self(ptr) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl BaseEntity for CPlayerController { | ||||
|     fn from_index(ctx: &mut CheatCtx, entity_list: Address, index: i32) -> Result<Option<CPlayerController>> { | ||||
|         let list_entry = ctx.process.read_addr64(entity_list + 8 * (index >> 9) + 16)?; | ||||
|         if list_entry.is_null() && !list_entry.is_valid() { | ||||
|             return Ok(None); | ||||
|         } | ||||
|  | ||||
|         let player_ptr = ctx.process.read_addr64(list_entry + 120 * (index & 0x1FF))?; | ||||
|         if player_ptr.is_null() && !player_ptr.is_valid() { | ||||
|             return Ok(None); | ||||
|         } | ||||
|  | ||||
|         Ok(Some(Self::new(player_ptr))) | ||||
|     } | ||||
|  | ||||
|     fn pos(&self, ctx: &mut CheatCtx) -> anyhow::Result<Vec3> { | ||||
|         let node = ctx.process.read_addr64(self.0 + cs2dumper::client::C_BaseEntity::m_pGameSceneNode)?; | ||||
|         Ok(ctx.process.read(node + cs2dumper::client::CGameSceneNode::m_vecAbsOrigin)?) | ||||
|     } | ||||
|  | ||||
|     fn class_name(&self, ctx: &mut CheatCtx) -> anyhow::Result<String> { | ||||
|         let entity_identity_ptr = ctx.process.read_addr64(self.0 + cs2dumper::client::CEntityInstance::m_pEntity)?; | ||||
|         let class_name_ptr = ctx.process.read_addr64(entity_identity_ptr + cs2dumper::client::CEntityIdentity::m_designerName)?; | ||||
|         Ok(ctx.process.read_char_string_n(class_name_ptr, 32)?) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl CPlayerController { | ||||
|     pub fn get_pawn(&self, ctx: &mut CheatCtx, entity_list: Address) -> Result<Option<CPlayerPawn>> { | ||||
|         let uhandle = ctx.process.read(self.0 + cs2dumper::client::CCSPlayerController::m_hPlayerPawn)?; | ||||
|         CPlayerPawn::from_uhandle(ctx, entity_list, uhandle) | ||||
|     } | ||||
|  | ||||
|     // Technically something that should be in the BaseEntity trait, but we are only gonna use it on CPlayerController | ||||
|     pub fn get_team(&self, ctx: &mut CheatCtx) -> Result<Option<TeamID>> { | ||||
|         let team_num: i32 = ctx.process.read(self.0 + cs2dumper::client::C_BaseEntity::m_iTeamNum)?; | ||||
|         Ok(TeamID::from_i32(team_num)) | ||||
|     } | ||||
|  | ||||
|     pub fn get_player_type(&self, ctx: &mut CheatCtx, local: &CPlayerController) -> Result<Option<PlayerType>> { | ||||
|         if self.0 == local.0 { | ||||
|             return Ok(Some(PlayerType::Local)) | ||||
|         } | ||||
|  | ||||
|         let team = { | ||||
|             match self.get_team(ctx)? { | ||||
|                 Some(t) => t, | ||||
|                 None => { return Ok(None) }, | ||||
|             } | ||||
|         }; | ||||
|          | ||||
|         let local_team = { | ||||
|             match local.get_team(ctx)? { | ||||
|                 Some(t) => t, | ||||
|                 None => { return Ok(None) }, | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let player_type = { | ||||
|             if team == TeamID::Spectator {  | ||||
|                 PlayerType::Spectator  | ||||
|             } else if team != local_team { | ||||
|                 PlayerType::Enemy | ||||
|             } else { | ||||
|                 PlayerType::Team | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         Ok(Some(player_type)) | ||||
|     } | ||||
| } | ||||
| @@ -1,81 +0,0 @@ | ||||
| use memflow::{types::Address, mem::MemoryView}; | ||||
| use anyhow::Result; | ||||
|  | ||||
| use crate::{dma::CheatCtx, structs::Vec3, sdk::{cs2dumper, structs::MemoryClass}}; | ||||
|  | ||||
| pub struct CPlayerPawn(Address); | ||||
|  | ||||
| impl CPlayerPawn { | ||||
|     pub fn from_uhandle(ctx: &mut CheatCtx, entity_list: Address, uhandle: u32) -> Result<Option<Self>> { | ||||
|         let list_entry = ctx.process.read_addr64(entity_list + 0x8 * ((uhandle & 0x7FFF) >> 9) + 16)?; | ||||
|          | ||||
|         if list_entry.is_null() || !list_entry.is_valid() { | ||||
|             Ok(None) | ||||
|         } else { | ||||
|             let ptr = ctx.process.read_addr64(list_entry + 120 * (uhandle & 0x1FF))?; | ||||
|             Ok(Some(Self(ptr))) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Todo: Optimize this function: find another way to do this | ||||
|     pub fn has_c4(&self, ctx: &mut CheatCtx, entity_list: Address) -> Result<bool> { | ||||
|         let mut has_c4 = false; | ||||
|         let wep_services = ctx.process.read_addr64(self.0 + cs2dumper::client::C_BasePlayerPawn::m_pWeaponServices)?; | ||||
|         let wep_count: i32  = ctx.process.read(wep_services + cs2dumper::client::CPlayer_WeaponServices::m_hMyWeapons)?; | ||||
|         let wep_base = ctx.process.read_addr64(wep_services + cs2dumper::client::CPlayer_WeaponServices::m_hMyWeapons + 0x8)?; | ||||
|  | ||||
|         for wep_idx in 0..wep_count { | ||||
|             let handle: i32 = ctx.process.read(wep_base + wep_idx * 0x4)?; | ||||
|             if handle == -1 { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             let list_entry = ctx.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 = ctx.process.read_addr64(list_entry + 120 * (handle & 0x1FF))?; | ||||
|                     Some(ptr) | ||||
|                 } | ||||
|             } { | ||||
|                 let wep_data = ctx.process.read_addr64(wep_ptr + cs2dumper::client::C_BaseEntity::m_nSubclassID + 0x8)?; | ||||
|                 let id: i32 = ctx.process.read(wep_data + cs2dumper::client::CCSWeaponBaseVData::m_WeaponType)?; | ||||
|  | ||||
|                 if id == 7 { | ||||
|                     has_c4 = true; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(has_c4) | ||||
|     } | ||||
|  | ||||
|     pub fn pos(&self, ctx: &mut CheatCtx) -> Result<Vec3> { | ||||
|         Ok(ctx.process.read(self.0 + cs2dumper::client::C_BasePlayerPawn::m_vOldOrigin)?) | ||||
|     } | ||||
|  | ||||
|     pub fn angles(&self, ctx: &mut CheatCtx) -> Result<Vec3> { | ||||
|         Ok(ctx.process.read(self.0 + cs2dumper::client::C_CSPlayerPawnBase::m_angEyeAngles)?) | ||||
|     } | ||||
|  | ||||
|     pub fn health(&self, ctx: &mut CheatCtx) -> Result<u32> { | ||||
|         Ok(ctx.process.read(self.0 + cs2dumper::client::C_BaseEntity::m_iHealth)?) | ||||
|     } | ||||
|  | ||||
|     /// Same as ::get_health > 0 | ||||
|     pub fn is_alive(&self, ctx: &mut CheatCtx) -> Result<bool> { | ||||
|         Ok(self.health(ctx)? > 0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl MemoryClass for CPlayerPawn { | ||||
|     fn ptr(&self) -> Address { | ||||
|         self.0 | ||||
|     } | ||||
|  | ||||
|     fn new(ptr: Address) -> Self { | ||||
|         Self(ptr) | ||||
|     } | ||||
| } | ||||
| @@ -1,13 +0,0 @@ | ||||
| mod entity_classes; | ||||
|  | ||||
| pub use entity_classes::*; | ||||
|  | ||||
| use enum_primitive_derive::Primitive; | ||||
|  | ||||
| #[repr(i32)] | ||||
| #[derive(Debug, Eq, PartialEq, Primitive)] | ||||
| pub enum TeamID { | ||||
|     Spectator = 1, | ||||
|     T = 2, | ||||
|     CT = 3 | ||||
| } | ||||
| @@ -1,79 +0,0 @@ | ||||
| use serde::{Serialize, Deserialize}; | ||||
|  | ||||
| use super::Vec3; | ||||
|  | ||||
| #[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, Copy, Serialize, Deserialize, Default, PartialEq)] | ||||
| pub enum PlayerType { | ||||
|     #[default] | ||||
|     Unknown, | ||||
|     Spectator, | ||||
|     Local, | ||||
|     Enemy, | ||||
|     Team | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct RadarData { | ||||
|     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>) -> RadarData { | ||||
|         RadarData { ingame, map_name, player_data } | ||||
|     } | ||||
|  | ||||
|     /// Returns empty RadarData, it's also the same data that gets sent to clients when not ingame | ||||
|     pub fn empty() -> RadarData { | ||||
|         RadarData {  | ||||
|             ingame: false, | ||||
|             map_name: String::new(), | ||||
|             player_data: Vec::new(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,50 +0,0 @@ | ||||
| pub mod comms; | ||||
|  | ||||
| use clap::ValueEnum; | ||||
| pub use comms as communication; | ||||
|  | ||||
| mod vec3; | ||||
| pub use vec3::Vec3; | ||||
|  | ||||
|  | ||||
|  | ||||
| #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, 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"), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// 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, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| use serde::Serialize; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy, Serialize, serde::Deserialize)] | ||||
| #[repr(C)] | ||||
| pub struct Vec3 { | ||||
|     pub x: f32,  | ||||
|     pub y: f32, | ||||
|     pub z: f32 | ||||
| } | ||||
|  | ||||
| unsafe impl dataview::Pod for Vec3 {} | ||||
| @@ -1,66 +0,0 @@ | ||||
| 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::structs::communication::RadarData; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| struct AppState { | ||||
|     data_lock: Arc<RwLock<RadarData>> | ||||
| } | ||||
|  | ||||
| async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response { | ||||
|     ws.on_upgrade(|socket| handle_socket(socket, state)) | ||||
| } | ||||
|  | ||||
| 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 data = state.data_lock.read().await; | ||||
|  | ||||
|                 let str = { | ||||
|                     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() | ||||
|                         }, | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 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