1505 lines
51 KiB
Rust
1505 lines
51 KiB
Rust
mod sdk;
|
||
|
||
use sdk::*;
|
||
use std::ffi::{c_char, c_void};
|
||
use std::ffi::CStr;
|
||
use std::ptr;
|
||
use std::sync::OnceLock;
|
||
use libc::{dlopen, dlsym, RTLD_LAZY, RTLD_NOLOAD, mprotect, PROT_READ, PROT_WRITE, PROT_EXEC};
|
||
use std::fs::OpenOptions;
|
||
use std::io::Write;
|
||
use glow::HasContext;
|
||
|
||
static mut ORIG_CLIENT: Option<Client> = None;
|
||
static mut HOOKS_APPLIED: bool = false;
|
||
static mut ORIG_CREATEMOVE: Option<unsafe extern "C" fn(f32, *mut UserCmd, i32)> = None;
|
||
static mut ENGINE: *mut Engine = ptr::null_mut();
|
||
static mut PMOVE: *mut PlayerMove = ptr::null_mut();
|
||
static mut LOCALPLAYER: *mut Entity = ptr::null_mut();
|
||
static mut WAS_ONGROUND: bool = false;
|
||
|
||
static mut CLIENT_USERMSGS: *mut UserMsg = ptr::null_mut();
|
||
static mut ORIG_TEAMINFO: Option<unsafe extern "C" fn(usize, *const c_char, i32, *mut c_void) -> i32> = None;
|
||
static mut ORIG_HEALTH: Option<unsafe extern "C" fn(usize, *const c_char, i32, *mut c_void) -> i32> = None;
|
||
static mut ORIG_SCOREATTRIB: Option<unsafe extern "C" fn(usize, *const c_char, i32, *mut c_void) -> i32> = None;
|
||
static mut ORIG_RADAR: Option<unsafe extern "C" fn(usize, *const c_char, i32, *mut c_void) -> i32> = None;
|
||
static mut ORIG_RESETHUD: Option<unsafe extern "C" fn(usize, *const c_char, i32, *mut c_void) -> i32> = None;
|
||
|
||
static mut PLAYER_TEAM: [[u8; 16]; 33] = [[0; 16]; 33];
|
||
|
||
#[derive(Clone, Copy)]
|
||
struct PlayerData {
|
||
pos: Vector,
|
||
health: i32,
|
||
is_alive: bool,
|
||
last_update: f32,
|
||
}
|
||
|
||
static mut PLAYER_DATA: [PlayerData; 33] = [PlayerData {
|
||
pos: Vector { x: 0.0, y: 0.0, z: 0.0 },
|
||
health: 0,
|
||
is_alive: false,
|
||
last_update: 0.0,
|
||
}; 33];
|
||
|
||
// Parsemsg globals
|
||
static mut PARSEMSG_FLAGS: i32 = 0;
|
||
static mut PARSEMSG_SIZE: i32 = 0;
|
||
static mut PARSEMSG_MAXSIZE: i32 = 0;
|
||
static mut PARSEMSG_READSIZE: i32 = 0;
|
||
static mut PARSEMSG_READCOUNT: i32 = 0;
|
||
static mut PARSEMSG_DATA: *mut u8 = ptr::null_mut();
|
||
|
||
struct Settings {
|
||
show_menu: bool,
|
||
last_rshift_down: bool,
|
||
mouse_grabbed: bool,
|
||
have_angles: bool,
|
||
last_angles: Vector,
|
||
bhop_enabled: bool,
|
||
faststop_enabled: bool,
|
||
autostrafe_enabled: bool,
|
||
esp_enabled: bool,
|
||
wh_enabled: bool,
|
||
}
|
||
|
||
static mut SETTINGS: Settings = Settings {
|
||
show_menu: false,
|
||
last_rshift_down: false,
|
||
mouse_grabbed: false,
|
||
have_angles: false,
|
||
last_angles: Vector { x: 0.0, y: 0.0, z: 0.0 },
|
||
bhop_enabled: false,
|
||
faststop_enabled: false,
|
||
autostrafe_enabled: false,
|
||
esp_enabled: true,
|
||
wh_enabled: true,
|
||
};
|
||
|
||
static mut VIEW_ORIGIN: Vector = Vector { x: 0.0, y: 0.0, z: 0.0 };
|
||
static GET_PLAYER_INFO: OnceLock<Option<GetPlayerInfoFn>> = OnceLock::new();
|
||
|
||
type GetPlayerInfoFn = unsafe extern "C" fn(i32, *mut PlayerInfo);
|
||
|
||
fn parsemsg_begin(buf: *mut c_void, size_arg: i32, flags_arg: i32) {
|
||
unsafe {
|
||
PARSEMSG_FLAGS = flags_arg;
|
||
PARSEMSG_SIZE = size_arg;
|
||
PARSEMSG_MAXSIZE = 0;
|
||
PARSEMSG_READSIZE = 0;
|
||
PARSEMSG_READCOUNT = 0;
|
||
PARSEMSG_DATA = buf as *mut u8;
|
||
}
|
||
}
|
||
|
||
fn parsemsg_canread() -> bool {
|
||
unsafe {
|
||
if PARSEMSG_MAXSIZE == 0 {
|
||
return PARSEMSG_READCOUNT + PARSEMSG_READSIZE <= PARSEMSG_SIZE;
|
||
}
|
||
return PARSEMSG_READCOUNT + PARSEMSG_READSIZE <= PARSEMSG_SIZE && PARSEMSG_SIZE <= PARSEMSG_MAXSIZE;
|
||
}
|
||
}
|
||
|
||
fn parsemsg_readbyte() -> u8 {
|
||
unsafe {
|
||
PARSEMSG_READSIZE = 1;
|
||
if !parsemsg_canread() {
|
||
return 255; // PARSEMSG_INVALID
|
||
}
|
||
let byte = *PARSEMSG_DATA.offset(PARSEMSG_READCOUNT as isize);
|
||
PARSEMSG_READCOUNT += PARSEMSG_READSIZE;
|
||
byte
|
||
}
|
||
}
|
||
|
||
fn parsemsg_readshort() -> i16 {
|
||
unsafe {
|
||
PARSEMSG_READSIZE = 2;
|
||
if !parsemsg_canread() {
|
||
return -1;
|
||
}
|
||
let b1 = *PARSEMSG_DATA.offset(PARSEMSG_READCOUNT as isize) as i16;
|
||
let b2 = *PARSEMSG_DATA.offset(PARSEMSG_READCOUNT as isize + 1) as i16;
|
||
PARSEMSG_READCOUNT += PARSEMSG_READSIZE;
|
||
(b1 & 0xFF) | (b2 << 8)
|
||
}
|
||
}
|
||
|
||
fn parsemsg_readcoord() -> f32 {
|
||
parsemsg_readshort() as f32 * (1.0 / 8.0)
|
||
}
|
||
|
||
fn parsemsg_readstr() -> String {
|
||
unsafe {
|
||
PARSEMSG_READSIZE = 1;
|
||
if !parsemsg_canread() {
|
||
return String::new();
|
||
}
|
||
let len = if (PARSEMSG_FLAGS & 1) != 0 {
|
||
2048
|
||
} else if (PARSEMSG_FLAGS & 2) != 0 {
|
||
8192
|
||
} else {
|
||
2048
|
||
};
|
||
let mut buf = Vec::with_capacity(len as usize);
|
||
for _ in 0..(len - 1) {
|
||
let byte = parsemsg_readbyte();
|
||
if byte == 0 || byte == 255 {
|
||
break;
|
||
}
|
||
buf.push(byte);
|
||
}
|
||
String::from_utf8_lossy(&buf).into_owned()
|
||
}
|
||
}
|
||
|
||
fn set_player_team(idx: usize, team: &str) {
|
||
unsafe {
|
||
if idx >= PLAYER_TEAM.len() {
|
||
return;
|
||
}
|
||
log(&format!("SET TEAM: idx={}, team={}", idx, team));
|
||
let bytes = team.as_bytes();
|
||
let max = PLAYER_TEAM[idx].len() - 1;
|
||
let len = bytes.len().min(max);
|
||
let mut entry = [0u8; 16];
|
||
entry[..len].copy_from_slice(&bytes[..len]);
|
||
PLAYER_TEAM[idx] = entry;
|
||
}
|
||
}
|
||
|
||
fn player_team(idx: usize) -> &'static str {
|
||
unsafe {
|
||
if idx >= PLAYER_TEAM.len() {
|
||
return "";
|
||
}
|
||
CStr::from_ptr(PLAYER_TEAM[idx].as_ptr() as *const c_char)
|
||
.to_str()
|
||
.unwrap_or("")
|
||
}
|
||
}
|
||
|
||
fn normalize_team_key(team: &str) -> String {
|
||
team.chars()
|
||
.filter(|c| c.is_ascii_alphanumeric())
|
||
.flat_map(|c| c.to_uppercase())
|
||
.collect()
|
||
}
|
||
|
||
fn is_ct_team(team: &str) -> bool {
|
||
let key = normalize_team_key(team);
|
||
matches!(
|
||
key.as_str(),
|
||
"CT" | "COUNTERTERRORIST" | "COUNTERTERRORISTS" | "COUNTER" | "BLUE" | "2"
|
||
)
|
||
}
|
||
|
||
fn is_t_team(team: &str) -> bool {
|
||
let key = normalize_team_key(team);
|
||
matches!(
|
||
key.as_str(),
|
||
"T" | "TT" | "TERRORIST" | "TERRORISTS" | "TERROR" | "RED" | "1"
|
||
)
|
||
}
|
||
|
||
fn team_color(team: &str) -> imgui::ImColor32 {
|
||
if is_ct_team(team) {
|
||
imgui::ImColor32::from_rgba(40, 40, 255, 220)
|
||
} else if is_t_team(team) {
|
||
imgui::ImColor32::from_rgba(255, 40, 40, 220)
|
||
} else {
|
||
if !team.is_empty() && team != "SPECTATOR" && team != "UNASSIGNED" {
|
||
log(&format!("UNKNOWN TEAM: {}", team));
|
||
}
|
||
imgui::ImColor32::from_rgba(255, 255, 255, 220)
|
||
}
|
||
}
|
||
|
||
fn get_entity_world_origin(ent: *mut Entity) -> Vector {
|
||
unsafe {
|
||
let idx = (*ent).i as usize;
|
||
let origin = (*ent).origin;
|
||
|
||
// В GoldSrc если игрок вне PVS, его origin часто 0 или перестает обновляться.
|
||
// Мы проверяем msgnum (номер сообщения), чтобы понять, приходят ли свежие данные.
|
||
let in_pvs = (origin.x != 0.0 || origin.y != 0.0 || origin.z != 0.0) && (*ent).curstate.msgnum > 0;
|
||
|
||
if idx < PLAYER_DATA.len() {
|
||
let pdata = &PLAYER_DATA[idx];
|
||
// Если у нас есть данные из Radar/Sound, а игрок вне зоны видимости (PVS)
|
||
if pdata.pos.x != 0.0 && !in_pvs {
|
||
return pdata.pos;
|
||
}
|
||
}
|
||
|
||
if !in_pvs {
|
||
(*ent).curstate.origin
|
||
} else {
|
||
origin
|
||
}
|
||
}
|
||
}
|
||
|
||
fn get_entity_health(ent: *mut Entity) -> Option<i32> {
|
||
unsafe {
|
||
if ent.is_null() {
|
||
return None;
|
||
}
|
||
let idx = (*ent).i as usize;
|
||
if idx < PLAYER_DATA.len() && PLAYER_DATA[idx].health > 0 {
|
||
return Some(PLAYER_DATA[idx].health);
|
||
}
|
||
|
||
// Fallback to brute-force if no message data (some servers don't send it)
|
||
let base = &(*ent).curstate as *const EntityState as *const u8;
|
||
let max_offset = std::mem::size_of::<EntityState>().saturating_sub(4);
|
||
for offset in (40..=max_offset).step_by(4) {
|
||
let value = std::ptr::read_unaligned(base.add(offset) as *const i32);
|
||
if (1..=100).contains(&value) {
|
||
return Some(value);
|
||
}
|
||
}
|
||
None
|
||
}
|
||
}
|
||
|
||
fn findusermsg(name: *const c_char) -> *mut UserMsg {
|
||
unsafe {
|
||
let mut usermsg = CLIENT_USERMSGS;
|
||
if usermsg.is_null() { return ptr::null_mut(); }
|
||
|
||
while !usermsg.is_null() {
|
||
let msg_name_ptr = (*usermsg).name.as_ptr() as *const c_char;
|
||
if !msg_name_ptr.is_null() && *msg_name_ptr != 0 {
|
||
if libc::strcmp(msg_name_ptr, name) == 0 {
|
||
return usermsg;
|
||
}
|
||
}
|
||
usermsg = (*usermsg).next;
|
||
}
|
||
ptr::null_mut()
|
||
}
|
||
}
|
||
|
||
unsafe extern "C" fn teaminfo(dummy: usize, name: *const c_char, size: i32, buf: *mut c_void) -> i32 {
|
||
parsemsg_begin(buf, size, 1); // PARSEMSG_USERMSG
|
||
let i = parsemsg_readbyte() as usize;
|
||
let team = parsemsg_readstr();
|
||
if i < PLAYER_TEAM.len() {
|
||
set_player_team(i, &team);
|
||
}
|
||
ORIG_TEAMINFO.unwrap()(dummy, name, size, buf)
|
||
}
|
||
|
||
unsafe extern "C" fn health_hook(dummy: usize, name: *const c_char, size: i32, buf: *mut c_void) -> i32 {
|
||
parsemsg_begin(buf, size, 1);
|
||
let hp = parsemsg_readbyte() as i32;
|
||
if let Some(lp) = ENGINE.as_ref().map(|e| (e.getlocalplayer)()) {
|
||
if !lp.is_null() {
|
||
let idx = (*lp).i as usize;
|
||
if idx < PLAYER_DATA.len() {
|
||
PLAYER_DATA[idx].health = hp;
|
||
PLAYER_DATA[idx].is_alive = hp > 0;
|
||
}
|
||
}
|
||
}
|
||
ORIG_HEALTH.unwrap()(dummy, name, size, buf)
|
||
}
|
||
|
||
unsafe extern "C" fn scoreattrib_hook(dummy: usize, name: *const c_char, size: i32, buf: *mut c_void) -> i32 {
|
||
parsemsg_begin(buf, size, 1);
|
||
let idx = parsemsg_readbyte() as usize;
|
||
let flags = parsemsg_readbyte();
|
||
if idx < PLAYER_DATA.len() {
|
||
PLAYER_DATA[idx].is_alive = (flags & 1) == 0;
|
||
}
|
||
ORIG_SCOREATTRIB.unwrap()(dummy, name, size, buf)
|
||
}
|
||
|
||
unsafe extern "C" fn radar_hook(dummy: usize, name: *const c_char, size: i32, buf: *mut c_void) -> i32 {
|
||
parsemsg_begin(buf, size, 1);
|
||
let idx = parsemsg_readbyte() as usize;
|
||
let x = parsemsg_readcoord();
|
||
let y = parsemsg_readcoord();
|
||
let z = parsemsg_readcoord();
|
||
if idx < PLAYER_DATA.len() {
|
||
PLAYER_DATA[idx].pos = Vector { x, y, z };
|
||
}
|
||
ORIG_RADAR.unwrap()(dummy, name, size, buf)
|
||
}
|
||
|
||
unsafe extern "C" fn resethud_hook(dummy: usize, name: *const c_char, size: i32, buf: *mut c_void) -> i32 {
|
||
for i in 0..PLAYER_DATA.len() {
|
||
PLAYER_DATA[i].health = 0;
|
||
PLAYER_DATA[i].is_alive = false;
|
||
// Keep pos as last known? Or clear it?
|
||
// Better clear it to avoid "ghost" ESP.
|
||
PLAYER_DATA[i].pos = Vector { x: 0.0, y: 0.0, z: 0.0 };
|
||
}
|
||
ORIG_RESETHUD.unwrap()(dummy, name, size, buf)
|
||
}
|
||
fn log(msg: &str) {
|
||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open("/tmp/basturma.log") {
|
||
let _ = writeln!(file, "[DEBUG] {}", msg);
|
||
}
|
||
}
|
||
|
||
fn cstr_to_str<'a>(ptr: *const c_char) -> &'a str {
|
||
if ptr.is_null() {
|
||
return "<null>";
|
||
}
|
||
unsafe { CStr::from_ptr(ptr) }.to_str().unwrap_or("<non-utf8>")
|
||
}
|
||
|
||
// -------- Movement helpers (ported from deboogerxyz/ahc) --------
|
||
|
||
fn vector_len2d(v: Vector) -> f32 {
|
||
(v.x * v.x + v.y * v.y).sqrt()
|
||
}
|
||
|
||
fn vector_toang2d(v: Vector) -> f32 {
|
||
v.y.atan2(v.x).to_degrees()
|
||
}
|
||
|
||
fn vector_fromang2d(ang_deg: f32) -> Vector {
|
||
let ang = ang_deg.to_radians();
|
||
Vector { x: ang.cos(), y: ang.sin(), z: 0.0 }
|
||
}
|
||
|
||
fn isalive(ent: *mut Entity) -> bool {
|
||
if ent.is_null() {
|
||
return false;
|
||
}
|
||
unsafe {
|
||
let idx = (*ent).i as usize;
|
||
if idx < PLAYER_DATA.len() && PLAYER_DATA[idx].is_alive {
|
||
return true;
|
||
}
|
||
let mt = (*ent).curstate.movetype;
|
||
mt != MOVETYPE_TOSS && mt != MOVETYPE_NONE
|
||
}
|
||
}
|
||
|
||
fn bhop_apply(cmd: &mut UserCmd) {
|
||
unsafe {
|
||
if !isalive(LOCALPLAYER) {
|
||
return;
|
||
}
|
||
if PMOVE.is_null() {
|
||
return;
|
||
}
|
||
let pm = &*PMOVE;
|
||
let on_ground = (pm.flags & FL_ONGROUND) != 0;
|
||
|
||
// Same behavior as ahc: if we're in-air and were already in-air last tick, clear IN_JUMP.
|
||
if !on_ground && !WAS_ONGROUND {
|
||
cmd.buttons &= !IN_JUMP;
|
||
}
|
||
WAS_ONGROUND = on_ground;
|
||
}
|
||
}
|
||
|
||
fn faststop_apply(cmd: &mut UserCmd) {
|
||
unsafe {
|
||
if (cmd.buttons & (IN_MOVELEFT | IN_MOVERIGHT | IN_FORWARD | IN_BACK)) != 0 {
|
||
return;
|
||
}
|
||
if (cmd.buttons & IN_JUMP) != 0 {
|
||
return;
|
||
}
|
||
if PMOVE.is_null() {
|
||
return;
|
||
}
|
||
let pm = &*PMOVE;
|
||
if (pm.flags & FL_ONGROUND) == 0 {
|
||
return;
|
||
}
|
||
if !isalive(LOCALPLAYER) {
|
||
return;
|
||
}
|
||
|
||
let speed = vector_len2d(pm.velocity);
|
||
if speed < 15.0 {
|
||
return;
|
||
}
|
||
|
||
let dir = cmd.viewangles.y - vector_toang2d(pm.velocity);
|
||
let negdir = vector_fromang2d(dir);
|
||
// TODO (upstream): use runtime cvars, for now hardcode 400 like ahc.
|
||
cmd.forwardmove = negdir.x * -400.0;
|
||
cmd.sidemove = negdir.y * -400.0;
|
||
}
|
||
}
|
||
|
||
fn angledelta_deg(a: f32, b: f32) -> f32 {
|
||
// remainder in degrees, keep within [-180, 180]
|
||
let mut d = (a - b).rem_euclid(360.0);
|
||
if d > 180.0 {
|
||
d -= 360.0;
|
||
}
|
||
d
|
||
}
|
||
|
||
fn autostrafe_apply(cmd: &mut UserCmd) {
|
||
unsafe {
|
||
if (cmd.buttons & IN_JUMP) == 0 {
|
||
return;
|
||
}
|
||
if !isalive(LOCALPLAYER) {
|
||
return;
|
||
}
|
||
if PMOVE.is_null() {
|
||
return;
|
||
}
|
||
let pm = &*PMOVE;
|
||
let speed = vector_len2d(pm.velocity);
|
||
if speed < 15.0 {
|
||
return;
|
||
}
|
||
|
||
// TODO (upstream): runtime cvars: sv_airaccelerate=10, sv_maxspeed=320
|
||
let term = 10.0 / 320.0 * 100.0 / speed;
|
||
if !(term > -1.0 && term < 1.0) {
|
||
return;
|
||
}
|
||
let bestdelta = term.acos(); // radians
|
||
|
||
let yaw = cmd.viewangles.y.to_radians();
|
||
let veldir = pm.velocity.y.atan2(pm.velocity.x) - yaw;
|
||
let wishang = (-cmd.sidemove).atan2(cmd.forwardmove);
|
||
let delta = angledelta_deg(veldir.to_degrees(), wishang.to_degrees()).to_radians();
|
||
|
||
let movedir = if delta < 0.0 {
|
||
veldir + bestdelta
|
||
} else {
|
||
veldir - bestdelta
|
||
};
|
||
|
||
cmd.forwardmove = movedir.cos() * 400.0;
|
||
cmd.sidemove = -movedir.sin() * 400.0;
|
||
}
|
||
}
|
||
|
||
fn fixmovement(cmd: &mut UserCmd, old_yaw_deg: f32) {
|
||
// Port of ahc fixmovement: preserve intended movement when view yaw changes.
|
||
let oldyaw = old_yaw_deg + if old_yaw_deg < 0.0 { 360.0 } else { 0.0 };
|
||
let newyaw = cmd.viewangles.y + if cmd.viewangles.y < 0.0 { 360.0 } else { 0.0 };
|
||
|
||
let mut delta = if newyaw < oldyaw {
|
||
(newyaw - oldyaw).abs()
|
||
} else {
|
||
360.0 - (newyaw - oldyaw).abs()
|
||
};
|
||
delta = 360.0 - delta;
|
||
|
||
let forward = cmd.forwardmove;
|
||
let side = cmd.sidemove;
|
||
let dr = delta.to_radians();
|
||
cmd.forwardmove = dr.cos() * forward + (dr + 90.0_f32.to_radians()).cos() * side;
|
||
cmd.sidemove = dr.sin() * forward + (dr + 90.0_f32.to_radians()).sin() * side;
|
||
}
|
||
|
||
fn vec3_sub(a: Vector, b: Vector) -> Vector {
|
||
Vector {
|
||
x: a.x - b.x,
|
||
y: a.y - b.y,
|
||
z: a.z - b.z,
|
||
}
|
||
}
|
||
|
||
fn vec3_dot(a: Vector, b: Vector) -> f32 {
|
||
a.x * b.x + a.y * b.y + a.z * b.z
|
||
}
|
||
|
||
fn angle_vectors(ang: Vector) -> (Vector, Vector, Vector) {
|
||
let pitch = ang.x.to_radians();
|
||
let yaw = ang.y.to_radians();
|
||
let sp = pitch.sin();
|
||
let cp = pitch.cos();
|
||
let sy = yaw.sin();
|
||
let cy = yaw.cos();
|
||
let forward = Vector {
|
||
x: cp * cy,
|
||
y: cp * sy,
|
||
z: -sp,
|
||
};
|
||
let right = Vector {
|
||
x: -sy,
|
||
y: cy,
|
||
z: 0.0,
|
||
};
|
||
let up = Vector {
|
||
x: sp * cy,
|
||
y: sp * sy,
|
||
z: cp,
|
||
};
|
||
(forward, right, up)
|
||
}
|
||
|
||
fn world_to_screen(world: Vector, cam_origin: Vector, view_angles: Vector, width: f32, height: f32) -> Option<(f32, f32, f32)> {
|
||
let (forward, right, up) = angle_vectors(view_angles);
|
||
let delta = vec3_sub(world, cam_origin);
|
||
let z = vec3_dot(delta, forward);
|
||
if z <= 0.1 {
|
||
return None;
|
||
}
|
||
|
||
let x = vec3_dot(delta, right);
|
||
let y = vec3_dot(delta, up);
|
||
let fov: f32 = 90.0;
|
||
let tan_half_fov_x = (fov * 0.5).to_radians().tan();
|
||
let tan_half_fov_y = tan_half_fov_x * height / width;
|
||
let screen_x = width * 0.5 * (1.0 - x / (z * tan_half_fov_x));
|
||
let screen_y = height * 0.5 * (1.0 - y / (z * tan_half_fov_y));
|
||
Some((screen_x, screen_y, z))
|
||
}
|
||
|
||
fn get_player_name(idx: i32) -> Option<String> {
|
||
let mut info = PlayerInfo::default();
|
||
unsafe {
|
||
let mut called = false;
|
||
if let Some(Some(get_info)) = GET_PLAYER_INFO.get() {
|
||
(get_info)(idx, &mut info);
|
||
called = true;
|
||
} else if !ENGINE.is_null() {
|
||
((*ENGINE).getplayerinfo)(idx, &mut info);
|
||
called = true;
|
||
}
|
||
|
||
if called && !info.name.is_null() {
|
||
let name = CStr::from_ptr(info.name).to_string_lossy();
|
||
let name = name.trim_matches(char::from(0)).trim();
|
||
if !name.is_empty() {
|
||
return Some(name.to_owned());
|
||
}
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
fn render_esp(draw_list: &imgui::DrawListMut, width: f32, height: f32) {
|
||
unsafe {
|
||
if !SETTINGS.wh_enabled && !SETTINGS.esp_enabled {
|
||
return;
|
||
}
|
||
if ENGINE.is_null() || LOCALPLAYER.is_null() {
|
||
return;
|
||
}
|
||
|
||
let cam_origin = VIEW_ORIGIN;
|
||
let view_angles = SETTINGS.last_angles;
|
||
let max_clients = ((*ENGINE).getmaxclients)();
|
||
if max_clients <= 0 {
|
||
return;
|
||
}
|
||
|
||
for idx in 1..=32 {
|
||
if idx > max_clients as usize { break; }
|
||
let ent = ((*ENGINE).getentitybyindex)(idx as i32);
|
||
if ent.is_null() || ent == LOCALPLAYER {
|
||
continue;
|
||
}
|
||
if !isalive(ent) {
|
||
continue;
|
||
}
|
||
if (*ent).player == 0 {
|
||
continue;
|
||
}
|
||
|
||
let origin = get_entity_world_origin(ent);
|
||
let head = Vector {
|
||
x: origin.x,
|
||
y: origin.y,
|
||
z: origin.z + 36.0,
|
||
};
|
||
let body = origin;
|
||
|
||
let head_screen = world_to_screen(head, cam_origin, view_angles, width, height);
|
||
let screen_pos = head_screen
|
||
.or_else(|| world_to_screen(body, cam_origin, view_angles, width, height));
|
||
|
||
if let Some((hx, hy, depth)) = screen_pos {
|
||
let fov_rad = 90.0f32.to_radians();
|
||
let tan_half_fov = (fov_rad * 0.5).tan();
|
||
let tan_half_fov_y = tan_half_fov * height as f32 / width as f32;
|
||
let screen_scale_y = height as f32 * 0.5 / tan_half_fov_y;
|
||
let screen_scale_x = width as f32 * 0.5 / tan_half_fov;
|
||
let height_px = 72.0 * screen_scale_y / depth;
|
||
let width_px = 32.0 * screen_scale_x / depth;
|
||
let center_x = hx;
|
||
let top = if head_screen.is_some() {
|
||
hy
|
||
} else {
|
||
hy - height_px * 0.5
|
||
};
|
||
let bottom = top + height_px;
|
||
let left = center_x - width_px * 0.5;
|
||
let right = center_x + width_px * 0.5;
|
||
|
||
let base_name = if let Some(name) = get_player_name(idx as i32) {
|
||
name
|
||
} else {
|
||
format!("P{}", idx)
|
||
};
|
||
let health = get_entity_health(ent);
|
||
let label = if let Some(hp) = health {
|
||
format!("{} [{}]", base_name, hp)
|
||
} else {
|
||
base_name
|
||
};
|
||
|
||
let mut team = player_team(idx);
|
||
if team.is_empty() {
|
||
// Fallback to model-based team detection
|
||
let mut info = PlayerInfo::default();
|
||
unsafe {
|
||
let mut called = false;
|
||
if let Some(Some(get_info)) = GET_PLAYER_INFO.get() {
|
||
(get_info)(idx as i32, &mut info);
|
||
called = true;
|
||
} else if !ENGINE.is_null() {
|
||
((*ENGINE).getplayerinfo)(idx as i32, &mut info);
|
||
called = true;
|
||
}
|
||
|
||
if called && !info.model.is_null() {
|
||
let model = CStr::from_ptr(info.model).to_string_lossy().to_uppercase();
|
||
if model.contains("TERROR") || model.contains("LEET") || model.contains("ARCTIC") || model.contains("GUERILLA") {
|
||
team = "T";
|
||
} else if model.contains("URBAN") || model.contains("GSG9") || model.contains("SAS") || model.contains("GIGN") || model.contains("VIP") {
|
||
team = "CT";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let color = team_color(team);
|
||
let bg_color = imgui::ImColor32::from_rgba(0, 0, 0, 120);
|
||
let health_color = if let Some(hp) = health {
|
||
if hp > 60 {
|
||
imgui::ImColor32::from_rgba(40, 220, 40, 200)
|
||
} else if hp > 30 {
|
||
imgui::ImColor32::from_rgba(220, 200, 40, 200)
|
||
} else {
|
||
imgui::ImColor32::from_rgba(220, 40, 40, 200)
|
||
}
|
||
} else {
|
||
imgui::ImColor32::from_rgba(40, 220, 40, 200)
|
||
};
|
||
|
||
if SETTINGS.wh_enabled {
|
||
draw_list
|
||
.add_rect([left - 1.5, top - 1.5], [right + 1.5, bottom + 1.5], imgui::ImColor32::from_rgba(0, 0, 0, 60))
|
||
.thickness(0.0)
|
||
.filled(true)
|
||
.build();
|
||
draw_list.add_rect([left, top], [right, bottom], color).thickness(2.5).build();
|
||
draw_list.add_line([left, top], [right, top], color).thickness(1.2).build();
|
||
draw_list.add_line([left, bottom], [right, bottom], color).thickness(1.2).build();
|
||
}
|
||
|
||
if SETTINGS.esp_enabled {
|
||
let text_height = 14.0;
|
||
let label_pos = [left, top - text_height - 4.0];
|
||
let label_width = width_px.max(80.0);
|
||
draw_list
|
||
.add_rect(label_pos, [label_pos[0] + label_width, label_pos[1] + text_height + 4.0], bg_color)
|
||
.filled(true)
|
||
.thickness(0.0)
|
||
.build();
|
||
draw_list.add_text([label_pos[0] + 2.0, label_pos[1] + 2.0], imgui::ImColor32::from_rgba(255, 255, 255, 240), &label);
|
||
let mid_x = left - 8.0;
|
||
let health_top = bottom - (bottom - top) * health.map_or(1.0, |hp| hp.clamp(0, 100) as f32 / 100.0);
|
||
let health_bottom = bottom;
|
||
draw_list
|
||
.add_rect([mid_x - 4.0, top], [mid_x, bottom], imgui::ImColor32::from_rgba(0, 0, 0, 100))
|
||
.filled(true)
|
||
.thickness(0.0)
|
||
.build();
|
||
draw_list
|
||
.add_rect([mid_x - 4.0, health_top], [mid_x, health_bottom], health_color)
|
||
.filled(true)
|
||
.thickness(0.0)
|
||
.build();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// -------- SDL2 input polling (preferred, avoids Xlib threading crashes) --------
|
||
|
||
#[derive(Clone, Copy)]
|
||
struct Sdl2Api {
|
||
sdl_get_keyboard_state: unsafe extern "C" fn(*mut i32) -> *const u8,
|
||
sdl_get_mouse_state: unsafe extern "C" fn(*mut i32, *mut i32) -> u32,
|
||
sdl_set_relative_mouse_mode: Option<unsafe extern "C" fn(i32) -> i32>,
|
||
sdl_show_cursor: Option<unsafe extern "C" fn(i32) -> i32>,
|
||
sdl_capture_mouse: Option<unsafe extern "C" fn(i32) -> i32>,
|
||
sdl_get_mouse_focus: Option<unsafe extern "C" fn() -> *mut c_void>,
|
||
sdl_set_window_grab: Option<unsafe extern "C" fn(*mut c_void, i32)>,
|
||
}
|
||
|
||
static SDL2_API: OnceLock<Option<Sdl2Api>> = OnceLock::new();
|
||
|
||
fn sdl2_init() -> Option<Sdl2Api> {
|
||
unsafe {
|
||
// Most common soname for SDL2 on Linux.
|
||
let lib = dlopen(b"libSDL2-2.0.so.0\0".as_ptr() as *const c_char, RTLD_LAZY);
|
||
if lib.is_null() {
|
||
return None;
|
||
}
|
||
|
||
let gks = dlsym(lib, b"SDL_GetKeyboardState\0".as_ptr() as *const c_char);
|
||
let gms = dlsym(lib, b"SDL_GetMouseState\0".as_ptr() as *const c_char);
|
||
if gks.is_null() || gms.is_null() {
|
||
return None;
|
||
}
|
||
|
||
let sdl_set_relative_mouse_mode =
|
||
dlsym(lib, b"SDL_SetRelativeMouseMode\0".as_ptr() as *const c_char);
|
||
let sdl_show_cursor = dlsym(lib, b"SDL_ShowCursor\0".as_ptr() as *const c_char);
|
||
let sdl_capture_mouse = dlsym(lib, b"SDL_CaptureMouse\0".as_ptr() as *const c_char);
|
||
let sdl_get_mouse_focus = dlsym(lib, b"SDL_GetMouseFocus\0".as_ptr() as *const c_char);
|
||
let sdl_set_window_grab = dlsym(lib, b"SDL_SetWindowGrab\0".as_ptr() as *const c_char);
|
||
|
||
Some(Sdl2Api {
|
||
sdl_get_keyboard_state: std::mem::transmute(gks),
|
||
sdl_get_mouse_state: std::mem::transmute(gms),
|
||
sdl_set_relative_mouse_mode: if sdl_set_relative_mouse_mode.is_null() {
|
||
None
|
||
} else {
|
||
Some(std::mem::transmute(sdl_set_relative_mouse_mode))
|
||
},
|
||
sdl_show_cursor: if sdl_show_cursor.is_null() {
|
||
None
|
||
} else {
|
||
Some(std::mem::transmute(sdl_show_cursor))
|
||
},
|
||
sdl_capture_mouse: if sdl_capture_mouse.is_null() {
|
||
None
|
||
} else {
|
||
Some(std::mem::transmute(sdl_capture_mouse))
|
||
},
|
||
sdl_get_mouse_focus: if sdl_get_mouse_focus.is_null() {
|
||
None
|
||
} else {
|
||
Some(std::mem::transmute(sdl_get_mouse_focus))
|
||
},
|
||
sdl_set_window_grab: if sdl_set_window_grab.is_null() {
|
||
None
|
||
} else {
|
||
Some(std::mem::transmute(sdl_set_window_grab))
|
||
},
|
||
})
|
||
}
|
||
}
|
||
|
||
fn sdl2_rshift_down() -> Option<bool> {
|
||
let api = SDL2_API.get_or_init(|| sdl2_init());
|
||
let Some(api) = api.as_ref() else { return None };
|
||
unsafe {
|
||
let mut len: i32 = 0;
|
||
let state = (api.sdl_get_keyboard_state)(&mut len as *mut i32);
|
||
if state.is_null() || len <= 0 {
|
||
return None;
|
||
}
|
||
// SDL2 scancode for Right Shift is SDL_SCANCODE_RSHIFT = 229.
|
||
let idx = 229i32;
|
||
if idx >= len {
|
||
return None;
|
||
}
|
||
Some(*state.add(idx as usize) != 0)
|
||
}
|
||
}
|
||
|
||
fn sdl2_space_down() -> Option<bool> {
|
||
let api = SDL2_API.get_or_init(|| sdl2_init());
|
||
let Some(api) = api.as_ref() else { return None };
|
||
unsafe {
|
||
let mut len: i32 = 0;
|
||
let state = (api.sdl_get_keyboard_state)(&mut len as *mut i32);
|
||
if state.is_null() || len <= 0 {
|
||
return None;
|
||
}
|
||
// SDL2 scancode for Space is SDL_SCANCODE_SPACE = 44.
|
||
let idx = 44i32;
|
||
if idx >= len {
|
||
return None;
|
||
}
|
||
Some(*state.add(idx as usize) != 0)
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Default)]
|
||
struct Sdl2Mouse {
|
||
x: f32,
|
||
y: f32,
|
||
left: bool,
|
||
right: bool,
|
||
middle: bool,
|
||
}
|
||
|
||
fn sdl2_mouse_state() -> Option<Sdl2Mouse> {
|
||
let api = SDL2_API.get_or_init(|| sdl2_init());
|
||
let Some(api) = api.as_ref() else { return None };
|
||
unsafe {
|
||
let mut x: i32 = 0;
|
||
let mut y: i32 = 0;
|
||
let buttons = (api.sdl_get_mouse_state)(&mut x as *mut i32, &mut y as *mut i32);
|
||
// SDL button masks: left=1, middle=2, right=4
|
||
Some(Sdl2Mouse {
|
||
x: x as f32,
|
||
y: y as f32,
|
||
left: (buttons & 1) != 0,
|
||
middle: (buttons & 2) != 0,
|
||
right: (buttons & 4) != 0,
|
||
})
|
||
}
|
||
}
|
||
|
||
fn sdl2_on_menu_state(show_menu: bool) {
|
||
unsafe {
|
||
let api = SDL2_API.get_or_init(|| sdl2_init());
|
||
let api_opt = api.as_ref();
|
||
|
||
// Most games "capture" the mouse via relative mode and/or window grab.
|
||
// Force-disable these when the menu is open so the game can't consume motion.
|
||
if let Some(api) = api_opt {
|
||
if let (Some(get_focus), Some(set_grab)) = (api.sdl_get_mouse_focus, api.sdl_set_window_grab)
|
||
{
|
||
let win = get_focus();
|
||
if !win.is_null() {
|
||
set_grab(win, if show_menu { 0 } else { 1 });
|
||
}
|
||
}
|
||
if let Some(set_rel) = api.sdl_set_relative_mouse_mode {
|
||
let _ = set_rel(if show_menu { 0 } else { 1 });
|
||
}
|
||
if let Some(show_cursor) = api.sdl_show_cursor {
|
||
let _ = show_cursor(if show_menu { 1 } else { 0 });
|
||
}
|
||
if let Some(capture_mouse) = api.sdl_capture_mouse {
|
||
let _ = capture_mouse(if show_menu { 0 } else { 1 });
|
||
}
|
||
} else {
|
||
let _ = x11_set_pointer_grab(show_menu);
|
||
}
|
||
|
||
SETTINGS.mouse_grabbed = show_menu;
|
||
}
|
||
}
|
||
|
||
// -------- X11 Right Shift polling (fallback if hudkeyevent doesn't receive modifiers) --------
|
||
|
||
type Display = c_void;
|
||
type Window = u64;
|
||
type Cursor = u64;
|
||
type Time = u64;
|
||
|
||
#[derive(Clone, Copy)]
|
||
struct X11Api {
|
||
x_init_threads: Option<unsafe extern "C" fn() -> i32>,
|
||
x_lock_display: Option<unsafe extern "C" fn(*mut Display)>,
|
||
x_unlock_display: Option<unsafe extern "C" fn(*mut Display)>,
|
||
x_open_display: unsafe extern "C" fn(*const c_char) -> *mut Display,
|
||
x_query_keymap: unsafe extern "C" fn(*mut Display, *mut c_char) -> i32,
|
||
x_keysym_to_keycode: unsafe extern "C" fn(*mut Display, u64) -> u32,
|
||
x_default_root_window: unsafe extern "C" fn(*mut Display) -> Window,
|
||
x_query_pointer: unsafe extern "C" fn(
|
||
*mut Display,
|
||
Window,
|
||
*mut Window,
|
||
*mut Window,
|
||
*mut i32,
|
||
*mut i32,
|
||
*mut i32,
|
||
*mut i32,
|
||
*mut u32,
|
||
) -> i32,
|
||
x_grab_pointer: unsafe extern "C" fn(
|
||
*mut Display,
|
||
Window,
|
||
i32,
|
||
u32,
|
||
i32,
|
||
i32,
|
||
Window,
|
||
Cursor,
|
||
Time,
|
||
) -> i32,
|
||
x_ungrab_pointer: unsafe extern "C" fn(*mut Display, Time) -> i32,
|
||
}
|
||
|
||
struct X11State {
|
||
api: X11Api,
|
||
// Store as usize so X11State is Sync for static storage.
|
||
display: usize,
|
||
root: Window,
|
||
rshift_keycode: u8,
|
||
}
|
||
|
||
static X11_STATE: OnceLock<Option<X11State>> = OnceLock::new();
|
||
|
||
fn x11_init() -> Option<X11State> {
|
||
unsafe {
|
||
// Try common sonames.
|
||
let lib = dlopen(b"libX11.so.6\0".as_ptr() as *const c_char, RTLD_LAZY);
|
||
if lib.is_null() {
|
||
return None;
|
||
}
|
||
|
||
// Optional but important for stability if engine uses threads + Xlib.
|
||
let x_init_threads = dlsym(lib, b"XInitThreads\0".as_ptr() as *const c_char);
|
||
let x_lock_display = dlsym(lib, b"XLockDisplay\0".as_ptr() as *const c_char);
|
||
let x_unlock_display = dlsym(lib, b"XUnlockDisplay\0".as_ptr() as *const c_char);
|
||
|
||
let x_open_display = dlsym(lib, b"XOpenDisplay\0".as_ptr() as *const c_char);
|
||
let x_query_keymap = dlsym(lib, b"XQueryKeymap\0".as_ptr() as *const c_char);
|
||
let x_keysym_to_keycode = dlsym(lib, b"XKeysymToKeycode\0".as_ptr() as *const c_char);
|
||
let x_default_root_window = dlsym(lib, b"XDefaultRootWindow\0".as_ptr() as *const c_char);
|
||
let x_query_pointer = dlsym(lib, b"XQueryPointer\0".as_ptr() as *const c_char);
|
||
let x_grab_pointer = dlsym(lib, b"XGrabPointer\0".as_ptr() as *const c_char);
|
||
let x_ungrab_pointer = dlsym(lib, b"XUngrabPointer\0".as_ptr() as *const c_char);
|
||
|
||
if x_open_display.is_null()
|
||
|| x_query_keymap.is_null()
|
||
|| x_keysym_to_keycode.is_null()
|
||
|| x_default_root_window.is_null()
|
||
|| x_query_pointer.is_null()
|
||
|| x_grab_pointer.is_null()
|
||
|| x_ungrab_pointer.is_null()
|
||
{
|
||
return None;
|
||
}
|
||
|
||
let api = X11Api {
|
||
x_init_threads: if x_init_threads.is_null() { None } else { Some(std::mem::transmute(x_init_threads)) },
|
||
x_lock_display: if x_lock_display.is_null() { None } else { Some(std::mem::transmute(x_lock_display)) },
|
||
x_unlock_display: if x_unlock_display.is_null() { None } else { Some(std::mem::transmute(x_unlock_display)) },
|
||
x_open_display: std::mem::transmute(x_open_display),
|
||
x_query_keymap: std::mem::transmute(x_query_keymap),
|
||
x_keysym_to_keycode: std::mem::transmute(x_keysym_to_keycode),
|
||
x_default_root_window: std::mem::transmute(x_default_root_window),
|
||
x_query_pointer: std::mem::transmute(x_query_pointer),
|
||
x_grab_pointer: std::mem::transmute(x_grab_pointer),
|
||
x_ungrab_pointer: std::mem::transmute(x_ungrab_pointer),
|
||
};
|
||
|
||
if let Some(init_threads) = api.x_init_threads {
|
||
// If it fails, we still proceed but avoid crashing when possible.
|
||
let _ = init_threads();
|
||
}
|
||
|
||
let display = (api.x_open_display)(ptr::null());
|
||
if display.is_null() {
|
||
return None;
|
||
}
|
||
|
||
let root = (api.x_default_root_window)(display);
|
||
if root == 0 {
|
||
return None;
|
||
}
|
||
|
||
// XK_Shift_R keysym = 0xFFE2
|
||
let kc = (api.x_keysym_to_keycode)(display, 0xFFE2);
|
||
if kc == 0 || kc > 255 {
|
||
return None;
|
||
}
|
||
|
||
Some(X11State {
|
||
api,
|
||
display: display as usize,
|
||
root,
|
||
rshift_keycode: kc as u8,
|
||
})
|
||
}
|
||
}
|
||
|
||
fn x11_rshift_down() -> Option<bool> {
|
||
let state = X11_STATE.get_or_init(|| x11_init());
|
||
let Some(st) = state.as_ref() else { return None };
|
||
|
||
unsafe {
|
||
let mut keys: [c_char; 32] = [0; 32];
|
||
let display = st.display as *mut Display;
|
||
if let Some(lock) = st.api.x_lock_display { lock(display); }
|
||
(st.api.x_query_keymap)(display, keys.as_mut_ptr());
|
||
if let Some(unlock) = st.api.x_unlock_display { unlock(display); }
|
||
|
||
let kc = st.rshift_keycode as usize;
|
||
let byte = kc / 8;
|
||
let bit = kc % 8;
|
||
let mask = 1u8 << bit;
|
||
let val = keys[byte] as u8;
|
||
Some((val & mask) != 0)
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Default)]
|
||
struct X11Mouse {
|
||
x: f32,
|
||
y: f32,
|
||
left: bool,
|
||
right: bool,
|
||
middle: bool,
|
||
}
|
||
|
||
fn x11_mouse_state() -> Option<X11Mouse> {
|
||
let state = X11_STATE.get_or_init(|| x11_init());
|
||
let Some(st) = state.as_ref() else { return None };
|
||
|
||
unsafe {
|
||
let display = st.display as *mut Display;
|
||
let mut root_return: Window = 0;
|
||
let mut child_return: Window = 0;
|
||
let mut root_x: i32 = 0;
|
||
let mut root_y: i32 = 0;
|
||
let mut win_x: i32 = 0;
|
||
let mut win_y: i32 = 0;
|
||
let mut mask: u32 = 0;
|
||
|
||
if let Some(lock) = st.api.x_lock_display { lock(display); }
|
||
let ok = (st.api.x_query_pointer)(
|
||
display,
|
||
st.root,
|
||
&mut root_return,
|
||
&mut child_return,
|
||
&mut root_x,
|
||
&mut root_y,
|
||
&mut win_x,
|
||
&mut win_y,
|
||
&mut mask,
|
||
);
|
||
if let Some(unlock) = st.api.x_unlock_display { unlock(display); }
|
||
if ok == 0 {
|
||
return None;
|
||
}
|
||
|
||
// Button masks from X11: Button1=1<<8, Button2=1<<9, Button3=1<<10.
|
||
let button1 = (mask & (1 << 8)) != 0;
|
||
let button2 = (mask & (1 << 9)) != 0;
|
||
let button3 = (mask & (1 << 10)) != 0;
|
||
|
||
Some(X11Mouse {
|
||
x: root_x as f32,
|
||
y: root_y as f32,
|
||
left: button1,
|
||
middle: button2,
|
||
right: button3,
|
||
})
|
||
}
|
||
}
|
||
|
||
fn x11_set_pointer_grab(grab: bool) -> bool {
|
||
let state = X11_STATE.get_or_init(|| x11_init());
|
||
let Some(st) = state.as_ref() else { return false };
|
||
|
||
unsafe {
|
||
let display = st.display as *mut Display;
|
||
if let Some(lock) = st.api.x_lock_display { lock(display); }
|
||
if grab {
|
||
// Event masks: ButtonPress(1<<2), ButtonRelease(1<<3), PointerMotion(1<<6).
|
||
let event_mask: u32 = (1 << 2) | (1 << 3) | (1 << 6);
|
||
let owner_events: i32 = 0;
|
||
let pointer_mode: i32 = 1; // GrabModeAsync
|
||
let keyboard_mode: i32 = 1; // GrabModeAsync
|
||
let status = (st.api.x_grab_pointer)(
|
||
display,
|
||
st.root,
|
||
owner_events,
|
||
event_mask,
|
||
pointer_mode,
|
||
keyboard_mode,
|
||
0,
|
||
0,
|
||
0,
|
||
);
|
||
if let Some(unlock) = st.api.x_unlock_display { unlock(display); }
|
||
status == 0
|
||
} else {
|
||
(st.api.x_ungrab_pointer)(display, 0);
|
||
if let Some(unlock) = st.api.x_unlock_display { unlock(display); }
|
||
true
|
||
}
|
||
}
|
||
}
|
||
|
||
// -------- Minimal ImGui overlay rendering in hudredraw --------
|
||
|
||
type GlXGetProcAddressARB = unsafe extern "C" fn(*const u8) -> *const c_void;
|
||
|
||
struct Overlay {
|
||
imgui: imgui::Context,
|
||
renderer: imgui_glow_renderer::AutoRenderer,
|
||
last_frame: std::time::Instant,
|
||
}
|
||
|
||
static mut OVERLAY: Option<Overlay> = None;
|
||
|
||
fn get_gl_loader() -> Option<GlXGetProcAddressARB> {
|
||
unsafe {
|
||
let libgl = dlopen(b"libGL.so.1\0".as_ptr() as *const c_char, RTLD_LAZY);
|
||
if libgl.is_null() {
|
||
return None;
|
||
}
|
||
let sym = dlsym(libgl, b"glXGetProcAddressARB\0".as_ptr() as *const c_char);
|
||
if sym.is_null() {
|
||
return None;
|
||
}
|
||
Some(std::mem::transmute(sym))
|
||
}
|
||
}
|
||
|
||
fn overlay_init() -> Option<Overlay> {
|
||
let glx_get_proc = get_gl_loader()?;
|
||
|
||
unsafe {
|
||
let gl = glow::Context::from_loader_function(|name| {
|
||
// Try RTLD_DEFAULT first (some drivers export core symbols).
|
||
if let Ok(c_name) = std::ffi::CString::new(name) {
|
||
let sym = libc::dlsym(libc::RTLD_DEFAULT, c_name.as_ptr() as *const c_char);
|
||
if !sym.is_null() {
|
||
return sym as *const c_void;
|
||
}
|
||
}
|
||
glx_get_proc(name.as_ptr())
|
||
});
|
||
|
||
let mut imgui = imgui::Context::create();
|
||
imgui.set_ini_filename(None);
|
||
imgui.set_log_filename(None);
|
||
|
||
// Slightly nicer defaults.
|
||
imgui.io_mut().config_flags |= imgui::ConfigFlags::NO_MOUSE_CURSOR_CHANGE;
|
||
|
||
let renderer = imgui_glow_renderer::AutoRenderer::initialize(gl, &mut imgui).ok()?;
|
||
|
||
Some(Overlay {
|
||
imgui,
|
||
renderer,
|
||
last_frame: std::time::Instant::now(),
|
||
})
|
||
}
|
||
}
|
||
|
||
fn overlay_render(show_menu: bool) {
|
||
unsafe {
|
||
if OVERLAY.is_none() {
|
||
OVERLAY = overlay_init();
|
||
}
|
||
let Some(ov) = OVERLAY.as_mut() else { return };
|
||
|
||
// Delta time.
|
||
let now = std::time::Instant::now();
|
||
let dt = (now - ov.last_frame).as_secs_f32().max(1.0 / 1000.0);
|
||
ov.last_frame = now;
|
||
ov.imgui.io_mut().delta_time = dt;
|
||
|
||
// Feed mouse state via SDL2 (no Xlib calls).
|
||
ov.imgui.io_mut().mouse_draw_cursor = show_menu;
|
||
if show_menu {
|
||
if let Some(ms) = sdl2_mouse_state() {
|
||
ov.imgui.io_mut().mouse_pos = [ms.x, ms.y];
|
||
ov.imgui.io_mut().mouse_down = [ms.left, ms.right, ms.middle, false, false];
|
||
}
|
||
} else {
|
||
ov.imgui.io_mut().mouse_pos = [-1.0, -1.0];
|
||
ov.imgui.io_mut().mouse_down = [false; 5];
|
||
}
|
||
|
||
// Display size from GL viewport.
|
||
let mut vp = [0i32; 4];
|
||
ov.renderer
|
||
.gl_context()
|
||
.get_parameter_i32_slice(glow::VIEWPORT, &mut vp);
|
||
ov.imgui.io_mut().display_size = [vp[2] as f32, vp[3] as f32];
|
||
|
||
let ui = ov.imgui.frame();
|
||
if show_menu {
|
||
ui.window("basturma")
|
||
.always_auto_resize(true)
|
||
.build(|| {
|
||
ui.text("Menu is open");
|
||
ui.text("Hotkey: Right Shift");
|
||
ui.separator();
|
||
ui.checkbox("Bhop (hold Space)", &mut SETTINGS.bhop_enabled);
|
||
ui.checkbox("Fast stop", &mut SETTINGS.faststop_enabled);
|
||
ui.checkbox("Autostrafe", &mut SETTINGS.autostrafe_enabled);
|
||
ui.checkbox("Wallhack (WH)", &mut SETTINGS.wh_enabled);
|
||
ui.checkbox("ESP labels", &mut SETTINGS.esp_enabled);
|
||
});
|
||
}
|
||
|
||
let hide_window_flags = imgui::WindowFlags::NO_TITLE_BAR
|
||
| imgui::WindowFlags::NO_COLLAPSE
|
||
| imgui::WindowFlags::NO_RESIZE
|
||
| imgui::WindowFlags::NO_MOVE
|
||
| imgui::WindowFlags::NO_SAVED_SETTINGS
|
||
| imgui::WindowFlags::NO_NAV
|
||
| imgui::WindowFlags::NO_INPUTS
|
||
| imgui::WindowFlags::NO_BACKGROUND;
|
||
|
||
ui.window("##basturma_esp")
|
||
.flags(hide_window_flags)
|
||
.position([0.0, 0.0], imgui::Condition::Always)
|
||
.size([vp[2] as f32, vp[3] as f32], imgui::Condition::Always)
|
||
.build(|| {
|
||
let draw_list = ui.get_window_draw_list();
|
||
render_esp(&draw_list, vp[2] as f32, vp[3] as f32);
|
||
});
|
||
|
||
let draw_data = ov.imgui.render();
|
||
let _ = ov.renderer.render(draw_data);
|
||
}
|
||
}
|
||
|
||
#[ctor::ctor]
|
||
fn init() {
|
||
log("------------------------------------------");
|
||
log("Injected: LEGACY SIGNATURE TEST V17");
|
||
|
||
std::thread::spawn(|| {
|
||
unsafe {
|
||
let hw = dlopen(b"hw.so\0".as_ptr() as *const c_char, RTLD_LAZY | RTLD_NOLOAD);
|
||
if hw.is_null() || HOOKS_APPLIED { return; }
|
||
|
||
let client_ptr = dlsym(hw, b"cl_funcs\0".as_ptr() as *const c_char) as *mut Client;
|
||
if client_ptr.is_null() {
|
||
log("FATAL: cl_funcs not found");
|
||
return;
|
||
}
|
||
|
||
let engine_ptr = dlsym(hw, b"cl_enginefuncs\0".as_ptr() as *const c_char) as *mut Engine;
|
||
if engine_ptr.is_null() {
|
||
log("WARN: cl_enginefuncs not found");
|
||
} else {
|
||
ENGINE = engine_ptr;
|
||
}
|
||
|
||
let mut get_player_info_sym = dlsym(hw, b"GetPlayerInfo\0".as_ptr() as *const c_char);
|
||
|
||
let pmove_sym = dlsym(hw, b"pmove\0".as_ptr() as *const c_char);
|
||
if pmove_sym.is_null() {
|
||
log("WARN: pmove not found");
|
||
} else {
|
||
// In ahc: playermove = *(PlayerMove **)dlsym(hw, "pmove");
|
||
PMOVE = *(pmove_sym as *mut *mut PlayerMove);
|
||
}
|
||
|
||
let shutdownusermsgs = dlsym(hw, b"CL_ShutDownUsrMessages\0".as_ptr() as *const c_char);
|
||
if !shutdownusermsgs.is_null() {
|
||
let clientusermsgs_sym = dlsym(hw, b"clientusermsgs\0".as_ptr() as *const c_char);
|
||
if !clientusermsgs_sym.is_null() {
|
||
CLIENT_USERMSGS = clientusermsgs_sym as *mut UserMsg;
|
||
} else {
|
||
// Try to extract from CL_ShutDownUsrMessages signature:
|
||
// mov clientusermsgs, eax (or similar)
|
||
// This is very architecture/build dependent.
|
||
CLIENT_USERMSGS = *( (shutdownusermsgs as *mut u8).offset(5) as *mut *mut UserMsg );
|
||
}
|
||
}
|
||
|
||
let clientdll = *(dlsym(hw, b"hClientDLL\0".as_ptr() as *const c_char) as *mut *mut c_void);
|
||
if get_player_info_sym.is_null() && !clientdll.is_null() {
|
||
get_player_info_sym = dlsym(clientdll, b"GetPlayerInfo\0".as_ptr() as *const c_char);
|
||
}
|
||
if !get_player_info_sym.is_null() {
|
||
let _ = GET_PLAYER_INFO.set(Some(std::mem::transmute(get_player_info_sym)));
|
||
log("INFO: GetPlayerInfo resolved");
|
||
} else {
|
||
log("WARN: GetPlayerInfo not found");
|
||
}
|
||
|
||
ORIG_CLIENT = Some(*client_ptr);
|
||
ORIG_CREATEMOVE = Some((*client_ptr).createmove);
|
||
|
||
let addr = client_ptr as usize;
|
||
let page_size = libc::sysconf(libc::_SC_PAGESIZE) as usize;
|
||
let page_start = addr & !(page_size - 1);
|
||
mprotect(page_start as *mut c_void, page_size * 2, PROT_READ | PROT_WRITE | PROT_EXEC);
|
||
|
||
(*client_ptr).hudredraw = hooked_hudredraw;
|
||
(*client_ptr).hudkeyevent = hooked_hudkeyevent;
|
||
(*client_ptr).createmove = hooked_createmove;
|
||
|
||
HOOKS_APPLIED = true;
|
||
log("SUCCESS: Hooks applied with hidden arg fix.");
|
||
}
|
||
});
|
||
}
|
||
|
||
extern "C" fn hooked_hudredraw(dummy: usize, time: f32, intermission: i32) -> i32 {
|
||
unsafe {
|
||
// Retry hooking messages if they weren't found at init.
|
||
if !CLIENT_USERMSGS.is_null() {
|
||
if ORIG_TEAMINFO.is_none() {
|
||
let msg = findusermsg(b"TeamInfo\0".as_ptr() as *const c_char);
|
||
if !msg.is_null() {
|
||
ORIG_TEAMINFO = Some((*msg).fn_);
|
||
(*msg).fn_ = teaminfo;
|
||
log("INFO: TeamInfo hook applied");
|
||
}
|
||
}
|
||
if ORIG_HEALTH.is_none() {
|
||
let msg = findusermsg(b"Health\0".as_ptr() as *const c_char);
|
||
if !msg.is_null() {
|
||
ORIG_HEALTH = Some((*msg).fn_);
|
||
(*msg).fn_ = health_hook;
|
||
log("INFO: Health hook applied");
|
||
}
|
||
}
|
||
if ORIG_SCOREATTRIB.is_none() {
|
||
let msg = findusermsg(b"ScoreAttrib\0".as_ptr() as *const c_char);
|
||
if !msg.is_null() {
|
||
ORIG_SCOREATTRIB = Some((*msg).fn_);
|
||
(*msg).fn_ = scoreattrib_hook;
|
||
log("INFO: ScoreAttrib hook applied");
|
||
}
|
||
}
|
||
if ORIG_RADAR.is_none() {
|
||
let msg = findusermsg(b"Radar\0".as_ptr() as *const c_char);
|
||
if !msg.is_null() {
|
||
ORIG_RADAR = Some((*msg).fn_);
|
||
(*msg).fn_ = radar_hook;
|
||
log("INFO: Radar hook applied");
|
||
}
|
||
}
|
||
if ORIG_RESETHUD.is_none() {
|
||
let msg = findusermsg(b"ResetHUD\0".as_ptr() as *const c_char);
|
||
if !msg.is_null() {
|
||
ORIG_RESETHUD = Some((*msg).fn_);
|
||
(*msg).fn_ = resethud_hook;
|
||
log("INFO: ResetHUD hook applied");
|
||
}
|
||
}
|
||
}
|
||
|
||
// Keep mouse uncaptured while the menu is open (the game may try to re-enable it).
|
||
if SETTINGS.show_menu {
|
||
sdl2_on_menu_state(true);
|
||
}
|
||
|
||
// Toggle menu on Right Shift via SDL2 (more reliable than hudkeyevent for modifiers).
|
||
if let Some(down) = sdl2_rshift_down() {
|
||
if down && !SETTINGS.last_rshift_down {
|
||
SETTINGS.show_menu = !SETTINGS.show_menu;
|
||
sdl2_on_menu_state(SETTINGS.show_menu);
|
||
log(&format!("MENU TOGGLED (SDL2 RSHIFT): {}", SETTINGS.show_menu));
|
||
}
|
||
SETTINGS.last_rshift_down = down;
|
||
}
|
||
|
||
if let Some(orig) = ORIG_CLIENT {
|
||
let ret = (orig.hudredraw)(dummy, time, intermission);
|
||
// Render overlay after original HUD.
|
||
// overlay_render(SETTINGS.show_menu);
|
||
ret
|
||
} else { 0 }
|
||
}
|
||
}
|
||
|
||
extern "C" fn hooked_hudkeyevent(dummy: usize, eventcode: i32, keynum: i32, binding: *const c_char) -> i32 {
|
||
unsafe {
|
||
// Log *all* key events (press/release/etc) to learn what the engine sends for modifiers.
|
||
log(&format!(
|
||
"KEYEVENT: eventcode={}, code={}, dummy={:X}, binding={}",
|
||
eventcode,
|
||
keynum,
|
||
dummy,
|
||
cstr_to_str(binding)
|
||
));
|
||
|
||
// Toggle only on key-press (eventcode==1) to avoid double toggles.
|
||
if eventcode == 1 {
|
||
// "Right Shift" keycodes differ between builds and input backends.
|
||
// We also keep the old "Insert" key as a fallback in case Shift never arrives via hudkeyevent.
|
||
if matches!(keynum, 134 | 54 | 303 | 146 | 147) {
|
||
SETTINGS.show_menu = !SETTINGS.show_menu;
|
||
sdl2_on_menu_state(SETTINGS.show_menu);
|
||
log(&format!("MENU TOGGLED: {}", SETTINGS.show_menu));
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
if let Some(orig) = ORIG_CLIENT {
|
||
(orig.hudkeyevent)(dummy, eventcode, keynum, binding)
|
||
} else { 1 }
|
||
}
|
||
}
|
||
|
||
extern "C" fn hooked_createmove(frametime: f32, cmd: *mut UserCmd, active: i32) {
|
||
unsafe {
|
||
if let Some(orig) = ORIG_CREATEMOVE {
|
||
(orig)(frametime, cmd, active);
|
||
}
|
||
|
||
if cmd.is_null() {
|
||
return;
|
||
}
|
||
|
||
if SETTINGS.show_menu {
|
||
// Freeze view and suppress movement/actions while menu is open.
|
||
if SETTINGS.have_angles {
|
||
(*cmd).viewangles = SETTINGS.last_angles;
|
||
} else {
|
||
SETTINGS.last_angles = (*cmd).viewangles;
|
||
SETTINGS.have_angles = true;
|
||
}
|
||
|
||
(*cmd).forwardmove = 0.0;
|
||
(*cmd).sidemove = 0.0;
|
||
(*cmd).upmove = 0.0;
|
||
(*cmd).buttons = 0;
|
||
return;
|
||
} else {
|
||
// Track last view angles when not in menu.
|
||
SETTINGS.last_angles = (*cmd).viewangles;
|
||
SETTINGS.have_angles = true;
|
||
}
|
||
|
||
// Refresh localplayer pointer (needed for isalive checks).
|
||
if !ENGINE.is_null() {
|
||
let lp = ((*ENGINE).getlocalplayer)();
|
||
if !lp.is_null() {
|
||
LOCALPLAYER = lp;
|
||
let mut view_height = Vector { x: 0.0, y: 0.0, z: 0.0 };
|
||
if !(*ENGINE).eventapi.is_null() {
|
||
((*(*ENGINE).eventapi).localplayerviewheight)(&mut view_height);
|
||
}
|
||
VIEW_ORIGIN = Vector {
|
||
x: (*lp).origin.x,
|
||
y: (*lp).origin.y,
|
||
z: (*lp).origin.z + view_height.z,
|
||
};
|
||
}
|
||
}
|
||
|
||
let cmd_ref = &mut *cmd;
|
||
let old_yaw = cmd_ref.viewangles.y;
|
||
|
||
// bhop (proper): if enabled and Space is held, keep IN_JUMP pressed,
|
||
// but drop it mid-air to avoid repeated jumps (same as ahc).
|
||
if SETTINGS.bhop_enabled {
|
||
if let Some(space) = sdl2_space_down() {
|
||
if space {
|
||
cmd_ref.buttons |= IN_JUMP;
|
||
}
|
||
}
|
||
bhop_apply(cmd_ref);
|
||
}
|
||
|
||
if SETTINGS.faststop_enabled {
|
||
faststop_apply(cmd_ref);
|
||
}
|
||
if SETTINGS.autostrafe_enabled {
|
||
autostrafe_apply(cmd_ref);
|
||
}
|
||
|
||
// Preserve intended move direction if view yaw changed in the future (e.g. aimbot).
|
||
// (Doesn't hurt even if yaw stays the same.)
|
||
fixmovement(cmd_ref, old_yaw);
|
||
}
|
||
}
|