From 7738b1ffb64df44dc7470495b5e04dd319622c95 Mon Sep 17 00:00:00 2001 From: Nocrex <20463817+Nocrex@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:30:27 +0100 Subject: [PATCH 01/10] process in background thread, ui adjustments --- src/base/cheat_analyser_base.rs | 6 +- src/bin/cli.rs | 2 +- src/bin/gui.rs | 168 +++++++++++++++++++++----------- src/lib.rs | 4 + src/lib/algorithm.rs | 4 +- src/util/nocrex/jankguard.rs | 92 +++++------------ 6 files changed, 144 insertions(+), 132 deletions(-) diff --git a/src/base/cheat_analyser_base.rs b/src/base/cheat_analyser_base.rs index 619cbd9..979e0bd 100644 --- a/src/base/cheat_analyser_base.rs +++ b/src/base/cheat_analyser_base.rs @@ -343,7 +343,7 @@ lazy_static! { pub struct CheatAnalyser<'a> { pub state: CheatAnalyserState, - pub algorithms: Vec + 'a>>, + pub algorithms: Vec + 'a + Send>>, pub detections: Vec, pub header: Option
, pub tick: DemoTick, @@ -503,7 +503,7 @@ impl BorrowMessageHandler for CheatAnalyser<'_> { } impl<'a> CheatAnalyser<'a> { - pub fn new(algorithms: Vec + 'a>>) -> Self { + pub fn new(algorithms: Vec + 'a + Send>>) -> Self { let mut message_types = HANDLED_MESSAGE_TYPES.lock().unwrap(); // Figure out what message types we're going to be using. let mut specified_message_types: Vec = vec![]; @@ -634,6 +634,8 @@ impl<'a> CheatAnalyser<'a> { fn check_progress(&mut self) { const PROGRESS_UPDATE_INTERVAL_MS: u128 = 1000; const TPS_ROLLING_AVERAGE_WINDOW: u32 = 10; + crate::PROGRESS_CURRENT.store(self.tick.into(), std::sync::atomic::Ordering::Relaxed); + crate::PROGRESS_TOTAL.store(self.get_tick_count_u32(), std::sync::atomic::Ordering::Relaxed); if self.last_progress_update_time.elapsed().as_millis() < PROGRESS_UPDATE_INTERVAL_MS { return; } diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 95ed667..836f107 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -45,7 +45,7 @@ fn main() -> Result<(), Error> { // To add your algorithm, call new() on it and store inside a Box. // You will need to import it at the top of the file. - let mut algorithms: Vec> = get_algorithms(); + let mut algorithms: Vec> = get_algorithms(); let specified_algorithms = matches.opt_strs("a"); if specified_algorithms.is_empty() && !matches.opt_present("a") { algorithms.retain(|a| a.default()); diff --git a/src/bin/gui.rs b/src/bin/gui.rs index 718d86e..69f19d5 100644 --- a/src/bin/gui.rs +++ b/src/bin/gui.rs @@ -1,6 +1,12 @@ use std::collections::HashMap; -use analysis_template::{base::cheat_analyser_base::CheatAnalyser, lib::{algorithm::{analyse, get_algorithms, Detection}, parameters::{Parameter, Parameters}}}; +use analysis_template::{ + base::cheat_analyser_base::CheatAnalyser, + lib::{ + algorithm::{analyse, get_algorithms, Detection}, + parameters::{Parameter, Parameters}, + }, +}; use eframe::egui; use itertools::Itertools; use tf_demo_parser::Demo; @@ -19,7 +25,6 @@ fn main() -> eframe::Result { ) } -#[derive(Default)] struct Gui { algos: HashMap, params: HashMap, @@ -30,6 +35,9 @@ struct Gui { selected_detection: Option, analyser: Option>, + + recv: std::sync::mpsc::Receiver>>, + send: std::sync::mpsc::Sender>>, } impl Gui { @@ -41,9 +49,7 @@ impl Gui { } } if let Ok(data) = std::fs::read_to_string("params.json") { - if let Ok(saved_params) = - serde_json::from_str::>(&data) - { + if let Ok(saved_params) = serde_json::from_str::>(&data) { for saved_algo in saved_params { if let Some(algo) = params.get_mut(&saved_algo.0) { for saved_param in saved_algo.1 { @@ -55,6 +61,7 @@ impl Gui { } } } + let (send, recv) = std::sync::mpsc::channel(); Self { algos: HashMap::from_iter( get_algorithms() @@ -62,7 +69,14 @@ impl Gui { .map(|a| (a.algorithm_name().to_string(), a.default())), ), params, - ..Default::default() + file: None, + processing: false, + detections: HashMap::new(), + selected_player: None, + selected_detection: None, + analyser: None, + recv, + send, } } @@ -81,76 +95,109 @@ impl Gui { } } - let file = std::fs::read(self.file.as_ref().unwrap()).unwrap(); - let demo: Demo = Demo::new(&file); - let analyser = analyse(&demo, algorithms).unwrap(); - self.analyser = Some(analyser); - self.detections.clear(); - for det in self.analyser.as_ref().unwrap().detections.clone() { - self.detections.entry(det.player).or_default().push(det); - } - self.analyser.as_ref().unwrap().print_detection_summary(); + let file = self.file.clone().unwrap(); + let send = self.send.clone(); + + std::thread::spawn(move || { + send.send((|| -> anyhow::Result> { + let file = std::fs::read(&file)?; + let demo: Demo = Demo::new(&file); + Ok(analyse(&demo, algorithms)?) + })()) + .unwrap(); + }); + self.processing = true; } } impl eframe::App for Gui { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - let hovered = !ctx.input(|i| i.raw.hovered_files.is_empty()); + if !ctx.input(|i| i.raw.hovered_files.is_empty()) { + egui::Window::new("Hover") + .movable(false) + .title_bar(false) + .resizable(false) + .anchor(egui::Align2::CENTER_CENTER, &[0.0, 0.0]) + .show(ctx, |ui| { + ui.heading("Drop to analyze"); + }); + } egui::CentralPanel::default().show(ctx, |ui| { if self.processing { ui.disable(); + if let Ok(result) = self.recv.try_recv() { + if let Err(e) = &result { + println!("Error while parsing demo: {e:#?}"); + } + self.analyser = result.ok(); + self.processing = false; + self.detections.clear(); + for det in self.analyser.as_ref().unwrap().detections.clone() { + self.detections.entry(det.player).or_default().push(det); + } + self.analyser.as_ref().unwrap().print_detection_summary(); + } } ui.horizontal(|ui|{ + let mut algo_to_configure = None; ui.vertical(|ui|{ ui.heading("Algorithms"); for mut algo in self.algos.iter_mut().sorted_by_key(|a| a.0) { - ui.checkbox(&mut algo.1, algo.0); + ui.horizontal(|ui|{ + ui.checkbox(&mut algo.1, algo.0); + if *algo.1 && self.params.get(algo.0).is_some_and(|p|!p.is_empty()) { + if ui.small_button("⚙").clicked() { + algo_to_configure = Some(algo.0.clone()); + } + } + }); } }); ui.add_space(10.0); ui.separator(); ui.add_space(10.0); - ui.vertical(|ui|{ - ui.horizontal(|ui|{ - ui.heading("Parameters"); - if ui.button("Save").clicked(){ - std::fs::write("params.json", &serde_json::to_vec_pretty(&self.params).unwrap()).unwrap(); - } - }); - ui.separator(); - for (name, params) in self.params.iter_mut().sorted_by_key(|a|a.0) { - if !self.algos[name]{ - continue; - } - ui.add_space(10.0); - ui.heading(name); + egui::ScrollArea::vertical().min_scrolled_height(300.0).show(ui, |ui|{ + ui.vertical(|ui|{ + ui.horizontal(|ui|{ + ui.heading("Parameters"); + if ui.button("Save").clicked(){ + std::fs::write("params.json", &serde_json::to_vec_pretty(&self.params).unwrap()).unwrap(); + } + }); ui.separator(); - for param in params.iter_mut().sorted_by_key(|p|p.0){ - // ui.horizontal(|ui|{ - // ui.add(egui::DragValue::new(param.1).max_decimals(50)); - // ui.label(*param.0); - // }); - match param.1 { - Parameter::Float(f) => { - ui.horizontal(|ui|{ - ui.add(egui::DragValue::new(f).speed(0.001).max_decimals(50)); - ui.label(param.0); - }); - } - Parameter::Int(i) => { + for (name, params) in self.params.iter_mut().sorted_by_key(|a|a.0) { + if !self.algos[name]{ + continue; + } + ui.add_space(10.0); + let h = ui.heading(name); + if algo_to_configure.as_ref().is_some_and(|n|*n == *name) { + h.scroll_to_me(Some(egui::Align::TOP)); + } + ui.separator(); + for param in params.iter_mut().sorted_by_key(|p|p.0){ + match param.1 { + Parameter::Float(f) => { ui.horizontal(|ui|{ - ui.add(egui::DragValue::new(i).speed(1).max_decimals(0)); + ui.add(egui::DragValue::new(f).speed(0.001).max_decimals(50)); ui.label(param.0); }); - } - Parameter::Bool(b) => { - ui.horizontal(|ui|{ - ui.checkbox(b, param.0); - }); + } + Parameter::Int(i) => { + ui.horizontal(|ui|{ + ui.add(egui::DragValue::new(i).speed(1).max_decimals(0)); + ui.label(param.0); + }); + } + Parameter::Bool(b) => { + ui.horizontal(|ui|{ + ui.checkbox(b, param.0); + }); + } } } } - } + }); }); }); ui.add_space(10.0); @@ -161,10 +208,9 @@ impl eframe::App for Gui { .pick_file() { self.file = Some(path); - self.analyse(); } } - if self.file.is_some() { + ui.add_enabled_ui(self.file.is_some(), |ui|{ if ui.button("Analyse").clicked() { self.analyse(); } @@ -182,11 +228,17 @@ impl eframe::App for Gui { } } } - } - if hovered { - ui.label("Drop to analyse"); - } + }); }); + if self.processing { + ui.horizontal(|ui|{ + ui.spinner(); + ui.label("Analysing..."); + let progress = analysis_template::PROGRESS_CURRENT.load(std::sync::atomic::Ordering::Relaxed); + let total = analysis_template::PROGRESS_TOTAL.load(std::sync::atomic::Ordering::Relaxed); + ui.add(egui::widgets::ProgressBar::new(progress as f32 / total as f32).show_percentage().text(format!("{} / {}", progress, total))); + }); + } ui.add_space(10.0); if let Some(p) = &self.file { ui.heading(p.file_name().unwrap().to_string_lossy()); @@ -197,6 +249,7 @@ impl eframe::App for Gui { egui::ScrollArea::vertical() .id_salt("players") .show(ui, |ui| { + ui.set_min_width(160.0); ui.vertical(|ui| { for player in self.detections.iter().sorted_by_key(|d| d.1.len()).rev() { @@ -221,6 +274,7 @@ impl eframe::App for Gui { egui::ScrollArea::vertical() .id_salt("detections") .show(ui, |ui| { + ui.set_min_width(160.0); ui.vertical(|ui| { if let Some(detections) = self.selected_player.and_then(|p| self.detections.get(&p)) diff --git a/src/lib.rs b/src/lib.rs index d37a1f9..b2062d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +use std::sync::atomic::AtomicU32; pub mod base { pub mod cheat_analyser_base; pub mod demo_handler_base; @@ -29,6 +30,9 @@ pub mod lib { pub static SILENT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); +pub static PROGRESS_CURRENT: AtomicU32 = AtomicU32::new(0); +pub static PROGRESS_TOTAL: AtomicU32 = AtomicU32::new(0); + #[macro_export] macro_rules! dev_print { ($($arg:tt)*) => { diff --git a/src/lib/algorithm.rs b/src/lib/algorithm.rs index 906c75a..3042cd5 100644 --- a/src/lib/algorithm.rs +++ b/src/lib/algorithm.rs @@ -24,7 +24,7 @@ pub use tf_demo_parser::{Demo, DemoParser, Parse, ParseError, ParserState, Strea use crate::{base::{cheat_analyser_base::CheatAnalyser, demo_handler_base::CheatDemoHandler}, dev_print}; -pub fn get_algorithms() -> Vec>> { +pub fn get_algorithms() -> Vec + Send>> { vec![ Box::new(AllMessages::new()), Box::new(ViewAngles180Degrees::new()), @@ -36,7 +36,7 @@ pub fn get_algorithms() -> Vec>> { ] } -pub fn analyse<'a>(demo: &Demo, algorithms: Vec>>) -> anyhow::Result> { +pub fn analyse<'a>(demo: &Demo, algorithms: Vec + Send>>) -> anyhow::Result> { let mut stream = demo.get_stream(); let header: Header = Header::read(&mut stream)?; let mut packets = RawPacketStream::new(stream); diff --git a/src/util/nocrex/jankguard.rs b/src/util/nocrex/jankguard.rs index 0ad25e9..7f9f084 100644 --- a/src/util/nocrex/jankguard.rs +++ b/src/util/nocrex/jankguard.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use crate::base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}; use steamid_ng::SteamID; +use tf_demo_parser::demo::sendprop::SendPropIdentifier; const TELEPORT_DIST: f32 = 256.0; @@ -48,7 +49,7 @@ impl JankGuard { &mut self, message: &tf_demo_parser::demo::message::Message, state: &CheatAnalyserState, - _parser_state: &tf_demo_parser::ParserState, + parser_state: &tf_demo_parser::ParserState, tick: tf_demo_parser::demo::data::DemoTick, ) { match message { @@ -72,79 +73,30 @@ impl JankGuard { } _ => (), }, - // Try to find firing events through tracers and player animations, thx to megascatterbomb for this snippet + // Try to find firing events through tracers and player animations, simplified from megascatterbomb's snippet tf_demo_parser::demo::message::Message::TempEntities(msg) => { for event in &msg.events { - match u16::from(event.class_id) { - //Bullet tracer - 152 => { - for prop in &event.props { - match prop.identifier.names() { - Some((table_name, prop_name)) => { - if table_name == "DT_TEFireBullets" - && prop_name == "m_iPlayer" - { - let ent_id: i64 = match prop.value { - tf_demo_parser::demo::sendprop::SendPropValue::Integer(x) => x.try_into().unwrap_or_default(), - _ => {continue} - }; - let entity_id = - tf_demo_parser::demo::message::EntityId::from( - ent_id as u32, - ); - match state - .entid_to_userid - .get(&entity_id) - .and_then(|userid| state.userid_to_id64.get(userid)) - { - Some(id64) => { - self.player_data - .entry(*id64) - .or_default() - .last_fire = tick.into(); - } - None => continue, - }; - } - } - None => {} - } - } - } - // Animation - 165 => { - for prop in &event.props { - match prop.identifier.names() { - Some((table_name, prop_name)) => { - if table_name == "DT_TEPlayerAnimEvent" - && prop_name == "m_hPlayer" - { - let handle_id: u32 = match prop.value { - tf_demo_parser::demo::sendprop::SendPropValue::Integer(x) => x.try_into().unwrap_or_default(), - _ => {continue} - }; - let entity_id = - crate::util::helpers::handle_to_entid(handle_id); - match state - .entid_to_userid - .get(&entity_id) - .and_then(|userid| state.userid_to_id64.get(userid)) - { - Some(id64) => { - self.player_data - .entry(*id64) - .or_default() - .last_fire = tick.into(); - } - None => continue, - }; - } - } - None => {} - } + let class = &parser_state.server_classes[usize::from(event.class_id)].name; + if matches!(class.as_str(), "CTEFireBullets" | "CTEPlayerAnimEvent") { + const BULLETS_PLAYER: SendPropIdentifier = + SendPropIdentifier::new("DT_TEFireBullets", "m_iPlayer"); + const ANIM_PLAYER: SendPropIdentifier = + SendPropIdentifier::new("DT_TEPlayerAnimEvent", "m_hPlayer"); + + if let Some(prop) = event + .props + .iter() + .find(|p| matches!(p.identifier, BULLETS_PLAYER | ANIM_PLAYER)) + { + if let Some(id64) = i64::try_from(&prop.value) + .ok() + .and_then(|id| id.try_into().ok()) + .and_then(|id: u32| state.entid_to_userid.get(&id.into())) + .and_then(|uid| state.userid_to_id64.get(uid)) + { + self.player_data.entry(*id64).or_default().last_fire = tick.into(); } } - _ => continue, } } } From f3192ceb09880c68fdd1232f36d9c5464e173bac Mon Sep 17 00:00:00 2001 From: tf2query <246169462+tf2query@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:09:13 +0100 Subject: [PATCH 02/10] Fixed false positives happening due to packet losses --- src/algorithms/nocrex/aimsnap.rs | 56 ++++++++++++++++++--------- src/algorithms/nocrex/angle_repeat.rs | 44 ++++++++++++++------- 2 files changed, 68 insertions(+), 32 deletions(-) diff --git a/src/algorithms/nocrex/aimsnap.rs b/src/algorithms/nocrex/aimsnap.rs index ff67c00..1e5bd9c 100644 --- a/src/algorithms/nocrex/aimsnap.rs +++ b/src/algorithms/nocrex/aimsnap.rs @@ -1,9 +1,9 @@ -// Written by Nocrex +// Written by Nocrex, Patched for Command Batching by Ciam use std::{collections::HashMap, ops::Range}; use crate::{ - base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}, lib::parameters::get_parameter_value, util::{helpers::angle_delta, nocrex::jankguard::JankGuard} + base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}, lib::parameters::get_parameter_value, util::nocrex::jankguard::JankGuard }; use anyhow::Error; use serde_json::json; @@ -15,7 +15,7 @@ use crate::lib::parameters::{Parameter, Parameters}; #[derive(Default)] pub struct AimSnap { - ticks: Vec>, + ticks: Vec<(u32, HashMap)>, jg: JankGuard, params: Parameters, detections: Vec, @@ -58,7 +58,7 @@ impl<'a> CheatAlgorithm<'a> for AimSnap { let noise_range: Range = noise_min..noise_max; - self.ticks.insert(0, HashMap::new()); + self.ticks.insert(0, (ticknum, HashMap::new())); self.ticks.truncate(5); for player in players.iter().filter(|p| { @@ -79,7 +79,6 @@ impl<'a> CheatAlgorithm<'a> for AimSnap { .min(self.jg.spawned(&steam_id, ticknum)); if ticks_since_event < 60 { - // Ignore detections +-60 ticks from a teleport or spawn event if ticks_since_event == 0 { self.detections .retain(|det| det.player != steam_id || (ticknum - det.tick) > 60); @@ -90,23 +89,39 @@ impl<'a> CheatAlgorithm<'a> for AimSnap { self.ticks .get_mut(0) .unwrap() - .insert(steam_id.clone(), player.clone()); // Store angle for this tick for next ticks + .1 + .insert(steam_id.clone(), player.clone()); - let mut angles: Vec<_> = self - .ticks - .iter() - .map(|m| m.get(&steam_id).map(|p| (p.view_angle, p.pitch_angle))) - .rev() - .collect(); + let mut angle_history: Vec<(u32, f32, f32)> = Vec::new(); + for (t, m) in self.ticks.iter().rev() { + if let Some(p) = m.get(&steam_id) { + angle_history.push((*t, p.view_angle, p.pitch_angle)); + } + } - if angles.iter().any(|o| o.is_none()) { - continue; + if angle_history.len() < self.ticks.len() { + continue; } - let angles: Vec<(f32, f32)> = angles.drain(..).map(|o| o.unwrap()).collect(); let mut deltas = Vec::new(); - for (a, b) in angles.iter().zip(angles.iter().skip(1)) { - deltas.push(angle_delta(*a, *b)); + + for window in angle_history.windows(2) { + let (t1, yaw1, pitch1) = window[0]; + let (t2, yaw2, pitch2) = window[1]; + + let tick_delta = (t2 as f32 - t1 as f32).max(1.0); + + let mut yaw_diff = yaw2 - yaw1; + while yaw_diff > 180.0 { yaw_diff -= 360.0; } + while yaw_diff < -180.0 { yaw_diff += 360.0; } + + let pitch_diff = pitch2 - pitch1; + + let va_delta_real = yaw_diff / tick_delta; + let pa_delta_real = pitch_diff / tick_delta; + + let mag_delta = (va_delta_real * va_delta_real + pa_delta_real * pa_delta_real).sqrt(); + deltas.push(mag_delta); } if noise_range.contains(deltas.first().unwrap()) @@ -124,8 +139,11 @@ impl<'a> CheatAlgorithm<'a> for AimSnap { algorithm: self.algorithm_name().to_string(), player: steam_id, data: json!({ - "deltas": deltas + "deltas": deltas, + "note": "Tick Delta division applied." }), + hits: 0, + crits: 0, }); } } @@ -154,4 +172,4 @@ impl<'a> CheatAlgorithm<'a> for AimSnap { fn params(&mut self) -> Option<&mut Parameters> { Some(&mut self.params) } -} +} \ No newline at end of file diff --git a/src/algorithms/nocrex/angle_repeat.rs b/src/algorithms/nocrex/angle_repeat.rs index 35baaf2..7a6537e 100644 --- a/src/algorithms/nocrex/angle_repeat.rs +++ b/src/algorithms/nocrex/angle_repeat.rs @@ -1,9 +1,9 @@ -// Written by Nocrex +// Written by Nocrex, Patched for Command Batching By Ciam use std::collections::HashMap; use crate::{ - base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}, util::{helpers::{angle_delta}, nocrex::jankguard::JankGuard} + base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}, util::nocrex::jankguard::JankGuard }; use crate::lib::algorithm::{CheatAlgorithm, Detection}; @@ -16,7 +16,7 @@ use tf_demo_parser::ParserState; #[derive(Default)] pub struct AngleRepeat { - ticks: Vec>, + ticks: Vec<(u32, HashMap)>, jg: JankGuard, params: Parameters, @@ -54,7 +54,7 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { let ticknum = u32::from(state.tick); let players = &state.players; - self.ticks.insert(0, HashMap::new()); + self.ticks.insert(0, (ticknum, HashMap::new())); self.ticks.truncate(3); let min_angle_diff_ratio: f32 = get_parameter_value(&self.params, "min_angle_diff_ratio"); @@ -73,8 +73,8 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { let steam_id: u64 = u64::from(SteamID::from_steam3(&info.steam_id).unwrap()); - let prev_player = self.ticks.get(1).and_then(|m| m.get(&steam_id)).cloned(); - let second_prev_player = self.ticks.get(2).and_then(|m| m.get(&steam_id)).cloned(); + let prev_data = self.ticks.get(1).and_then(|(t, m)| m.get(&steam_id).map(|p| (*t, p.clone()))); + let second_prev_data = self.ticks.get(2).and_then(|(t, m)| m.get(&steam_id).map(|p| (*t, p.clone()))); let ticks_since_event = self .jg @@ -82,7 +82,6 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { .min(self.jg.spawned(&steam_id, ticknum)); if ticks_since_event < 60 { - // Ignore detections +-60 ticks from a teleport or spawn event if ticks_since_event == 0 { self.detections .retain(|det| det.player != steam_id || (ticknum - det.tick) > 60); @@ -94,16 +93,32 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { self.ticks .get_mut(0) .unwrap() - .insert(steam_id.clone(), player.clone()); // Store angle for this tick for next ticks + .1 + .insert(steam_id.clone(), player.clone()); - if let (Some(second_data), Some(first_data)) = (prev_player, second_prev_player) { + if let (Some((second_t, second_data)), Some((first_t, first_data))) = (prev_data, second_prev_data) { let first_angle = (first_data.view_angle, first_data.pitch_angle); let second_angle = (second_data.view_angle, second_data.pitch_angle); - let first_second_delta = angle_delta(first_angle, second_angle); - let first_third_delta = angle_delta(first_angle, third_angle); + + let calc_real_delta = |t_old: u32, a_old: (f32, f32), t_new: u32, a_new: (f32, f32)| -> f32 { + let tick_delta = (t_new as f32 - t_old as f32).max(1.0); + + let mut yaw_diff = a_new.0 - a_old.0; + while yaw_diff > 180.0 { yaw_diff -= 360.0; } + while yaw_diff < -180.0 { yaw_diff += 360.0; } + + let pitch_diff = a_new.1 - a_old.1; + + let va_real = yaw_diff / tick_delta; + let pa_real = pitch_diff / tick_delta; + + (va_real * va_real + pa_real * pa_real).sqrt() + }; + + let first_second_delta = calc_real_delta(first_t, first_angle, second_t, second_angle); + let first_third_delta = calc_real_delta(first_t, first_angle, ticknum, third_angle); if first_second_delta < min_first_second_angle_delta { - // Ignore players with only a tiny adjustment in second angle continue; } @@ -124,7 +139,10 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { "1_3_delta": first_third_delta, "1_2_delta": first_second_delta, "ratio": ratio, + "note": "Tick Delta division applied." }), + hits: 0, + crits: 0, }); } } @@ -154,4 +172,4 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { fn params(&mut self) -> Option<&mut Parameters> { Some(&mut self.params) } -} +} \ No newline at end of file From b88cba7f31dbd59aefe0ad29a07b46156d3f5893 Mon Sep 17 00:00:00 2001 From: Nocrex <20463817+Nocrex@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:55:53 +0100 Subject: [PATCH 03/10] cargo fmt --- src/bin/cli.rs | 58 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 836f107..942b1fe 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -1,12 +1,21 @@ -use std::{collections::HashMap, env, fs::{self}}; -use analysis_template::{dev_print, lib::{algorithm::{analyse, get_algorithms, CheatAlgorithm}, parameters::Parameters}, SILENT}; +use analysis_template::{ + dev_print, + lib::{ + algorithm::{analyse, get_algorithms, CheatAlgorithm}, + parameters::Parameters, + }, + SILENT, +}; +use std::{ + collections::HashMap, + env, + fs::{self}, +}; use anyhow::Error; - pub use tf_demo_parser::{Demo, DemoParser, Parse, ParseError, ParserState, Stream}; - use getopts::Options; fn main() -> Result<(), Error> { @@ -14,12 +23,25 @@ fn main() -> Result<(), Error> { let mut opts = Options::new(); opts.optopt("i", "input", "set input file path", "PATH"); - opts.optflag("q", "quiet", "silence all output except for the final JSON string"); - opts.optflag("Q", "quiet-pretty", "same as -q, but with more human-readable json"); + opts.optflag( + "q", + "quiet", + "silence all output except for the final JSON string", + ); + opts.optflag( + "Q", + "quiet-pretty", + "same as -q, but with more human-readable json", + ); opts.optmulti("a", "algorithm", "specify the algorithm to run. Include multiple -a flags to run multiple algorithms. If not specified, the default algorithms are run.", "ALGORITHM [-a ALGORITHM]..."); opts.optflag("c", "count", "only print the number of detections"); opts.optflag("h", "help", "print this help menu"); - opts.optopt("p", "params", "Parameter json file to use for the algorithms", "PATH"); + opts.optopt( + "p", + "params", + "Parameter json file to use for the algorithms", + "PATH", + ); fn print_help(opts: &getopts::Options) { println!("{}", opts.usage("Usage: analysis-template [options]")); @@ -52,12 +74,13 @@ fn main() -> Result<(), Error> { } else { algorithms.retain(|a| specified_algorithms.contains(&a.algorithm_name().to_string())); } - + if let Some(param_file_path) = matches.opt_str("p") { dev_print!("Loading parameters from {}:", param_file_path); let c = fs::read(param_file_path).expect("Couldn't read parameter file"); - let config = serde_json::from_slice::>(&c).expect("Couldn't decode parameter file"); - for algo in algorithms.iter_mut(){ + let config = serde_json::from_slice::>(&c) + .expect("Couldn't decode parameter file"); + for algo in algorithms.iter_mut() { let algorithm_name: String = algo.algorithm_name().to_owned(); let algo_params = algo.params(); @@ -78,7 +101,7 @@ fn main() -> Result<(), Error> { dev_print!(" {} = {:?} (default)", k, v); } }); - } + } } let unknown_algorithms: Vec = specified_algorithms @@ -86,7 +109,10 @@ fn main() -> Result<(), Error> { .filter(|a| algorithms.iter().all(|b| b.algorithm_name() != *a)) .collect(); if !unknown_algorithms.is_empty() { - panic!("Unknown algorithms specified: {}", unknown_algorithms.join(", ")); + panic!( + "Unknown algorithms specified: {}", + unknown_algorithms.join(", ") + ); } else if algorithms.is_empty() { panic!("No algorithms specified"); } @@ -110,8 +136,12 @@ fn main() -> Result<(), Error> { let total_ticks = analyser.get_tick_count_u32(); let total_time = start.elapsed().as_secs_f64(); let total_tps = (total_ticks as f64) / total_time; - dev_print!("Done! (Processed {} ticks in {:.2} seconds averaging {:.2} tps)", total_ticks, total_time, total_tps); + dev_print!( + "Done! (Processed {} ticks in {:.2} seconds averaging {:.2} tps)", + total_ticks, + total_time, + total_tps + ); Ok(()) } - From be9f24d2dc8231e685400c7c0126560dc74a7769 Mon Sep 17 00:00:00 2001 From: tf2query <246169462+tf2query@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:56:47 +0100 Subject: [PATCH 04/10] Done --- src/algorithms/nocrex/aimsnap.rs | 29 ++++++++++++--------------- src/algorithms/nocrex/angle_repeat.rs | 22 +++++++------------- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/algorithms/nocrex/aimsnap.rs b/src/algorithms/nocrex/aimsnap.rs index 1e5bd9c..9b7f4b5 100644 --- a/src/algorithms/nocrex/aimsnap.rs +++ b/src/algorithms/nocrex/aimsnap.rs @@ -79,6 +79,7 @@ impl<'a> CheatAlgorithm<'a> for AimSnap { .min(self.jg.spawned(&steam_id, ticknum)); if ticks_since_event < 60 { + // Ignore detections +-60 ticks from a teleport or spawn event if ticks_since_event == 0 { self.detections .retain(|det| det.player != steam_id || (ticknum - det.tick) > 60); @@ -89,34 +90,33 @@ impl<'a> CheatAlgorithm<'a> for AimSnap { self.ticks .get_mut(0) .unwrap() - .1 - .insert(steam_id.clone(), player.clone()); + .1 + .insert(steam_id.clone(), player.clone()); // Store angle for this tick for next ticks - let mut angle_history: Vec<(u32, f32, f32)> = Vec::new(); - for (t, m) in self.ticks.iter().rev() { - if let Some(p) = m.get(&steam_id) { - angle_history.push((*t, p.view_angle, p.pitch_angle)); - } - } + let angle_history: Vec<_> = self + .ticks + .iter() + .filter_map(|(t, m)| m.get(&steam_id).map(|p| (*t, p.view_angle, p.pitch_angle))) + .rev() + .collect(); if angle_history.len() < self.ticks.len() { - continue; + continue; } let mut deltas = Vec::new(); - for window in angle_history.windows(2) { let (t1, yaw1, pitch1) = window[0]; let (t2, yaw2, pitch2) = window[1]; - let tick_delta = (t2 as f32 - t1 as f32).max(1.0); + let tick_delta = (t2 as f32 - t1 as f32).max(1.0); let mut yaw_diff = yaw2 - yaw1; while yaw_diff > 180.0 { yaw_diff -= 360.0; } while yaw_diff < -180.0 { yaw_diff += 360.0; } let pitch_diff = pitch2 - pitch1; - + let va_delta_real = yaw_diff / tick_delta; let pa_delta_real = pitch_diff / tick_delta; @@ -139,11 +139,8 @@ impl<'a> CheatAlgorithm<'a> for AimSnap { algorithm: self.algorithm_name().to_string(), player: steam_id, data: json!({ - "deltas": deltas, - "note": "Tick Delta division applied." + "deltas": deltas }), - hits: 0, - crits: 0, }); } } diff --git a/src/algorithms/nocrex/angle_repeat.rs b/src/algorithms/nocrex/angle_repeat.rs index 7a6537e..b136e87 100644 --- a/src/algorithms/nocrex/angle_repeat.rs +++ b/src/algorithms/nocrex/angle_repeat.rs @@ -1,9 +1,9 @@ -// Written by Nocrex, Patched for Command Batching By Ciam +// Written by Nocrex, Patched for Command Batching by Ciam use std::collections::HashMap; use crate::{ - base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}, util::nocrex::jankguard::JankGuard + base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}, util::{helpers::viewangle_delta, nocrex::jankguard::JankGuard} }; use crate::lib::algorithm::{CheatAlgorithm, Detection}; @@ -82,6 +82,7 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { .min(self.jg.spawned(&steam_id, ticknum)); if ticks_since_event < 60 { + // Ignore detections +-60 ticks from a teleport or spawn event if ticks_since_event == 0 { self.detections .retain(|det| det.player != steam_id || (ticknum - det.tick) > 60); @@ -94,23 +95,16 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { .get_mut(0) .unwrap() .1 - .insert(steam_id.clone(), player.clone()); + .insert(steam_id.clone(), player.clone()); // Store angle for this tick for next ticks if let (Some((second_t, second_data)), Some((first_t, first_data))) = (prev_data, second_prev_data) { let first_angle = (first_data.view_angle, first_data.pitch_angle); let second_angle = (second_data.view_angle, second_data.pitch_angle); let calc_real_delta = |t_old: u32, a_old: (f32, f32), t_new: u32, a_new: (f32, f32)| -> f32 { - let tick_delta = (t_new as f32 - t_old as f32).max(1.0); + let tick_delta = t_new.saturating_sub(t_old); - let mut yaw_diff = a_new.0 - a_old.0; - while yaw_diff > 180.0 { yaw_diff -= 360.0; } - while yaw_diff < -180.0 { yaw_diff += 360.0; } - - let pitch_diff = a_new.1 - a_old.1; - - let va_real = yaw_diff / tick_delta; - let pa_real = pitch_diff / tick_delta; + let (va_real, pa_real) = viewangle_delta(a_new.0, a_new.1, a_old.0, a_old.1, tick_delta); (va_real * va_real + pa_real * pa_real).sqrt() }; @@ -119,6 +113,7 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { let first_third_delta = calc_real_delta(first_t, first_angle, ticknum, third_angle); if first_second_delta < min_first_second_angle_delta { + // Ignore players with only a tiny adjustment in second angle continue; } @@ -139,10 +134,7 @@ impl<'a> CheatAlgorithm<'a> for AngleRepeat { "1_3_delta": first_third_delta, "1_2_delta": first_second_delta, "ratio": ratio, - "note": "Tick Delta division applied." }), - hits: 0, - crits: 0, }); } } From 05910c9324f7508dc11f9f71f1e83bc7e8b30082 Mon Sep 17 00:00:00 2001 From: tf2query <246169462+tf2query@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:13:10 +0100 Subject: [PATCH 05/10] Update aimsnap.rs --- src/algorithms/nocrex/aimsnap.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/algorithms/nocrex/aimsnap.rs b/src/algorithms/nocrex/aimsnap.rs index 9b7f4b5..3d142f4 100644 --- a/src/algorithms/nocrex/aimsnap.rs +++ b/src/algorithms/nocrex/aimsnap.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, ops::Range}; use crate::{ - base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}, lib::parameters::get_parameter_value, util::nocrex::jankguard::JankGuard + base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}, lib::parameters::get_parameter_value, util::{helpers::viewangle_delta, nocrex::jankguard::JankGuard} }; use anyhow::Error; use serde_json::json; @@ -109,16 +109,9 @@ impl<'a> CheatAlgorithm<'a> for AimSnap { let (t1, yaw1, pitch1) = window[0]; let (t2, yaw2, pitch2) = window[1]; - let tick_delta = (t2 as f32 - t1 as f32).max(1.0); + let tick_delta = t2.saturating_sub(t1); - let mut yaw_diff = yaw2 - yaw1; - while yaw_diff > 180.0 { yaw_diff -= 360.0; } - while yaw_diff < -180.0 { yaw_diff += 360.0; } - - let pitch_diff = pitch2 - pitch1; - - let va_delta_real = yaw_diff / tick_delta; - let pa_delta_real = pitch_diff / tick_delta; + let (va_delta_real, pa_delta_real) = viewangle_delta(yaw2, pitch2, yaw1, pitch1, tick_delta); let mag_delta = (va_delta_real * va_delta_real + pa_delta_real * pa_delta_real).sqrt(); deltas.push(mag_delta); From 8640aff036a9de8adf08a1471d2679963be60551 Mon Sep 17 00:00:00 2001 From: Nocrex <20463817+Nocrex@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:04:20 +0100 Subject: [PATCH 06/10] fix shot detection logic --- src/util/nocrex/jankguard.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/util/nocrex/jankguard.rs b/src/util/nocrex/jankguard.rs index 7f9f084..7810a8f 100644 --- a/src/util/nocrex/jankguard.rs +++ b/src/util/nocrex/jankguard.rs @@ -91,7 +91,8 @@ impl JankGuard { if let Some(id64) = i64::try_from(&prop.value) .ok() .and_then(|id| id.try_into().ok()) - .and_then(|id: u32| state.entid_to_userid.get(&id.into())) + .map(|id|crate::util::helpers::handle_to_entid(id)) + .and_then(|id| state.entid_to_userid.get(&id)) .and_then(|uid| state.userid_to_id64.get(uid)) { self.player_data.entry(*id64).or_default().last_fire = tick.into(); From ab4391c100bdfb39bf76f11a5e1c4c0f17705882 Mon Sep 17 00:00:00 2001 From: BrokenMartin Date: Tue, 2 Jun 2026 20:53:04 +0200 Subject: [PATCH 07/10] my old repo from local to github fork --- Cargo.toml | 2 +- src/algorithms/angle_history.rs | 183 +++++++++++++++++ src/algorithms/backtrack.rs | 193 ++++++++++++++++++ src/algorithms/double_tap.rs | 192 +++++++++++++++++ .../nocrex/old algos/old-double_tap.rs | 173 ++++++++++++++++ .../nocrex/old algos/old-oob_pitch.rs | 90 ++++++++ src/algorithms/nocrex/oob_pitch.rs | 96 +++++---- src/lib.rs | 3 + src/lib/algorithm.rs | 9 +- src/lib/parameters.rs | 3 +- 10 files changed, 905 insertions(+), 39 deletions(-) create mode 100644 src/algorithms/angle_history.rs create mode 100644 src/algorithms/backtrack.rs create mode 100644 src/algorithms/double_tap.rs create mode 100644 src/algorithms/nocrex/old algos/old-double_tap.rs create mode 100644 src/algorithms/nocrex/old algos/old-oob_pitch.rs diff --git a/Cargo.toml b/Cargo.toml index cc4c519..3a63e0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "analysis-template" -version = "0.1.0" +version = "0.2.2" edition = "2021" default-run = "cli" diff --git a/src/algorithms/angle_history.rs b/src/algorithms/angle_history.rs new file mode 100644 index 0000000..8d6142b --- /dev/null +++ b/src/algorithms/angle_history.rs @@ -0,0 +1,183 @@ +use std::collections::HashMap; + +use crate::{ + base::cheat_analyser_base::{CheatAnalyserState, Player, PlayerState}, util::{helpers::{angle_delta}, nocrex::jankguard::JankGuard} +}; + +use crate::lib::algorithm::{CheatAlgorithm, Detection}; +use crate::lib::parameters::{Parameter, Parameters, get_parameter_value}; + +use anyhow::Error; +use serde_json::json; +use steamid_ng::SteamID; +use tf_demo_parser::ParserState; + +#[derive(Default)] +pub struct AngleHistory { + ticks: Vec>, + + jg: JankGuard, + params: Parameters, + detections: Vec, +} + +impl AngleHistory { + pub fn new() -> Self { + Self { + params: HashMap::from([ + ("tick_window".to_string(), Parameter::Int(4)), + ("max_delta_first_third".to_string(), Parameter::Float(0.5)), + ("min_delta_second_third".to_string(), Parameter::Float(10.0)), + ]), + ..Default::default() + } + } +} + +impl<'a> CheatAlgorithm<'a> for AngleHistory { + fn default(&self) -> bool { + true + } + + fn algorithm_name(&self) -> &str { + "angle_history" + } + + fn on_tick( + &mut self, + state: &CheatAnalyserState, + _: &ParserState, + ) -> Result, Error> { + self.jg.on_tick(state); + let ticknum = u32::from(state.tick); + let players = &state.players; + + let tick_window: i32 = get_parameter_value(&self.params, "tick_window"); + let max_delta_first_third: f32 = get_parameter_value(&self.params, "max_delta_first_third"); + let min_delta_second_third: f32 = get_parameter_value(&self.params, "min_delta_second_third"); + + self.ticks.insert(0, HashMap::new()); + self.ticks.truncate(tick_window as usize); + + for player in players.iter().filter(|p| { + p.in_pvs + && p.state == PlayerState::Alive + && p.info.as_ref().is_some_and(|info| info.steam_id != "BOT") + }) { + + let info = match &player.info { + Some(info) => info, + None => continue, + }; + + let steam_id: u64 = u64::from(SteamID::from_steam3(&info.steam_id).unwrap()); + + let ticks_since_event = self + .jg + .teleported(&steam_id, ticknum) + .min(self.jg.spawned(&steam_id, ticknum)); + + if ticks_since_event < 60 { + if ticks_since_event == 0 { + self.detections + .retain(|det| det.player != steam_id || (ticknum - det.tick) > 60); + } + continue; + } + + self.ticks + .get_mut(0) + .unwrap() + .insert(steam_id, player.clone()); + + let current_angle = (player.view_angle, player.pitch_angle); + + let mut match_index: Option = None; + let mut delta_one = 0.0; + + for i in 1..self.ticks.len() { + + let past_player = match self.ticks.get(i).and_then(|m| m.get(&steam_id)) { + Some(p) => p, + None => continue, + }; + + if !(past_player.in_pvs && past_player.state == PlayerState::Alive) { + continue; + } + + let past_angle = (past_player.view_angle, past_player.pitch_angle); + let delta = angle_delta(current_angle, past_angle); + + if delta < max_delta_first_third { + delta_one = delta; + match_index = Some(i); + break; + } + } + + if let Some(i) = match_index { + + let mid = (1 + i) / 2; + let mut mids = vec![mid]; + + if (i - 1) % 2 != 0 { + mids.push(mid + 1); + } + + for m in &mids { + + if let Some(mid_player) = self.ticks.get(*m).and_then(|map| map.get(&steam_id)) { + + let mid_angle = (mid_player.view_angle, mid_player.pitch_angle); + let mid_delta = angle_delta(current_angle, mid_angle); + + if mid_delta > min_delta_second_third + && self.jg.fired(&steam_id, ticknum) <= (i as u32 + 5) { + + self.detections.push(Detection { + tick: ticknum, + algorithm: self.algorithm_name().to_string(), + player: steam_id, + data: json!({ + "angle_current": current_angle, + "angle_middle": (mid_player.view_angle, mid_player.pitch_angle), + "angle_trigger": (player.view_angle, player.pitch_angle), + "delta_1_3": delta_one, + "delta_2_3": mid_delta, + "match_index": i, + "middle_indices": mids, + "middle_trigger": m, + }), + }); + } + } + } + } + } + Ok(vec![]) + } + + fn handled_messages(&self) -> Result, bool> { + self.jg.handled_messages() + } + + fn on_message( + &mut self, + message: &tf_demo_parser::demo::message::Message, + state: &CheatAnalyserState, + parser_state: &ParserState, + tick: tf_demo_parser::demo::data::DemoTick, + ) -> Result, Error> { + self.jg.on_message(message, state, parser_state, tick); + Ok(vec![]) + } + + fn finish(&mut self) -> Result, Error> { + Ok(self.detections.clone()) + } + + fn params(&mut self) -> Option<&mut Parameters> { + Some(&mut self.params) + } +} diff --git a/src/algorithms/backtrack.rs b/src/algorithms/backtrack.rs new file mode 100644 index 0000000..d7c556c --- /dev/null +++ b/src/algorithms/backtrack.rs @@ -0,0 +1,193 @@ +use std::{collections::HashMap}; + +use crate::{base::cheat_analyser_base::{CheatAnalyserState}}; + +use crate::lib::algorithm::{CheatAlgorithm, Detection}; +use crate::lib::parameters::{Parameter, Parameters, get_parameter_value}; + +use anyhow::Error; +use itertools::any; +use serde_json::{Map, Value}; +use steamid_ng::SteamID; +use tf_demo_parser::demo::message::Message; +use tf_demo_parser::demo::gameevent_gen::GameEvent; +use tf_demo_parser::ParserState; + +#[derive(Default)] +pub struct BackTrack { + params: Parameters, +} + +impl BackTrack { + pub fn new() -> Self { + Self { + params: HashMap::from([ + ("distance".to_string(), Parameter::Float(200.0)), + ("max_distance".to_string(), Parameter::Float(600.0)), + ("max_angle_diff".to_string(), Parameter::Float(110.0)), + ]), + ..Default::default() + } + } +} + +fn angle_diff(a: f32, b: f32) -> f32 { + (b - a + 180.0).rem_euclid(360.0) - 180.0 +} + +fn is_backstab(damage: u16, is_crit: bool, weapon_id: u16, victim_health: u16) -> bool { + return is_crit && weapon_id == 7 && (damage > 600 || damage as f32 > victim_health as f32 * 5.5); +} + +impl<'a> CheatAlgorithm<'a> for BackTrack { + fn default(&self) -> bool { + true + } + + fn algorithm_name(&self) -> &str { + "backtrack" + } + + fn on_tick( + &mut self, + _: &CheatAnalyserState, + _: &ParserState, + ) -> Result, Error> { + Ok(vec![]) + } + + fn handled_messages(&self) -> Result, bool> { + Ok(vec![tf_demo_parser::MessageType::GameEvent]) + } + + fn on_message( + &mut self, + message: &tf_demo_parser::demo::message::Message, + state: &CheatAnalyserState, + _: &ParserState, + tick: tf_demo_parser::demo::data::DemoTick, + ) -> Result, Error> { + let mut detections = Vec::new(); + + if let Message::GameEvent(event_msg) = message { + if let GameEvent::PlayerHurt(hurt) = &event_msg.event { + let victim_uid = u32::from(hurt.user_id); + let attacker_uid = u32::from(hurt.attacker); + + let is_crit = hurt.crit; + + let weapon_id = hurt.weapon_id as u16; + let damage_amount = hurt.damage_amount; + + let distance: f32 = get_parameter_value(&self.params, "distance"); + let max_distance: f32 = get_parameter_value(&self.params, "max_distance"); + let max_angle_diff: f32 = get_parameter_value(&self.params, "max_angle_diff"); + + // get steam id64 from uids + let mut attacker_sid = 0; + let mut victim_sid = 0; + for player in &state.players { + if let Some(info) = &player.info { + if u32::from(info.user_id) == attacker_uid { attacker_sid = u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()); + } if u32::from(info.user_id) == victim_uid { victim_sid = u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()); + } + } + } + if any([attacker_sid, victim_sid], |x| x == 0) { + return Ok(vec![]); + } + + let players = &state.players; + + let attacker = match players.iter().find(|x| x.info.as_ref().is_some_and(|info| u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()) == attacker_sid )) { + Some(p) => p, + None => return Ok(vec![]) + }; + let attacker_pos = attacker.position; + + let victim = match players.iter().find(|x| x.info.as_ref().is_some_and(|info| u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()) == victim_sid )) { + Some(p) => p, + None => return Ok(vec![]) + }; + let victim_pos = victim.position; + let victim_health = victim.health; + + if !attacker.in_pvs || !victim.in_pvs { + return Ok(vec![]); + } + + if attacker_uid != victim_uid && is_backstab(damage_amount, is_crit, weapon_id, victim_health) { + + let angle_diff = angle_diff(attacker.view_angle, victim.view_angle).abs(); + + // 3d distance + // let pos_diff = (f32::powi(attacker_pos.x - victim_pos.x, 2) + + // f32::powi(attacker_pos.y - victim_pos.y, 2) + + // f32::powi(attacker_pos.z - victim_pos.z, 2)).sqrt(); + + // 2d distance + let pos_diff = (f32::powi(attacker_pos.x - victim_pos.x, 2) + + f32::powi(attacker_pos.y - victim_pos.y, 2)).sqrt(); + + + + + + + if (pos_diff > distance && pos_diff < max_distance) || (angle_diff > max_angle_diff && pos_diff < max_distance) { + + // let data = json!({ + // "angle_attacker": attacker.view_angle, + // "angle_victim": victim.view_angle, + // "angle_diff": angle_diff, + // "pos_attacker": attacker_pos, + // "pos_victim": victim_pos, + // "distance": pos_diff, + // "damage": damage_amount, + // "type": if angle_diff > max_angle_diff && pos_diff < max_distance {"Angle"} else {"Distance"}, + // "victim_class": victim.class, + // "victim_health": victim_health, + // }); + + // ######################## + + let u200b = "​"; + let data: Vec<(&str, Value)> = vec![ + ("angle_attacker", Value::from(attacker.view_angle)), + ("angle_victim", Value::from(victim.view_angle)), + ("angle_diff", Value::from(angle_diff)), + ("pos_attacker", Value::from(vec![attacker_pos.x,attacker_pos.y,attacker_pos.z])), + ("pos_victim", Value::from(vec![victim_pos.x,victim_pos.y,victim_pos.z])), + ("distance", Value::from(pos_diff)), + ("damage", Value::from(damage_amount)), + ("type", Value::from(if angle_diff > max_angle_diff && pos_diff < max_distance {"Angle" } else { "Distance"})), + ("victim_class", Value::from(victim.class.to_string())), + ("victim_health", Value::from(victim.health)), + ]; + let mut new_data = Map::new(); + for (i, (key, value)) in data.into_iter().enumerate() { new_data.insert(format!("{}{}", u200b.repeat(i), key), value); } + let new_data = Value::Object(new_data); + + detections.push(Detection { + tick: tick.into(), + algorithm: self.algorithm_name().to_string(), + player: attacker_sid, + data: new_data, + }); + } + + + } + } + } + Ok(detections) + } + + fn finish(&mut self) -> Result, Error> { + Ok(vec![]) + } + + fn params(&mut self) -> Option<&mut Parameters> { + Some(&mut self.params) + } +} diff --git a/src/algorithms/double_tap.rs b/src/algorithms/double_tap.rs new file mode 100644 index 0000000..729f965 --- /dev/null +++ b/src/algorithms/double_tap.rs @@ -0,0 +1,192 @@ +use std::collections::HashMap; + +use crate::base::cheat_analyser_base::CheatAnalyserState; + +use crate::lib::algorithm::{CheatAlgorithm, Detection}; +use crate::lib::parameters::{get_parameter_value, Parameter, Parameters}; + +use anyhow::Error; +use serde_json::{Map, Value}; +use steamid_ng::SteamID; +use tf_demo_parser::demo::gameevent_gen::GameEvent; +use tf_demo_parser::demo::message::Message; +use tf_demo_parser::ParserState; + +#[derive(Default)] +pub struct DoubleTap { + params: Parameters, + + shots: HashMap>, +} + +impl DoubleTap { + pub fn new() -> Self { + Self { + params: HashMap::from([ + ("assert_pvs".to_string(), Parameter::Bool(true)), + ("min_tick_scout".to_string(), Parameter::Int(17)), + ("min_tick_heavy".to_string(), Parameter::Int(3)), + ]), + shots: HashMap::new(), + ..Default::default() + } + } +} + +fn is_cleaver_or_wrap_assassin(weapon_id: u32, damage: u32) -> bool { + (weapon_id == 16 && damage <= 8 && damage >= 3) || (weapon_id == 16 && damage == 50) +} + +impl<'a> CheatAlgorithm<'a> for DoubleTap { + fn default(&self) -> bool { + true + } + + fn algorithm_name(&self) -> &str { + "doubletap" + } + + fn on_tick( + &mut self, + _: &CheatAnalyserState, + _: &ParserState, + ) -> Result, Error> { + Ok(vec![]) + } + + fn handled_messages(&self) -> Result, bool> { + Ok(vec![tf_demo_parser::MessageType::GameEvent]) + } + + fn on_message( + &mut self, + message: &tf_demo_parser::demo::message::Message, + state: &CheatAnalyserState, + _: &ParserState, + tick: tf_demo_parser::demo::data::DemoTick, + ) -> Result, Error> { + let mut detections = Vec::new(); + let ticknum = u32::from(tick); + let players = &state.players; + + if let Message::GameEvent(event_msg) = message { + if let GameEvent::PlayerHurt(hurt) = &event_msg.event { + let assert_pvs: bool = get_parameter_value(&self.params, "assert_pvs"); + let min_tick_scout: i32 = get_parameter_value(&self.params, "min_tick_scout primary weapon"); + let min_tick_heavy: i32 = get_parameter_value(&self.params, "min_tick_heavy primary weapon"); + + // format; + // weapon id, min ticks + let weapon_mapping = HashMap::from([ + (16, min_tick_scout as u32), // scout primary + (18, min_tick_heavy as u32), // heavy primary + ]); + + let dmg = hurt.damage_amount as u32; + let weapon = hurt.weapon_id as u32; + + if is_cleaver_or_wrap_assassin(weapon, dmg) { + // ignore wrap assassin bleed + return Ok(vec![]); + } + + let attacker_uid = u32::from(hurt.attacker); + let victim_uid = u32::from(hurt.user_id); + + // get steam id64 from uids + let mut attacker_sid = 0; + let mut victim_sid = 0; + for player in &state.players { + if let Some(info) = &player.info { + if u32::from(info.user_id) == attacker_uid { + attacker_sid = + u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()); + } else if u32::from(info.user_id) == victim_uid { + victim_sid = + u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()); + } + } + } + if attacker_sid == 0 || victim_sid == 0 { + return Ok(vec![]); + } + + let attacker = match players.iter().find(|x| { + x.info.as_ref().is_some_and(|info| { + u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()) + == attacker_sid + }) + }) { + Some(p) => p, + None => return Ok(vec![]), + }; + + if assert_pvs && !attacker.in_pvs { + return Ok(vec![]); + } + + let shots = self.shots.entry(attacker_sid).or_default(); + + let past_shot = shots.clone(); + + shots.clear(); + shots.extend([ticknum, weapon, victim_uid, dmg]); + + if past_shot.len() < 2 { + return Ok(vec![]); + } + + let past_shot: [u32; 4] = match past_shot.try_into() { + Ok(arr) => arr, + Err(_) => return Ok(vec![]), // or handle error + }; + + let [past_tick, past_weapon, past_victim, past_dmg] = past_shot; + let Some(&min_diff) = weapon_mapping.get(&weapon) else { + return Ok(vec![]); + }; + + if past_weapon != weapon || past_victim != victim_uid { + return Ok(vec![]); + } + + let diff = ticknum - past_tick; + + if diff < min_diff && diff > 0 { + let u200b = "​"; + let data: Vec<(&str, Value)> = vec![ + ("class", Value::from(attacker.class.to_string())), + ("tick_1", Value::from(past_tick)), + ("tick_2", Value::from(ticknum)), + ("tick_diff", Value::from(diff)), + ("victim", Value::from(victim_uid)), + ("weapon_id", Value::from(weapon)), + ("damage_1", Value::from(past_dmg)), + ("damage_2", Value::from(dmg)), + ]; + let mut new_data = Map::new(); + for (i, (key, value)) in data.into_iter().enumerate() { + new_data.insert(format!("{}{}", u200b.repeat(i), key), value); + } + let new_data = Value::Object(new_data); + + detections.push(Detection { + tick: tick.into(), + algorithm: self.algorithm_name().to_string(), + player: attacker_sid, + data: new_data, + }); + } + } + } + Ok(detections) + } + + fn finish(&mut self) -> Result, Error> { + Ok(vec![]) + } + + fn params(&mut self) -> Option<&mut Parameters> { + Some(&mut self.params) + } +} diff --git a/src/algorithms/nocrex/old algos/old-double_tap.rs b/src/algorithms/nocrex/old algos/old-double_tap.rs new file mode 100644 index 0000000..de8f142 --- /dev/null +++ b/src/algorithms/nocrex/old algos/old-double_tap.rs @@ -0,0 +1,173 @@ +use std::{collections::HashMap}; + +use crate::{base::cheat_analyser_base::CheatAnalyserState}; + +use crate::lib::algorithm::{CheatAlgorithm, Detection}; +use crate::lib::parameters::{Parameter, Parameters, get_parameter_value}; + +use anyhow::Error; +use serde_json::{Map, Value}; +use steamid_ng::SteamID; +use tf_demo_parser::demo::message::Message; +use tf_demo_parser::demo::gameevent_gen::GameEvent; +use tf_demo_parser::ParserState; + +#[derive(Default)] +pub struct DoubleTap { + params: Parameters, + + shots: HashMap> +} + +impl DoubleTap { + pub fn new() -> Self { + Self { + params: HashMap::from([ + ("assert_pvs".to_string(), Parameter::Bool(true)), + ("min_tick".to_string(), Parameter::Int(20)), + ("weapon_id".to_string(), Parameter::Int(16)), + ]), + shots: HashMap::new(), + ..Default::default() + } + } +} + +impl<'a> CheatAlgorithm<'a> for DoubleTap { + fn default(&self) -> bool { + true + } + + fn algorithm_name(&self) -> &str { + "doubletap" + } + + fn on_tick( + &mut self, + _: &CheatAnalyserState, + _: &ParserState, + ) -> Result, Error> { + Ok(vec![]) + } + + fn handled_messages(&self) -> Result, bool> { + Ok(vec![tf_demo_parser::MessageType::GameEvent]) + } + + fn on_message( + &mut self, + message: &tf_demo_parser::demo::message::Message, + state: &CheatAnalyserState, + _: &ParserState, + tick: tf_demo_parser::demo::data::DemoTick, + ) -> Result, Error> { + let mut detections = Vec::new(); + let ticknum = u32::from(tick); + let players = &state.players; + + if let Message::GameEvent(event_msg) = message { + if let GameEvent::PlayerHurt(hurt) = &event_msg.event { + let assert_pvs: bool = get_parameter_value(&self.params, "assert_pvs"); + let min_tick: i32 = get_parameter_value(&self.params, "min_tick"); + let weapon_id: i32 = get_parameter_value(&self.params, "weapon_id"); + let weapon_mapping = HashMap::from([ + (weapon_id as u32, min_tick as u32), // scout primary + ]); + + let dmg = hurt.damage_amount as u32; + + if dmg == 4 { // ignore wrap assassin bleed + return Ok(vec![]); + } + + let attacker_uid = u32::from(hurt.attacker); + let victim_uid = u32::from(hurt.user_id); + let weapon = hurt.weapon_id as u32; + + // get steam id64 from uids + let mut attacker_sid = 0; + for player in &state.players { + if let Some(info) = &player.info { + if u32::from(info.user_id) == attacker_uid { attacker_sid = u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()); + } + } + } + if attacker_sid == 0 { + return Ok(vec![]); + } + + if assert_pvs { + let attacker = match players.iter().find(|x| x.info.as_ref().is_some_and(|info| u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()) == attacker_sid )) { + Some(p) => p, + None => return Ok(vec![]) + }; + if !attacker.in_pvs { + return Ok(vec![]); + } + } + + let shots = self.shots.entry(attacker_sid).or_default(); + + let past_shot = shots.clone(); + + shots.clear(); + shots.extend([ticknum, weapon, victim_uid, dmg]); + + if past_shot.len() < 2 { + return Ok(vec![]); + } + + let past_shot: [u32; 4] = match past_shot.try_into() { + Ok(arr) => arr, + Err(_) => return Ok(vec![]), // or handle error + }; + + let [past_tick, past_weapon, past_victim, past_dmg] = past_shot; + let Some(&min_diff) = weapon_mapping.get(&weapon) else { + return Ok(vec![]); + }; + + if past_weapon != weapon || + past_victim != victim_uid { + return Ok(vec![]); + } + + let diff = ticknum - past_tick; + + if diff < min_diff && diff > 0 { + let u200b = "​"; + let data: Vec<(&str, Value)> = vec![ + ("tick_1", Value::from(past_tick)), + ("tick_2", Value::from(ticknum)), + ("tick_diff", Value::from(diff)), + ("victim", Value::from(victim_uid)), + ("weapon_id", Value::from(weapon)), + ("damage_1", Value::from(past_dmg)), + ("damage_2", Value::from(dmg)), + ]; + let mut new_data = Map::new(); + for (i, (key, value)) in data.into_iter().enumerate() { new_data.insert(format!("{}{}", u200b.repeat(i), key), value); } + let new_data = Value::Object(new_data); + + detections.push(Detection { + tick: tick.into(), + algorithm: self.algorithm_name().to_string(), + player: attacker_sid, + data: new_data, + }); + } + + + } + } + Ok(detections) + } + + fn finish(&mut self) -> Result, Error> { + Ok(vec![]) + } + + fn params(&mut self) -> Option<&mut Parameters> { + Some(&mut self.params) + } +} diff --git a/src/algorithms/nocrex/old algos/old-oob_pitch.rs b/src/algorithms/nocrex/old algos/old-oob_pitch.rs new file mode 100644 index 0000000..4aafb1d --- /dev/null +++ b/src/algorithms/nocrex/old algos/old-oob_pitch.rs @@ -0,0 +1,90 @@ +// Written by Nocrex + +use std::collections::{HashMap, HashSet}; + +use crate::{ + base::cheat_analyser_base::{CheatAnalyserState, PlayerState} +}; +use anyhow::Error; +use serde_json::json; +use steamid_ng::SteamID; +use tf_demo_parser::ParserState; + +use crate::lib::algorithm::{CheatAlgorithm, Detection}; +use crate::lib::parameters::{Parameter, Parameters, get_parameter_value}; + +pub struct OOBPitch { + last_detections: HashSet, + + params: Parameters, +} + +impl OOBPitch { + pub fn new() -> Self { + let analyser: OOBPitch = OOBPitch { + last_detections: HashSet::new(), + params: HashMap::from([ + ("min_pitch".to_string(), Parameter::Float(-89.999)), + ("max_pitch".to_string(), Parameter::Float(89.999)), + ]), + }; + analyser + } +} + +impl<'a> CheatAlgorithm<'a> for OOBPitch { + fn default(&self) -> bool { + true + } + + fn algorithm_name(&self) -> &str { + "nocrex/oob_pitch" + } + + fn on_tick( + &mut self, + state: &CheatAnalyserState, + _: &ParserState, + ) -> Result, Error> { + let ticknum = u32::from(state.tick); + let players = &state.players; + + let mut submitted_detections = Vec::new(); + + let mut detections = HashSet::new(); + + let min_pitch: f32 = get_parameter_value(&self.params, "min_pitch"); + let max_pitch: f32 = get_parameter_value(&self.params, "max_pitch"); + + for player in players.iter().filter(|p| { + p.in_pvs + && p.state == PlayerState::Alive + && p.info.as_ref().is_some_and(|info| info.steam_id != "BOT") + }) { + let info = match &player.info { + Some(info) => info, + None => continue, + }; + + let steam_id = &info.steam_id; + + if !(min_pitch..=max_pitch).contains(&player.pitch_angle) { + detections.insert(steam_id.clone()); + if !self.last_detections.contains(steam_id){ + submitted_detections.push(Detection { + tick: ticknum, + algorithm: self.algorithm_name().to_string(), + player: u64::from(SteamID::from_steam3(&steam_id).unwrap()), + data: json!({ "pitch": player.pitch_angle }), + }); + } + } + } + self.last_detections = detections; + Ok(submitted_detections) + } + + fn params(&mut self) -> Option<&mut Parameters> { + Some(&mut self.params) + } +} diff --git a/src/algorithms/nocrex/oob_pitch.rs b/src/algorithms/nocrex/oob_pitch.rs index 4aafb1d..4c44bbf 100644 --- a/src/algorithms/nocrex/oob_pitch.rs +++ b/src/algorithms/nocrex/oob_pitch.rs @@ -1,20 +1,22 @@ // Written by Nocrex -use std::collections::{HashMap, HashSet}; +use std::{collections::{HashMap, HashSet}}; use crate::{ base::cheat_analyser_base::{CheatAnalyserState, PlayerState} }; -use anyhow::Error; + +use anyhow::{Error, Ok}; use serde_json::json; use steamid_ng::SteamID; -use tf_demo_parser::ParserState; +use tf_demo_parser::{ParserState, demo::message::Message}; use crate::lib::algorithm::{CheatAlgorithm, Detection}; use crate::lib::parameters::{Parameter, Parameters, get_parameter_value}; pub struct OOBPitch { last_detections: HashSet, + server_name: String, params: Parameters, } @@ -23,6 +25,7 @@ impl OOBPitch { pub fn new() -> Self { let analyser: OOBPitch = OOBPitch { last_detections: HashSet::new(), + server_name: "".to_string(), params: HashMap::from([ ("min_pitch".to_string(), Parameter::Float(-89.999)), ("max_pitch".to_string(), Parameter::Float(89.999)), @@ -41,47 +44,68 @@ impl<'a> CheatAlgorithm<'a> for OOBPitch { "nocrex/oob_pitch" } - fn on_tick( - &mut self, + fn handled_messages(&self) -> Result, bool> { + Err(true) + } + + fn on_message(&mut self, + message: &Message, state: &CheatAnalyserState, _: &ParserState, - ) -> Result, Error> { - let ticknum = u32::from(state.tick); - let players = &state.players; - + _: tf_demo_parser::demo::data::DemoTick) -> Result, Error> { let mut submitted_detections = Vec::new(); - let mut detections = HashSet::new(); - - let min_pitch: f32 = get_parameter_value(&self.params, "min_pitch"); - let max_pitch: f32 = get_parameter_value(&self.params, "max_pitch"); - - for player in players.iter().filter(|p| { - p.in_pvs - && p.state == PlayerState::Alive - && p.info.as_ref().is_some_and(|info| info.steam_id != "BOT") - }) { - let info = match &player.info { - Some(info) => info, - None => continue, - }; - - let steam_id = &info.steam_id; - - if !(min_pitch..=max_pitch).contains(&player.pitch_angle) { - detections.insert(steam_id.clone()); - if !self.last_detections.contains(steam_id){ - submitted_detections.push(Detection { - tick: ticknum, - algorithm: self.algorithm_name().to_string(), - player: u64::from(SteamID::from_steam3(&steam_id).unwrap()), - data: json!({ "pitch": player.pitch_angle }), - }); + if let Message::ServerInfo(event) = message { + if self.server_name == "" { + self.server_name = event.server_name.trim().to_string(); + } + } + + if let Message::NetTick(_) = message { + let ticknum = u32::from(state.tick); + let players = &state.players; + + let mut detections = HashSet::new(); + + let min_pitch: f32 = get_parameter_value(&self.params, "min_pitch"); + let max_pitch: f32 = get_parameter_value(&self.params, "max_pitch"); + + let is_valve_server = self.server_name.starts_with("Valve Matchmaking Server"); + + for player in players.iter().filter(|p| { + p.in_pvs + && p.state == PlayerState::Alive + && p.info.as_ref().is_some_and(|info| info.steam_id != "BOT") + }) { + let info = match &player.info { + Some(info) => info, + None => continue, + }; + + let steam_id = &info.steam_id; + + if !(min_pitch..=max_pitch).contains(&player.pitch_angle) { + detections.insert(steam_id.clone()); + if !self.last_detections.contains(steam_id){ + submitted_detections.push(Detection { + tick: ticknum, + algorithm: self.algorithm_name().to_string(), + player: u64::from(SteamID::from_steam3(&steam_id).unwrap()), + data: json!({ + "pitch": player.pitch_angle, + "valve_server": is_valve_server + }), + }); + } } } + + self.last_detections = detections; + } - self.last_detections = detections; + Ok(submitted_detections) + } fn params(&mut self) -> Option<&mut Parameters> { diff --git a/src/lib.rs b/src/lib.rs index b2062d6..d742a70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,9 @@ pub mod algorithms { pub mod viewangles_180degrees; pub mod viewangles_to_csv; pub mod write_to_file; + pub mod angle_history; + pub mod backtrack; + pub mod double_tap; pub mod nocrex { pub mod aimsnap; pub mod angle_repeat; diff --git a/src/lib/algorithm.rs b/src/lib/algorithm.rs index 3042cd5..6f54cc9 100644 --- a/src/lib/algorithm.rs +++ b/src/lib/algorithm.rs @@ -1,10 +1,14 @@ + + // Import algorithm struct here. pub use crate::algorithms::{ all_messages::AllMessages, viewangles_180degrees::ViewAngles180Degrees, viewangles_to_csv::ViewAnglesToCSV, write_to_file::WriteToFile, - + angle_history::AngleHistory, + backtrack::BackTrack, + double_tap::DoubleTap, nocrex:: { aimsnap::AimSnap, angle_repeat::AngleRepeat, @@ -32,7 +36,10 @@ pub fn get_algorithms() -> Vec + Send>> { Box::new(WriteToFile::new()), Box::new(OOBPitch::new()), Box::new(AngleRepeat::new()), + Box::new(AngleHistory::new()), Box::new(AimSnap::new()), + Box::new(BackTrack::new()), + Box::new(DoubleTap::new()), ] } diff --git a/src/lib/parameters.rs b/src/lib/parameters.rs index a345714..71a5f39 100644 --- a/src/lib/parameters.rs +++ b/src/lib/parameters.rs @@ -145,4 +145,5 @@ where }, None => panic!("Parameter {} not found", param_name), } -} \ No newline at end of file +} + From dbaa05ab54026cceb52589ce0a8cd2dc15239be5 Mon Sep 17 00:00:00 2001 From: BrokenMartin Date: Tue, 2 Jun 2026 21:26:15 +0200 Subject: [PATCH 08/10] fixed crash fixed bug with double_tap crashing program --- src/algorithms/double_tap.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/algorithms/double_tap.rs b/src/algorithms/double_tap.rs index 729f965..7e53dbc 100644 --- a/src/algorithms/double_tap.rs +++ b/src/algorithms/double_tap.rs @@ -72,8 +72,8 @@ impl<'a> CheatAlgorithm<'a> for DoubleTap { if let Message::GameEvent(event_msg) = message { if let GameEvent::PlayerHurt(hurt) = &event_msg.event { let assert_pvs: bool = get_parameter_value(&self.params, "assert_pvs"); - let min_tick_scout: i32 = get_parameter_value(&self.params, "min_tick_scout primary weapon"); - let min_tick_heavy: i32 = get_parameter_value(&self.params, "min_tick_heavy primary weapon"); + let min_tick_scout: i32 = get_parameter_value(&self.params, "min_tick_scout"); + let min_tick_heavy: i32 = get_parameter_value(&self.params, "min_tick_heavy"); // format; // weapon id, min ticks From d11f340ae76674e0a850195ecc5acf253287788d Mon Sep 17 00:00:00 2001 From: Nocrex <20463817+Nocrex@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:37:52 +0200 Subject: [PATCH 09/10] tidy up changes --- Cargo.toml | 2 +- src/algorithms/angle_history.rs | 1 + src/algorithms/backtrack.rs | 1 + src/algorithms/double_tap.rs | 1 + .../nocrex/old algos/old-double_tap.rs | 173 ------------------ .../nocrex/old algos/old-oob_pitch.rs | 90 --------- src/lib/algorithm.rs | 2 - src/lib/parameters.rs | 3 +- 8 files changed, 5 insertions(+), 268 deletions(-) delete mode 100644 src/algorithms/nocrex/old algos/old-double_tap.rs delete mode 100644 src/algorithms/nocrex/old algos/old-oob_pitch.rs diff --git a/Cargo.toml b/Cargo.toml index 3a63e0b..cc4c519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "analysis-template" -version = "0.2.2" +version = "0.1.0" edition = "2021" default-run = "cli" diff --git a/src/algorithms/angle_history.rs b/src/algorithms/angle_history.rs index 8d6142b..37bfc64 100644 --- a/src/algorithms/angle_history.rs +++ b/src/algorithms/angle_history.rs @@ -1,3 +1,4 @@ +// Written by Tellta use std::collections::HashMap; use crate::{ diff --git a/src/algorithms/backtrack.rs b/src/algorithms/backtrack.rs index d7c556c..feada9f 100644 --- a/src/algorithms/backtrack.rs +++ b/src/algorithms/backtrack.rs @@ -1,3 +1,4 @@ +// Written by Tellta use std::{collections::HashMap}; use crate::{base::cheat_analyser_base::{CheatAnalyserState}}; diff --git a/src/algorithms/double_tap.rs b/src/algorithms/double_tap.rs index 7e53dbc..db4b400 100644 --- a/src/algorithms/double_tap.rs +++ b/src/algorithms/double_tap.rs @@ -1,3 +1,4 @@ +// Written by Tellta use std::collections::HashMap; use crate::base::cheat_analyser_base::CheatAnalyserState; diff --git a/src/algorithms/nocrex/old algos/old-double_tap.rs b/src/algorithms/nocrex/old algos/old-double_tap.rs deleted file mode 100644 index de8f142..0000000 --- a/src/algorithms/nocrex/old algos/old-double_tap.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::{collections::HashMap}; - -use crate::{base::cheat_analyser_base::CheatAnalyserState}; - -use crate::lib::algorithm::{CheatAlgorithm, Detection}; -use crate::lib::parameters::{Parameter, Parameters, get_parameter_value}; - -use anyhow::Error; -use serde_json::{Map, Value}; -use steamid_ng::SteamID; -use tf_demo_parser::demo::message::Message; -use tf_demo_parser::demo::gameevent_gen::GameEvent; -use tf_demo_parser::ParserState; - -#[derive(Default)] -pub struct DoubleTap { - params: Parameters, - - shots: HashMap> -} - -impl DoubleTap { - pub fn new() -> Self { - Self { - params: HashMap::from([ - ("assert_pvs".to_string(), Parameter::Bool(true)), - ("min_tick".to_string(), Parameter::Int(20)), - ("weapon_id".to_string(), Parameter::Int(16)), - ]), - shots: HashMap::new(), - ..Default::default() - } - } -} - -impl<'a> CheatAlgorithm<'a> for DoubleTap { - fn default(&self) -> bool { - true - } - - fn algorithm_name(&self) -> &str { - "doubletap" - } - - fn on_tick( - &mut self, - _: &CheatAnalyserState, - _: &ParserState, - ) -> Result, Error> { - Ok(vec![]) - } - - fn handled_messages(&self) -> Result, bool> { - Ok(vec![tf_demo_parser::MessageType::GameEvent]) - } - - fn on_message( - &mut self, - message: &tf_demo_parser::demo::message::Message, - state: &CheatAnalyserState, - _: &ParserState, - tick: tf_demo_parser::demo::data::DemoTick, - ) -> Result, Error> { - let mut detections = Vec::new(); - let ticknum = u32::from(tick); - let players = &state.players; - - if let Message::GameEvent(event_msg) = message { - if let GameEvent::PlayerHurt(hurt) = &event_msg.event { - let assert_pvs: bool = get_parameter_value(&self.params, "assert_pvs"); - let min_tick: i32 = get_parameter_value(&self.params, "min_tick"); - let weapon_id: i32 = get_parameter_value(&self.params, "weapon_id"); - let weapon_mapping = HashMap::from([ - (weapon_id as u32, min_tick as u32), // scout primary - ]); - - let dmg = hurt.damage_amount as u32; - - if dmg == 4 { // ignore wrap assassin bleed - return Ok(vec![]); - } - - let attacker_uid = u32::from(hurt.attacker); - let victim_uid = u32::from(hurt.user_id); - let weapon = hurt.weapon_id as u32; - - // get steam id64 from uids - let mut attacker_sid = 0; - for player in &state.players { - if let Some(info) = &player.info { - if u32::from(info.user_id) == attacker_uid { attacker_sid = u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()); - } - } - } - if attacker_sid == 0 { - return Ok(vec![]); - } - - if assert_pvs { - let attacker = match players.iter().find(|x| x.info.as_ref().is_some_and(|info| u64::from(SteamID::from_steam3(&info.steam_id).unwrap_or_default()) == attacker_sid )) { - Some(p) => p, - None => return Ok(vec![]) - }; - if !attacker.in_pvs { - return Ok(vec![]); - } - } - - let shots = self.shots.entry(attacker_sid).or_default(); - - let past_shot = shots.clone(); - - shots.clear(); - shots.extend([ticknum, weapon, victim_uid, dmg]); - - if past_shot.len() < 2 { - return Ok(vec![]); - } - - let past_shot: [u32; 4] = match past_shot.try_into() { - Ok(arr) => arr, - Err(_) => return Ok(vec![]), // or handle error - }; - - let [past_tick, past_weapon, past_victim, past_dmg] = past_shot; - let Some(&min_diff) = weapon_mapping.get(&weapon) else { - return Ok(vec![]); - }; - - if past_weapon != weapon || - past_victim != victim_uid { - return Ok(vec![]); - } - - let diff = ticknum - past_tick; - - if diff < min_diff && diff > 0 { - let u200b = "​"; - let data: Vec<(&str, Value)> = vec![ - ("tick_1", Value::from(past_tick)), - ("tick_2", Value::from(ticknum)), - ("tick_diff", Value::from(diff)), - ("victim", Value::from(victim_uid)), - ("weapon_id", Value::from(weapon)), - ("damage_1", Value::from(past_dmg)), - ("damage_2", Value::from(dmg)), - ]; - let mut new_data = Map::new(); - for (i, (key, value)) in data.into_iter().enumerate() { new_data.insert(format!("{}{}", u200b.repeat(i), key), value); } - let new_data = Value::Object(new_data); - - detections.push(Detection { - tick: tick.into(), - algorithm: self.algorithm_name().to_string(), - player: attacker_sid, - data: new_data, - }); - } - - - } - } - Ok(detections) - } - - fn finish(&mut self) -> Result, Error> { - Ok(vec![]) - } - - fn params(&mut self) -> Option<&mut Parameters> { - Some(&mut self.params) - } -} diff --git a/src/algorithms/nocrex/old algos/old-oob_pitch.rs b/src/algorithms/nocrex/old algos/old-oob_pitch.rs deleted file mode 100644 index 4aafb1d..0000000 --- a/src/algorithms/nocrex/old algos/old-oob_pitch.rs +++ /dev/null @@ -1,90 +0,0 @@ -// Written by Nocrex - -use std::collections::{HashMap, HashSet}; - -use crate::{ - base::cheat_analyser_base::{CheatAnalyserState, PlayerState} -}; -use anyhow::Error; -use serde_json::json; -use steamid_ng::SteamID; -use tf_demo_parser::ParserState; - -use crate::lib::algorithm::{CheatAlgorithm, Detection}; -use crate::lib::parameters::{Parameter, Parameters, get_parameter_value}; - -pub struct OOBPitch { - last_detections: HashSet, - - params: Parameters, -} - -impl OOBPitch { - pub fn new() -> Self { - let analyser: OOBPitch = OOBPitch { - last_detections: HashSet::new(), - params: HashMap::from([ - ("min_pitch".to_string(), Parameter::Float(-89.999)), - ("max_pitch".to_string(), Parameter::Float(89.999)), - ]), - }; - analyser - } -} - -impl<'a> CheatAlgorithm<'a> for OOBPitch { - fn default(&self) -> bool { - true - } - - fn algorithm_name(&self) -> &str { - "nocrex/oob_pitch" - } - - fn on_tick( - &mut self, - state: &CheatAnalyserState, - _: &ParserState, - ) -> Result, Error> { - let ticknum = u32::from(state.tick); - let players = &state.players; - - let mut submitted_detections = Vec::new(); - - let mut detections = HashSet::new(); - - let min_pitch: f32 = get_parameter_value(&self.params, "min_pitch"); - let max_pitch: f32 = get_parameter_value(&self.params, "max_pitch"); - - for player in players.iter().filter(|p| { - p.in_pvs - && p.state == PlayerState::Alive - && p.info.as_ref().is_some_and(|info| info.steam_id != "BOT") - }) { - let info = match &player.info { - Some(info) => info, - None => continue, - }; - - let steam_id = &info.steam_id; - - if !(min_pitch..=max_pitch).contains(&player.pitch_angle) { - detections.insert(steam_id.clone()); - if !self.last_detections.contains(steam_id){ - submitted_detections.push(Detection { - tick: ticknum, - algorithm: self.algorithm_name().to_string(), - player: u64::from(SteamID::from_steam3(&steam_id).unwrap()), - data: json!({ "pitch": player.pitch_angle }), - }); - } - } - } - self.last_detections = detections; - Ok(submitted_detections) - } - - fn params(&mut self) -> Option<&mut Parameters> { - Some(&mut self.params) - } -} diff --git a/src/lib/algorithm.rs b/src/lib/algorithm.rs index 6f54cc9..97aa34f 100644 --- a/src/lib/algorithm.rs +++ b/src/lib/algorithm.rs @@ -1,5 +1,3 @@ - - // Import algorithm struct here. pub use crate::algorithms::{ all_messages::AllMessages, diff --git a/src/lib/parameters.rs b/src/lib/parameters.rs index 71a5f39..a345714 100644 --- a/src/lib/parameters.rs +++ b/src/lib/parameters.rs @@ -145,5 +145,4 @@ where }, None => panic!("Parameter {} not found", param_name), } -} - +} \ No newline at end of file From 9d9b3c46c5e0642c76fddf7fa67b1d802caccc4f Mon Sep 17 00:00:00 2001 From: Nocrex <20463817+Nocrex@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:41:27 +0200 Subject: [PATCH 10/10] Update oob_pitch.rs --- src/algorithms/nocrex/oob_pitch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/algorithms/nocrex/oob_pitch.rs b/src/algorithms/nocrex/oob_pitch.rs index 4c44bbf..5129ba1 100644 --- a/src/algorithms/nocrex/oob_pitch.rs +++ b/src/algorithms/nocrex/oob_pitch.rs @@ -6,7 +6,7 @@ use crate::{ base::cheat_analyser_base::{CheatAnalyserState, PlayerState} }; -use anyhow::{Error, Ok}; +use anyhow::Error; use serde_json::json; use steamid_ng::SteamID; use tf_demo_parser::{ParserState, demo::message::Message}; @@ -45,7 +45,7 @@ impl<'a> CheatAlgorithm<'a> for OOBPitch { } fn handled_messages(&self) -> Result, bool> { - Err(true) + Ok(vec![tf_demo_parser::MessageType::ServerInfo, tf_demo_parser::MessageType::NetTick]) } fn on_message(&mut self,