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>) -> 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 "" // 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 == "" { 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(()) }