Initial Commit

This commit is contained in:
Janek 2023-10-31 17:23:15 +01:00
parent 048eab43ef
commit fd1a4c03dc
65 changed files with 9080 additions and 0 deletions

5
.cargo/config.toml Normal file

@ -0,0 +1,5 @@
[target.'cfg(unix)']
runner = "sudo -E"
[build]
rustflags = ["-C", "target-cpu=native"]

1
.gitignore vendored Normal file

@ -0,0 +1 @@
/target

2861
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

46
Cargo.toml Normal file

@ -0,0 +1,46 @@
[package]
name = "radarflow-cs2"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# dma and memory
memflow = "=0.2.0-beta10"
dataview = "1.0.1"
# logging
log = "0.4.19"
env_logger = "0.10.0"
# error handling
anyhow = "1.0.72"
# tokio
tokio = { version = "1.29.1", features = ["full"] }
field_types = "1.1.0"
futures-util = "0.3.28"
# serde
serde = { version = "1.0.181", features = ["derive"] }
serde_json = "1.0.104"
toml = "0.7.6"
# networking
axum = { version = "0.6.20", features = ["ws"] }
tower-http = { version = "0.4.3", features = ["fs"] }
tower = "0.4.13"
tokio-timerfd = "0.2.0"
# other
local-ip-address = "0.5.4"
enum-primitive-derive = "0.2.2"
num-traits = "0.2.16"
clap = { version = "4.3.19", features = ["derive", "string"] }
simple_logger = "4.2.0"
chrono = "0.4.26"
[build-dependencies]
reqwest = { version = "0.11.18", features = ["blocking"] }
vergen = { version = "8.0.0", features = ["build", "cargo", "git", "gitcl", "rustc", "si"] }

22
README.md Normal file

@ -0,0 +1,22 @@
# radarflow
A Web radar for CS:GO using [memflow](https://github.com/memflow/memflow)
## How can I run this?
First, you need to set up a virtual machine on linux using qemu.
As of now, memflow's pcileech connector is not supported.
How to set up a VM on linux is way out of scope for this. You can find plenty of information online on how to do it.
After you have set up your VM, you can clone this repository on your host:
`git clone https://github.com/superyu1337/radarflow.git`
Now you can run radarflow:
`cargo run --release`
For an overview of CLI commands, run this:
`cargo run --release -- --help`
## Detection Status
VAC: ✅ (Undetected)
FaceIt: ❓ (Unknown, could work with proper spoofing)
ESEA: ❓ (Unknown, could work with proper spoofing)

42
build.rs Normal file

@ -0,0 +1,42 @@
use std::error::Error;
use vergen::EmitBuilder;
fn download(url: &str, to: &str) -> Result<(), Box<dyn Error>> {
let content = reqwest::blocking::get(url)
.unwrap_or_else(|_| panic!("Downloading \"{to}\""))
.text()
.expect("Convert response to text");
std::fs::write(to, content)
.expect("Write offsets to file");
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
download(
"https://raw.githubusercontent.com/a2x/cs2-dumper/main/generated/client.dll.rs",
"./src/sdk/cs2dumper/client.rs"
).expect("Failed to download build file");
download(
"https://raw.githubusercontent.com/a2x/cs2-dumper/main/generated/offsets.rs",
"./src/sdk/cs2dumper/offsets.rs"
).expect("Failed to download build file");
download(
"https://raw.githubusercontent.com/a2x/cs2-dumper/main/generated/engine2.dll.rs",
"./src/sdk/cs2dumper/engine2.rs"
).expect("Failed to download build file");
EmitBuilder::builder()
.git_sha(true)
.git_commit_date()
.cargo_debug()
.cargo_target_triple()
.rustc_semver()
.rustc_llvm_version()
.emit()?;
Ok(())
}

93
src/cli.rs Normal file

@ -0,0 +1,93 @@
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 {
/// Connector to use for DMA
#[clap(value_enum, short, long, ignore_case = true, default_value_t = Connector::Qemu)]
pub connector: Connector,
/// Pcileech device name
#[clap(long, default_value_t = String::from("FPGA"))]
pub pcileech_device: String,
/// Port to run Webserver on
#[arg(short, long, default_value_t = 8000, value_parser = port_in_range)]
pub port: u16,
/// Path to serve on 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
#[arg(short = 'r', long, default_value_t = 60, value_parser = poll_in_range)]
pub poll_rate: u16,
/// Loglevel verbosity
#[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()
))
}
}

268
src/dma/mod.rs Normal file

@ -0,0 +1,268 @@
use ::std::sync::Arc;
use memflow::prelude::v1::*;
use tokio::{sync::RwLock, time::{Duration, Instant}};
use crate::{structs::{Connector, communication::{RadarData, PlayerType, EntityData, PlayerData}}, sdk::{self, cs2dumper, structs::{CPlayerPawn, CCSPlayerController}}};
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;
loop {
if ctx.process.state().is_dead() {
println!("is dead");
break;
}
if sdk::is_ingame(&mut ctx)? {
let globals = sdk::get_globals(&mut ctx)?;
let entity_list = sdk::get_entity_list(&mut ctx)?;
let map_name = sdk::map_name(globals, &mut ctx)?;
let local = sdk::get_local(&mut ctx)?;
let local_pawn_ptr: u32 = ctx.process.read(local.ptr() + cs2dumper::client::CCSPlayerController::m_hPlayerPawn)?;
if let Some(local_pawn) = CPlayerPawn::from_uhandle(local_pawn_ptr, entity_list, &mut ctx) {
let local_yaw = local_pawn.angles(&mut ctx)?.y;
let local_pos = local_pawn.pos(&mut ctx)?;
let mut player_data = Vec::with_capacity(64);
if local_pawn.is_alive(&mut ctx)? {
player_data.push(
EntityData::Player(
PlayerData::new(
local_pos,
local_yaw,
PlayerType::Local,
false
)
)
);
}
let max_clients = sdk::max_clients(globals, &mut ctx)?;
for idx in 1..max_clients {
let list_entry = ctx.process.read_addr64(entity_list + ((8 * (idx & 0x7FFF)) >> 9) + 16)?;
if list_entry.is_null() && !list_entry.is_valid() {
continue;
}
let player_ptr = ctx.process.read_addr64(list_entry + 120 * (idx & 0x1FF))?;
if player_ptr.is_null() && !player_ptr.is_valid() {
continue;
}
let pawn_uhandle = ctx.process.read(player_ptr + cs2dumper::client::CCSPlayerController::m_hPlayerPawn)?;
if let (Some(pawn), player) = (CPlayerPawn::from_uhandle(pawn_uhandle, entity_list, &mut ctx), CCSPlayerController::new(player_ptr)) {
if player.entity_identity(&mut ctx)?.designer_name(&mut ctx)? == "cs_player_controller" && pawn.is_alive(&mut ctx)? {
let pos = pawn.pos(&mut ctx)?;
let angles = pawn.angles(&mut ctx)?;
let player_type = {
match player.get_player_type(&mut ctx, &local)? {
Some(t) => {
if t == PlayerType::Spectator { continue } else { t }
},
None => { continue },
}
};
player_data.push(
EntityData::Player(
PlayerData::new(
pos,
angles.y,
player_type,
false
)
)
);
}
}
}
let mut data = data_lock.write().await;
*data = RadarData::new(true, map_name, player_data, local_yaw)
}
//let local_pawn = sdk::get_local_pawn(&mut ctx)?;
//let local_pawn = CPlayerPawn::new(local_cs_player_pawn);
/*
let mut next_ent = {
let mut iter_ent = local.to_base();
while iter_ent.entity_identity(&mut ctx)?.prev_by_class(&mut ctx).is_ok() {
iter_ent = iter_ent.entity_identity(&mut ctx)?.prev_by_class(&mut ctx)?;
}
iter_ent
};
let mut count = 0;
let mut pawn_count = 0;
println!("prev by class ok? {}", next_ent.entity_identity(&mut ctx)?.prev_by_class(&mut ctx).is_ok());
while next_ent.entity_identity(&mut ctx)?.next_by_class(&mut ctx).is_ok() {
count += 1;
let pawn = next_ent.to_controller().pawn(entity_list, &mut ctx)?;
if let Some(p) = pawn {
pawn_count += 1;
if !p.is_alive(&mut ctx)? {
next_ent = next_ent.entity_identity(&mut ctx).unwrap().next_by_class(&mut ctx).unwrap();
continue
}
let pos = p.pos(&mut ctx)?;
let angles = p.angles(&mut ctx)?;
let player_type = {
match next_ent.to_controller().get_player_type(&mut ctx, &local)? {
Some(t) => {
if t == PlayerType::Spectator {
next_ent = next_ent.entity_identity(&mut ctx).unwrap().next_by_class(&mut ctx).unwrap();
continue
} else { t }
},
None => {
next_ent = next_ent.entity_identity(&mut ctx).unwrap().next_by_class(&mut ctx).unwrap();
continue
},
}
};
player_data.push(
EntityData::Player(
PlayerData::new(
pos,
angles.y,
player_type,
false
)
)
);
}
//let pawn = next_ent.to_controller().pawn2(entity_list, &mut ctx)?;
next_ent = next_ent.entity_identity(&mut ctx)?.next_by_class(&mut ctx)?;
}
println!("next by class ok? {}", next_ent.entity_identity(&mut ctx)?.next_by_class(&mut ctx).is_ok());
*/
} 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::trace!("polling at {:.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!("missmatch count: {}", missmatch_count);
last_iteration_time = Instant::now();
}
}
println!("DMA loop exited for some reason");
Ok(())
}

62
src/main.rs Normal file

@ -0,0 +1,62 @@
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();
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()
.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(())
}

4138
src/sdk/cs2dumper/client.rs Normal file

File diff suppressed because it is too large Load Diff

@ -0,0 +1,242 @@
/*
* Created using https://github.com/a2x/cs2-dumper
* Fri, 27 Oct 2023 01:03:22 +0000
*/
#![allow(non_snake_case, non_upper_case_globals)]
pub mod CEmptyEntityInstance {
}
pub mod CEntityComponent {
}
pub mod CEntityComponentHelper {
pub const m_flags: usize = 0x8; // uint32_t
pub const m_pInfo: usize = 0x10; // EntComponentInfo_t*
pub const m_nPriority: usize = 0x18; // int32_t
pub const m_pNext: usize = 0x20; // CEntityComponentHelper*
}
pub mod CEntityIOOutput {
pub const m_Value: usize = 0x18; // CVariantBase<CVariantDefaultAllocator>
}
pub mod CEntityIdentity {
pub const m_nameStringableIndex: usize = 0x14; // int32_t
pub const m_name: usize = 0x18; // CUtlSymbolLarge
pub const m_designerName: usize = 0x20; // CUtlSymbolLarge
pub const m_flags: usize = 0x30; // uint32_t
pub const m_worldGroupId: usize = 0x38; // WorldGroupId_t
pub const m_fDataObjectTypes: usize = 0x3C; // uint32_t
pub const m_PathIndex: usize = 0x40; // ChangeAccessorFieldPathIndex_t
pub const m_pPrev: usize = 0x58; // CEntityIdentity*
pub const m_pNext: usize = 0x60; // CEntityIdentity*
pub const m_pPrevByClass: usize = 0x68; // CEntityIdentity*
pub const m_pNextByClass: usize = 0x70; // CEntityIdentity*
}
pub mod CEntityInstance {
pub const m_iszPrivateVScripts: usize = 0x8; // CUtlSymbolLarge
pub const m_pEntity: usize = 0x10; // CEntityIdentity*
pub const m_CScriptComponent: usize = 0x28; // CScriptComponent*
}
pub mod CNetworkVarChainer {
pub const m_PathIndex: usize = 0x20; // ChangeAccessorFieldPathIndex_t
}
pub mod CScriptComponent { // CEntityComponent
pub const m_scriptClassName: usize = 0x30; // CUtlSymbolLarge
}
pub mod CVariantDefaultAllocator {
}
pub mod EngineLoopState_t {
pub const m_nPlatWindowWidth: usize = 0x18; // int32_t
pub const m_nPlatWindowHeight: usize = 0x1C; // int32_t
pub const m_nRenderWidth: usize = 0x20; // int32_t
pub const m_nRenderHeight: usize = 0x24; // int32_t
}
pub mod EntComponentInfo_t {
pub const m_pName: usize = 0x0; // char*
pub const m_pCPPClassname: usize = 0x8; // char*
pub const m_pNetworkDataReferencedDescription: usize = 0x10; // char*
pub const m_pNetworkDataReferencedPtrPropDescription: usize = 0x18; // char*
pub const m_nRuntimeIndex: usize = 0x20; // int32_t
pub const m_nFlags: usize = 0x24; // uint32_t
pub const m_pBaseClassComponentHelper: usize = 0x60; // CEntityComponentHelper*
}
pub mod EntInput_t {
}
pub mod EntOutput_t {
}
pub mod EventAdvanceTick_t { // EventSimulate_t
pub const m_nCurrentTick: usize = 0x30; // int32_t
pub const m_nCurrentTickThisFrame: usize = 0x34; // int32_t
pub const m_nTotalTicksThisFrame: usize = 0x38; // int32_t
pub const m_nTotalTicks: usize = 0x3C; // int32_t
}
pub mod EventAppShutdown_t {
pub const m_nDummy0: usize = 0x0; // int32_t
}
pub mod EventClientAdvanceTick_t { // EventAdvanceTick_t
}
pub mod EventClientFrameSimulate_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_flRealTime: usize = 0x28; // float
pub const m_flFrameTime: usize = 0x2C; // float
}
pub mod EventClientOutput_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_flRenderTime: usize = 0x28; // float
pub const m_flRealTime: usize = 0x2C; // float
pub const m_flRenderFrameTimeUnbounded: usize = 0x30; // float
pub const m_bRenderOnly: usize = 0x34; // bool
}
pub mod EventClientPauseSimulate_t { // EventSimulate_t
}
pub mod EventClientPollInput_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_flRealTime: usize = 0x28; // float
}
pub mod EventClientPollNetworking_t {
pub const m_nTickCount: usize = 0x0; // int32_t
}
pub mod EventClientPostAdvanceTick_t { // EventPostAdvanceTick_t
}
pub mod EventClientPostOutput_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_flRenderTime: usize = 0x28; // double
pub const m_flRenderFrameTime: usize = 0x30; // float
pub const m_flRenderFrameTimeUnbounded: usize = 0x34; // float
pub const m_bRenderOnly: usize = 0x38; // bool
}
pub mod EventClientPostSimulate_t { // EventSimulate_t
}
pub mod EventClientPreOutput_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_flRenderTime: usize = 0x28; // double
pub const m_flRenderFrameTime: usize = 0x30; // double
pub const m_flRenderFrameTimeUnbounded: usize = 0x38; // double
pub const m_flRealTime: usize = 0x40; // float
pub const m_bRenderOnly: usize = 0x44; // bool
}
pub mod EventClientPreSimulate_t { // EventSimulate_t
}
pub mod EventClientPredictionPostNetupdate_t {
}
pub mod EventClientProcessGameInput_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_flRealTime: usize = 0x28; // float
pub const m_flFrameTime: usize = 0x2C; // float
}
pub mod EventClientProcessInput_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_flRealTime: usize = 0x28; // float
}
pub mod EventClientProcessNetworking_t {
}
pub mod EventClientSceneSystemThreadStateChange_t {
pub const m_bThreadsActive: usize = 0x0; // bool
}
pub mod EventClientSendInput_t {
pub const m_bFinalClientCommandTick: usize = 0x0; // bool
pub const m_nAdditionalClientCommandsToCreate: usize = 0x4; // int32_t
}
pub mod EventClientSimulate_t { // EventSimulate_t
}
pub mod EventFrameBoundary_t {
pub const m_flFrameTime: usize = 0x0; // float
}
pub mod EventModInitialized_t {
}
pub mod EventPostAdvanceTick_t { // EventSimulate_t
pub const m_nCurrentTick: usize = 0x30; // int32_t
pub const m_nCurrentTickThisFrame: usize = 0x34; // int32_t
pub const m_nTotalTicksThisFrame: usize = 0x38; // int32_t
pub const m_nTotalTicks: usize = 0x3C; // int32_t
}
pub mod EventPostDataUpdate_t {
pub const m_nCount: usize = 0x0; // int32_t
}
pub mod EventPreDataUpdate_t {
pub const m_nCount: usize = 0x0; // int32_t
}
pub mod EventProfileStorageAvailable_t {
pub const m_nSplitScreenSlot: usize = 0x0; // CSplitScreenSlot
}
pub mod EventServerAdvanceTick_t { // EventAdvanceTick_t
}
pub mod EventServerPollNetworking_t { // EventSimulate_t
}
pub mod EventServerPostAdvanceTick_t { // EventPostAdvanceTick_t
}
pub mod EventServerPostSimulate_t { // EventSimulate_t
}
pub mod EventServerProcessNetworking_t { // EventSimulate_t
}
pub mod EventServerSimulate_t { // EventSimulate_t
}
pub mod EventSetTime_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_nClientOutputFrames: usize = 0x28; // int32_t
pub const m_flRealTime: usize = 0x30; // double
pub const m_flRenderTime: usize = 0x38; // double
pub const m_flRenderFrameTime: usize = 0x40; // double
pub const m_flRenderFrameTimeUnbounded: usize = 0x48; // double
pub const m_flRenderFrameTimeUnscaled: usize = 0x50; // double
pub const m_flTickRemainder: usize = 0x58; // double
}
pub mod EventSimpleLoopFrameUpdate_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_flRealTime: usize = 0x28; // float
pub const m_flFrameTime: usize = 0x2C; // float
}
pub mod EventSimulate_t {
pub const m_LoopState: usize = 0x0; // EngineLoopState_t
pub const m_bFirstTick: usize = 0x28; // bool
pub const m_bLastTick: usize = 0x29; // bool
}
pub mod EventSplitScreenStateChanged_t {
}

4
src/sdk/cs2dumper/mod.rs Normal file

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

@ -0,0 +1,46 @@
/*
* Created using https://github.com/a2x/cs2-dumper
* Mon, 30 Oct 2023 00:17:09 +0000
*/
#![allow(non_snake_case, non_upper_case_globals)]
pub mod client_dll { // client.dll
pub const dwBaseEntityModel_setModel: usize = 0x57EA50;
pub const dwEntityList: usize = 0x17995C0;
pub const dwForceAttack: usize = 0x169EE60;
pub const dwForceAttack2: usize = 0x169EEF0;
pub const dwForceBackward: usize = 0x169F130;
pub const dwForceCrouch: usize = 0x169F400;
pub const dwForceForward: usize = 0x169F0A0;
pub const dwForceJump: usize = 0x169F370;
pub const dwForceLeft: usize = 0x169F1C0;
pub const dwForceRight: usize = 0x169F250;
pub const dwGameEntitySystem: usize = 0x1952588;
pub const dwGameEntitySystem_getBaseEntity: usize = 0x602050;
pub const dwGameEntitySystem_getHighestEntityIndex: usize = 0x5F3D40;
pub const dwGameRules: usize = 0x17F5488;
pub const dwGlobalVars: usize = 0x169AFE0;
pub const dwGlowManager: usize = 0x17F4C10;
pub const dwInterfaceLinkList: usize = 0x1980298;
pub const dwLocalPlayerController: usize = 0x17E8158;
pub const dwLocalPlayerPawn: usize = 0x1886C48;
pub const dwPlantedC4: usize = 0x188BFE0;
pub const dwViewAngles: usize = 0x18E6770;
pub const dwViewMatrix: usize = 0x1887730;
pub const dwViewRender: usize = 0x1888128;
}
pub mod engine2_dll { // engine2.dll
pub const dwBuildNumber: usize = 0x488514;
pub const dwNetworkGameClient: usize = 0x487AB0;
pub const dwNetworkGameClient_getLocalPlayer: usize = 0xF0;
pub const dwNetworkGameClient_maxClients: usize = 0x250;
pub const dwNetworkGameClient_signOnState: usize = 0x240;
pub const dwWindowHeight: usize = 0x5396DC;
pub const dwWindowWidth: usize = 0x5396D8;
}
pub mod inputsystem_dll { // inputsystem.dll
pub const dwInputSystem: usize = 0x35770;
}

43
src/sdk/mod.rs Normal file

@ -0,0 +1,43 @@
pub mod structs;
pub mod cs2dumper;
use crate::dma::CheatCtx;
use memflow::prelude::v1::*;
use anyhow::Result;
use self::structs::{CCSPlayerController, CPlayerPawn};
pub fn get_local(ctx: &mut CheatCtx) -> Result<CCSPlayerController> {
let ptr = ctx.process.read_addr64(ctx.client_module.base + cs2dumper::offsets::client_dll::dwLocalPlayerController)?;
Ok(CCSPlayerController::new(ptr))
}
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 max_clients(global_vars: Address, ctx: &mut CheatCtx) -> Result<i32> {
Ok(ctx.process.read(global_vars + 0x10)?)
}
pub fn 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)
}

206
src/sdk/structs/entity.rs Normal file

@ -0,0 +1,206 @@
use crate::{dma::CheatCtx, sdk::cs2dumper, structs::{Vec3, communication::PlayerType}};
use enum_primitive_derive::Primitive;
use memflow::{prelude::MemoryView, types::Address};
use anyhow::{Result, anyhow};
use num_traits::FromPrimitive;
#[repr(i32)]
#[derive(Debug, Eq, PartialEq, Primitive)]
pub enum TeamID {
Spectator = 1,
T = 2,
CT = 3
}
pub struct CEntityIdentity(Address);
impl CEntityIdentity {
pub fn prev_by_class(&self, ctx: &mut CheatCtx) -> Result<CBaseEntity> {
let prev1 = ctx.process.read_addr64(self.0 + cs2dumper::client::CEntityIdentity::m_pPrevByClass)?;
let prev = ctx.process.read_addr64(prev1)?;
if prev.is_null() || !prev.is_valid() {
Err(anyhow!("Invalid or Null"))
} else {
Ok(CBaseEntity(prev))
}
}
pub fn next_by_class(&self, ctx: &mut CheatCtx) -> Result<CBaseEntity> {
let next1 = ctx.process.read_addr64(self.0 + cs2dumper::client::CEntityIdentity::m_pNextByClass)?;
let next = ctx.process.read_addr64(next1)?;
if next.is_null() || !next.is_valid() {
Err(anyhow!("Invalid or Null"))
} else {
Ok(CBaseEntity(next))
}
}
pub fn designer_name(&self, ctx: &mut CheatCtx) -> Result<String> {
let ptr = ctx.process.read_addr64(self.0 + cs2dumper::client::CEntityIdentity::m_designerName)?;
Ok(ctx.process.read_char_string_n(ptr, 32)?)
}
}
pub struct CBaseEntity(Address);
impl CBaseEntity {
pub fn new(ptr: Address) -> CBaseEntity {
CBaseEntity(ptr)
}
pub fn ptr(&self) -> Address {
self.0
}
pub fn to_controller(&self) -> CCSPlayerController {
CCSPlayerController(self.0)
}
pub fn get_from_list(ctx: &mut CheatCtx, entity_list: Address, idx: usize) -> Result<CBaseEntity> {
let list_entry = ctx.process.read_addr64(entity_list + ((( idx & 0x7FFF ) >> 9 ) * 0x8)).unwrap();
if list_entry.is_null() || !list_entry.is_valid() {
Err(anyhow!("Invalid or Null"))
} else {
let ptr = ctx.process.read_addr64(list_entry + 120 * (idx & 0x1FF)).unwrap();
Ok(CBaseEntity(ptr))
}
}
pub fn entity_identity(&self, ctx: &mut CheatCtx) -> Result<CEntityIdentity> {
let ptr = ctx.process.read_addr64(self.0 + cs2dumper::client::CEntityInstance::m_pEntity)?;
Ok(CEntityIdentity(ptr))
}
}
pub struct CPlayerPawn(Address);
impl CPlayerPawn {
pub fn new(ptr: Address) -> CPlayerPawn {
CPlayerPawn(ptr)
}
pub fn from_uhandle(uhandle: u32, entity_list: Address, ctx: &mut CheatCtx) -> Option<CPlayerPawn> {
let list_entry = ctx.process.read_addr64(entity_list + 0x8 * ((uhandle & 0x7FFF) >> 9) + 16).unwrap();
if list_entry.is_null() || !list_entry.is_valid() {
None
} else {
let ptr = ctx.process.read_addr64(list_entry + 120 * (uhandle & 0x1FF)).unwrap();
Some(CPlayerPawn(ptr))
}
}
/*
DWORD64 entityPawnBase = Memory::Read<unsigned __int64>(EntitiesList + ((hEntity & 0x7FFF) * ENTITY_SPACING));
auto pawn = read<C_CSPlayerPawnBase*>(entityPawnBase + 0x78 * (hEntity & 0x1FF));
*/
pub fn from_uhandle2(uhandle: u32, entity_list: Address, ctx: &mut CheatCtx) -> Option<CPlayerPawn> {
let ent_pawn_base = ctx.process.read_addr64(entity_list + (uhandle & 0x7FFF) * 0x10).unwrap();
if ent_pawn_base.is_null() || !ent_pawn_base.is_valid() {
None
} else {
let ptr = ctx.process.read_addr64(ent_pawn_base + 0x78 * (uhandle & 0x1FF)).unwrap();
Some(CPlayerPawn(ptr))
}
}
pub fn ptr(&self) -> Address {
self.0
}
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)
}
}
pub struct CCSPlayerController(Address);
impl CCSPlayerController {
pub fn ptr(&self) -> Address {
self.0
}
pub fn new(ptr: Address) -> CCSPlayerController {
CCSPlayerController(ptr)
}
pub fn get_team(&self, ctx: &mut CheatCtx) -> Result<Option<TeamID>> {
let team: i32 = ctx.process.read(self.0 + cs2dumper::client::C_BaseEntity::m_iTeamNum)?;
Ok(TeamID::from_i32(team))
}
pub fn get_player_type(&self, ctx: &mut CheatCtx, local: &CCSPlayerController) -> 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, entity_list: Address, ctx: &mut CheatCtx) -> Result<Option<CPlayerPawn>> {
let uhandle = ctx.process.read(self.0 + cs2dumper::client::CCSPlayerController::m_hPlayerPawn)?;
Ok(CPlayerPawn::from_uhandle(uhandle, entity_list, ctx))
}
pub fn pawn2(&self, entity_list: Address, ctx: &mut CheatCtx) -> Result<Option<CPlayerPawn>> {
let uhandle = ctx.process.read(self.0 + cs2dumper::client::CBasePlayerController::m_hPawn)?;
Ok(CPlayerPawn::from_uhandle2(uhandle, entity_list, ctx))
}
pub fn player_name(&self, ctx: &mut CheatCtx) -> Result<String> {
let ptr = ctx.process.read_addr64(self.0 + cs2dumper::client::CCSPlayerController::m_sSanitizedPlayerName)?;
Ok(ctx.process.read_char_string_n(ptr, 32)?)
}
pub fn entity_identity(&self, ctx: &mut CheatCtx) -> Result<CEntityIdentity> {
let ptr = ctx.process.read_addr64(self.0 + cs2dumper::client::CEntityInstance::m_pEntity)?;
Ok(CEntityIdentity(ptr))
}
pub fn to_base(&self) -> CBaseEntity {
CBaseEntity(self.0)
}
}

2
src/sdk/structs/mod.rs Normal file

@ -0,0 +1,2 @@
mod entity;
pub use entity::*;

79
src/structs/comms.rs Normal file

@ -0,0 +1,79 @@
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
}
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, 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>, local_yaw: f32) -> RadarData {
RadarData { ingame, map_name, player_data, local_yaw }
}
/// 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(),
local_yaw: 0.0,
}
}
}

50
src/structs/mod.rs Normal file

@ -0,0 +1,50 @@
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,
}
}
}

11
src/structs/vec3.rs Normal file

@ -0,0 +1,11 @@
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 {}

66
src/webserver.rs Normal file

@ -0,0 +1,66 @@
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(())
}

Binary file not shown.

After

(image error) Size: 101 KiB

Binary file not shown.

After

(image error) Size: 186 KiB

Binary file not shown.

After

(image error) Size: 152 KiB

Binary file not shown.

After

(image error) Size: 212 KiB

Binary file not shown.

After

(image error) Size: 156 KiB

Binary file not shown.

After

(image error) Size: 126 KiB

Binary file not shown.

After

(image error) Size: 137 KiB

Binary file not shown.

After

(image error) Size: 76 KiB

Binary file not shown.

After

(image error) Size: 196 KiB

Binary file not shown.

After

(image error) Size: 97 KiB

@ -0,0 +1,7 @@
{
"pos_x": -2647.0,
"pos_y": 2592.0,
"scale": 4.6,
"rotate": 1,
"zoom": 1.5
}

@ -0,0 +1,7 @@
{
"pos_x": -1838.0,
"pos_y": 1858.0,
"scale": 4.1,
"rotate": 0,
"zoom": 0.0
}

@ -0,0 +1,7 @@
{
"pos_x": -2953.0,
"pos_y": 2164.0,
"scale": 5.0,
"rotate": 0,
"zoom": 0.0
}

@ -0,0 +1,7 @@
{
"pos_x": -2796.0,
"pos_y": 3328.0,
"scale": 5.22,
"rotate": 0,
"zoom": 0.0
}

@ -0,0 +1,7 @@
{
"pos_x": -2476.0,
"pos_y": 3239.0,
"scale": 4.400000095367432,
"rotate": 1,
"zoom": 1.100000023841858
}

@ -0,0 +1,7 @@
{
"pos_x": -2087.0,
"pos_y": 3870.0,
"scale": 4.9,
"rotate": 0,
"zoom": 0.0
}

@ -0,0 +1,7 @@
{
"pos_x": -3230.0,
"pos_y": 1713.0,
"scale": 5.0,
"rotate": 0,
"zoom": 0.0
}

@ -0,0 +1,7 @@
{
"pos_x": -3453.0,
"pos_y": 2887.0,
"scale": 7.0,
"rotate": 0,
"zoom": 0.0
}

@ -0,0 +1,7 @@
{
"pos_x": -4831.0,
"pos_y": 1781.0,
"scale": 5.2,
"rotate": 0,
"zoom": 0.0
}

@ -0,0 +1,7 @@
{
"pos_x": -3168.0,
"pos_y": 1762.0,
"scale": 4.0,
"rotate": 0,
"zoom": 0.0
}

Binary file not shown.

After

(image error) Size: 101 KiB

Binary file not shown.

After

(image error) Size: 186 KiB

Binary file not shown.

After

(image error) Size: 152 KiB

Binary file not shown.

After

(image error) Size: 212 KiB

Binary file not shown.

After

(image error) Size: 156 KiB

Binary file not shown.

After

(image error) Size: 126 KiB

Binary file not shown.

After

(image error) Size: 137 KiB

Binary file not shown.

After

(image error) Size: 76 KiB

Binary file not shown.

After

(image error) Size: 196 KiB

Binary file not shown.

After

(image error) Size: 97 KiB

Binary file not shown.

After

(image error) Size: 474 KiB

@ -0,0 +1,31 @@
// HLTV overview description file for cs_italy.bsp
"cs_italy"
{
"material" "overviews/cs_italy" // texture file
"pos_x" "-2647" // upper left world coordinate
"pos_y" "2592"
"scale" "4.6"
"rotate" "1"
"zoom" "1.5"
// loading screen icons and positions
"CTSpawn_x" "0.41"
"CTSpawn_y" "0.91"
"TSpawn_x" "0.6"
"TSpawn_y" "0.1"
"Hostage1_x" "0.43"
"Hostage1_y" "0.29"
"Hostage2_x" "0.48"
"Hostage2_y" "0.24"
"Hostage3_x" "0.64"
"Hostage3_y" "0.03"
"Hostage4_x" "0.72"
"Hostage4_y" "0.05"
// "Hostage5_x" "0.8"
// "Hostage5_y" "0.3"
// "Hostage6_x" "0.6"
// "Hostage6_y" "0.9"
}

@ -0,0 +1,29 @@
// HLTV overview description file for cs_office.bsp
"cs_office"
{
"material" "overviews/cs_office" // texture file
"pos_x" "-1838" // upper left world coordinate
"pos_y" "1858"
"scale" "4.1"
// loading screen icons and positions
"CTSpawn_x" "0.16"
"CTSpawn_y" "0.89"
"TSpawn_x" "0.78"
"TSpawn_y" "0.30"
"Hostage1_x" "0.84"
"Hostage1_y" "0.27"
"Hostage2_x" "0.84"
"Hostage2_y" "0.48"
"Hostage3_x" "0.91"
"Hostage3_y" "0.48"
"Hostage4_x" "0.77"
"Hostage4_y" "0.48"
"Hostage5_x" "0.77"
"Hostage5_y" "0.55"
// "Hostage6_x" "0.6"
// "Hostage6_y" "0.9"
}

@ -0,0 +1,23 @@
// HLTV overview description file for de_ancient.bsp
"de_ancient"
{
"material" "overviews/de_ancient" // texture file
"pos_x" "-2953" // upper left world coordinate
"pos_y" "2164"
"scale" "5"
"rotate" "0"
"zoom" "0"
// loading screen icons and positions
"CTSpawn_x" "0.51"
"CTSpawn_y" "0.17"
"TSpawn_x" "0.485"
"TSpawn_y" "0.87"
"bombA_x" "0.31"
"bombA_y" "0.25"
"bombB_x" "0.80"
"bombB_y" "0.40"
}

@ -0,0 +1,12 @@
// TAVR - AUTO RADAR. v 2.5.0a
"de_anubis"
{
"CTSpawn_x" "0.610000"
"CTSpawn_y" "0.220000"
"TSpawn_x" "0.580000"
"TSpawn_y" "0.930000"
"material" "overviews/de_anubis"
"pos_x" "-2796.000000"
"pos_y" "3328.000000"
"scale" "5.220000"
}

@ -0,0 +1,27 @@
// HLTV overview description file for de_dust2_v2.bsp
"de_dust2"
{
"material" "overviews/de_dust2_v2" // texture file
"pos_x" "-2476" // upper left world coordinate
"pos_y" "3239"
"scale" "4.4"
"rotate" "1"
"zoom" "1.1"
"inset_left" "0.0"
"inset_top" "0.0"
"inset_right" "0.0"
"inset_bottom" "0.0"
// loading screen icons and positions
"CTSpawn_x" "0.62"
"CTSpawn_y" "0.21"
"TSpawn_x" "0.39"
"TSpawn_y" "0.91"
"bombA_x" "0.80"
"bombA_y" "0.16"
"bombB_x" "0.21"
"bombB_y" "0.12"
}

@ -0,0 +1,20 @@
// HLTV overview description file for de_inferno.bsp
"de_inferno"
{
"material" "overviews/de_inferno" // texture file
"pos_x" "-2087" // upper left world coordinate
"pos_y" "3870"
"scale" "4.9"
// loading screen icons and positions
"CTSpawn_x" "0.9"
"CTSpawn_y" "0.35"
"TSpawn_x" "0.1"
"TSpawn_y" "0.67"
"bombA_x" "0.81"
"bombA_y" "0.69"
"bombB_x" "0.49"
"bombB_y" "0.22"
}

@ -0,0 +1,25 @@
"de_mirage"
{
"material" "overviews/de_mirage" // texture file
"pos_x" "-3230" // X coordinate,
"pos_y" "1713" // Y coordinate,
"scale" "5.00" // and used scale used when taking the screenshot
"rotate" "0" // map was rotated by 90 degress in image editor
"zoom" "0" // optimal zoom factor if map is shown in full size
// loading screen icons and positions
"CTSpawn_x" "0.28"
"CTSpawn_y" "0.70"
"TSpawn_x" "0.87"
"TSpawn_y" "0.36"
"bombA_x" "0.54"
"bombA_y" "0.76"
"bombB_x" "0.23"
"bombB_y" "0.28"
"inset_left" "0.135"
"inset_top" "0.08"
"inset_right" "0.105"
"inset_bottom" "0.08"
}

@ -0,0 +1,41 @@
// HLTV overview description file for de_nuke.bsp
"de_nuke"
{
"material" "overviews/de_nuke" // texture file
"pos_x" "-3453" // upper left world coordinate
"pos_y" "2887"
"scale" "7"
"verticalsections"
{
"default" // use the primary radar image
{
"AltitudeMax" "10000"
"AltitudeMin" "-495"
}
"lower" // i.e. de_nuke_lower_radar.dds
{
"AltitudeMax" "-495"
"AltitudeMin" "-10000"
}
}
// loading screen icons and positions
"CTSpawn_x" "0.82"
"CTSpawn_y" "0.45"
"TSpawn_x" "0.19"
"TSpawn_y" "0.54"
"bombA_x" "0.58"
"bombA_y" "0.48"
"bombB_x" "0.58"
"bombB_y" "0.58"
"inset_left" "0.33"
"inset_top" "0.2"
"inset_right" "0.2"
"inset_bottom" "0.2"
}

@ -0,0 +1,22 @@
// HLTV overview description file for de_overpass.bsp
"de_overpass"
{
"material" "overviews/de_overpass" // texture file
"pos_x" "-4831" // upper left world coordinate
"pos_y" "1781"
"scale" "5.2"
"rotate" "0"
"zoom" "0"
// loading screen icons and positions
"CTSpawn_x" "0.49"
"CTSpawn_y" "0.2"
"TSpawn_x" "0.66"
"TSpawn_y" "0.93"
"bombA_x" "0.55"
"bombA_y" "0.23"
"bombB_x" "0.7"
"bombB_y" "0.31"
}

@ -0,0 +1,40 @@
// HLTV overview description file for de_vertigo.bsp
"de_vertigo"
{
"material" "overviews/de_vertigo_radar" // texture file
"pos_x" "-3168" // upper left world coordinate
"pos_y" "1762"
"scale" "4.0"
"verticalsections"
{
"default" // use the primary radar image
{
"AltitudeMax" "20000"
"AltitudeMin" "11700"
}
"lower" // i.e. de_nuke_lower_radar.dds
{
"AltitudeMax" "11700"
"AltitudeMin" "-10000"
}
}
// loading screen icons and positions
"CTSpawn_x" "0.54"
"CTSpawn_y" "0.25"
"TSpawn_x" "0.20"
"TSpawn_y" "0.75"
"bombA_x" "0.705"
"bombA_y" "0.585"
"bombB_x" "0.222"
"bombB_y" "0.223"
"inset_left" "0.1"
"inset_top" "0.1"
"inset_right" "0.2"
"inset_bottom" "0.15"
}

@ -0,0 +1,7 @@
"workshop_preview"
{
"autogenerated_tga" "1"
"pos_x" "-2071"
"pos_y" "711"
"scale" "1.699219"
}

13
web/index.html Normal file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>radarflow</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<canvas id="canvas"></canvas>
<script src="script.js"></script>
</body>
</html>

416
web/script.js Normal file

@ -0,0 +1,416 @@
// Colors
const localColor = "#109856"
const teamColor = "#68a3e5"
const enemyColor = "#ec040b"
const bombColor = "#eda338"
const textColor = "#d1d1d1"
// Should zoom or not
shouldZoom = true
// Common
canvas = null
ctx = null
// radarflow specific
image = null
map = null
mapName = null
loaded = false
entityData = null
update = false
/// Radarflow zoom in
zoomSet = false
safetyBound = 50
boundingRect = null
// networking
websocket = null
if (location.protocol == 'https:') {
websocketAddr = `wss://${window.location.host}/ws`
} else {
websocketAddr = `ws://${window.location.host}/ws`
}
// Util functions
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
const degreesToRadians = (degrees) => degrees * (Math.PI/180);
function makeBoundingRect(x1, y1, x2, y2, aspectRatio) {
const topLeftX = x1;
const topLeftY = y1;
const bottomRightX = x2;
const bottomRightY = y2;
const width = bottomRightX - topLeftX;
const height = bottomRightY - topLeftY;
let newWidth, newHeight;
if (width / height > aspectRatio) {
// Wider rectangle
newHeight = width / aspectRatio;
newWidth = width;
} else {
// Taller rectangle
newWidth = height * aspectRatio;
newHeight = height;
}
const centerX = (topLeftX + bottomRightX) / 2;
const centerY = (topLeftY + bottomRightY) / 2;
const rectMinX = centerX - newWidth / 2;
const rectMaxX = centerX + newWidth / 2;
const rectMinY = centerY - newHeight / 2;
const rectMaxY = centerY + newHeight / 2;
const boundingRectangle = {
x: rectMinX,
y: rectMinY,
width: rectMaxX - rectMinX,
height: rectMaxY - rectMinY,
}
const boundingRectangle2 = {
x: 0,
y: 0,
width: image.width / 1.2,
height: image.width / 1.2,
}
return boundingRectangle;
}
function boundingCoordinates(coordinates, boundingRect) {
const xScale = boundingRect.width / image.width;
const yScale = boundingRect.height / image.height;
const newX = (coordinates.x - boundingRect.x) / xScale;
const newY = (coordinates.y - boundingRect.y) / yScale;
return {x: newX, y: newY};
}
function boundingScale(value, boundingRect) {
const scale = image.width / boundingRect.width;
return value * scale
}
function mapCoordinates(coordinates) {
let offset_x = coordinates.x - map.pos_x;
let offset_y = coordinates.y - map.pos_y;
offset_x /= map.scale;
offset_y /= -map.scale;
return {x: offset_x, y: offset_y}
}
function render() {
if (update) {
fillCanvas()
if (loaded) {
update = false
// Iterate through the array and update the min/max values
if (entityData != null && map != null && image != null && shouldZoom) {
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
entityData.forEach((data) => {
let mapCords = null
if (data.Bomb !== undefined) {
mapCords = mapCoordinates(data.Bomb.pos)
} else {
mapCords = mapCoordinates(data.Player.pos)
}
minX = Math.min(minX, mapCords.x);
minY = Math.min(minY, mapCords.y);
maxX = Math.max(maxX, mapCords.x);
maxY = Math.max(maxY, mapCords.y);
});
boundingRect = makeBoundingRect(minX-safetyBound, minY-safetyBound, maxX+safetyBound, maxY+safetyBound, image.width/image.height)
zoomSet = true
} else if (zoomSet) {
zoomSet = false
}
drawImage()
if (entityData != null) {
entityData.forEach((data) => {
if (data.Bomb !== undefined) {
drawBomb(data.Bomb.pos, data.Bomb.isPlanted)
} else {
let fillStyle = localColor
switch (data.Player.playerType) {
case "Team":
fillStyle = teamColor
break;
case "Enemy":
fillStyle = enemyColor
break;
}
drawEntity(
data.Player.pos,
fillStyle,
data.Player.isDormant,
data.Player.hasBomb,
data.Player.yaw
)
}
});
}
} else {
if (websocket != null) {
ctx.font = "100px Arial";
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillStyle = textColor
ctx.fillText("Not on a server", 1024/2, 1024/2);
} else {
ctx.font = "100px Arial";
ctx.textAlign = "center"
ctx.fillStyle = textColor
ctx.fillText("Disconnected", 1024/2, 1024/2);
}
}
}
if (websocket != null) {
websocket.send("requestInfo");
}
}
function fillCanvas() {
ctx.fillStyle = "#0f0f0f";
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
function drawImage() {
if (image == null || canvas == null)
return
if (zoomSet != false && boundingRect.x != null) {
ctx.drawImage(image, boundingRect.x, boundingRect.y, boundingRect.width, boundingRect.height, 0, 0, canvas.width, canvas.height)
} else {
ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height)
}
}
function drawBomb(pos, planted) {
if (map == null)
return
if (zoomSet) {
pos = boundingCoordinates(mapCoordinates(pos), boundingRect)
size = boundingScale(5, boundingRect);
} else {
pos = mapCoordinates(pos)
size = 5
}
ctx.beginPath();
ctx.arc(pos.x, pos.y, size, 0, 2 * Math.PI);
ctx.fillStyle = bombColor;
ctx.fill();
ctx.closePath();
if (planted && ((new Date().getTime() / 1000) % 1) > 0.5) {
ctx.strokeStyle = enemyColor
ctx.lineWidth = 1;
ctx.stroke()
}
}
function drawEntity(pos, fillStyle, dormant, hasBomb, yaw) {
if (map == null)
return
if (zoomSet) {
pos = boundingCoordinates(mapCoordinates(pos), boundingRect)
circleRadius = boundingScale(7, boundingRect);
distance = circleRadius + boundingScale(2, boundingRect);
radius = distance + boundingScale(2, boundingRect)
arrowWidth = 35
} else {
pos = mapCoordinates(pos)
circleRadius = 7
distance = circleRadius + 2
radius = distance + 5;
arrowWidth = 35;
}
if (dormant) {
ctx.font = "20px Arial";
ctx.textAlign = "center"
ctx.fillStyle = fillStyle
ctx.fillText("?", pos.x, pos.y);
} else {
// Draw circle
ctx.beginPath();
ctx.arc(pos.x, pos.y, circleRadius, 0, 2 * Math.PI);
ctx.fillStyle = fillStyle;
ctx.fill();
if (hasBomb) {
ctx.beginPath();
ctx.arc(pos.x, pos.y, circleRadius / 2, 0, 2 * Math.PI);
ctx.fillStyle = "#dbb81d";
ctx.fill();
}
ctx.closePath();
// Calculate arrowhead points
const arrowHeadX = pos.x + radius * Math.cos(yaw * (Math.PI / 180))
const arrowHeadY = pos.y - radius * Math.sin(yaw * (Math.PI / 180))
const arrowCornerX1 = pos.x + distance * Math.cos((yaw - arrowWidth) * (Math.PI / 180))
const arrowCornerY1 = pos.y - distance * Math.sin((yaw - arrowWidth) * (Math.PI / 180))
const arrowCornerX2 = pos.x + distance * Math.cos((yaw + arrowWidth) * (Math.PI / 180))
const arrowCornerY2 = pos.y - distance * Math.sin((yaw + arrowWidth) * (Math.PI / 180))
const cicleYaw = 90-yaw
const startAngle = degreesToRadians(cicleYaw-arrowWidth)-Math.PI/2
const endAngle = degreesToRadians(cicleYaw+arrowWidth)-Math.PI/2
// Draw arrow
/// Backside of the arrow
ctx.beginPath();
ctx.arc(pos.x, pos.y, distance, startAngle, endAngle)
/// Draw from corners to arrowhead
ctx.lineTo(arrowCornerX1, arrowCornerY1);
ctx.lineTo(arrowHeadX, arrowHeadY);
ctx.lineTo(arrowCornerX2, arrowCornerY2);
ctx.closePath()
ctx.fillStyle = 'white'
ctx.fill();
}
}
function loadMap(mapName) {
console.log(`[radarflow] loading map ${mapName}`)
loaded = true;
const map_img = new Image();
map_img.src = `assets/image/${mapName}_radar_psd.png`;
fetch(`assets/json/${mapName}.json`)
.then(response => response.json())
.then(data => {
map = data;
})
.catch(error => {
console.error('Error loading JSON file:', error);
});
map_img.onload = () => {
image = map_img;
update = true;
};
}
function unloadMap() {
console.log("[radarflow] unloading map")
ctx.clearRect(0, 0, canvas.width, canvas.height)
map = null
mapName = null
loaded = false,
update = true
requestAnimationFrame(render);
}
function connect() {
if (websocket == null) {
let socket = new WebSocket(websocketAddr)
socket.onopen = () => {
console.log("[radarflow] Connection established")
websocket.send("requestInfo");
};
socket.onmessage = (event) => {
if (event.data == "error") {
console.log("[radarflow] Server had an unknown error")
} else {
let data = JSON.parse(event.data);
if (data.ingame == false) {
mapName = null
entityData = null
if (loaded)
unloadMap()
} else {
if (!loaded) {
mapName = data.mapName
entityData = data.entityData
loadMap(mapName)
} else {
entityData = data.entityData
}
}
update = true
requestAnimationFrame(render);
}
};
socket.onclose = (event) => {
if (event.wasClean) {
console.log("[radarflow] connection closed");
} else {
console.log("[radarflow] connection died");
}
playerData = null
websocket = null
unloadMap()
setTimeout(function() {
connect();
}, 1000);
};
socket.onerror = (error) => {
console.log(`[radarflow] websocket error: ${error}`);
};
websocket = socket;
} else {
setTimeout(() => {
connect();
}, 1000);
}
}
addEventListener("DOMContentLoaded", (e) => {
canvas = document.getElementById('canvas');
canvas.width = 1024;
canvas.height = 1024;
canvasAspectRatio = canvas.width / canvas.height
ctx = canvas.getContext('2d');
console.log(`[radarflow] connecting to ${websocketAddr}`)
connect()
});

17
web/styles.css Normal file

@ -0,0 +1,17 @@
body {
margin: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #000000; /* Change the background color as needed */
}
canvas {
width: 100%;
height: 100%;
overflow: hidden;
display: block;
object-fit: contain;
}