diff --git a/src/cli.rs b/src/cli.rs index aff9fcb..15625f7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,27 +11,27 @@ const POLL_RANGE: std::ops::RangeInclusive<usize> = 1..=1000; #[derive(Parser)] #[command(author, version = version(), about, long_about = None)] pub struct Cli { - /// Connector to use for DMA + /// Specifies the connector type for DMA #[clap(value_enum, short, long, ignore_case = true, default_value_t = Connector::Qemu)] pub connector: Connector, - /// Pcileech device name + /// Name of the Pcileech device #[clap(long, default_value_t = String::from("FPGA"))] pub pcileech_device: String, - /// Port to run Webserver on + /// 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 serve on webserver + /// Path to the directory served by the Webserver #[arg(short, long, default_value = "./web", value_parser = valid_path)] pub web_path: PathBuf, - /// How often per second the DMA thread should poll for data + /// 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, - /// Loglevel verbosity + /// Verbosity level for logging to the console #[arg(value_enum, long, short, ignore_case = true, default_value_t = Loglevel::Warn)] pub loglevel: Loglevel, } diff --git a/src/dma/cache.rs b/src/dma/cache.rs new file mode 100644 index 0000000..c9ca099 --- /dev/null +++ b/src/dma/cache.rs @@ -0,0 +1,87 @@ +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"))?, + }) + } +} \ No newline at end of file diff --git a/src/dma/mod.rs b/src/dma/mod.rs index b6ff52f..0baddf2 100644 --- a/src/dma/mod.rs +++ b/src/dma/mod.rs @@ -1,9 +1,14 @@ +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::{PlayerController, Cache, MemoryClass}}}; +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>, @@ -67,48 +72,45 @@ pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, let mut last_iteration_time = Instant::now(); let mut missmatch_count = 0; - let mut cache = Cache::new(); + let mut cache = Cache::new_invalid(); loop { if ctx.process.state().is_dead() { break; } - if cache.is_outdated() { - cache.clean(); + 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)?; - cache.common().update( - map_name, - entity_list - ); - let local = sdk::get_local(&mut ctx)?; - if local.pawn(&mut ctx, entity_list)?.is_some() { - cache.push_data(sdk::structs::CachedEntityData::Player { + 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) = PlayerController::from_entity_list_v2(&mut ctx, entity_list, idx)? { + 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" => { - cache.push_data(sdk::structs::CachedEntityData::Bomb { + cached_entities.push(cache::CachedEntity::Bomb { ptr: entity.ptr() }) }, "cs_player_controller" => { + let controller = entity.to_player_controller(); + let player_type = { - match entity.get_player_type(&mut ctx, &local)? { + match controller.get_player_type(&mut ctx, &local)? { Some(t) => { if t == PlayerType::Spectator { continue } else { t } }, @@ -116,7 +118,7 @@ pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, } }; - cache.push_data(sdk::structs::CachedEntityData::Player { + cached_entities.push(cache::CachedEntity::Player { ptr: entity.ptr(), player_type, }) @@ -127,9 +129,13 @@ pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, } } - log::debug!("Rebuilt cache."); + cache = CacheBuilder::new() + .entity_cache(cached_entities) + .entity_list(entity_list) + .map_name(map_name) + .build()?; - cache.new_time(); + log::debug!("Rebuilt cache."); } if sdk::is_ingame(&mut ctx)? { @@ -146,12 +152,12 @@ pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, ); } - for cached_data in cache.data() { + for cached_data in cache.entity_cache() { match cached_data { - sdk::structs::CachedEntityData::Bomb { ptr } => { + cache::CachedEntity::Bomb { ptr } => { if sdk::is_bomb_dropped(&mut ctx)? { - let controller = PlayerController::new(ptr); - let pos = controller.pos(&mut ctx)?; + let bomb_entity = CBaseEntity::new(ptr); + let pos = bomb_entity.pos(&mut ctx)?; radar_data.push( EntityData::Bomb( @@ -163,13 +169,13 @@ pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, ); } }, - sdk::structs::CachedEntityData::Player { ptr, player_type } => { - let controller = PlayerController::new(ptr); - if let Some(pawn) = controller.pawn(&mut ctx, cache.common().entity_list())? { + 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.common().entity_list())?; + let has_bomb = pawn.has_c4(&mut ctx, cache.entity_list())?; radar_data.push( EntityData::Player( @@ -188,7 +194,11 @@ pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, } let mut data = data_lock.write().await; - *data = RadarData::new(true, cache.common().map_name(), radar_data) + 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() @@ -221,8 +231,8 @@ pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16, 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: {}", elapsed.as_nanos()); - log::trace!("target: {}", target_interval.as_nanos()); + 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(); diff --git a/src/main.rs b/src/main.rs index ca51150..9fc1969 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,10 +15,6 @@ mod webserver; async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); - if std::env::var("RADARFLOW_LOG").is_err() { - std::env::set_var("RADARFLOW_LOG", "warn") - } - simple_logger::SimpleLogger::new() .with_level(cli.loglevel.into()) .init() @@ -35,7 +31,6 @@ async fn main() -> anyhow::Result<()> { 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 { diff --git a/src/sdk/mod.rs b/src/sdk/mod.rs index 7bf4726..ae91d48 100644 --- a/src/sdk/mod.rs +++ b/src/sdk/mod.rs @@ -5,17 +5,17 @@ use crate::dma::CheatCtx; use memflow::prelude::v1::*; use anyhow::Result; -use self::structs::{PlayerController, PlayerPawn, MemoryClass, Bomb}; +use self::structs::{CPlayerController, CBaseEntity, MemoryClass, CPlayerPawn}; -pub fn get_local(ctx: &mut CheatCtx) -> Result<PlayerController> { +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(PlayerController::new(ptr)) + Ok(CPlayerController::new(ptr)) } -pub fn get_plantedc4(ctx: &mut CheatCtx) -> Result<Bomb> { +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(Bomb::new(ptr2)) + Ok(CBaseEntity::new(ptr2)) } pub fn is_bomb_planted(ctx: &mut CheatCtx) -> Result<bool> { @@ -30,10 +30,16 @@ pub fn is_bomb_dropped(ctx: &mut CheatCtx) -> Result<bool> { 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)?; + Ok(data != 1) +} + #[allow(dead_code)] -pub fn get_local_pawn(ctx: &mut CheatCtx) -> Result<PlayerPawn> { +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(PlayerPawn::new(ptr)) + Ok(CPlayerPawn::new(ptr)) } pub fn get_entity_list(ctx: &mut CheatCtx) -> Result<Address> { @@ -51,20 +57,16 @@ pub fn map_name(global_vars: Address, ctx: &mut CheatCtx) -> Result<String> { Ok(ctx.process.read_char_string_n(ptr, 32)?) } -/* -pub fn max_clients(global_vars: Address, ctx: &mut CheatCtx) -> Result<i32> { - Ok(ctx.process.read(global_vars + 0x10)?) -} -*/ - 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 is_ingame(ctx: &mut CheatCtx) -> Result<bool> { +/* +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) -} \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/src/sdk/structs/entity.rs b/src/sdk/structs/entity.rs deleted file mode 100644 index ea6699e..0000000 --- a/src/sdk/structs/entity.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::{dma::CheatCtx, sdk::cs2dumper, structs::{Vec3, communication::PlayerType}}; -use enum_primitive_derive::Primitive; -use memflow::{prelude::MemoryView, types::Address}; -use anyhow::Result; -use num_traits::FromPrimitive; - -pub trait MemoryClass { - fn ptr(&self) -> Address; - fn new(ptr: Address) -> Self; -} - -#[repr(i32)] -#[derive(Debug, Eq, PartialEq, Primitive)] -pub enum TeamID { - Spectator = 1, - T = 2, - CT = 3 -} - -pub struct PlayerController(Address); - -impl PlayerController { - - /* - pub fn from_entity_list(ctx: &mut CheatCtx, entity_list: Address, index: i32) -> Result<Option<Self>> { - let list_entry = ctx.process.read_addr64(entity_list + ((8 * (index & 0x7FFF)) >> 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))) - } - */ - - pub fn from_entity_list_v2(ctx: &mut CheatCtx, entity_list: Address, index: i32) -> Result<Option<Self>> { - 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))) - } - - pub fn pos(&self, ctx: &mut CheatCtx) -> 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)?) - } - - pub fn class_name(&self, ctx: &mut CheatCtx) -> 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)?) - } - - 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: &PlayerController) -> 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)) - } - - pub fn pawn(&self, ctx: &mut CheatCtx, entity_list: Address) -> Result<Option<PlayerPawn>> { - let uhandle = ctx.process.read(self.0 + cs2dumper::client::CCSPlayerController::m_hPlayerPawn)?; - PlayerPawn::from_uhandle(ctx, entity_list, uhandle) - } - -} - -impl MemoryClass for PlayerController { - fn ptr(&self) -> Address { - self.0 - } - - fn new(ptr: Address) -> Self { - Self(ptr) - } -} - -pub struct PlayerPawn(Address); - -impl PlayerPawn { - 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))) - } - } - - 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 PlayerPawn { - fn ptr(&self) -> Address { - self.0 - } - - fn new(ptr: Address) -> Self { - Self(ptr) - } -} - -pub struct Bomb(Address); - -impl MemoryClass for Bomb { - fn ptr(&self) -> Address { - self.0 - } - - fn new(ptr: Address) -> Self { - Self(ptr) - } -} - -impl Bomb { - pub fn pos(&self, ctx: &mut CheatCtx) -> Result<Vec3> { - let c4_node = ctx.process.read_addr64(self.0 + cs2dumper::client::C_BaseEntity::m_pGameSceneNode)?; - Ok(ctx.process.read(c4_node + cs2dumper::client::CGameSceneNode::m_vecAbsOrigin)?) - } -} \ No newline at end of file diff --git a/src/sdk/structs/entity_classes/base_entity.rs b/src/sdk/structs/entity_classes/base_entity.rs new file mode 100644 index 0000000..6a68247 --- /dev/null +++ b/src/sdk/structs/entity_classes/base_entity.rs @@ -0,0 +1,50 @@ +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) + } +} \ No newline at end of file diff --git a/src/sdk/structs/entity_classes/mod.rs b/src/sdk/structs/entity_classes/mod.rs new file mode 100644 index 0000000..a2f9d62 --- /dev/null +++ b/src/sdk/structs/entity_classes/mod.rs @@ -0,0 +1,26 @@ +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; +} \ No newline at end of file diff --git a/src/sdk/structs/entity_classes/player_controller.rs b/src/sdk/structs/entity_classes/player_controller.rs new file mode 100644 index 0000000..4dbae41 --- /dev/null +++ b/src/sdk/structs/entity_classes/player_controller.rs @@ -0,0 +1,91 @@ +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)) + } +} \ No newline at end of file diff --git a/src/sdk/structs/entity_classes/player_pawn.rs b/src/sdk/structs/entity_classes/player_pawn.rs new file mode 100644 index 0000000..183875a --- /dev/null +++ b/src/sdk/structs/entity_classes/player_pawn.rs @@ -0,0 +1,81 @@ +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) + } +} \ No newline at end of file diff --git a/src/sdk/structs/mod.rs b/src/sdk/structs/mod.rs index ab1e9a9..792961d 100644 --- a/src/sdk/structs/mod.rs +++ b/src/sdk/structs/mod.rs @@ -1,82 +1,13 @@ -mod entity; -pub use entity::*; -use memflow::types::Address; +mod entity_classes; -use crate::structs::communication::PlayerType; +pub use entity_classes::*; -#[derive(Clone, Copy)] -pub enum CachedEntityData { - Bomb {ptr: Address}, - Player {ptr: Address, player_type: PlayerType}, -} +use enum_primitive_derive::Primitive; -pub struct CommonCache { - map_name: String, - entity_list: Address, -} - -impl CommonCache { - pub fn new() -> CommonCache { - CommonCache { - map_name: String::from("unknown"), - entity_list: Address::null(), - } - } - - pub fn update(&mut self, map_name: String, entity_list: Address) { - self.map_name = map_name; - self.entity_list = entity_list; - } - - pub fn map_name(&self) -> String { - self.map_name.clone() - } - - pub fn entity_list(&self) -> Address { - self.entity_list - } -} - -pub struct Cache { - last_cached: std::time::Instant, - data: Vec<CachedEntityData>, - common: CommonCache -} - -impl Cache { - pub fn new() -> Cache { - Cache { - last_cached: std::time::Instant::now().checked_sub(std::time::Duration::from_millis(500)).unwrap(), - data: Vec::new(), - common: CommonCache::new(), - } - } - - pub fn is_outdated(&self) -> bool { - if self.last_cached.elapsed() > std::time::Duration::from_millis(250) { - return true; - } - - false - } - - pub fn new_time(&mut self) { - self.last_cached = std::time::Instant::now(); - } - - pub fn clean(&mut self) { - self.data.clear(); - } - - pub fn data(&self) -> Vec<CachedEntityData> { - self.data.clone() - } - - pub fn push_data(&mut self, data: CachedEntityData) { - self.data.push(data); - } - - pub fn common(&mut self) -> &mut CommonCache { - &mut self.common - } +#[repr(i32)] +#[derive(Debug, Eq, PartialEq, Primitive)] +pub enum TeamID { + Spectator = 1, + T = 2, + CT = 3 } \ No newline at end of file