csflow:
 - Create structs for gamerules and global vars

radarflow:
 - new dma loop with less frequent cache invalidation
 - The new loop tries to run at a fixed 128 hz. Thats the max tickrate in cs2. The data is also only updated when a tick change is detected, so that should keep data fetching to a minimum.
 - todo: more testing for cache invalidation
This commit is contained in:
Janek
2023-12-31 04:32:12 +01:00
parent 0f0f7232fb
commit 7c652cb984
15 changed files with 305 additions and 175 deletions

View File

@@ -1,6 +1,6 @@
[package]
name = "radarflow"
version = "0.2.0"
version = "0.2.1"
authors = ["Janek S <development@superyu.xyz>"]
edition = "2021"

View File

@@ -4,7 +4,6 @@ use clap::{Parser, ValueEnum};
use csflow::{Connector, memflow::Inventory};
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)]
@@ -25,10 +24,6 @@ pub struct Cli {
#[arg(short, long, default_value = "./webradar", 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,
@@ -75,21 +70,6 @@ fn valid_path(s: &str) -> Result<PathBuf, 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()
))
}
}
/// Wrapper because log::LevelFilter doesn't implement ValueEnum
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
pub enum Loglevel {

View File

@@ -1,4 +1,4 @@
use csflow::{memflow::Address, enums::PlayerType};
use csflow::{memflow::Address, enums::PlayerType, structs::{GlobalVars, GameRules}, traits::MemoryClass};
#[derive(Clone, Copy)]
pub enum CachedEntity {
@@ -8,54 +8,71 @@ pub enum CachedEntity {
pub struct Cache {
timestamp: std::time::Instant,
valid: bool,
entity_cache: Vec<CachedEntity>,
map_name: String,
entity_list: Address,
globals: GlobalVars,
gamerules: GameRules,
}
impl Cache {
pub fn is_valid(&self) -> bool {
if self.timestamp.elapsed() > std::time::Duration::from_millis(250) {
return false;
}
if self.valid {
if self.timestamp.elapsed() > std::time::Duration::from_secs(60 * 3) {
log::info!("Invalidated cache! Reason: time");
return false
}
true
true
} else { false }
}
pub fn new_invalid() -> Cache {
Cache {
timestamp: std::time::Instant::now().checked_sub(std::time::Duration::from_millis(500)).unwrap(),
valid: false,
entity_cache: Vec::new(),
map_name: String::new(),
entity_list: Address::null(),
globals: GlobalVars::new(Address::null()),
gamerules: GameRules::new(Address::null()),
}
}
pub fn invalidate(&mut self) {
self.valid = false;
}
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 fn globals(&self) -> GlobalVars {
self.globals
}
pub fn gamerules(&self) -> GameRules {
self.gamerules
}
}
pub struct CacheBuilder {
entity_cache: Option<Vec<CachedEntity>>,
map_name: Option<String>,
entity_list: Option<Address>
entity_list: Option<Address>,
globals: Option<GlobalVars>,
gamerules: Option<GameRules>
}
impl CacheBuilder {
pub fn new() -> CacheBuilder {
CacheBuilder {
entity_cache: None,
map_name: None,
entity_list: None,
globals: None,
gamerules: None,
}
}
@@ -64,22 +81,29 @@ impl CacheBuilder {
self
}
pub fn map_name(mut self, map_name: String) -> CacheBuilder {
self.map_name = Some(map_name);
pub fn entity_list(mut self, entity_list: Address) -> CacheBuilder {
self.entity_list = Some(entity_list);
self
}
pub fn entity_list(mut self, entity_list: Address) -> CacheBuilder {
self.entity_list = Some(entity_list);
pub fn globals(mut self, globals: GlobalVars) -> CacheBuilder {
self.globals = Some(globals);
self
}
pub fn gamerules(mut self, gamerules: GameRules) -> CacheBuilder {
self.gamerules = Some(gamerules);
self
}
pub fn build(self) -> anyhow::Result<Cache> {
Ok(Cache {
timestamp: std::time::Instant::now(),
valid: true,
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"))?,
globals: self.globals.ok_or(anyhow::anyhow!("globals not set on builder"))?,
gamerules: self.gamerules.ok_or(anyhow::anyhow!("gamerules not set on builder"))?,
})
}
}

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use csflow::{CheatCtx, Connector, memflow::Process, traits::{MemoryClass, BaseEntity}, enums::PlayerType, structs::{CBaseEntity, CPlayerController}};
use tokio::{sync::RwLock, time::{Duration, Instant}};
use tokio::{sync::RwLock, time::Duration};
use crate::{comms::{RadarData, EntityData, BombData, PlayerData}, dma::cache::CacheBuilder};
@@ -10,30 +10,22 @@ use self::cache::Cache;
mod cache;
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<()> {
pub async fn run(connector: Connector, pcileech_device: String, data_lock: Arc<RwLock<RadarData>>) -> anyhow::Result<()> {
let mut ctx = CheatCtx::setup(connector, pcileech_device)?;
println!("---------------------------------------------------");
println!("Found cs2.exe at {:X}", ctx.process.info().address);
println!("Found engine module at cs2.exe+{:X}", ctx.engine_module.base);
println!("Found client module at cs2.exe+{:X}", ctx.client_module.base);
println!("---------------------------------------------------");
// 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();
let mut last_tickcount = -1;
let mut last_round = -1;
let mut last_gamephase = -1;
// Duration for a single tick on 128 ticks. Im assuming 128 ticks because I dont fucking know how to read the current tickrate off cs2 memory lol
let target_interval = Duration::from_nanos(SECOND_AS_NANO / 128);
loop {
let start_stamp = tokio::time::Instant::now();
if ctx.process.state().is_dead() {
break;
}
@@ -43,8 +35,8 @@ pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16,
let globals = ctx.get_globals()?;
let highest_index = ctx.highest_entity_index()?;
let map_name = ctx.map_name(globals)?;
let entity_list = ctx.get_entity_list()?;
let gamerules = ctx.get_gamerules()?;
let local = ctx.get_local()?;
@@ -87,116 +79,147 @@ pub async fn run(connector: Connector, pcileech_device: String, poll_rate: u16,
}
}
}
cache = CacheBuilder::new()
.entity_cache(cached_entities)
.entity_list(entity_list)
.map_name(map_name)
.globals(globals)
.gamerules(gamerules)
.build()?;
log::debug!("Rebuilt cache.");
log::info!("Rebuilt cache.");
}
if ctx.network_is_ingame()? {
let mut radar_data = Vec::with_capacity(64);
// Check if mapname is "<empty>"
// That means we are not in-game, so we can just write empty radar data and run the next loop.
let map_name = cache.globals().map_name(&mut ctx)?;
if ctx.is_bomb_planted()? {
let bomb = ctx.get_plantedc4()?;
let bomb_pos = bomb.pos(&mut ctx)?;
radar_data.push(
EntityData::Bomb(BombData::new(
bomb_pos,
true
))
);
if map_name == "<empty>" {
last_round = -1;
last_gamephase = -1;
let mut data = data_lock.write().await;
*data = RadarData::empty();
continue;
} else if map_name.is_empty() { // Check if mapname is empty, this usually means a bad globals pointer -> rebuild our cache
cache.invalidate();
log::info!("Invalidated cache! Reason: invalid globals pointer");
continue;
}
for cached_data in cache.entity_cache() {
match cached_data {
cache::CachedEntity::Bomb { ptr } => {
if ctx.is_bomb_dropped()? {
let bomb_entity = CBaseEntity::new(ptr);
let pos = bomb_entity.pos(&mut ctx)?;
let cur_round = cache.gamerules().total_rounds_played(&mut ctx)?;
// New round started, invalidate cache and run next loop
if cur_round != last_round {
last_round = cur_round;
cache.invalidate();
log::info!("Invalidated cache! Reason: new round");
continue;
}
let cur_gamephase = cache.gamerules().game_phase(&mut ctx)?;
// New game phase, invalidate cache and run next loop
if cur_gamephase != last_gamephase {
last_gamephase = cur_gamephase;
cache.invalidate();
log::info!("Invalidated cache! Reason: new gamephase");
continue;
}
let cur_tickcount = cache.globals().tick_count(&mut ctx)?;
// New tick, now we want to fetch our data
if cur_tickcount != last_tickcount {
// We dont expect more than 16 entries in our radar data
let mut radar_data = Vec::with_capacity(16);
if cache.gamerules().bomb_planted(&mut ctx)? {
let bomb = ctx.get_plantedc4()?;
let bomb_pos = bomb.pos(&mut ctx)?;
radar_data.push(
EntityData::Bomb(BombData::new(
bomb_pos,
true
))
);
}
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())?;
for cached_data in cache.entity_cache() {
match cached_data {
cache::CachedEntity::Bomb { ptr } => {
if cache.gamerules().bomb_dropped(&mut ctx)? {
let bomb_entity = CBaseEntity::new(ptr);
let pos = bomb_entity.pos(&mut ctx)?;
radar_data.push(
EntityData::Player(
PlayerData::new(
pos,
yaw,
player_type,
has_bomb
EntityData::Bomb(
BombData::new(
pos,
false
)
)
);
}
}
},
}
}
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;
},
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
)
)
);
}
}
},
}
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();
let mut data = data_lock.write().await;
*data = RadarData::new(
true,
map_name,
radar_data
);
last_tickcount = cur_tickcount;
}
}
// Elapsed time since we started our loop
let elapsed = start_stamp.elapsed();
let remaining = match target_interval.checked_sub(elapsed) {
// This gives us the remaining time we can sleep in our loop
Some(t) => t,
// No time left, start next loop.
None => continue
};
// On linux we may use tokio_timerfd for a more finely grained sleep function
#[cfg(target_os = "linux")]
tokio_timerfd::sleep(remaining).await?;
// On non linux build targets we need to use the regular sleep function, this one is only accurate to millisecond precision
#[cfg(not(target_os = "linux"))]
tokio::time::sleep(remaining).await;
log::debug!("poll rate: {:.2}Hz", SECOND_AS_NANO as f64 / start_stamp.elapsed().as_nanos() as f64);
log::debug!("elapsed: {}ns", elapsed.as_nanos());
log::debug!("target: {}ns", target_interval.as_nanos());
}
Ok(())
}
}

View File

@@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> {
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 {
if let Err(err) = dma::run(cli.connector, cli.pcileech_device, rwlock_clone).await {
log::error!("Error in dma thread: [{}]", err.to_string());
}
});