From 1fa3b61163b233f6c8c6b86dfa196fb798a6f094 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 12:39:45 +0100 Subject: ESP: classify NPC damage immunity/armour/vulnerability - Resolve server m_takedamage (DAMAGE_NO/DAMAGE_EVENTS_ONLY = immune). - Classify gunship/strider/dropship as ARMORED (OnTakeDamage only accepts DMG_BLAST; only the RPG passes), and headcrab/antlion/manhack as VULN (gravity gun / physics impact). - ESP draws ARMORED/VULN/IMMUNE tags above the name (new "Damage tags" toggle, [esp] dmg). - Aimbot, silent aim, triggerbot and melee skip immune targets and armoured targets unless the active weapon is the RPG. --- CONTEXT.md | 18 +++++++++++++++ src/esp.cpp | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/gui.cpp | 5 +++-- src/hl2h.h | 4 +++- src/menu.cpp | 11 +++++++--- src/server.cpp | 19 ++++++++++++++-- 6 files changed, 118 insertions(+), 8 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 2199be6..7f8b131 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -188,6 +188,24 @@ vocabulary / decisions. not aimbot targets and draw with the neutral colour. Lamarr (`models/Lamarr.mdl`) is additionally listed as neutral by model. +## Damage classification + +- 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. + - `VULNERABLE` — headcrab, antlion, manhack: punting them with the gravity + gun / physics impact damage kills them. + - `NORMAL` — everything else. +- ESP draws `ARMORED` (orange), `VULN` (cyan) or `IMMUNE` (grey) above the + name; toggle is `ESP → Damage tags` (`[esp] dmg=1`). +- 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`). +- Server `m_takedamage` (`FIELD_INTEGER`) is resolved alongside + `m_NPCState`; `0/1` means no damage. + ## Bones / ESP - `SetupBones(BONE_USED_BY_ANYTHING)` with engine `Time()`; studio header via diff --git a/src/esp.cpp b/src/esp.cpp index 4a6b513..aa9f645 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -23,6 +23,7 @@ struct EspTarget { bool show_health; bool friendly; bool neutral; + int damage; // NpcDamageClass char name[48]; }; static EspTarget g_targets[128]; @@ -220,6 +221,48 @@ static bool npc_is_friendly(int idx, const char *model, int local_idx, } +enum NpcDamageClass { + NPC_DMG_NORMAL = 0, + NPC_DMG_ARMORED, // only blast damage (RPG) hurts it + NPC_DMG_VULNERABLE, // gravity gun / physics damage works well + NPC_DMG_IMMUNE // m_takedamage says no damage at all +}; + +static bool model_has(const char *model, const char *needle) { + return model && strcasestr(model, needle) != nullptr; +} + +// Server m_takedamage first (authoritative immunity), then the HL2 damage +// rules for units whose OnTakeDamage filters specific damage types. +static int npc_damage_class(int idx, const char *model) { + int td = server_npc_takedamage(idx); + 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")) + return NPC_DMG_ARMORED; + if (model_has(model, "headcrab") || model_has(model, "antlion") || + model_has(model, "manhack")) + return NPC_DMG_VULNERABLE; + return NPC_DMG_NORMAL; +} + +static bool active_weapon_contains(void *local_ent, const char *needle); + +// Armoured units need blast damage; the RPG is the only stock weapon that +// passes their OnTakeDamage filters. Immune units can never be damaged. +static bool npc_can_be_damaged(int idx, const char *model) { + int dc = npc_damage_class(idx, model); + if (dc == NPC_DMG_IMMUNE) + return false; + if (dc != NPC_DMG_ARMORED) + return true; + int local = engine_get_local_player(); + void *le = local > 0 ? entitylist_get(local) : nullptr; + return active_weapon_contains(le, "rocket_launcher") || + active_weapon_contains(le, "rpg"); +} + static void short_name(const char *model, char *out, size_t outn) { if (!model) { snprintf(out, outn, "NPC"); return; } const char *base = strrchr(model, '/'); @@ -334,6 +377,7 @@ static void esp_collect(int local_idx) { bool neutral = false; t.friendly = npc_is_friendly(idx, model, local_idx, neutral); t.neutral = neutral; + t.damage = npc_damage_class(idx, model); short_name(model, t.name, sizeof(t.name)); if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); } @@ -603,6 +647,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, bool neutral = false; if (npc_is_friendly(idx, model, local, neutral) || neutral) return false; + if (!npc_can_be_damaged(idx, model)) + return false; if (!bones_update(idx, ent)) return false; Vector pts[32]; @@ -682,6 +728,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, bool neutral = false; if (npc_is_friendly(idx, model, local, neutral) || neutral) continue; + if (!npc_can_be_damaged(idx, model)) + continue; float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; if (!(maxz > 4.0f && maxz < 512.0f)) @@ -1063,6 +1111,8 @@ bool melee_aimbot_apply(CUserCmd *cmd) { bool neutral = false; if (npc_is_friendly(idx, model, local, neutral) || neutral) continue; + if (!npc_can_be_damaged(idx, model)) + continue; Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), @@ -1394,6 +1444,8 @@ bool triggerbot_apply(CUserCmd *cmd) { bool neutral = false; if (npc_is_friendly(idx, model, local, neutral) || neutral) return false; + if (!npc_can_be_damaged(idx, model)) + return false; cmd->buttons |= IN_ATTACK; static int logs = 0; if (logs < 5) { @@ -1517,6 +1569,23 @@ void esp_draw() { } if (esp_option(ESP_OPT_SHOW_NAME)) draw_text_col(left, top - 14, t.name, 255, 255, 255); + if (esp_option(ESP_OPT_DAMAGE_TAGS)) { + const char *tag = nullptr; + int tr = 255, tg = 255, tb = 255; + if (t.damage == NPC_DMG_ARMORED) { + tag = "ARMORED"; + tr = 255; tg = 170; tb = 60; + } else if (t.damage == NPC_DMG_VULNERABLE) { + tag = "VULN"; + tr = 90; tg = 220; tb = 255; + } else if (t.damage == NPC_DMG_IMMUNE) { + tag = "IMMUNE"; + tr = 180; tg = 180; tb = 180; + } + if (tag) + draw_text_col(left, top - (esp_option(ESP_OPT_SHOW_NAME) ? 28 : 14), + tag, tr, tg, tb); + } } // Groundwork probe: HL2H_BONE_DEBUG=1 draws a white marker at each NPC's diff --git a/src/gui.cpp b/src/gui.cpp index ebc1f62..b5fbc1d 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -359,12 +359,13 @@ static void build_cards() { static const char *esp_names[ESP_OPT_COUNT] = { "Show neutral", "Show healthbar", "Show healthnum", "Show name", "Show skeleton", "Show box", - "Show speed graph"}; + "Show speed graph", "Damage tags"}; static const char *esp_descs[ESP_OPT_COUNT] = { "Include neutral wildlife.", "Draw a health bar.", "Draw the numeric health value.", "Draw the model name.", "Draw the animated bone links.", "Draw a bounding box.", - "Draw the lower-center speed graph."}; + "Draw the lower-center speed graph.", + "Label ARMORED (needs blast/RPG) and VULN (gravity gun) units."}; for (int i = 0; i < ESP_OPT_COUNT; i++) add_toggle(esp, esp_names[i], esp_descs[i], &g_esp_opt[i]); diff --git a/src/hl2h.h b/src/hl2h.h index 3ccf7b5..287563c 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -192,7 +192,8 @@ enum EspOption { ESP_OPT_SHOW_SKELETON = 4, ESP_OPT_SHOW_BOX = 5, ESP_OPT_SHOW_SPEEDGRAPH = 6, - ESP_OPT_COUNT = 7 + ESP_OPT_DAMAGE_TAGS = 7, + ESP_OPT_COUNT = 8 }; enum AimBone { AIM_BONE_HEAD = 0, @@ -273,6 +274,7 @@ int server_active_weapon(int idx); int server_npc_relation(int idx, int local_idx); bool server_npc_has_follow_target(int idx); int server_npc_state(int idx); +int server_npc_takedamage(int idx); int server_npc_turret_flags(int idx); int server_model_index(void *base); void find_modules(); diff --git a/src/menu.cpp b/src/menu.cpp index 0ce6c05..eb1075a 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -78,7 +78,8 @@ bool g_aim_fov_circle = true; int g_silent_fov = 60; // ESP sub-options. Defaults match the pre-option behaviour: everything on. -bool g_esp_opt[ESP_OPT_COUNT] = {true, true, true, true, true, true, true}; +bool g_esp_opt[ESP_OPT_COUNT] = {true, true, true, true, true, true, true, + true}; // --------------------------------------------------------------------------- // config persistence (INI format) @@ -221,12 +222,14 @@ void config_save() { (int)g_item_ammo, (int)g_item_armor, (int)g_item_weapons, (int)g_item_crates, (int)g_item_distance, g_item_max_dist); fprintf(f, "[esp]\nactive=%d\nbind=%d\nneutral=%d\nhealthbar=%d\n" - "healthnum=%d\nname=%d\nskeleton=%d\nbox=%d\nspeedgraph=%d\n", + "healthnum=%d\nname=%d\nskeleton=%d\nbox=%d\nspeedgraph=%d\n" + "dmg=%d\n", g_esp.active, g_esp.bind, g_esp_opt[ESP_OPT_SHOW_NEUTRAL], g_esp_opt[ESP_OPT_SHOW_HEALTHBAR], g_esp_opt[ESP_OPT_SHOW_HEALTHNUM], g_esp_opt[ESP_OPT_SHOW_NAME], g_esp_opt[ESP_OPT_SHOW_SKELETON], g_esp_opt[ESP_OPT_SHOW_BOX], - g_esp_opt[ESP_OPT_SHOW_SPEEDGRAPH]); + g_esp_opt[ESP_OPT_SHOW_SPEEDGRAPH], + g_esp_opt[ESP_OPT_DAMAGE_TAGS]); gui_config_write(f); bool ok = ferror(f) == 0; if (fflush(f) != 0) @@ -427,6 +430,8 @@ static bool config_load() { g_esp_opt[ESP_OPT_SHOW_BOX] = atoi(val) != 0; else if (strcmp(key, "speedgraph") == 0) g_esp_opt[ESP_OPT_SHOW_SPEEDGRAPH] = atoi(val) != 0; + else if (strcmp(key, "dmg") == 0) + g_esp_opt[ESP_OPT_DAMAGE_TAGS] = atoi(val) != 0; } if (strcmp(key, "active") == 0) cur->active = atoi(val) != 0; diff --git a/src/server.cpp b/src/server.cpp index c2864c8..e9fd921 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -51,6 +51,7 @@ static int g_srv_followtarget = -1; static int g_srv_spawnflags = -1; static int g_srv_hacked = -1; static int g_srv_npcstate = -1; +static int g_srv_takedamage = -1; static bool g_srv_ready = false; static bool g_srv_checked = false; @@ -141,15 +142,17 @@ bool resolve_server_netvars() { g_srv_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4); g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN); g_srv_npcstate = datamap_offset("m_NPCState", FIELD_INTEGER); + g_srv_takedamage = datamap_offset("m_takedamage", FIELD_INTEGER); g_srv_ready = g_srv_health > 0; if (g_srv_ready) { logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " - "followtarget=%d spawnflags=%d hacked=%d npcstate=%d", + "followtarget=%d spawnflags=%d hacked=%d npcstate=%d " + "takedamage=%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_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage); fail_logged = false; } else if (!fail_logged) { fail_logged = true; @@ -268,6 +271,18 @@ int server_npc_state(int idx) { return *(int *)((uintptr_t)base + g_srv_npcstate); } +// TAKEDAMAGE: 0 = DAMAGE_NO, 1 = DAMAGE_EVENTS_ONLY, 2 = DAMAGE_YES, +// 3 = DAMAGE_AIM. 0/1 cannot be damaged at all. +int server_npc_takedamage(int idx) { + if (!g_srv_ready || g_srv_takedamage < 0) + return -1; + void *base = nullptr; + server_entity_health(idx, nullptr, nullptr, &base); + if (!base) + return -1; + return *(int *)((uintptr_t)base + g_srv_takedamage); +} + bool server_npc_has_follow_target(int idx) { if (!g_srv_ready || g_srv_followtarget < 0) return false; -- cgit v1.2.3