From 074e315c8550712dce0c7b530a90eee6aca29e63 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 23:26:53 +0100 Subject: Add grenade trajectory, RPG armour priority, empty chargers, mega gravity gun - Projectiles gets a "Grenade trajectory" toggle. While a frag grenade is held, simulate the weapon_frag throw (1200 u/s forward + 120 z, player velocity, gravity 600) and trace each step with a new trace_solid_segment(); reflect on impact, stop on a slow upward-normal hit, and draw the path, landing cross and a 300-unit blast ring. Verified on :99 (arcs, bounces off the tunnel wall, lands). - With the RPG equipped, armoured NPCs (striders, gunships, dropships, hunter-choppers) sort before normal enemies during target acquisition. Added helicopter/chopper to the ARMORED model list. - Empty health/suit chargers are hidden: read m_iJuice from the server datamap (0 = depleted, -1 = unreadable/fail open). - The citadel supercharged gravity gun (d3_citadel_03, d3_citadel_04, d3_breen_01 via IVEngineClient::GetLevelName) is treated as a normal weapon, so the melee aimbot no longer restricts it to VULNERABLE NPCs. --- CONTEXT.md | 35 +++++++++--- src/esp.cpp | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- src/gui.cpp | 3 ++ src/hl2h.h | 5 +- src/menu.cpp | 9 +++- src/server.cpp | 18 ++++++- src/vischeck.cpp | 21 ++++++++ 7 files changed, 232 insertions(+), 17 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 1e860bf..889c3ab 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -201,9 +201,10 @@ vocabulary / decisions. - Every NPC is classified for ESP and target selection: - `IMMUNE` — server `m_takedamage` is `DAMAGE_NO`/`DAMAGE_EVENTS_ONLY`. - - `ARMORED` — gunship, strider, dropship: their `OnTakeDamage` only accepts - blast damage (`DMG_BLAST`, with a damage threshold on gunships and a - distance check on striders). Only the RPG passes; bullets/grenades do not. + - `ARMORED` — gunship, strider, dropship, hunter-chopper: their + `OnTakeDamage` only accepts blast damage (`DMG_BLAST`, with a damage + threshold on gunships and a distance check on striders). Only the RPG + passes; bullets/grenades do not. - `VULNERABLE` — headcrab, antlion, manhack: punting them with the gravity gun / physics impact damage kills them. - `NORMAL` — everything else. @@ -213,7 +214,10 @@ vocabulary / decisions. created at glyph size 10 alongside the 16 px body font (`g_font_small`). - Aimbot/silent aim/triggerbot/melee skip `IMMUNE` targets always and `ARMORED` targets unless the active weapon is the RPG - (`active_weapon_contains` for `rocket_launcher`/`rpg`). + (`active_weapon_contains` for `rocket_launcher`/`rpg`). With the RPG + equipped, armoured candidates are sorted before normal enemies (a + `fov - 1000` rank key), so a strider/gunship is acquired even when a normal + NPC is closer to the crosshair. - Server `m_takedamage` is a `char` in the 2013 SDK (`CNetworkVarForDerived(char, m_takedamage)`, `FIELD_CHARACTER`), so it is resolved with that type (int fallback) and read as a byte. `0/1` means no @@ -402,7 +406,11 @@ vocabulary / decisions. `IN_ATTACK`: the primary attack launches a held object along `EyeVectors()`. No aim-punch compensation is applied there (EyeVectors does not add punch); the crowbar path keeps it. Without a held object the primary - is a short physics punch. + is a short physics punch. The citadel supercharges the gravity gun + (`d3_citadel_03`, `d3_citadel_04`, `d3_breen_01`; detected through + `IVEngineClient::GetLevelName`, slot 51), and there it is treated as a + normal weapon: the `VULNERABLE`-only filter is dropped, so any damageable + hostile NPC is selected. - Map transitions often switch the active weapon away from the crowbar. The state change is logged as `melee: active ... crowbar=0/1` (normal log), and a one-time toast says to equip the crowbar. If the client weapon model is @@ -694,7 +702,10 @@ vocabulary / decisions. Reading at the raw offset shifted every count by one type (e.g. RPG read the buckshot count 30 against a max of 3, permanently hiding rockets). - Chargers and ammo/supply crates are dispensers and are exempt from the - fullness filter. + fullness filter. Empty health/suit chargers are still hidden: both + `CWallHealth` and `CNewRecharge` expose `m_iJuice` (server datamap + `FIELD_INTEGER`, resolved at offset 0x47c in this build), and a value of 0 + removes the charger tag until it recharges. `-1` (unreadable) fails open. - Scans the client entity list every 100 ms and tags health, ammo, armor/suit, world weapon pickups, the two supply-crate models and generic items. - Each tag is a colored marker with a name and optional distance, filtered by @@ -733,7 +744,17 @@ vocabulary / decisions. is a 24-segment horizontal circle projected at the projectile height, with every segment near-plane/screen clipped independently, so it still draws when the grenade itself is off-screen or behind the camera. - Toggles: Fuse timer, Blast radius. + Toggles: Fuse timer, Blast radius, Grenade trajectory + (`[projectiles] trajectory=1`). +- Grenade trajectory: while the frag weapon is active, simulate the throw + from `weapon_frag.cpp` (eye + forward*18 + right*8, player velocity + + (forward + 0.1 z)*1200), gravity 600, dt 0.02, restitution 0.35, friction + 0.35. Each step traces with `trace_solid_segment` (EngineTraceClient003 slot + 4, `MASK_PLAYERSOLID`, local player ignored); hits reflect about the impact + normal, and an upward normal with a slow vertical speed marks the landing. + The polyline, landing cross and a 300-unit blast-radius ring at the landing + are drawn with the same near-plane-clipped projector as the radius ring. + Verified on `:99` (the line arcs, bounces off a tunnel wall and lands). ## Roadmap / TODO diff --git a/src/esp.cpp b/src/esp.cpp index d32a8bc..594a8fa 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -320,7 +320,8 @@ static int npc_damage_class(int idx, const char *model) { if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY return NPC_DMG_IMMUNE; if (model_has(model, "gunship") || model_has(model, "combine_strider") || - model_has(model, "dropship")) + model_has(model, "dropship") || model_has(model, "helicopter") || + model_has(model, "chopper")) return NPC_DMG_ARMORED; if (model_has(model, "headcrab") || model_has(model, "antlion") || model_has(model, "manhack")) @@ -822,6 +823,12 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, *(int *)((uintptr_t)local_ent + g_net.health) <= 0) return false; + // With the RPG equipped, armoured units (striders, gunships, dropships, + // hunter-choppers) are the ones ordinary shots cannot hurt, so prefer them + // over normal NPCs when acquiring. + bool rpg = active_weapon_contains(local_ent, "rocket_launcher") || + active_weapon_contains(local_ent, "rpg"); + auto try_target = [&](int idx, bool enforce_fov, Vector &pt, float &fov) -> bool { if (idx <= 0 || idx == local) @@ -899,6 +906,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, int idx; float fov; Vector approx; + bool armored; + float key; }; Candidate cands[16]; int ncands = 0; @@ -928,6 +937,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, continue; if (!npc_can_be_damaged(idx, model)) continue; + bool armored = npc_damage_class(idx, model) == NPC_DMG_ARMORED; float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; if (!(maxz > 4.0f && maxz < 512.0f)) @@ -940,16 +950,17 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, float fov = getfov(view, angle_to(g_cam_eye, approx)); if (fov > max_fov) continue; + float key = (rpg && armored) ? fov - 1000.0f : fov; int pos = ncands; - while (pos > 0 && cands[pos - 1].fov > fov) + while (pos > 0 && cands[pos - 1].key > key) pos--; if (ncands < 16) ncands++; for (int j = ncands - 1; j > pos; j--) cands[j] = cands[j - 1]; if (pos < 16) - cands[pos] = {idx, fov, approx}; + cands[pos] = {idx, fov, approx, armored, key}; } if (ncands == 0) { @@ -1309,6 +1320,26 @@ bool gravity_gun_active(void *local_ent) { (model && strcasestr(model, "physcannon")); } +// The citadel supercharges the gravity gun (d3_citadel_03 onward). It can then +// punt any NPC, so the melee aimbot should not restrict itself to +// physics-vulnerable units. +static bool supercharged_gravity_gun() { + if (!g_iface.engine) + return false; + using Fn = const char *(*)(void *); + const char *m = vfunc(g_iface.engine, 51)(g_iface.engine); + if (!m) + return false; + bool supe = strstr(m, "d3_citadel_03") || strstr(m, "d3_citadel_04") || + strstr(m, "d3_breen_01"); + static const char *logged_map = nullptr; + if (logged_map != m) { + logged_map = m; + logf("melee: gravity gun map=%s supercharged=%d", m, (int)supe); + } + return supe; +} + bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { if (!g_iface.entitylist || g_net.origin < 0) return false; @@ -1324,6 +1355,7 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { // The punted object can travel much farther than the crowbar. float max_range = gravity ? 1200.0f : MELEE_RANGE; + bool supercharged = gravity && supercharged_gravity_gun(); using Fn = int (*)(void *); int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); @@ -1363,8 +1395,10 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) { continue; if (!npc_can_be_damaged(idx, model)) continue; - // The gravity gun is only the tool for physics-vulnerable units. - if (gravity && npc_damage_class(idx, model) != NPC_DMG_VULNERABLE) + // The normal gravity gun is only the tool for physics-vulnerable units; + // the citadel version behaves like a normal weapon. + if (gravity && !supercharged && + npc_damage_class(idx, model) != NPC_DMG_VULNERABLE) continue; Vector origin{*(float *)(e + g_net.origin), @@ -2747,6 +2781,11 @@ static void items_collect(int local_idx) { int kind = classify_item(model, cls, label, sizeof(label)); if (kind < 0) continue; + // Health/suit chargers are dispensers that stay useful after a refill, but + // an empty one cannot give anything. m_iJuice is 0 when depleted (until it + // recharges); only hide the tag once the server value is readable. + if (strcasestr(label, "charger") && server_item_juice(idx) == 0) + continue; Vector origin{*(float *)((uintptr_t)ent + g_net.origin), *(float *)((uintptr_t)ent + g_net.origin + 4), @@ -3045,6 +3084,114 @@ static void projs_collect(int local_idx) { } } +// Frag-grenade path preview: throw velocity from weapon_frag.cpp (eye/right +// offset, forward*1200 + 0.1 z), gravity 600, and a simple bounce model. +// Shows the bounces and the first landing with the blast-radius ring. +static void draw_grenade_trajectory(int local_idx) { + if (!g_proj_traj || !g_iface.entitylist) + return; + void *le = entitylist_get(local_idx); + if (!le) + return; + if (!active_weapon_contains(le, "frag") && + !active_weapon_contains(le, "w_grenade")) + return; + + QAngle va = engine_view_angles(); + float pitch = va.x * PI_F / 180.0f; + float yaw = va.y * PI_F / 180.0f; + float cp = cosf(pitch), sp = sinf(pitch); + float cy = cosf(yaw), sy = sinf(yaw); + Vector fwd{cp * cy, cp * sy, -sp}; + Vector right{sy, -cy, 0.0f}; + + Vector src{g_cam_eye.x + fwd.x * 18.0f + right.x * 8.0f, + g_cam_eye.y + fwd.y * 18.0f + right.y * 8.0f, + g_cam_eye.z + fwd.z * 18.0f}; + Vector vel{}; + if (g_net.velocity >= 0) { + const float *pv = (const float *)((uintptr_t)le + g_net.velocity); + vel = Vector{pv[0], pv[1], pv[2]}; + } + vel.x += fwd.x * 1200.0f; + vel.y += fwd.y * 1200.0f; + vel.z += fwd.z * 1200.0f + 120.0f; // vForward[2] += 0.1 + + const float gravity = 600.0f; + const float dt = 0.02f; + const float restitution = 0.35f; + const float friction = 0.35f; + + Vector pts[128]; + int n = 0; + Vector pos = src; + pts[n++] = pos; + bool landed = false; + for (int step = 0; step < 240 && n < 127; step++) { + Vector nv = vel; + nv.z -= gravity * dt; + Vector next{pos.x + nv.x * dt, pos.y + nv.y * dt, pos.z + nv.z * dt}; + Vector hit_end{}, normal{}; + if (trace_solid_segment(pos, next, le, hit_end, normal)) { + pts[n++] = hit_end; + float vn = vel.x * normal.x + vel.y * normal.y + vel.z * normal.z; + Vector vt{vel.x - vn * normal.x, vel.y - vn * normal.y, + vel.z - vn * normal.z}; + float keep = 1.0f - friction; + vel.x = vt.x * keep - normal.x * vn * restitution; + vel.y = vt.y * keep - normal.y * vn * restitution; + vel.z = vt.z * keep - normal.z * vn * restitution; + float vlen2 = vel.x * vel.x + vel.y * vel.y + vel.z * vel.z; + pos.x = hit_end.x + normal.x * 1.0f; + pos.y = hit_end.y + normal.y * 1.0f; + pos.z = hit_end.z + normal.z * 1.0f; + if (normal.z > 0.6f && (vn > -80.0f || vlen2 < 2500.0f)) { + landed = true; + break; + } + } else { + pos = next; + vel = nv; + pts[n++] = pos; + } + } + + surf_color(255, 210, 90, 200); + for (int i = 0; i + 1 < n; i++) { + int x0, y0, x1, y1; + if (project_segment(pts[i], pts[i + 1], x0, y0, x1, y1)) + surf_line(x0, y0, x1, y1); + } + + Vector land = landed ? pos : pts[n - 1]; + float ssx, ssy; + if (world_to_screen(land, ssx, ssy)) { + int x = (int)ssx, y = (int)ssy; + surf_color(255, 210, 90, 230); + surf_rect(0, x - 3, y - 3, x + 3, y + 3); + surf_line(x - 9, y, x - 4, y); + surf_line(x + 4, y, x + 9, y); + surf_line(x, y - 9, x, y - 4); + surf_line(x, y + 4, x, y + 9); + } + if (landed) { + const int SEG = 24; + const float radius = 300.0f; // CBaseGrenade m_DmgRadius + surf_color(255, 120, 40, 120); + for (int k = 0; k < SEG; k++) { + float a0 = (float)k * 2.0f * PI_F / (float)SEG; + float a1 = (float)(k + 1) * 2.0f * PI_F / (float)SEG; + Vector p0{land.x + cosf(a0) * radius, land.y + sinf(a0) * radius, + land.z + 1.0f}; + Vector p1{land.x + cosf(a1) * radius, land.y + sinf(a1) * radius, + land.z + 1.0f}; + int x0, y0, x1, y1; + if (project_segment(p0, p1, x0, y0, x1, y1)) + surf_line(x0, y0, x1, y1); + } + } +} + void esp_draw_projectiles() { if (!g_surface || !g_iface.entitylist || g_net.origin < 0) return; @@ -3055,6 +3202,7 @@ void esp_draw_projectiles() { return; } projs_collect(engine_get_local_player()); + draw_grenade_trajectory(engine_get_local_player()); for (int i = 0; i < g_nprojs; i++) { const ProjTarget &p = g_projs[i]; int r = 255, g = 170, b = 60; diff --git a/src/gui.cpp b/src/gui.cpp index fe82762..454e2cb 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -454,6 +454,9 @@ static void build_cards() { &g_proj_fuse); add_toggle(proj, "Blast radius", "Draw the projectile's damage radius on the ground.", &g_proj_radius); + add_toggle(proj, "Grenade trajectory", + "Predict the frag grenade path, bounces and landing point.", + &g_proj_traj); int chams = add_card(2, &g_chams, "Chams", "Per-relationship model colours.", true); diff --git a/src/hl2h.h b/src/hl2h.h index 34bf04f..32cee8f 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -230,7 +230,7 @@ struct FeatureState { extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp, g_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright, g_world_tint, g_fog, g_skybox, g_projectiles, g_chams; -extern bool g_proj_fuse, g_proj_radius; +extern bool g_proj_fuse, g_proj_radius, g_proj_traj; // Chams settings per relationship, and separate visible / through-wall // (invisible) material and colour. The viewmodel has a single style. @@ -319,6 +319,7 @@ int server_player_ammo(int idx, int type); uint32_t server_player_weapon(int idx, int slot); float server_grenade_detonate(int idx); float server_grenade_radius(int idx); +int server_item_juice(int idx); int server_entity_count(); void *server_entity_base(int idx); const char *server_entity_classname(int idx); @@ -366,6 +367,8 @@ void set_silent_restore(const QAngle &angles); bool is_visible(const Vector &eye, const Vector &point, void *self = nullptr, void *target = nullptr); void *trace_entity(const Vector &start, const Vector &end, void *ignore); +bool trace_solid_segment(const Vector &start, const Vector &end, void *ignore, + Vector &out_end, Vector &out_normal); float ground_distance(const Vector &origin, void *ignore, float max_dist); bool point_ground(const Vector &point, void *ignore); diff --git a/src/menu.cpp b/src/menu.cpp index 873f5ed..1064365 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -63,6 +63,7 @@ FeatureState g_skybox{false, 0}; // skybox.png image override FeatureState g_projectiles{true, 0}; // grenade / rocket ESP bool g_proj_fuse = true; bool g_proj_radius = true; +bool g_proj_traj = true; FeatureState g_chams{false, 0}; ChamsGroup g_chams_hostile{true, 3, {255, 60, 60, 255}, true, 3, {255, 60, 60, 255}}; @@ -241,9 +242,10 @@ void config_save() { g_fog_start, g_fog_end, (int)g_fog_skybox, (int)g_fog_disable); fprintf(f, "[skybox]\nactive=%d\nbind=%d\n", g_skybox.active, g_skybox.bind); - fprintf(f, "[projectiles]\nactive=%d\nbind=%d\nfuse=%d\nradius=%d\n", + fprintf(f, "[projectiles]\nactive=%d\nbind=%d\nfuse=%d\nradius=%d\n" + "trajectory=%d\n", g_projectiles.active, g_projectiles.bind, (int)g_proj_fuse, - (int)g_proj_radius); + (int)g_proj_radius, (int)g_proj_traj); fprintf(f, "[chams]\nactive=%d\nbind=%d\n", g_chams.active, g_chams.bind); auto save_chams = [&](const char *name, const ChamsGroup &g) { fprintf(f, "[%s]\nvis=%d\nvis_mat=%d\nvis_r=%d\nvis_g=%d\nvis_b=%d\n" @@ -425,6 +427,8 @@ static bool config_load() { g_proj_fuse = atoi(val) != 0; else if (strcmp(key, "radius") == 0) g_proj_radius = atoi(val) != 0; + else if (strcmp(key, "trajectory") == 0) + g_proj_traj = atoi(val) != 0; } if (chams_grp) { ChamsGroup &g = *chams_grp; @@ -702,6 +706,7 @@ void config_reset_defaults() { g_projectiles = {true, 0}; g_proj_fuse = true; g_proj_radius = true; + g_proj_traj = true; g_chams = {false, 0}; g_chams_hostile = {true, 3, {255, 60, 60, 255}, true, 3, {255, 60, 60, 255}}; diff --git a/src/server.cpp b/src/server.cpp index bae355e..6479b25 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -58,6 +58,7 @@ static int g_srv_mapname = -1; static int g_srv_armor = -1; static int g_srv_detonate = -1; static int g_srv_grenade_radius = -1; +static int g_srv_juice = -1; static int g_srv_ammo = -1; static int g_srv_myweapons = -1; static bool g_srv_ready = false; @@ -162,6 +163,7 @@ bool resolve_server_netvars() { // CBaseGrenade fuse / blast radius (m_flDetonateTime is not networked). g_srv_detonate = datamap_offset("m_flDetonateTime", FIELD_TIME); g_srv_grenade_radius = datamap_offset("m_DmgRadius", FIELD_FLOAT); + g_srv_juice = datamap_offset("m_iJuice", FIELD_INTEGER); // CBaseCombatCharacter arrays: ammo counts and the weapon handle list. g_srv_ammo = datamap_offset("m_iAmmo", FIELD_INTEGER, 0x40, 0x4000); g_srv_myweapons = datamap_offset("m_hMyWeapons", FIELD_EHANDLE); @@ -171,13 +173,13 @@ bool resolve_server_netvars() { "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " "followtarget=%d spawnflags=%d hacked=%d npcstate=%d " "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d " - "ammo=%d myweapons=%d", + "ammo=%d myweapons=%d juice=%d", g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage, g_srv_mapname, g_srv_armor, g_srv_detonate, g_srv_grenade_radius, - g_srv_ammo, g_srv_myweapons); + g_srv_ammo, g_srv_myweapons, g_srv_juice); fail_logged = false; } else if (!fail_logged) { fail_logged = true; @@ -475,6 +477,18 @@ float server_grenade_radius(int idx) { return *(float *)((uintptr_t)base + g_srv_grenade_radius); } +// CWallHealth / CNewRecharge share the same m_iJuice layout (the fields before +// it match), so one datamap offset works for both health and suit chargers. +int server_item_juice(int idx) { + if (!g_srv_ready || g_srv_juice < 0) + return -1; + void *base = nullptr; + server_entity_health(idx, nullptr, nullptr, &base); + if (!base) + return -1; + return *(int *)((uintptr_t)base + g_srv_juice); +} + // Server-side CBaseCombatCharacter::m_iAmmo array (reserve ammo). The datamap // offset points at the CNetworkArray length header; the elements start 4 bytes // later (confirmed by the shifted current/max table). diff --git a/src/vischeck.cpp b/src/vischeck.cpp index c8c0942..6b531b9 100644 --- a/src/vischeck.cpp +++ b/src/vischeck.cpp @@ -232,6 +232,27 @@ void *trace_entity(const Vector &start, const Vector &end, void *ignore) { return tr.ent; } +// First solid/world hit along a segment, for grenade bounce prediction. +// Returns false on a clear path; otherwise fills the impact point and normal. +bool trace_solid_segment(const Vector &start, const Vector &end, void *ignore, + Vector &out_end, Vector &out_normal) { + if (!g_iface.enginetrace) + return false; + RayCompat ray; + make_ray(ray, start, end); + GameTraceCompat tr{}; + EntityFilter filter(ignore); + using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *, + GameTraceCompat *); + vfunc(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, + MASK_PLAYERSOLID, &filter, &tr); + if (tr.startsolid || tr.fraction >= 1.0f) + return false; + out_end = tr.endpos; + out_normal = tr.normal; + return true; +} + // True when a downward point trace from just above `point` finds a standable // surface. Used by edge jump: a ledge has no floor within the probe, slopes and // stairs do. -- cgit v1.2.3