Full rewrite

- Removed csflow, as its basically not getting used when high optimization is needed
- Fully rewrote radarflow dma logic.
- Speed increase from 20hz to over 130 hz over pcileech, thanks to scatter reads and improved caching
- Removed docs, because those were for csflow, which is now removed
This commit is contained in:
Janek
2024-01-08 00:22:24 +01:00
parent f186b19255
commit 16f7791628
233 changed files with 805 additions and 16929 deletions

95
src/cli.rs Executable file
View File

@@ -0,0 +1,95 @@
use std::path::PathBuf;
use clap::{Parser, ValueEnum};
use memflow::plugins::Inventory;
use crate::dma::Connector;
const PORT_RANGE: std::ops::RangeInclusive<usize> = 8000..=65535;
#[derive(Parser)]
#[command(author, version = version(), about, long_about = None)]
pub struct Cli {
/// Specifies the connector type for DMA
#[clap(value_enum, short, long, ignore_case = true, default_value_t = Connector::Qemu)]
pub connector: Connector,
/// Name of the Pcileech device
#[clap(long, default_value_t = String::from("FPGA"))]
pub pcileech_device: String,
/// Port number for the Webserver to run on
#[arg(short, long, default_value_t = 8000, value_parser = port_in_range)]
pub port: u16,
/// Path to the directory served by the Webserver
#[arg(short, long, default_value = "./webradar", value_parser = valid_path)]
pub web_path: PathBuf,
/// Verbosity level for logging to the console
#[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)
}
/// 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,
}
}
}

75
src/comms.rs Executable file
View File

@@ -0,0 +1,75 @@
use serde::{Serialize, Deserialize};
use crate::{structs::Vec3, enums::PlayerType};
#[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
}
#[allow(dead_code)]
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)]
pub struct RadarData {
freq: usize,
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>, freq: usize) -> RadarData {
RadarData { ingame, map_name, player_data, freq }
}
/// Returns empty RadarData, it's also the same data that gets sent to clients when not ingame
pub fn empty(freq: usize) -> RadarData {
RadarData {
ingame: false,
map_name: String::new(),
player_data: Vec::new(),
freq
}
}
}
unsafe impl Send for RadarData {}
pub type ArcRwlockRadarData = std::sync::Arc<tokio::sync::RwLock<RadarData>>;

17
src/dma/context/connector.rs Executable file
View File

@@ -0,0 +1,17 @@
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::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"),
}
}
}

182
src/dma/context/mod.rs Executable file
View File

@@ -0,0 +1,182 @@
use memflow::prelude::v1::*;
mod connector;
pub use connector::Connector;
use num_traits::FromPrimitive;
use crate::{structs::Vec3, enums::TeamID};
use super::cs2dumper;
pub struct DmaCtx {
pub process: IntoProcessInstanceArcBox<'static>,
pub client_module: ModuleInfo,
pub engine_module: ModuleInfo,
}
impl DmaCtx {
fn check_version(&mut self) -> anyhow::Result<()> {
let game_build_number: u32 = self.process.read(self.engine_module.base + cs2dumper::offsets::engine2_dll::dwBuildNumber)?;
let offset_build_number = cs2dumper::offsets::game_info::buildNumber;
if game_build_number as usize != offset_build_number {
return Err(anyhow::anyhow!(
"game build is {}, but offsets are for {}",
game_build_number,
offset_build_number
));
}
Ok(())
}
pub fn setup(connector: Connector, pcileech_device: String) -> anyhow::Result<DmaCtx> {
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 mut ctx = Self {
process,
client_module,
engine_module,
};
ctx.check_version()?;
Ok(ctx)
}
pub fn pawn_from_controller(&mut self, controller: Address, entity_list: Address) -> anyhow::Result<Option<Address>> {
let uhandle: u32 = self.process.read(controller + cs2dumper::client::CCSPlayerController::m_hPlayerPawn)?;
let list_entry = self.process.read_addr64(entity_list + 0x8 * ((uhandle & 0x7FFF) >> 9) + 16)?;
if list_entry.is_null() || !list_entry.is_valid() {
Ok(None)
} else {
let ptr = self.process.read_addr64(list_entry + 120 * (uhandle & 0x1FF))?;
Ok(Some(ptr))
}
//super::CPlayerPawn::from_uhandle(ctx, entity_list, uhandle)
}
pub fn batched_player_read(&mut self, controller: Address, pawn: Address) -> anyhow::Result<BatchedPlayerData> {
let mut pos = Vec3::default();
let mut yaw = 0f32;
let mut health = 0u32;
let mut team = 0i32;
{
let mut batcher = MemoryViewBatcher::new(&mut self.process);
batcher.read_into(pawn + cs2dumper::client::C_BasePlayerPawn::m_vOldOrigin, &mut pos);
batcher.read_into(pawn + cs2dumper::client::C_CSPlayerPawnBase::m_angEyeAngles + 4, &mut yaw);
batcher.read_into(pawn + cs2dumper::client::C_BaseEntity::m_iHealth, &mut health);
batcher.read_into(controller + cs2dumper::client::C_BaseEntity::m_iTeamNum, &mut team);
}
let team = TeamID::from_i32(team);
Ok(BatchedPlayerData {
pos,
yaw,
team,
health
})
}
pub fn get_plantedc4(&mut self) -> anyhow::Result<Address> {
let ptr = self.process.read_addr64(self.client_module.base + cs2dumper::offsets::client_dll::dwPlantedC4)?;
let ptr2 = self.process.read_addr64(ptr)?;
Ok(ptr2)
}
/// Professionally engineered function to quickly check if the entity has class name "weapon_c4"
pub fn is_dropped_c4(&mut self, entity_ptr: Address) -> anyhow::Result<bool> {
let entity_identity_ptr = self.process.read_addr64(entity_ptr + cs2dumper::client::CEntityInstance::m_pEntity)?;
let class_name_ptr = self.process.read_addr64(entity_identity_ptr + cs2dumper::client::CEntityIdentity::m_designerName)?;
let data = self.process.read_raw(class_name_ptr + 7, 2)?;
let is_c4 = data == "c4".as_bytes();
Ok(is_c4)
}
/// Professionally engineered function to quickly check if the entity has class name "cs_player_controller"
pub fn is_cs_player_controller(&mut self, entity_ptr: Address) -> anyhow::Result<bool> {
let entity_identity_ptr = self.process.read_addr64(entity_ptr + cs2dumper::client::CEntityInstance::m_pEntity)?;
let class_name_ptr = self.process.read_addr64(entity_identity_ptr + cs2dumper::client::CEntityIdentity::m_designerName)?;
let data = self.process.read_raw(class_name_ptr, 20)?;
let is_controller = data == "cs_player_controller".as_bytes();
Ok(is_controller)
}
// Todo: Optimize this function: find another way to do this
pub fn has_c4(&mut self, pawn: Address, entity_list: Address) -> anyhow::Result<bool> {
let mut has_c4 = false;
let wep_services = self.process.read_addr64(pawn + cs2dumper::client::C_BasePlayerPawn::m_pWeaponServices)?;
let wep_count: i32 = self.process.read(wep_services + cs2dumper::client::CPlayer_WeaponServices::m_hMyWeapons)?;
let wep_base = self.process.read_addr64(wep_services + cs2dumper::client::CPlayer_WeaponServices::m_hMyWeapons + 0x8)?;
for wep_idx in 0..wep_count {
let handle: i32 = self.process.read(wep_base + wep_idx * 0x4)?;
if handle == -1 {
continue;
}
let list_entry = self.process.read_addr64(entity_list + 0x8 * ((handle & 0x7FFF) >> 9) + 16)?;
if let Some(wep_ptr) = {
if list_entry.is_null() || !list_entry.is_valid() {
None
} else {
let ptr = self.process.read_addr64(list_entry + 120 * (handle & 0x1FF))?;
Some(ptr)
}
} {
let wep_data = self.process.read_addr64(wep_ptr + cs2dumper::client::C_BaseEntity::m_nSubclassID + 0x8)?;
let id: i32 = self.process.read(wep_data + cs2dumper::client::CCSWeaponBaseVData::m_WeaponType)?;
if id == 7 {
has_c4 = true;
break;
}
}
}
Ok(has_c4)
}
}
pub struct BatchedPlayerData {
pub pos: Vec3,
pub yaw: f32,
pub team: Option<TeamID>,
pub health: u32,
}

4
src/dma/cs2dumper/mod.rs Executable file
View File

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

190
src/dma/mod.rs Executable file
View File

@@ -0,0 +1,190 @@
use std::{thread, time::{Duration, Instant}};
use memflow::{os::Process, types::Address, mem::MemoryView};
use crate::{enums::{TeamID, PlayerType}, comms::{EntityData, PlayerData, RadarData, ArcRwlockRadarData, BombData}};
use self::{context::DmaCtx, threaddata::CsData};
mod context;
mod threaddata;
mod cs2dumper;
pub use context::Connector;
pub async fn run(radar_data: ArcRwlockRadarData, connector: Connector, pcileech_device: String) -> anyhow::Result<()> {
let mut ctx = DmaCtx::setup(connector, pcileech_device)?;
let mut data = CsData::default();
// For read timing
let mut last_bomb_dropped = false;
let mut last_bomb_planted = false;
let mut last_tick_count = 0;
let mut last_big_read = Instant::now();
// For frequency info
let mut start_stamp = Instant::now();
let mut iters = 0;
let mut freq = 0;
data.update_pointers(&mut ctx);
data.update_common(&mut ctx);
data.update_players(&mut ctx);
data.update_bomb(&mut ctx);
loop {
if ctx.process.state().is_dead() {
break;
}
if last_big_read.elapsed().as_millis() > 10000 {
data.update_pointers(&mut ctx);
data.update_players(&mut ctx);
last_big_read = Instant::now();
}
data.update_common(&mut ctx);
// Bomb update
if (data.bomb_dropped && !last_bomb_dropped) || (data.bomb_planted && !last_bomb_planted) {
data.update_bomb(&mut ctx);
}
if (!data.bomb_dropped && last_bomb_dropped) || !data.bomb_planted {
data.recheck_bomb_holder = true;
}
last_bomb_dropped = data.bomb_dropped;
last_bomb_planted = data.bomb_planted;
// Poll entity data
let ingame = !data.map.is_empty() && data.map != "<empty>";
let update_data = data.tick_count != last_tick_count;
if ingame {
if !update_data {
continue;
}
let mut entity_data = Vec::new();
// Bomb
if data.bomb_dropped || data.bomb_planted {
let 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)));
}
// Local player
let local_data = ctx.batched_player_read(
data.local.into(), data.local_pawn.into()
).unwrap();
if local_data.health > 0 {
let has_bomb = {
if data.bomb_planted {
false
} else if data.recheck_bomb_holder {
if local_data.team == Some(TeamID::T) && !data.bomb_dropped && !data.bomb_planted {
let is_holder = ctx.has_c4(
data.local_pawn.into(), data.entity_list.into()
).unwrap_or(false);
if is_holder {
data.bomb_holder = data.local.into();
data.recheck_bomb_holder = false;
}
is_holder
} else { false }
} else { Address::from(data.local) == data.bomb_holder }
};
entity_data.push(
EntityData::Player(
PlayerData::new(
local_data.pos,
local_data.yaw,
PlayerType::Local,
has_bomb
)
)
);
}
// Other players
for (controller, pawn) in &data.players {
let player_data = ctx.batched_player_read(*controller, *pawn).unwrap();
if player_data.health < 1 {
continue;
}
let has_bomb = {
if data.bomb_planted {
false
} else if data.recheck_bomb_holder {
if player_data.team == Some(TeamID::T) && !data.bomb_dropped && !data.bomb_planted {
let is_holder = ctx.has_c4(*pawn, data.entity_list.into()).unwrap_or(false);
if is_holder {
data.bomb_holder = *controller;
data.recheck_bomb_holder = false;
}
is_holder
} else { false }
} else { *controller == data.bomb_holder }
};
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
)
)
);
}
let mut radar = radar_data.write().await;
*radar = RadarData::new(
true,
data.map.clone(),
entity_data,
freq
);
} else {
let mut radar = radar_data.write().await;
*radar = RadarData::empty(freq);
}
last_tick_count = data.tick_count;
iters += 1;
if start_stamp.elapsed().as_secs() > 1 {
freq = iters;
iters = 0;
start_stamp = Instant::now();
}
thread::sleep(Duration::from_millis(1));
}
Ok(())
}

175
src/dma/threaddata/mod.rs Executable file
View File

@@ -0,0 +1,175 @@
use itertools::Itertools;
use memflow::{mem::MemoryView, types::Address};
use super::{context::DmaCtx, cs2dumper};
#[derive(Clone, Debug, Default)]
pub struct CsData {
// Entities
pub players: Vec<(Address, Address)>,
pub bomb: Address,
pub bomb_holder: Address,
pub recheck_bomb_holder: bool,
// Pointers
pub globals: u64,
pub gamerules: u64,
pub entity_list: u64,
pub game_ent_sys: u64,
// Common
pub local: u64,
pub local_pawn: u64,
pub is_dead: bool,
pub tick_count: i32,
pub bomb_dropped: bool,
pub bomb_planted: bool,
pub highest_index: i32,
pub map: String
}
impl CsData {
pub fn update_bomb(&mut self, ctx: &mut DmaCtx) {
// If the bomb is dropped, do a reverse entity list loop with early exit when we found the bomb. ( Now with BATCHING!!! :O )
if self.bomb_dropped {
// We search 16 entities at a time.
for chunk in &(0..=self.highest_index).rev().chunks(16) {
let indexes: Vec<i32> = chunk.collect();
let mut data_array = [(0u64, 0i32); 16];
{
let mut batcher = ctx.process.batcher();
let ent_list: Address = self.entity_list.into();
data_array.iter_mut().zip(indexes).for_each(|((data_ptr, data_idx), index)| {
batcher.read_into(ent_list + 8 * (index >> 9) + 16, data_ptr);
*data_idx = index;
});
}
{
let mut batcher = ctx.process.batcher();
data_array.iter_mut().for_each(|(ptr, index)| {
let handle: Address = (*ptr).into();
batcher.read_into(handle + 120 * (*index & 0x1FF), ptr);
});
}
// You can actually optimize this EVEN more
let bomb = data_array.into_iter().find(|(ptr, _)| {
// By doing this with a batcher too...
ctx.is_dropped_c4((*ptr).into()).unwrap_or(false)
});
if let Some(bomb) = bomb {
self.bomb = bomb.0.into();
}
}
} else if self.bomb_planted {
let bomb = ctx.get_plantedc4()
.expect("Failed to get planted bomb");
self.bomb = bomb;
}
}
pub fn update_players(&mut self, ctx: &mut DmaCtx) {
let mut list_entries = [0u64; 64];
{
let mut batcher = ctx.process.batcher();
let ent_list: Address = self.entity_list.into();
list_entries.iter_mut().enumerate().for_each(|(idx, data)| {
let index = idx as i32;
batcher.read_into(ent_list + 8 * (index >> 9) + 16, data);
});
}
let mut player_ptrs = [0u64; 64];
{
let mut batcher = ctx.process.batcher();
player_ptrs.iter_mut().enumerate().for_each(|(idx, data)| {
let list_entry: Address = list_entries[idx].into();
batcher.read_into(list_entry + 120 * (idx & 0x1FF), data);
});
}
let mut new_players: Vec<u64> = Vec::new();
player_ptrs
.into_iter()
.for_each(|ptr| {
if ctx.is_cs_player_controller(ptr.into()).unwrap_or(false) {
new_players.push(ptr)
}
});
let new_players: Vec<(Address, Address)> = new_players
.into_iter()
.map(Address::from)
.filter(|ptr| !ptr.is_null())
.filter(|ptr| *ptr != self.local.into())
.map(|ptr| {
let pawn = ctx.pawn_from_controller(ptr, self.entity_list.into()).unwrap();
(ptr, pawn)
})
.filter(|(_, pawn)| pawn.is_some())
.map(|(controller, pawn)| (controller, pawn.unwrap()))
.collect();
self.players = new_players;
}
pub fn update_common(&mut self, ctx: &mut DmaCtx) {
let mut bomb_dropped = 0u8;
let mut bomb_planted = 0u8;
let mut map_ptr = 0u64;
{
// Globals
let tick_count_addr = (self.globals + 0x40).into();
let map_addr = (self.globals + 0x188).into();
// Gamerules
let bomb_dropped_addr = (self.gamerules + cs2dumper::client::C_CSGameRules::m_bBombDropped as u64).into();
let bomb_planted_addr = (self.gamerules + cs2dumper::client::C_CSGameRules::m_bBombPlanted as u64).into();
// Game Entity System
let highest_index_addr = (self.game_ent_sys + cs2dumper::offsets::client_dll::dwGameEntitySystem_getHighestEntityIndex as u64).into();
let mut batcher = ctx.process.batcher();
batcher.read_into(
ctx.client_module.base + cs2dumper::offsets::client_dll::dwLocalPlayerController,
&mut self.local
);
batcher.read_into(
ctx.client_module.base + cs2dumper::offsets::client_dll::dwLocalPlayerPawn,
&mut self.local_pawn
);
batcher.read_into(tick_count_addr, &mut self.tick_count);
batcher.read_into(bomb_dropped_addr, &mut bomb_dropped);
batcher.read_into(bomb_planted_addr, &mut bomb_planted);
batcher.read_into(highest_index_addr, &mut self.highest_index);
batcher.read_into(map_addr, &mut map_ptr);
}
let map_string = ctx.process.read_char_string_n(map_ptr.into(), 32).unwrap_or(String::from("<empty>"));
self.map = map_string;
self.bomb_dropped = bomb_dropped != 0;
self.bomb_planted = bomb_planted != 0;
}
pub fn update_pointers(&mut self, ctx: &mut DmaCtx) {
let mut batcher = ctx.process.batcher();
batcher.read_into(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGlobalVars, &mut self.globals);
batcher.read_into(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGameRules, &mut self.gamerules);
batcher.read_into(ctx.client_module.base + cs2dumper::offsets::client_dll::dwEntityList, &mut self.entity_list);
batcher.read_into(ctx.client_module.base + cs2dumper::offsets::client_dll::dwGameEntitySystem, &mut self.game_ent_sys);
}
}

5
src/enums/mod.rs Executable file
View File

@@ -0,0 +1,5 @@
mod teamid;
mod player_type;
pub use teamid::TeamID;
pub use player_type::PlayerType;

9
src/enums/player_type.rs Executable file
View File

@@ -0,0 +1,9 @@
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Default, PartialEq)]
pub enum PlayerType {
#[default]
Unknown,
Spectator,
Local,
Enemy,
Team
}

7
src/enums/teamid.rs Executable file
View File

@@ -0,0 +1,7 @@
#[repr(i32)]
#[derive(Debug, Eq, PartialEq, enum_primitive_derive::Primitive)]
pub enum TeamID {
Spectator = 1,
T = 2,
CT = 3
}

56
src/main.rs Executable file
View File

@@ -0,0 +1,56 @@
use std::sync::Arc;
use clap::Parser;
use cli::Cli;
use comms::RadarData;
use tokio::sync::RwLock;
mod cli;
mod structs;
mod enums;
mod comms;
mod dma;
mod websocket;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli: Cli = Cli::parse();
simple_logger::SimpleLogger::new()
.with_level(cli.loglevel.into())
.init()
.expect("Initializing logger");
let radar_data = Arc::new(
RwLock::new(
RadarData::empty(0)
)
);
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).await {
log::error!("Error in dma thread: [{}]", err.to_string());
} else {
println!("CS2 Process exited, exiting program...")
}
});
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);
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) = websocket::run(cli.web_path, cli.port, radar_data).await {
log::error!("Error in ws server: [{}]", err.to_string());
}
});
dma_handle.await?;
Ok(())
}

3
src/structs/mod.rs Executable file
View File

@@ -0,0 +1,3 @@
mod vec3;
pub use vec3::Vec3;

11
src/structs/vec3.rs Executable file
View File

@@ -0,0 +1,11 @@
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[repr(C)]
pub struct Vec3 {
pub x: f32,
pub y: f32,
pub z: f32
}
unsafe impl dataview::Pod for Vec3 {}

69
src/websocket.rs Normal file
View File

@@ -0,0 +1,69 @@
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::comms::RadarData;
#[derive(Clone)]
struct AppState {
data_lock: Arc<RwLock<RadarData>>
}
async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> Response {
let clone = state.clone();
ws.on_upgrade(|socket| handle_socket(socket, clone))
}
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 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()
},
}
};
//println!("{str}");
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(())
}