Compare commits
	
		
			39 Commits
		
	
	
		
			faster-bom
			...
			main
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1bdcbc8fb9 | |||
| d8982ead59 | |||
| 2df1b5a560 | |||
| a17146fee9 | |||
| 38ee14524f | |||
| b6300bcb0b | |||
| e9d197229a | |||
| 4846e045bb | |||
| d10fcdf15e | |||
| f5af3d6281 | |||
| 1eed104790 | |||
| 6f4e6a4f30 | |||
| 5e19f0af47 | |||
| ffa2979da3 | |||
| 3d0b960753 | |||
| 3a69f285f3 | |||
| cb9462ce9b | |||
| ba28f01247 | |||
| fd27f56365 | |||
| 64ebd83016 | |||
| 5f99e03b4c | |||
| d6719b0d54 | |||
| 363db1c56a | |||
| 61ea504f28 | |||
| bd8de00f10 | |||
| e6ca121b86 | |||
| 067fff4ef8 | |||
| 7bdc3fbaa3 | |||
| 30fcdde828 | |||
| 5e97135970 | |||
| 504d54bbeb | |||
| 235c3a6151 | |||
| f2824af426 | |||
| ec679b22cb | |||
|  | c86216ec6e | ||
|  | 02c3ac8dde | ||
|  | f3f08cbede | ||
|  | 831d232d23 | ||
|  | b7984a9168 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -2,3 +2,4 @@ | ||||
| /src/dma/cs2dumper/offsets_mod.rs | ||||
| /src/dma/cs2dumper/client_mod.rs | ||||
| /src/dma/cs2dumper/engine2_mod.rs | ||||
| dump.sh | ||||
|   | ||||
							
								
								
									
										578
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										578
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										42
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| [package] | ||||
| name = "radarflow" | ||||
| version = "0.2.4" | ||||
| version = "0.2.5" | ||||
| authors = ["Janek S <development@superyu.xyz"] | ||||
| edition = "2021" | ||||
|  | ||||
| @@ -8,39 +8,45 @@ edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
| # memory | ||||
| memflow = "0.2.1" | ||||
| memflow = "0.2.3" | ||||
| memflow-native = { git = "https://github.com/memflow/memflow-native" } | ||||
| dataview = "1.0.1" | ||||
|  | ||||
| # logging | ||||
| log = "0.4.21" | ||||
| simple_logger = "4.3.3" | ||||
| log = "0.4.22" | ||||
| simple_logger = "5.0.0" | ||||
|  | ||||
| # error handling | ||||
| anyhow = "1.0.81" | ||||
| anyhow = "1.0.93" | ||||
|  | ||||
| # derive stuff | ||||
| enum-primitive-derive = "0.3.0" | ||||
| num-traits = "0.2.18" | ||||
| serde = { version = "1.0.197", features = ["derive"] } | ||||
| serde_json = "1.0.115" | ||||
| clap = { version = "4.5.4", features = ["derive", "string"] } | ||||
| num-traits = "0.2.19" | ||||
| serde = { version = "1.0.215", features = ["derive"] } | ||||
| serde_json = "1.0.133" | ||||
| clap = { version = "4.5.21", features = ["derive", "string"] } | ||||
|  | ||||
| # tokio | ||||
| tokio = { version = "1.37.0", features = ["full"] } | ||||
|  | ||||
| # networking | ||||
| axum = { version = "0.7.5", features = ["ws"] } | ||||
| tower-http = { version = "0.5.2", features = ["fs"] } | ||||
| tower = "0.4.13" | ||||
| local-ip-address = "0.6.1" | ||||
| axum = { version = "0.7.9", features = ["ws"] } | ||||
| tower-http = { version = "0.6.2", features = ["fs"] } | ||||
| tower = "0.5.1" | ||||
| local-ip-address = "0.6.3" | ||||
|  | ||||
| # other | ||||
| itertools = "0.12.1" | ||||
| itertools = "0.13.0" | ||||
|  | ||||
| flate2 = "1.0" | ||||
| rand = "0.8" | ||||
|  | ||||
| lazy_static = "1.4" | ||||
|  | ||||
| uuid = { version = "1.3", features = ["v4"] } | ||||
|  | ||||
| [build-dependencies] | ||||
| reqwest = { version = "0.12.2", features = ["blocking"] } | ||||
| serde = { version = "1.0.197", features = ["derive"] } | ||||
| serde_json = "1.0.115" | ||||
| vergen = { version = "8.3.1", features = ["build", "cargo", "git", "gitcl", "rustc", "si"] } | ||||
| reqwest = { version = "0.12.9", features = ["blocking"] } | ||||
| serde = { version = "1.0.215", features = ["derive"] } | ||||
| serde_json = "1.0.133" | ||||
| vergen-gitcl = { version = "1.0.0", features = ["build", "cargo", "rustc",] } | ||||
|   | ||||
| @@ -13,7 +13,7 @@ Before you begin, install the necessary memflow plugins using memflowup from the | ||||
| The needed Plugins are `memflow-qemu` and `memflow-win32`  | ||||
|  | ||||
| Clone the repo on your vm host:   | ||||
| `git clone https://github.com/superyu1337/radarflow2.git` | ||||
| `git clone https://git.deadzone.lol/Wizzard/radarflow2-kvm.git` | ||||
|  | ||||
| Run radarflow:    | ||||
| `cargo run --release` | ||||
| @@ -38,7 +38,7 @@ Check the dlopen documentation for all possible import paths. | ||||
| ``` | ||||
|  | ||||
| Clone the repo on your attacking pc:   | ||||
| `git clone https://github.com/superyu1337/radarflow2.git` | ||||
| `git clone https://git.deadzone.lol/Wizzard/radarflow2-kvm.git` | ||||
|  | ||||
| Run radarflow:    | ||||
| `cargo run --release -- --connector pcileech` | ||||
|   | ||||
							
								
								
									
										20
									
								
								build.rs
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								build.rs
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| use std::error::Error; | ||||
|  | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use vergen::EmitBuilder; | ||||
| use vergen_gitcl::{Emitter, GitclBuilder}; | ||||
|  | ||||
| #[derive(Clone, Deserialize, Serialize)] | ||||
| struct InfoJson { | ||||
| @@ -36,29 +36,27 @@ fn build_number() -> Result<(), Box<dyn Error>> { | ||||
| fn main() -> Result<(), Box<dyn Error>> { | ||||
|  | ||||
|     download( | ||||
|         "https://raw.githubusercontent.com/a2x/cs2-dumper/main/output/client.dll.rs", | ||||
|         "https://raw.githubusercontent.com/a2x/cs2-dumper/refs/heads/main/output/client_dll.rs", | ||||
|         "./src/dma/cs2dumper/client_mod.rs" | ||||
|     ).expect("Failed to download build file \"client.dll.rs\""); | ||||
|  | ||||
|     download( | ||||
|         "https://raw.githubusercontent.com/a2x/cs2-dumper/main/output/offsets.rs", | ||||
|         "https://raw.githubusercontent.com/a2x/cs2-dumper/refs/heads/main/output/offsets.rs", | ||||
|         "./src/dma/cs2dumper/offsets_mod.rs" | ||||
|     ).expect("Failed to download build file \"offsets.rs\""); | ||||
|  | ||||
|     download( | ||||
|         "https://raw.githubusercontent.com/a2x/cs2-dumper/main/output/engine2.dll.rs", | ||||
|         "https://raw.githubusercontent.com/a2x/cs2-dumper/refs/heads/main/output/engine2_dll.rs", | ||||
|         "./src/dma/cs2dumper/engine2_mod.rs" | ||||
|     ).expect("Failed to download build file \"engine2.dll.rs\""); | ||||
|  | ||||
|     build_number()?; | ||||
|  | ||||
|     EmitBuilder::builder() | ||||
|         .git_sha(true) | ||||
|         .git_commit_date() | ||||
|         .cargo_debug() | ||||
|         .cargo_target_triple() | ||||
|         .rustc_semver() | ||||
|         .rustc_llvm_version() | ||||
|     let gitcl = GitclBuilder::all_git()?; | ||||
|  | ||||
|  | ||||
|     Emitter::new() | ||||
|         .add_instructions(&gitcl)? | ||||
|         .emit()?; | ||||
|  | ||||
|     Ok(()) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ use memflow::plugins::Inventory; | ||||
| use crate::dma::Connector; | ||||
| const PORT_RANGE: std::ops::RangeInclusive<usize> = 8000..=65535; | ||||
|  | ||||
| #[derive(Parser)] | ||||
| #[derive(Parser, Clone)] | ||||
| #[command(author, version = version(), about, long_about = None)] | ||||
| pub struct Cli { | ||||
|     /// Specifies the connector type for DMA | ||||
| @@ -45,7 +45,11 @@ fn version() -> String { | ||||
|         avail.join(", ") | ||||
|     }; | ||||
|  | ||||
|     format!(" {pkg_ver} (rev {git_hash})\nCommit Date: {commit_date}\nAvailable Connectors: {avail_cons}") | ||||
|     format!( | ||||
|         "{pkg_ver} (rev {git_hash})\n\ | ||||
|         Commit Date: {commit_date}\n\ | ||||
|         Available Connectors: {avail_cons}\n" | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fn port_in_range(s: &str) -> Result<u16, String> { | ||||
|   | ||||
							
								
								
									
										79
									
								
								src/comms.rs
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								src/comms.rs
									
									
									
									
									
								
							| @@ -16,12 +16,36 @@ pub struct PlayerData { | ||||
|     has_awp: bool, | ||||
|  | ||||
|     #[serde(rename = "isScoped")] | ||||
|     is_scoped: bool | ||||
|     is_scoped: bool, | ||||
|  | ||||
|     #[serde(rename = "playerName")] | ||||
|     player_name: String, | ||||
|  | ||||
|     #[serde(rename = "weaponId")] | ||||
|     weapon_id: i16, | ||||
|  | ||||
|     #[serde(rename = "money", default)] | ||||
|     money: i32, | ||||
|  | ||||
|     #[serde(rename = "health", default)] | ||||
|     health: u32, | ||||
| } | ||||
|  | ||||
| impl PlayerData { | ||||
|     pub fn new(pos: Vec3, yaw: f32, player_type: PlayerType, has_bomb: bool, has_awp: bool, is_scoped: bool) -> PlayerData { | ||||
|         PlayerData { pos, yaw, player_type, has_bomb, has_awp, is_scoped } | ||||
|     pub fn new(pos: Vec3, yaw: f32, player_type: PlayerType, has_bomb: bool, has_awp: bool, | ||||
|                     is_scoped: bool, player_name: String, weapon_id: i16, money: i32, health: u32) -> PlayerData { | ||||
|         PlayerData { | ||||
|             pos, | ||||
|             yaw, | ||||
|             player_type, | ||||
|             has_bomb, | ||||
|             has_awp, | ||||
|             is_scoped, | ||||
|             player_name, | ||||
|             weapon_id, | ||||
|             money, | ||||
|             health | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -45,6 +69,24 @@ pub enum EntityData { | ||||
|     Bomb(BombData) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct CheatOptions { | ||||
|     #[serde(rename = "revealMoney")] | ||||
|     pub reveal_money: bool, | ||||
|  | ||||
|     #[serde(rename = "displayMoney")] | ||||
|     pub display_money: bool, | ||||
| } | ||||
|  | ||||
| impl Default for CheatOptions { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             reveal_money: false, | ||||
|             display_money: true, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||||
| pub struct RadarData { | ||||
|     freq: usize, | ||||
| @@ -77,13 +119,30 @@ pub struct RadarData { | ||||
|     #[serde(rename(serialize = "entityData"))] | ||||
|     player_data: Vec<EntityData>, | ||||
|  | ||||
|     //#[serde(rename(serialize = "localYaw"))] | ||||
|     //local_yaw: f32, | ||||
|     #[serde(rename = "options")] | ||||
|     options: CheatOptions, | ||||
|  | ||||
|     #[serde(skip)] | ||||
|     pub money_reveal_enabled: bool, | ||||
| } | ||||
|  | ||||
| impl RadarData { | ||||
|     pub fn new(ingame: bool, map_name: String, player_data: Vec<EntityData>, freq: usize, bomb_planted: bool, bomb_cannot_defuse: bool, bomb_defuse_timeleft: f32, bomb_exploded: bool, bomb_being_defused: bool, bomb_defuse_length: f32, bomb_defuse_end: f32) -> RadarData { | ||||
|         RadarData { ingame, map_name, player_data, freq, bomb_planted, bomb_can_defuse: bomb_cannot_defuse, bomb_defuse_timeleft, bomb_exploded, bomb_being_defused, bomb_defuse_length, bomb_defuse_end } | ||||
|         RadarData { | ||||
|             ingame, | ||||
|             map_name, | ||||
|             player_data, | ||||
|             freq, | ||||
|             bomb_planted, | ||||
|             bomb_can_defuse: bomb_cannot_defuse, | ||||
|             bomb_defuse_timeleft, | ||||
|             bomb_exploded, | ||||
|             bomb_being_defused, | ||||
|             bomb_defuse_length, | ||||
|             bomb_defuse_end, | ||||
|             options: CheatOptions::default(), | ||||
|             money_reveal_enabled: false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Returns empty RadarData, it's also the same data that gets sent to clients when not ingame | ||||
| @@ -99,9 +158,15 @@ impl RadarData { | ||||
|             bomb_exploded: false, | ||||
|             bomb_being_defused: false, | ||||
|             bomb_defuse_length: 0.0, | ||||
|             bomb_defuse_end: 0.0 | ||||
|             bomb_defuse_end: 0.0, | ||||
|             options: CheatOptions::default(), | ||||
|             money_reveal_enabled: false | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_entities(&self) -> &Vec<EntityData> { | ||||
|         &self.player_data | ||||
|     } | ||||
| } | ||||
|  | ||||
| unsafe impl Send for RadarData {} | ||||
|   | ||||
| @@ -97,6 +97,9 @@ impl DmaCtx { | ||||
|         let mut team = 0i32; | ||||
|         let mut clipping_weapon = 0u64; | ||||
|         let mut is_scoped = 0u8; | ||||
|         let mut player_name_ptr = 0u64; | ||||
|         let mut money = 0i32; | ||||
|         let mut money_services_ptr = 0u64; | ||||
|  | ||||
|         { | ||||
|             let mut batcher = MemoryViewBatcher::new(&mut self.process); | ||||
| @@ -105,19 +108,41 @@ impl DmaCtx { | ||||
|             batcher.read_into(pawn + cs2dumper::client::C_BaseEntity::m_iHealth, &mut health); | ||||
|             batcher.read_into(controller + cs2dumper::client::C_BaseEntity::m_iTeamNum, &mut team); | ||||
|             batcher.read_into(pawn + cs2dumper::client::C_CSPlayerPawnBase::m_pClippingWeapon, &mut clipping_weapon); | ||||
|             batcher.read_into(pawn + cs2dumper::client::C_CSPlayerPawnBase::m_bIsScoped, &mut is_scoped); | ||||
|             batcher.read_into(pawn + cs2dumper::client::C_CSPlayerPawn::m_bIsScoped, &mut is_scoped); | ||||
|             batcher.read_into(controller + cs2dumper::client::CCSPlayerController::m_sSanitizedPlayerName, &mut player_name_ptr); | ||||
|  | ||||
|             batcher.read_into(controller + cs2dumper::client::CCSPlayerController::m_pInGameMoneyServices, &mut money_services_ptr); | ||||
|         } | ||||
|  | ||||
|         if money_services_ptr != 0 { | ||||
|             let money_addr: Address = money_services_ptr.into(); | ||||
|             money = self.process.read(money_addr + cs2dumper::client::CCSPlayerController_InGameMoneyServices::m_iAccount)?; | ||||
|         } | ||||
|  | ||||
|         let player_name = if player_name_ptr != 0 { | ||||
|             let mut name_buffer = [0u8; 32]; | ||||
|             self.process.read_raw_into(player_name_ptr.into(), &mut name_buffer)?; | ||||
|  | ||||
|             let name_len = name_buffer.iter().position(|&c| c == 0).unwrap_or(name_buffer.len()); | ||||
|             String::from_utf8_lossy(&name_buffer[..name_len]).to_string() | ||||
|         } else { | ||||
|             match team { | ||||
|                 2 => format!("T Player"), | ||||
|                 3 => format!("CT Player"), | ||||
|                 _ => format!("Unknown Player"), | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let team = TeamID::from_i32(team); | ||||
|  | ||||
|         let has_awp = { | ||||
|         let (has_awp, weapon_id) = { | ||||
|             let clipping_weapon: Address = clipping_weapon.into(); | ||||
|             let items_def_idx_addr = clipping_weapon + cs2dumper::client::C_EconEntity::m_AttributeManager  | ||||
|                 + cs2dumper::client::C_AttributeContainer::m_Item + cs2dumper::client::C_EconItemView::m_iItemDefinitionIndex; | ||||
|      | ||||
|             let items_def_idx: i16 = self.process.read(items_def_idx_addr)?; | ||||
|  | ||||
|             items_def_idx == 9 | ||||
|             (items_def_idx == 9, items_def_idx) | ||||
|         }; | ||||
|  | ||||
|         Ok(BatchedPlayerData { | ||||
| @@ -126,7 +151,10 @@ impl DmaCtx { | ||||
|             team, | ||||
|             health, | ||||
|             has_awp, | ||||
|             is_scoped: is_scoped != 0 | ||||
|             is_scoped: is_scoped != 0, | ||||
|             player_name, | ||||
|             weapon_id, | ||||
|             money, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
| @@ -178,7 +206,8 @@ impl DmaCtx { | ||||
|         let mut data_vec: Vec<(Address, u64, Vec<(u64, i32)>)> = data_vec | ||||
|             .into_iter() | ||||
|             .map(|(pawn, _, wep_count, wep_base)| { | ||||
|                 let weps = (0..wep_count).into_iter().map(|idx| (0u64, idx)).collect(); | ||||
|                 let safe_count = if wep_count < 0 || wep_count > 32 { 0 } else { wep_count }; | ||||
|                 let weps = (0..safe_count).into_iter().map(|idx| (0u64, idx)).collect(); | ||||
|                 (pawn, wep_base, weps) | ||||
|             }) | ||||
|             .collect(); | ||||
| @@ -188,7 +217,7 @@ impl DmaCtx { | ||||
|         data_vec.iter_mut().for_each(|(_, wep_base, wep_data_vec)| { | ||||
|             wep_data_vec.iter_mut().for_each(|(_, idx)| { | ||||
|                 let b: Address = (*wep_base).into(); | ||||
|                 batcher.read_into(b + * idx * 0x4, idx); | ||||
|                 batcher.read_into(b + u64::saturating_mul(*idx as u64, 0x4), idx); | ||||
|             }); | ||||
|         }); | ||||
|         drop(batcher); | ||||
| @@ -252,4 +281,7 @@ pub struct BatchedPlayerData { | ||||
|     pub health: u32, | ||||
|     pub has_awp: bool, | ||||
|     pub is_scoped: bool, | ||||
|     pub player_name: String, | ||||
|     pub weapon_id: i16, | ||||
|     pub money: i32, | ||||
| } | ||||
							
								
								
									
										139
									
								
								src/dma/mod.rs
									
									
									
									
									
								
							
							
						
						
									
										139
									
								
								src/dma/mod.rs
									
									
									
									
									
								
							| @@ -4,17 +4,28 @@ use memflow::{mem::MemoryView, os::Process, types::Address}; | ||||
|  | ||||
| use crate::{enums::PlayerType, comms::{EntityData, PlayerData, RadarData, ArcRwlockRadarData, BombData}}; | ||||
|  | ||||
| use crate::money_reveal::MoneyReveal; | ||||
|  | ||||
| use self::{context::DmaCtx, threaddata::CsData}; | ||||
|  | ||||
| mod context; | ||||
| mod threaddata; | ||||
| pub mod context; | ||||
| pub mod threaddata; | ||||
| mod cs2dumper; | ||||
|  | ||||
| pub use context::Connector; | ||||
|  | ||||
| pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_device: String, skip_version: bool) -> anyhow::Result<()> { | ||||
|     let mut ctx = DmaCtx::setup(connector, pcileech_device, skip_version)?; | ||||
|     let mut data = CsData { recheck_bomb_holder: true, ..Default::default() }; | ||||
|     let mut data = CsData { | ||||
|         recheck_bomb_holder: true, | ||||
|         money_reveal_enabled: false, | ||||
|         ..Default::default() | ||||
|     }; | ||||
|  | ||||
|     let mut money_reveal = MoneyReveal::new(); | ||||
|     if let Err(e) = money_reveal.init(&mut ctx.process, &ctx.client_module) { | ||||
|         log::warn!("Failed to initialize money reveal: {}", e); | ||||
|     } | ||||
|      | ||||
|     // For read timing | ||||
|     let mut last_bomb_dropped = false; | ||||
| @@ -47,6 +58,17 @@ pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_ | ||||
|  | ||||
|         data.update_common(&mut ctx); | ||||
|  | ||||
|         { | ||||
|             let radar = radar_data.read().await; | ||||
|             if radar.money_reveal_enabled != data.money_reveal_enabled { | ||||
|                 data.money_reveal_enabled = radar.money_reveal_enabled; | ||||
|  | ||||
|                 if let Err(e) = money_reveal.toggle(&mut ctx.process) { | ||||
|                     log::warn!("Failed to toggle money reveal: {}", e); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Bomb update | ||||
|         if (data.bomb_dropped && !last_bomb_dropped) || (data.bomb_planted && !last_bomb_planted) { | ||||
|             data.update_bomb(&mut ctx); | ||||
| @@ -79,7 +101,15 @@ pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_ | ||||
|          | ||||
|             pawns.push(data.local_pawn.into()); | ||||
|  | ||||
|             let prev_holder = data.bomb_holder; | ||||
|  | ||||
|             data.bomb_holder = ctx.get_c4_holder(pawns, data.entity_list.into(), &data); | ||||
|  | ||||
|             if data.bomb_holder.is_some() && prev_holder.is_none() { | ||||
|                 log::debug!("Bomb picked up by player"); | ||||
|                 data.bomb_dropped = false; | ||||
|             } | ||||
|  | ||||
|             data.recheck_bomb_holder = false; | ||||
|         } | ||||
|  | ||||
| @@ -138,18 +168,25 @@ pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_ | ||||
|  | ||||
|             // Bomb | ||||
|             if data.bomb_dropped || data.bomb_planted { | ||||
|                 let node = ctx.process.read_addr64( | ||||
|                 if let Ok(node) = ctx.process.read_addr64( | ||||
|                     data.bomb + cs2dumper::client::C_BaseEntity::m_pGameSceneNode as u64 | ||||
|                 ).unwrap(); | ||||
|                 let pos = ctx.process.read(node + cs2dumper::client::CGameSceneNode::m_vecAbsOrigin).unwrap(); | ||||
|      | ||||
|                 entity_data.push(EntityData::Bomb(BombData::new(pos, data.bomb_planted))); | ||||
|                 ) { | ||||
|                     if let Ok(pos) = ctx.process.read(node + cs2dumper::client::CGameSceneNode::m_vecAbsOrigin) { | ||||
|                         entity_data.push(EntityData::Bomb(BombData::new(pos, data.bomb_planted))); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Local player | ||||
|             let local_data = ctx.batched_player_read( | ||||
|             let local_data = match ctx.batched_player_read( | ||||
|                 data.local.into(), data.local_pawn.into() | ||||
|             ).unwrap(); | ||||
|             ) { | ||||
|                 Ok(data) => data, | ||||
|                 Err(e) => { | ||||
|                     log::warn!("Failed to read local player data: {}", e); | ||||
|                     continue; | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             if local_data.health > 0 { | ||||
|                 let has_bomb = match data.bomb_holder { | ||||
| @@ -165,7 +202,11 @@ pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_ | ||||
|                             PlayerType::Local, | ||||
|                             has_bomb, | ||||
|                             local_data.has_awp, | ||||
|                             local_data.is_scoped | ||||
|                             local_data.is_scoped, | ||||
|                             local_data.player_name, | ||||
|                             local_data.weapon_id, | ||||
|                             local_data.money, | ||||
|                             local_data.health | ||||
|                         ) | ||||
|                     ) | ||||
|                 ); | ||||
| @@ -173,39 +214,49 @@ pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_ | ||||
|  | ||||
|             // Other players | ||||
|             for (controller, pawn) in &data.players { | ||||
|                 let player_data = ctx.batched_player_read(*controller, *pawn).unwrap(); | ||||
|                 match ctx.batched_player_read(*controller, *pawn) { | ||||
|                     Ok(player_data) => { | ||||
|                         if player_data.health < 1 { | ||||
|                             continue; | ||||
|                         } | ||||
|  | ||||
|                 if player_data.health < 1 { | ||||
|                     continue; | ||||
|                 } | ||||
|                         let has_bomb = match data.bomb_holder { | ||||
|                             Some(bh) => *pawn == bh, | ||||
|                             None => false, | ||||
|                         }; | ||||
|  | ||||
|                 let has_bomb = match data.bomb_holder { | ||||
|                     Some(bh) => *pawn == bh, | ||||
|                     None => false, | ||||
|                 }; | ||||
|                         let player_type = { | ||||
|                             if local_data.team != player_data.team { | ||||
|                                 PlayerType::Enemy | ||||
|                             } else if local_data.team == player_data.team { | ||||
|                                 PlayerType::Team | ||||
|                             } else { | ||||
|                                 PlayerType::Unknown | ||||
|                             } | ||||
|                         }; | ||||
|  | ||||
|                 let player_type = { | ||||
|                     if local_data.team != player_data.team { | ||||
|                         PlayerType::Enemy | ||||
|                     } else if local_data.team == player_data.team { | ||||
|                         PlayerType::Team | ||||
|                     } else { | ||||
|                         PlayerType::Unknown | ||||
|                         entity_data.push( | ||||
|                             EntityData::Player( | ||||
|                                 PlayerData::new( | ||||
|                                     player_data.pos, | ||||
|                                     player_data.yaw, | ||||
|                                     player_type, | ||||
|                                     has_bomb, | ||||
|                                     player_data.has_awp, | ||||
|                                     player_data.is_scoped, | ||||
|                                     player_data.player_name, | ||||
|                                     player_data.weapon_id, | ||||
|                                     player_data.money, | ||||
|                                     player_data.health | ||||
|                                 ) | ||||
|                             ) | ||||
|                         ); | ||||
|                     }, | ||||
|                     Err(e) => { | ||||
|                         log::warn!("Failed to read player data: {}", e); | ||||
|                         continue; | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 entity_data.push( | ||||
|                     EntityData::Player( | ||||
|                         PlayerData::new( | ||||
|                             player_data.pos,  | ||||
|                             player_data.yaw, | ||||
|                             player_type, | ||||
|                             has_bomb, | ||||
|                             player_data.has_awp, | ||||
|                             player_data.is_scoped | ||||
|                         ) | ||||
|                     ) | ||||
|                 ); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             let mut radar = radar_data.write().await; | ||||
| @@ -222,9 +273,12 @@ pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_ | ||||
|                 data.bomb_defuse_length, | ||||
|                 bomb_defuse_end | ||||
|             ); | ||||
|  | ||||
|             radar.money_reveal_enabled = data.money_reveal_enabled; | ||||
|         } else { | ||||
|             let mut radar = radar_data.write().await; | ||||
|             *radar = RadarData::empty(freq); | ||||
|             radar.money_reveal_enabled = data.money_reveal_enabled; | ||||
|         } | ||||
|  | ||||
|         last_tick_count = data.tick_count; | ||||
| @@ -239,5 +293,10 @@ pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_ | ||||
|         thread::sleep(Duration::from_millis(1)); | ||||
|     } | ||||
|  | ||||
|     let cleanup_result = money_reveal.ensure_disabled(&mut ctx.process); | ||||
|     if let Err(e) = cleanup_result { | ||||
|         log::warn!("Failed to cleanup money reveal: {}", e); | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
| @@ -21,7 +21,7 @@ pub struct CsData { | ||||
|     // Common | ||||
|     pub local: u64, | ||||
|     pub local_pawn: u64, | ||||
|     pub is_dead: bool, | ||||
|     // pub is_dead: bool,   // TODO: Why is this here? | ||||
|     pub tick_count: i32, | ||||
|     pub freeze_period: bool, | ||||
|     pub round_start_count: u8, | ||||
| @@ -38,6 +38,7 @@ pub struct CsData { | ||||
|     pub bomb_defuse_length: f32, | ||||
|     pub bomb_exploded: bool, | ||||
|     pub bomb_defused: bool, | ||||
|     pub money_reveal_enabled: bool, | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -104,10 +105,12 @@ impl CsData { | ||||
|                 } | ||||
|             } | ||||
|         } else if self.bomb_planted { | ||||
|             let bomb = ctx.get_plantedc4() | ||||
|                 .expect("Failed to get planted bomb"); | ||||
|  | ||||
|             self.bomb = bomb; | ||||
|             match ctx.get_plantedc4() { | ||||
|                 Ok(bomb) => self.bomb = bomb, | ||||
|                 Err(e) => { | ||||
|                     log::warn!("Failed to get planted bomb: {}", e); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -169,7 +172,7 @@ impl CsData { | ||||
|         { | ||||
|             // Globals | ||||
|             let tick_count_addr = (self.globals + 0x40).into(); | ||||
|             let map_addr = (self.globals + 0x188).into(); | ||||
|             let map_addr = (self.globals + 384).into(); | ||||
|  | ||||
|             // Gamerules | ||||
|             let bomb_dropped_addr = (self.gamerules + cs2dumper::client::C_CSGameRules::m_bBombDropped as u64).into(); | ||||
| @@ -178,7 +181,7 @@ impl CsData { | ||||
|             let round_start_count_addr = (self.gamerules + cs2dumper::client::C_CSGameRules::m_nRoundStartCount as u64).into(); | ||||
|  | ||||
|             // Game Entity System | ||||
|             let highest_index_addr = (self.game_ent_sys + cs2dumper::offsets::client_dll::dwGameEntitySystem_getHighestEntityIndex as u64).into(); | ||||
|             let highest_index_addr = (self.game_ent_sys + cs2dumper::offsets::client_dll::dwGameEntitySystem_highestEntityIndex as u64).into(); | ||||
|  | ||||
|             let mut batcher = ctx.process.batcher(); | ||||
|             batcher.read_into( | ||||
| @@ -237,7 +240,7 @@ impl CsData { | ||||
|         } | ||||
|  | ||||
|  | ||||
|         let map_string = ctx.process.read_char_string_n(map_ptr.into(), 32).unwrap_or(String::from("<empty>")); | ||||
|         let map_string = ctx.process.read_utf8_lossy(map_ptr.into(), 32).unwrap_or(String::from("<empty>")); | ||||
|  | ||||
|         self.map = map_string; | ||||
|         self.bomb_dropped = bomb_dropped != 0; | ||||
|   | ||||
							
								
								
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -13,6 +13,9 @@ mod comms; | ||||
| mod dma; | ||||
| mod websocket; | ||||
|  | ||||
| mod pattern; | ||||
| mod money_reveal; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() -> anyhow::Result<()> { | ||||
|     let cli: Cli = Cli::parse(); | ||||
| @@ -29,6 +32,7 @@ async fn main() -> anyhow::Result<()> { | ||||
|     ); | ||||
|  | ||||
|     let radar_clone = radar_data.clone(); | ||||
|  | ||||
|     let dma_handle = tokio::spawn(async move { | ||||
|         if let Err(err) = dma::run(radar_clone, cli.connector, cli.pcileech_device, cli.skip_version).await { | ||||
|             log::error!("Error in dma thread: [{}]", err.to_string()); | ||||
| @@ -37,16 +41,19 @@ async fn main() -> anyhow::Result<()> { | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     let web_path = cli.web_path.clone(); | ||||
|     let port = cli.port; | ||||
|  | ||||
|     let _websocket_handle = tokio::spawn(async move { | ||||
|         if let Ok(my_local_ip) = local_ip_address::local_ip() { | ||||
|             let address = format!("http://{}:{}", my_local_ip, cli.port); | ||||
|             let address = format!("http://{}:{}", my_local_ip, port); | ||||
|             println!("Launched webserver at {}", address); | ||||
|         } else { | ||||
|             let address = format!("http://0.0.0.0:{}", cli.port); | ||||
|             let address = format!("http://0.0.0.0:{}", port); | ||||
|             println!("launched webserver at {}", address); | ||||
|         } | ||||
|  | ||||
|         if let Err(err) = websocket::run(cli.web_path, cli.port, radar_data).await { | ||||
|         if let Err(err) = websocket::run(web_path, port, radar_data).await { | ||||
|             log::error!("Error in ws server: [{}]", err.to_string()); | ||||
|         } | ||||
|     }); | ||||
|   | ||||
							
								
								
									
										96
									
								
								src/money_reveal.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/money_reveal.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| use memflow::{mem::MemoryView, types::Address, os::ModuleInfo}; | ||||
| use crate::pattern::pattern_scan; | ||||
|  | ||||
| const BUF_SIZE: usize = 3; | ||||
|  | ||||
| pub struct MoneyReveal { | ||||
|     pub is_enabled: bool, | ||||
|     pub address: Option<Address>, | ||||
|     original_bytes: Option<[u8; BUF_SIZE]>, | ||||
| } | ||||
|  | ||||
| impl MoneyReveal { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             is_enabled: false, | ||||
|             address: None, | ||||
|             original_bytes: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn init(&mut self, mem: &mut impl MemoryView, client_module: &ModuleInfo) -> anyhow::Result<()> { | ||||
|         self.address = self.find_function(mem, client_module)?; | ||||
|         log::info!("Money reveal function found at: {:?}", self.address); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub fn toggle(&mut self, mem: &mut impl MemoryView) -> anyhow::Result<bool> { | ||||
|         if let Some(addr) = self.address { | ||||
|             if self.is_enabled { | ||||
|                 if let Some(original) = self.original_bytes { | ||||
|                     self.restore(mem, addr, original)?; | ||||
|                     self.original_bytes = None; | ||||
|                     self.is_enabled = false; | ||||
|                 } | ||||
|             } else { | ||||
|                 let original = self.patch(mem, addr)?; | ||||
|                 self.original_bytes = Some(original); | ||||
|                 self.is_enabled = true; | ||||
|             } | ||||
|             Ok(self.is_enabled) | ||||
|         } else { | ||||
|             Err(anyhow::anyhow!("Money reveal not initialized")) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn ensure_disabled(&mut self, mem: &mut impl MemoryView) -> anyhow::Result<()> { | ||||
|         if self.is_enabled { | ||||
|             if let Some(addr) = self.address { | ||||
|                 if let Some(original) = self.original_bytes { | ||||
|                     self.restore(mem, addr, original)?; | ||||
|                     self.is_enabled = false; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     fn find_function(&self, mem: &mut impl MemoryView, module: &ModuleInfo) -> anyhow::Result<Option<Address>> { | ||||
|         let is_hltv = pattern_scan( | ||||
|             mem, | ||||
|             module, | ||||
|             "48 83 EC 28 48 8B 0D ?? ?? ?? ?? 48 8B 01 FF 90 ?? ?? ?? ?? 84 C0 75 0D" | ||||
|         )?; | ||||
|  | ||||
|         if is_hltv.is_none() { | ||||
|             Ok(pattern_scan( | ||||
|                 mem, | ||||
|                 module, | ||||
|                 "B0 01 C3 28 48 8B 0D ?? ?? ?? ?? 48 8B 01 FF 90 ?? ?? ?? ?? 84 C0 75 0D" | ||||
|             )?) | ||||
|         } else { | ||||
|             Ok(is_hltv) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn patch(&self, mem: &mut impl MemoryView, location: Address) -> anyhow::Result<[u8; BUF_SIZE]> { | ||||
|         let mut original_buf = [0u8; BUF_SIZE]; | ||||
|         mem.read_into(location, &mut original_buf)?; | ||||
|  | ||||
|         let new_buf: [u8; BUF_SIZE] = [ | ||||
|             0xB0, 0x01,     // MOV AL,1 | ||||
|             0xC3            // RET | ||||
|         ]; | ||||
|  | ||||
|         log::debug!("Patching memory for money reveal"); | ||||
|         mem.write(location, &new_buf)?; | ||||
|  | ||||
|         Ok(original_buf) | ||||
|     } | ||||
|  | ||||
|     fn restore(&self, mem: &mut impl MemoryView, location: Address, original: [u8; BUF_SIZE]) -> anyhow::Result<()> { | ||||
|         log::debug!("Restoring memory for money reveal"); | ||||
|         mem.write(location, &original)?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										51
									
								
								src/pattern.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/pattern.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| use std::io::Read; | ||||
| use log::warn; | ||||
| use memflow::{os::ModuleInfo, mem::MemoryView, types::Address}; | ||||
|  | ||||
| fn pattern_to_bytes(pattern: String) -> Vec<Option<u8>> { | ||||
|     pattern.split(' ') | ||||
|         .fold(Vec::new(), |mut accum, str| { | ||||
|             if str == "??" { | ||||
|                 accum.push(None); | ||||
|             } else { | ||||
|                 let byte = u8::from_str_radix(str, 16).unwrap(); | ||||
|                 accum.push(Some(byte)); | ||||
|             } | ||||
|  | ||||
|             accum | ||||
|         }) | ||||
| } | ||||
|  | ||||
| pub fn pattern_scan(mem: &mut impl MemoryView, module: &ModuleInfo, sig: &str) -> anyhow::Result<Option<Address>> { | ||||
|     let pattern_bytes = pattern_to_bytes(sig.to_owned()); | ||||
|     let mut cursor = mem.cursor(); | ||||
|  | ||||
|     log::debug!("Searching \"{}\" for pattern: \"{}\"", module.name, sig); | ||||
|  | ||||
|     for i in 0..module.size { | ||||
|         let mut found = true; | ||||
|         cursor.set_address(module.base + i); | ||||
|  | ||||
|         let mut buf = vec![0u8; pattern_bytes.len()]; | ||||
|  | ||||
|         if let Err(_) = cursor.read(&mut buf) { | ||||
|             warn!("Encountered read error while scanning for pattern"); | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         for (idx, byte) in buf.iter().enumerate() { | ||||
|             if let Some(pat_byte) = pattern_bytes[idx] { | ||||
|                 if *byte != pat_byte { | ||||
|                     found = false; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if found { | ||||
|             return Ok(Some(module.base + i)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(None) | ||||
| } | ||||
							
								
								
									
										142
									
								
								src/websocket.rs
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								src/websocket.rs
									
									
									
									
									
								
							| @@ -1,20 +1,27 @@ | ||||
| use std::{sync::Arc, path::PathBuf}; | ||||
|  | ||||
| use std::{sync::Arc, path::PathBuf, collections::HashMap}; | ||||
| use axum::{ | ||||
|     extract::{ws::{WebSocketUpgrade, WebSocket, Message}, State}, | ||||
|     response::Response, | ||||
|     routing::get, | ||||
|     Router, | ||||
| }; | ||||
|  | ||||
| use tokio::sync::RwLock; | ||||
| use flate2::{write::GzEncoder, Compression}; | ||||
| use std::io::Write; | ||||
| use tokio::sync::{RwLock, Mutex}; | ||||
| use tower_http::services::ServeDir; | ||||
|  | ||||
| use crate::comms::RadarData; | ||||
| use crate::comms::{RadarData}; | ||||
|  | ||||
| struct ClientState { | ||||
|     last_entity_count: usize, | ||||
|     ping_ms: u32, | ||||
|     high_latency: bool, | ||||
| } | ||||
|  | ||||
| #[derive(Clone)] | ||||
| struct AppState { | ||||
|     data_lock: Arc<RwLock<RadarData>> | ||||
|     data_lock: Arc<RwLock<RadarData>>, | ||||
|     clients: Arc<Mutex<HashMap<String, ClientState>>>, | ||||
| } | ||||
|  | ||||
| async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response { | ||||
| @@ -23,43 +30,128 @@ async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Resp | ||||
| } | ||||
|  | ||||
| async fn handle_socket(mut socket: WebSocket, state: AppState) { | ||||
|     let client_id = uuid::Uuid::new_v4().to_string(); | ||||
|  | ||||
|     { | ||||
|         let mut clients = state.clients.lock().await; | ||||
|         clients.insert(client_id.clone(), ClientState { | ||||
|             last_entity_count: 0, | ||||
|             ping_ms: 0, | ||||
|             high_latency: false, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     let mut compression_buffer: Vec<u8> = Vec::with_capacity(65536); | ||||
|     let mut frame_counter = 0; | ||||
|     let mut skip_frames = false; | ||||
|  | ||||
|     while let Some(msg) = socket.recv().await { | ||||
|         if let Ok(msg) = msg { | ||||
|             if msg == Message::Text("requestInfo".to_string()) { | ||||
|                 let str = { | ||||
|                     let data = state.data_lock.read().await; | ||||
|  | ||||
|                     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 let Ok(text) = msg.to_text() { | ||||
|                 if text == "requestInfo" { | ||||
|                     frame_counter += 1; | ||||
|                     if skip_frames && frame_counter % 2 != 0 { | ||||
|                         continue; | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 //println!("{str}"); | ||||
|                     let radar_data = state.data_lock.read().await; | ||||
|                     let mut clients = state.clients.lock().await; | ||||
|                     let client_state = clients.get_mut(&client_id).unwrap(); | ||||
|  | ||||
|                 if socket.send(Message::Text(str)).await.is_err() { | ||||
|                     // client disconnected | ||||
|                     return; | ||||
|                     let entity_count = radar_data.get_entities().len(); | ||||
|  | ||||
|                     if entity_count > 5 && !skip_frames && client_state.ping_ms > 100 { | ||||
|                         skip_frames = true; | ||||
|                         log::info!("Enabling frame skipping for high latency client"); | ||||
|                     } | ||||
|  | ||||
|                     client_state.last_entity_count = entity_count; | ||||
|  | ||||
|                     let Ok(json) = serde_json::to_string(&*radar_data) else { | ||||
|                         continue; | ||||
|                     }; | ||||
|  | ||||
|                     compression_buffer.clear(); | ||||
|  | ||||
|                     let compression_level = if json.len() > 20000 || client_state.high_latency { | ||||
|                         Compression::best() | ||||
|                     } else if json.len() > 5000 { | ||||
|                         Compression::default() | ||||
|                     } else { | ||||
|                         Compression::fast() | ||||
|                     }; | ||||
|  | ||||
|                     let mut encoder = GzEncoder::new(Vec::new(), compression_level); | ||||
|                     if encoder.write_all(json.as_bytes()).is_ok() { | ||||
|                         match encoder.finish() { | ||||
|                             Ok(compressed) => { | ||||
|                                 if compressed.len() < json.len() { | ||||
|                                     let mut message = vec![0x01]; | ||||
|                                     message.extend_from_slice(&compressed); | ||||
|                                     let _ = socket.send(Message::Binary(message)).await; | ||||
|                                 } else { | ||||
|                                     let mut uncompressed = vec![0x00]; | ||||
|                                     uncompressed.extend_from_slice(json.as_bytes()); | ||||
|                                     let _ = socket.send(Message::Binary(uncompressed)).await; | ||||
|                                 } | ||||
|                             }, | ||||
|                             Err(_) => { | ||||
|                                 let mut uncompressed = vec![0x00]; | ||||
|                                 uncompressed.extend_from_slice(json.as_bytes()); | ||||
|                                 let _ = socket.send(Message::Binary(uncompressed)).await; | ||||
|                             } | ||||
|                         } | ||||
|                     } else { | ||||
|                         let mut uncompressed = vec![0x00]; | ||||
|                         uncompressed.extend_from_slice(json.as_bytes()); | ||||
|                         let _ = socket.send(Message::Binary(uncompressed)).await; | ||||
|                     } | ||||
|                 } else if text == "toggleMoneyReveal" { | ||||
|                     let new_value = { | ||||
|                         let mut data = state.data_lock.write().await; | ||||
|                         data.money_reveal_enabled = !data.money_reveal_enabled; | ||||
|                         data.money_reveal_enabled | ||||
|                     }; | ||||
|  | ||||
|                     let response = serde_json::json!({ | ||||
|                         "action": "toggleMoneyReveal", | ||||
|                         "status": "success", | ||||
|                         "enabled": new_value | ||||
|                     }); | ||||
|  | ||||
|                     let _ = socket.send(Message::Text(response.to_string())).await; | ||||
|                 } else if text.starts_with("ping:") { | ||||
|                     if let Some(ping_str) = text.strip_prefix("ping:") { | ||||
|                         if let Ok(ping_ms) = ping_str.parse::<u32>() { | ||||
|                             let mut clients = state.clients.lock().await; | ||||
|                             if let Some(client) = clients.get_mut(&client_id) { | ||||
|                                 client.ping_ms = ping_ms; | ||||
|                                 client.high_latency = ping_ms > 100; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     let _ = socket.send(Message::Text("pong".to_string())).await; | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             // client disconnected | ||||
|             return; | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
|     let mut clients = state.clients.lock().await; | ||||
|     clients.remove(&client_id); | ||||
| } | ||||
|  | ||||
| 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 }); | ||||
|         .with_state(AppState { | ||||
|             data_lock, | ||||
|             clients: Arc::new(Mutex::new(HashMap::new())) | ||||
|         }); | ||||
|  | ||||
|     let address = format!("0.0.0.0:{}", port); | ||||
|     log::info!("Starting WebSocket server on {}", address); | ||||
|     let listener = tokio::net::TcpListener::bind(address).await?; | ||||
|     axum::serve(listener, app.into_make_service()) | ||||
|         .await?; | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								webradar/assets/image/background.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								webradar/assets/image/background.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 6.7 MiB | 
							
								
								
									
										
											BIN
										
									
								
								webradar/assets/image/de_train_radar_psd.png
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								webradar/assets/image/de_train_radar_psd.png
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 126 KiB | 
							
								
								
									
										7
									
								
								webradar/assets/json/de_train.json
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								webradar/assets/json/de_train.json
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "pos_x": -2308.0, | ||||
|   "pos_y": 2078.0, | ||||
|   "scale": 4.082077, | ||||
|   "rotate": 0, | ||||
|   "zoom": 0.0 | ||||
| } | ||||
| @@ -1,27 +1,107 @@ | ||||
| <!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 href="styles.css" rel="stylesheet" type="text/css"/> | ||||
|     <title>radar</title> | ||||
|     <link href="styles.css" rel="stylesheet" type="text/css" /> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|     <div id="canvasContainer"> | ||||
|         <button id="showMenuBtn" onclick="toggleMenu(true)" style="display: none;">Show Menu</button> | ||||
|         <div id="settingsHolder"> | ||||
|             <div class="settings"> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleZoom()" id="zoomCheck" name="zoom"/> | ||||
|                     <input type="checkbox" onclick="togglePerformanceMode()" id="performanceMode" /> | ||||
|                     <label for="performanceMode">Performance Mode</label> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleZoom()" id="zoomCheck" name="zoom" /> | ||||
|                     <label for="zoomCheck">Zoom</label> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleStats()" id="statsCheck" name="stats"/> | ||||
|                     <input type="checkbox" onclick="toggleStats()" id="statsCheck" name="stats" /> | ||||
|                     <label for="statsCheck">Stats</label> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleNames()" id="namesCheck" name="names" /> | ||||
|                     <label for="namesCheck">Player Names</label> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleGuns()" id="gunsCheck" name="guns" /> | ||||
|                     <label for="gunsCheck">Weapons</label> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleDisplayMoney()" id="moneyDisplay" name="money-display" | ||||
|                         checked /> | ||||
|                     <label for="moneyDisplay">Display Money</label> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleHealth()" id="healthCheck" name="health" checked /> | ||||
|                     <label for="healthCheck">Display Health</label> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleOffscreenIndicators()" id="offscreenCheck" name="offscreen" checked /> | ||||
|                     <label for="offscreenCheck">Off-screen Indicators</label> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleRotate()" id="rotateCheck" name="rotate" checked /> | ||||
|                     <label for="rotateCheck">Rotate Map</label> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <input type="checkbox" onclick="toggleCentered()" id="centerCheck" name="center" checked /> | ||||
|                     <label for="centerCheck">Player Centered</label> | ||||
|                     <div id="zoomLevelContainer" style="margin-top: 5px; margin-left: 20px; display: none;"> | ||||
|                         <label for="zoomLevelSlider">Zoom Level: </label> | ||||
|                         <span id="zoomLevelValue">1.0</span><br> | ||||
|                         <input type="range" id="zoomLevelSlider" min="1.0" max="5.0" step="0.1" value="1.0" | ||||
|                             style="width: 100%; margin: 5px 0;" oninput="updateZoomLevel(this.value)"> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="player-focus"> | ||||
|                     <label for="playerSelect">Focus Player:</label> | ||||
|                     <select id="playerSelect" onchange="changePlayerFocus()"> | ||||
|                         <option value="local">YOU</option> | ||||
|                     </select> | ||||
|                 </div> | ||||
|                 <div id="sizeControlsContainer" | ||||
|                     style="margin-top: 10px; padding: 5px; border-top: 1px solid rgba(255, 255, 255, 0.2);"> | ||||
|                     <div class="size-control" style="margin-bottom: 8px;"> | ||||
|                         <label for="textSizeSlider">Text Size: </label> | ||||
|                         <span id="textSizeValue">1.0</span><br> | ||||
|                         <input type="range" id="textSizeSlider" min="0.1" max="2.0" step="0.1" value="1.0" | ||||
|                             style="width: 100%; margin: 5px 0;" oninput="updateTextSize(this.value)"> | ||||
|                     </div> | ||||
|                     <div class="size-control"> | ||||
|                         <label for="entitySizeSlider">Player Size: </label> | ||||
|                         <span id="entitySizeValue">1.0</span><br> | ||||
|                         <input type="range" id="entitySizeSlider" min="0.5" max="2.0" step="0.1" value="1.0" | ||||
|                             style="width: 100%; margin: 5px 0;" oninput="updateEntitySize(this.value)"> | ||||
|                     </div> | ||||
|                     <div class="size-control" style="margin-top: 8px;"> | ||||
|                         <label for="healthBarSizeSlider">Health Bar Size: </label> | ||||
|                         <span id="healthBarSizeValue">1.0</span><br> | ||||
|                         <input type="range" id="healthBarSizeSlider" min="0.5" max="2.5" step="0.1" value="1.0" | ||||
|                             style="width: 100%; margin: 5px 0;" oninput="updateHealthBarSize(this.value)"> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <button id="showDangerousBtn" onclick="toggleDangerousOptions()">Show Dangerous Options</button> | ||||
|                 <div class="dangerous-options" id="dangerousOptions"> | ||||
|                     <div> | ||||
|                         <input type="checkbox" onclick="toggleMoneyReveal()" id="moneyReveal" name="money" /> | ||||
|                         <label for="moneyReveal">Money Reveal (Write Memory)</label> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <button id="hideMenuBtn" onclick="toggleMenu(false)">Hide Menu</button> | ||||
|             </div> | ||||
|         </div> | ||||
|         <canvas id="canvas"></canvas> | ||||
|     </div> | ||||
|     <script src="script.js"></script> | ||||
|     <script src="webstuff.js"></script> | ||||
| </body> | ||||
|  | ||||
| </html> | ||||
							
								
								
									
										1970
									
								
								webradar/script.js
									
									
									
									
									
								
							
							
						
						
									
										1970
									
								
								webradar/script.js
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -5,7 +5,10 @@ body { | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     height: 100vh; | ||||
|     background-color: #000000; /* Change the background color as needed */ | ||||
|     background-color: #000000; | ||||
|     background-image: url('assets/image/background.png'); | ||||
|     background-repeat: repeat; | ||||
|     background-size: 128px 128px; | ||||
| } | ||||
|  | ||||
| #canvasContainer { | ||||
| @@ -20,15 +23,51 @@ body { | ||||
| canvas { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     image-rendering: -webkit-optimize-contrast; | ||||
|     image-rendering: crisp-edges; | ||||
| } | ||||
|  | ||||
| #settingsHolder { | ||||
|     visibility: hidden; | ||||
|     position: absolute; | ||||
|     top: inherit; | ||||
|     left: inherit; | ||||
|     width: inherit; | ||||
|     height: 20%; | ||||
|     visibility: visible; | ||||
|     position: fixed; | ||||
|     top: 50%; | ||||
|     left: 0; | ||||
|     transform: translateY(-50%); | ||||
|     width: auto; | ||||
|     height: auto; | ||||
|     z-index: 100; | ||||
| } | ||||
|  | ||||
| #settingsHolder .settings { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||
|     color: white; | ||||
|     visibility: visible; | ||||
|     opacity: 0.8; | ||||
|     padding: 10px; | ||||
|     background-color: rgba(25, 25, 25, 0.7); | ||||
|     border-radius: 5px; | ||||
|     transition: opacity 0.3s ease; | ||||
|     font-size: 14px; | ||||
|     max-height: 90vh; | ||||
|     overflow-y: auto; | ||||
| } | ||||
|  | ||||
| @media (max-width: 768px) { | ||||
|     #settingsHolder .settings { | ||||
|         font-size: 16px; | ||||
|         padding: 12px; | ||||
|     } | ||||
|  | ||||
|     #settingsHolder .settings input[type="checkbox"] { | ||||
|         transform: scale(1.2); | ||||
|         margin-right: 8px; | ||||
|     } | ||||
|  | ||||
|     #settingsHolder .settings>div { | ||||
|         padding: 6px 0; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #settingsHolder:hover .settings { | ||||
| @@ -38,12 +77,219 @@ canvas { | ||||
| .settings { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|  | ||||
|     font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||
|     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||
|     color: white; | ||||
|     visibility: visible; | ||||
|     opacity: 0; | ||||
|     padding: 10px; | ||||
|     background-color: rgba(25, 25, 25, 0.7); /* Semi-transparent white background */ | ||||
|     transition: opacity 0.3s ease; /* Smooth transition */ | ||||
|     background-color: rgba(25, 25, 25, 0.7); | ||||
|     transition: opacity 0.3s ease; | ||||
| } | ||||
|  | ||||
| .dangerous-options { | ||||
|     display: none; | ||||
|     border-top: 1px solid rgba(255, 0, 0, 0.5); | ||||
|     margin-top: 10px; | ||||
|     padding-top: 10px; | ||||
| } | ||||
|  | ||||
| .dangerous-options.revealed { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| #showDangerousBtn { | ||||
|     background-color: #6b0000; | ||||
|     color: white; | ||||
|     border: none; | ||||
|     padding: 5px 10px; | ||||
|     margin-top: 10px; | ||||
|     border-radius: 3px; | ||||
|     cursor: pointer; | ||||
|     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||
|     transition: background-color 0.3s; | ||||
| } | ||||
|  | ||||
| #showDangerousBtn:hover { | ||||
|     background-color: #8a0000; | ||||
| } | ||||
|  | ||||
| .size-control { | ||||
|     margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| .size-control label { | ||||
|     display: inline-block; | ||||
|     margin-bottom: 3px; | ||||
| } | ||||
|  | ||||
| input[type="range"] { | ||||
|     width: 100%; | ||||
|     margin: 5px 0; | ||||
|     -webkit-appearance: none; | ||||
|     height: 6px; | ||||
|     background: #555; | ||||
|     border-radius: 3px; | ||||
|     outline: none; | ||||
| } | ||||
|  | ||||
| input[type="range"]::-webkit-slider-thumb { | ||||
|     -webkit-appearance: none; | ||||
|     appearance: none; | ||||
|     width: 16px; | ||||
|     height: 16px; | ||||
|     border-radius: 50%; | ||||
|     background: #68a3e5; | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| input[type="range"]::-moz-range-thumb { | ||||
|     width: 16px; | ||||
|     height: 16px; | ||||
|     border-radius: 50%; | ||||
|     background: #68a3e5; | ||||
|     cursor: pointer; | ||||
|     border: none; | ||||
| } | ||||
|  | ||||
| @media (max-width: 600px), | ||||
| (max-height: 600px) { | ||||
|     #settingsHolder { | ||||
|         display: none; | ||||
|     } | ||||
|  | ||||
|     #showMenuBtn { | ||||
|         display: block !important; | ||||
|         font-size: 16px !important; | ||||
|         padding: 8px 12px !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @media (max-width: 400px), | ||||
| (max-height: 400px) { | ||||
|     #canvasContainer::before { | ||||
|         content: ''; | ||||
|         display: none; | ||||
|     } | ||||
|  | ||||
|     #showMenuBtn { | ||||
|         padding: 6px 10px !important; | ||||
|         font-size: 14px !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
| #playerSelect { | ||||
|     background-color: #333; | ||||
|     color: #fff; | ||||
|     border: 1px solid #555; | ||||
|     padding: 5px; | ||||
|     border-radius: 4px; | ||||
|     margin-left: 5px; | ||||
|     cursor: pointer; | ||||
|     min-width: 150px; | ||||
|     font-size: inherit; | ||||
| } | ||||
|  | ||||
| #playerSelect:hover { | ||||
|     background-color: #444; | ||||
| } | ||||
|  | ||||
| #playerSelect option { | ||||
|     background-color: #333; | ||||
|     color: #fff; | ||||
|     padding: 5px; | ||||
| } | ||||
|  | ||||
| #playerSelect option:hover { | ||||
|     background-color: #444; | ||||
| } | ||||
|  | ||||
| .player-focus { | ||||
|     margin-top: 10px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     flex-wrap: wrap; | ||||
| } | ||||
|  | ||||
| #hideMenuBtn { | ||||
|     background-color: #333; | ||||
|     color: white; | ||||
|     border: none; | ||||
|     padding: 8px 10px; | ||||
|     margin-top: 10px; | ||||
|     border-radius: 3px; | ||||
|     cursor: pointer; | ||||
|     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||
|     transition: background-color 0.3s; | ||||
|     width: 100%; | ||||
|     font-size: inherit; | ||||
| } | ||||
|  | ||||
| #hideMenuBtn:hover { | ||||
|     background-color: #444; | ||||
| } | ||||
|  | ||||
| #showMenuBtn { | ||||
|     position: fixed; | ||||
|     top: 10px; | ||||
|     left: 10px; | ||||
|     background-color: rgba(25, 25, 25, 0.9); | ||||
|     color: white; | ||||
|     border: none; | ||||
|     padding: 8px 12px; | ||||
|     border-radius: 15px; | ||||
|     font-size: 16px; | ||||
|     cursor: pointer; | ||||
|     z-index: 101; | ||||
|     font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||
|     transition: opacity 0.3s; | ||||
|     opacity: 0.8; | ||||
|     box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); | ||||
| } | ||||
|  | ||||
| #showMenuBtn:hover { | ||||
|     opacity: 1; | ||||
| } | ||||
|  | ||||
| input[type="checkbox"] { | ||||
|     margin-right: 8px; | ||||
| } | ||||
|  | ||||
| label { | ||||
|     cursor: pointer; | ||||
|     user-select: none; | ||||
|     margin-bottom: 2px; | ||||
| } | ||||
|  | ||||
| .settings>div { | ||||
|     margin-bottom: 5px; | ||||
|     padding: 3px 0; | ||||
| } | ||||
|  | ||||
| .touch-device input[type="checkbox"] { | ||||
|     transform: scale(1.3); | ||||
|     margin: 2px 10px 2px 2px; | ||||
| } | ||||
|  | ||||
| .touch-device input[type="range"]::-webkit-slider-thumb { | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
| } | ||||
|  | ||||
| .touch-device input[type="range"]::-moz-range-thumb { | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
| } | ||||
|  | ||||
| .touch-device .settings>div { | ||||
|     padding: 6px 0; | ||||
| } | ||||
|  | ||||
| @media (prefers-color-scheme: dark) { | ||||
|     #settingsHolder .settings { | ||||
|         background-color: rgba(15, 15, 15, 0.85); | ||||
|     } | ||||
|  | ||||
|     #showMenuBtn { | ||||
|         background-color: rgba(15, 15, 15, 0.9); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										25
									
								
								webradar/webstuff.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								webradar/webstuff.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| function toggleDangerousOptions() { | ||||
|     const dangerousSection = document.getElementById('dangerousOptions'); | ||||
|     const button = document.getElementById('showDangerousBtn'); | ||||
|  | ||||
|     if (dangerousSection.classList.contains('revealed')) { | ||||
|         dangerousSection.classList.remove('revealed'); | ||||
|         button.textContent = 'Show Dangerous Options'; | ||||
|     } else { | ||||
|         dangerousSection.classList.add('revealed'); | ||||
|         button.textContent = 'Hide Dangerous Options'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function toggleMenu(show) { | ||||
|     const settingsHolder = document.getElementById('settingsHolder'); | ||||
|     const showMenuBtn = document.getElementById('showMenuBtn'); | ||||
|  | ||||
|     if (show) { | ||||
|         settingsHolder.style.display = 'block'; | ||||
|         showMenuBtn.style.display = 'none'; | ||||
|     } else { | ||||
|         settingsHolder.style.display = 'none'; | ||||
|         showMenuBtn.style.display = 'block'; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user