Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions src/algorithms/angle_history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Written by Tellta
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<HashMap<u64, Player>>,

jg: JankGuard,
params: Parameters,
detections: Vec<Detection>,
}

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<Vec<Detection>, 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<usize> = 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<Vec<tf_demo_parser::MessageType>, 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<Vec<Detection>, Error> {
self.jg.on_message(message, state, parser_state, tick);
Ok(vec![])
}

fn finish(&mut self) -> Result<Vec<Detection>, Error> {
Ok(self.detections.clone())
}

fn params(&mut self) -> Option<&mut Parameters> {
Some(&mut self.params)
}
}
194 changes: 194 additions & 0 deletions src/algorithms/backtrack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Written by Tellta
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<Vec<Detection>, Error> {
Ok(vec![])
}

fn handled_messages(&self) -> Result<Vec<tf_demo_parser::MessageType>, 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<Vec<Detection>, 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<Vec<Detection>, Error> {
Ok(vec![])
}

fn params(&mut self) -> Option<&mut Parameters> {
Some(&mut self.params)
}
}
Loading