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:
Janek
2023-12-30 18:07:55 +01:00
parent 462cfddfef
commit 45bba35a71
85 changed files with 6982 additions and 1036 deletions

View File

@@ -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()
))
}
}

View File

@@ -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"))?,
})
}
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -1,4 +0,0 @@
#![allow(dead_code)]
pub mod client;
pub mod engine2;
pub mod offsets;

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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;
}

View File

@@ -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))
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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(),
}
}
}

View File

@@ -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,
}
}
}

View File

@@ -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 {}

View File

@@ -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(())
}