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
225 lines
8.8 KiB
Rust
225 lines
8.8 KiB
Rust
use std::sync::Arc;
|
|
|
|
use csflow::{CheatCtx, Connector, memflow::Process, traits::{MemoryClass, BaseEntity}, enums::PlayerType, structs::{CBaseEntity, CPlayerController}};
|
|
use tokio::{sync::RwLock, time::Duration};
|
|
|
|
use crate::{comms::{RadarData, EntityData, BombData, PlayerData}, dma::cache::CacheBuilder};
|
|
|
|
use self::cache::Cache;
|
|
|
|
mod cache;
|
|
|
|
const SECOND_AS_NANO: u64 = 1000*1000*1000;
|
|
|
|
pub async fn run(connector: Connector, pcileech_device: String, data_lock: Arc<RwLock<RadarData>>) -> anyhow::Result<()> {
|
|
let mut ctx = CheatCtx::setup(connector, pcileech_device)?;
|
|
|
|
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;
|
|
}
|
|
|
|
if !cache.is_valid() {
|
|
let mut cached_entities = Vec::new();
|
|
|
|
let globals = ctx.get_globals()?;
|
|
let highest_index = ctx.highest_entity_index()?;
|
|
let entity_list = ctx.get_entity_list()?;
|
|
let gamerules = ctx.get_gamerules()?;
|
|
|
|
let local = ctx.get_local()?;
|
|
|
|
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)
|
|
.globals(globals)
|
|
.gamerules(gamerules)
|
|
.build()?;
|
|
|
|
log::info!("Rebuilt cache.");
|
|
}
|
|
|
|
if ctx.network_is_ingame()? {
|
|
// 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 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;
|
|
}
|
|
|
|
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
|
|
))
|
|
);
|
|
}
|
|
|
|
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::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;
|
|
*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(())
|
|
} |