summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 12:39:45 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 12:39:45 +0100
commit1fa3b61163b233f6c8c6b86dfa196fb798a6f094 (patch)
treee21fd421dcfc78ef6ec73883442a60cdb6fe6597
parentProps: remove studio diagnostics; document the prop tint path (diff)
downloadhl2h-1fa3b61163b233f6c8c6b86dfa196fb798a6f094.tar.gz
hl2h-1fa3b61163b233f6c8c6b86dfa196fb798a6f094.zip
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.
-rw-r--r--CONTEXT.md18
-rw-r--r--src/esp.cpp69
-rw-r--r--src/gui.cpp5
-rw-r--r--src/hl2h.h4
-rw-r--r--src/menu.cpp11
-rw-r--r--src/server.cpp19
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.
188 not aimbot targets and draw with the neutral colour. Lamarr 188 not aimbot targets and draw with the neutral colour. Lamarr
189 (`models/Lamarr.mdl`) is additionally listed as neutral by model. 189 (`models/Lamarr.mdl`) is additionally listed as neutral by model.
190 190
191## Damage classification
192
193- Every NPC is classified for ESP and target selection:
194 - `IMMUNE` — server `m_takedamage` is `DAMAGE_NO`/`DAMAGE_EVENTS_ONLY`.
195 - `ARMORED` — gunship, strider, dropship: their `OnTakeDamage` only accepts
196 blast damage (`DMG_BLAST`, with a damage threshold on gunships and a
197 distance check on striders). Only the RPG passes; bullets/grenades do not.
198 - `VULNERABLE` — headcrab, antlion, manhack: punting them with the gravity
199 gun / physics impact damage kills them.
200 - `NORMAL` — everything else.
201- ESP draws `ARMORED` (orange), `VULN` (cyan) or `IMMUNE` (grey) above the
202 name; toggle is `ESP → Damage tags` (`[esp] dmg=1`).
203- Aimbot/silent aim/triggerbot/melee skip `IMMUNE` targets always and
204 `ARMORED` targets unless the active weapon is the RPG
205 (`active_weapon_contains` for `rocket_launcher`/`rpg`).
206- Server `m_takedamage` (`FIELD_INTEGER`) is resolved alongside
207 `m_NPCState`; `0/1` means no damage.
208
191## Bones / ESP 209## Bones / ESP
192 210
193- `SetupBones(BONE_USED_BY_ANYTHING)` with engine `Time()`; studio header via 211- `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 {
23 bool show_health; 23 bool show_health;
24 bool friendly; 24 bool friendly;
25 bool neutral; 25 bool neutral;
26 int damage; // NpcDamageClass
26 char name[48]; 27 char name[48];
27}; 28};
28static EspTarget g_targets[128]; 29static EspTarget g_targets[128];
@@ -220,6 +221,48 @@ static bool npc_is_friendly(int idx, const char *model, int local_idx,
220} 221}
221 222
222 223
224enum NpcDamageClass {
225 NPC_DMG_NORMAL = 0,
226 NPC_DMG_ARMORED, // only blast damage (RPG) hurts it
227 NPC_DMG_VULNERABLE, // gravity gun / physics damage works well
228 NPC_DMG_IMMUNE // m_takedamage says no damage at all
229};
230
231static bool model_has(const char *model, const char *needle) {
232 return model && strcasestr(model, needle) != nullptr;
233}
234
235// Server m_takedamage first (authoritative immunity), then the HL2 damage
236// rules for units whose OnTakeDamage filters specific damage types.
237static int npc_damage_class(int idx, const char *model) {
238 int td = server_npc_takedamage(idx);
239 if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY
240 return NPC_DMG_IMMUNE;
241 if (model_has(model, "gunship") || model_has(model, "combine_strider") ||
242 model_has(model, "dropship"))
243 return NPC_DMG_ARMORED;
244 if (model_has(model, "headcrab") || model_has(model, "antlion") ||
245 model_has(model, "manhack"))
246 return NPC_DMG_VULNERABLE;
247 return NPC_DMG_NORMAL;
248}
249
250static bool active_weapon_contains(void *local_ent, const char *needle);
251
252// Armoured units need blast damage; the RPG is the only stock weapon that
253// passes their OnTakeDamage filters. Immune units can never be damaged.
254static bool npc_can_be_damaged(int idx, const char *model) {
255 int dc = npc_damage_class(idx, model);
256 if (dc == NPC_DMG_IMMUNE)
257 return false;
258 if (dc != NPC_DMG_ARMORED)
259 return true;
260 int local = engine_get_local_player();
261 void *le = local > 0 ? entitylist_get(local) : nullptr;
262 return active_weapon_contains(le, "rocket_launcher") ||
263 active_weapon_contains(le, "rpg");
264}
265
223static void short_name(const char *model, char *out, size_t outn) { 266static void short_name(const char *model, char *out, size_t outn) {
224 if (!model) { snprintf(out, outn, "NPC"); return; } 267 if (!model) { snprintf(out, outn, "NPC"); return; }
225 const char *base = strrchr(model, '/'); 268 const char *base = strrchr(model, '/');
@@ -334,6 +377,7 @@ static void esp_collect(int local_idx) {
334 bool neutral = false; 377 bool neutral = false;
335 t.friendly = npc_is_friendly(idx, model, local_idx, neutral); 378 t.friendly = npc_is_friendly(idx, model, local_idx, neutral);
336 t.neutral = neutral; 379 t.neutral = neutral;
380 t.damage = npc_damage_class(idx, model);
337 short_name(model, t.name, sizeof(t.name)); 381 short_name(model, t.name, sizeof(t.name));
338 if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); 382 if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC");
339 } 383 }
@@ -603,6 +647,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
603 bool neutral = false; 647 bool neutral = false;
604 if (npc_is_friendly(idx, model, local, neutral) || neutral) 648 if (npc_is_friendly(idx, model, local, neutral) || neutral)
605 return false; 649 return false;
650 if (!npc_can_be_damaged(idx, model))
651 return false;
606 if (!bones_update(idx, ent)) 652 if (!bones_update(idx, ent))
607 return false; 653 return false;
608 Vector pts[32]; 654 Vector pts[32];
@@ -682,6 +728,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
682 bool neutral = false; 728 bool neutral = false;
683 if (npc_is_friendly(idx, model, local, neutral) || neutral) 729 if (npc_is_friendly(idx, model, local, neutral) || neutral)
684 continue; 730 continue;
731 if (!npc_can_be_damaged(idx, model))
732 continue;
685 733
686 float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; 734 float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f;
687 if (!(maxz > 4.0f && maxz < 512.0f)) 735 if (!(maxz > 4.0f && maxz < 512.0f))
@@ -1063,6 +1111,8 @@ bool melee_aimbot_apply(CUserCmd *cmd) {
1063 bool neutral = false; 1111 bool neutral = false;
1064 if (npc_is_friendly(idx, model, local, neutral) || neutral) 1112 if (npc_is_friendly(idx, model, local, neutral) || neutral)
1065 continue; 1113 continue;
1114 if (!npc_can_be_damaged(idx, model))
1115 continue;
1066 1116
1067 Vector origin{*(float *)(e + g_net.origin), 1117 Vector origin{*(float *)(e + g_net.origin),
1068 *(float *)(e + g_net.origin + 4), 1118 *(float *)(e + g_net.origin + 4),
@@ -1394,6 +1444,8 @@ bool triggerbot_apply(CUserCmd *cmd) {
1394 bool neutral = false; 1444 bool neutral = false;
1395 if (npc_is_friendly(idx, model, local, neutral) || neutral) 1445 if (npc_is_friendly(idx, model, local, neutral) || neutral)
1396 return false; 1446 return false;
1447 if (!npc_can_be_damaged(idx, model))
1448 return false;
1397 cmd->buttons |= IN_ATTACK; 1449 cmd->buttons |= IN_ATTACK;
1398 static int logs = 0; 1450 static int logs = 0;
1399 if (logs < 5) { 1451 if (logs < 5) {
@@ -1517,6 +1569,23 @@ void esp_draw() {
1517 } 1569 }
1518 if (esp_option(ESP_OPT_SHOW_NAME)) 1570 if (esp_option(ESP_OPT_SHOW_NAME))
1519 draw_text_col(left, top - 14, t.name, 255, 255, 255); 1571 draw_text_col(left, top - 14, t.name, 255, 255, 255);
1572 if (esp_option(ESP_OPT_DAMAGE_TAGS)) {
1573 const char *tag = nullptr;
1574 int tr = 255, tg = 255, tb = 255;
1575 if (t.damage == NPC_DMG_ARMORED) {
1576 tag = "ARMORED";
1577 tr = 255; tg = 170; tb = 60;
1578 } else if (t.damage == NPC_DMG_VULNERABLE) {
1579 tag = "VULN";
1580 tr = 90; tg = 220; tb = 255;
1581 } else if (t.damage == NPC_DMG_IMMUNE) {
1582 tag = "IMMUNE";
1583 tr = 180; tg = 180; tb = 180;
1584 }
1585 if (tag)
1586 draw_text_col(left, top - (esp_option(ESP_OPT_SHOW_NAME) ? 28 : 14),
1587 tag, tr, tg, tb);
1588 }
1520 } 1589 }
1521 1590
1522 // Groundwork probe: HL2H_BONE_DEBUG=1 draws a white marker at each NPC's 1591 // 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() {
359 static const char *esp_names[ESP_OPT_COUNT] = { 359 static const char *esp_names[ESP_OPT_COUNT] = {
360 "Show neutral", "Show healthbar", "Show healthnum", 360 "Show neutral", "Show healthbar", "Show healthnum",
361 "Show name", "Show skeleton", "Show box", 361 "Show name", "Show skeleton", "Show box",
362 "Show speed graph"}; 362 "Show speed graph", "Damage tags"};
363 static const char *esp_descs[ESP_OPT_COUNT] = { 363 static const char *esp_descs[ESP_OPT_COUNT] = {
364 "Include neutral wildlife.", "Draw a health bar.", 364 "Include neutral wildlife.", "Draw a health bar.",
365 "Draw the numeric health value.", "Draw the model name.", 365 "Draw the numeric health value.", "Draw the model name.",
366 "Draw the animated bone links.", "Draw a bounding box.", 366 "Draw the animated bone links.", "Draw a bounding box.",
367 "Draw the lower-center speed graph."}; 367 "Draw the lower-center speed graph.",
368 "Label ARMORED (needs blast/RPG) and VULN (gravity gun) units."};
368 for (int i = 0; i < ESP_OPT_COUNT; i++) 369 for (int i = 0; i < ESP_OPT_COUNT; i++)
369 add_toggle(esp, esp_names[i], esp_descs[i], &g_esp_opt[i]); 370 add_toggle(esp, esp_names[i], esp_descs[i], &g_esp_opt[i]);
370 371
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 {
192 ESP_OPT_SHOW_SKELETON = 4, 192 ESP_OPT_SHOW_SKELETON = 4,
193 ESP_OPT_SHOW_BOX = 5, 193 ESP_OPT_SHOW_BOX = 5,
194 ESP_OPT_SHOW_SPEEDGRAPH = 6, 194 ESP_OPT_SHOW_SPEEDGRAPH = 6,
195 ESP_OPT_COUNT = 7 195 ESP_OPT_DAMAGE_TAGS = 7,
196 ESP_OPT_COUNT = 8
196}; 197};
197enum AimBone { 198enum AimBone {
198 AIM_BONE_HEAD = 0, 199 AIM_BONE_HEAD = 0,
@@ -273,6 +274,7 @@ int server_active_weapon(int idx);
273int server_npc_relation(int idx, int local_idx); 274int server_npc_relation(int idx, int local_idx);
274bool server_npc_has_follow_target(int idx); 275bool server_npc_has_follow_target(int idx);
275int server_npc_state(int idx); 276int server_npc_state(int idx);
277int server_npc_takedamage(int idx);
276int server_npc_turret_flags(int idx); 278int server_npc_turret_flags(int idx);
277int server_model_index(void *base); 279int server_model_index(void *base);
278void find_modules(); 280void 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;
78int g_silent_fov = 60; 78int g_silent_fov = 60;
79 79
80// ESP sub-options. Defaults match the pre-option behaviour: everything on. 80// ESP sub-options. Defaults match the pre-option behaviour: everything on.
81bool g_esp_opt[ESP_OPT_COUNT] = {true, true, true, true, true, true, true}; 81bool g_esp_opt[ESP_OPT_COUNT] = {true, true, true, true, true, true, true,
82 true};
82 83
83// --------------------------------------------------------------------------- 84// ---------------------------------------------------------------------------
84// config persistence (INI format) 85// config persistence (INI format)
@@ -221,12 +222,14 @@ void config_save() {
221 (int)g_item_ammo, (int)g_item_armor, (int)g_item_weapons, 222 (int)g_item_ammo, (int)g_item_armor, (int)g_item_weapons,
222 (int)g_item_crates, (int)g_item_distance, g_item_max_dist); 223 (int)g_item_crates, (int)g_item_distance, g_item_max_dist);
223 fprintf(f, "[esp]\nactive=%d\nbind=%d\nneutral=%d\nhealthbar=%d\n" 224 fprintf(f, "[esp]\nactive=%d\nbind=%d\nneutral=%d\nhealthbar=%d\n"
224 "healthnum=%d\nname=%d\nskeleton=%d\nbox=%d\nspeedgraph=%d\n", 225 "healthnum=%d\nname=%d\nskeleton=%d\nbox=%d\nspeedgraph=%d\n"
226 "dmg=%d\n",
225 g_esp.active, g_esp.bind, g_esp_opt[ESP_OPT_SHOW_NEUTRAL], 227 g_esp.active, g_esp.bind, g_esp_opt[ESP_OPT_SHOW_NEUTRAL],
226 g_esp_opt[ESP_OPT_SHOW_HEALTHBAR], 228 g_esp_opt[ESP_OPT_SHOW_HEALTHBAR],
227 g_esp_opt[ESP_OPT_SHOW_HEALTHNUM], g_esp_opt[ESP_OPT_SHOW_NAME], 229 g_esp_opt[ESP_OPT_SHOW_HEALTHNUM], g_esp_opt[ESP_OPT_SHOW_NAME],
228 g_esp_opt[ESP_OPT_SHOW_SKELETON], g_esp_opt[ESP_OPT_SHOW_BOX], 230 g_esp_opt[ESP_OPT_SHOW_SKELETON], g_esp_opt[ESP_OPT_SHOW_BOX],
229 g_esp_opt[ESP_OPT_SHOW_SPEEDGRAPH]); 231 g_esp_opt[ESP_OPT_SHOW_SPEEDGRAPH],
232 g_esp_opt[ESP_OPT_DAMAGE_TAGS]);
230 gui_config_write(f); 233 gui_config_write(f);
231 bool ok = ferror(f) == 0; 234 bool ok = ferror(f) == 0;
232 if (fflush(f) != 0) 235 if (fflush(f) != 0)
@@ -427,6 +430,8 @@ static bool config_load() {
427 g_esp_opt[ESP_OPT_SHOW_BOX] = atoi(val) != 0; 430 g_esp_opt[ESP_OPT_SHOW_BOX] = atoi(val) != 0;
428 else if (strcmp(key, "speedgraph") == 0) 431 else if (strcmp(key, "speedgraph") == 0)
429 g_esp_opt[ESP_OPT_SHOW_SPEEDGRAPH] = atoi(val) != 0; 432 g_esp_opt[ESP_OPT_SHOW_SPEEDGRAPH] = atoi(val) != 0;
433 else if (strcmp(key, "dmg") == 0)
434 g_esp_opt[ESP_OPT_DAMAGE_TAGS] = atoi(val) != 0;
430 } 435 }
431 if (strcmp(key, "active") == 0) 436 if (strcmp(key, "active") == 0)
432 cur->active = atoi(val) != 0; 437 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;
51static int g_srv_spawnflags = -1; 51static int g_srv_spawnflags = -1;
52static int g_srv_hacked = -1; 52static int g_srv_hacked = -1;
53static int g_srv_npcstate = -1; 53static int g_srv_npcstate = -1;
54static int g_srv_takedamage = -1;
54static bool g_srv_ready = false; 55static bool g_srv_ready = false;
55static bool g_srv_checked = false; 56static bool g_srv_checked = false;
56 57
@@ -141,15 +142,17 @@ bool resolve_server_netvars() {
141 g_srv_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4); 142 g_srv_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4);
142 g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN); 143 g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN);
143 g_srv_npcstate = datamap_offset("m_NPCState", FIELD_INTEGER); 144 g_srv_npcstate = datamap_offset("m_NPCState", FIELD_INTEGER);
145 g_srv_takedamage = datamap_offset("m_takedamage", FIELD_INTEGER);
144 g_srv_ready = g_srv_health > 0; 146 g_srv_ready = g_srv_health > 0;
145 if (g_srv_ready) { 147 if (g_srv_ready) {
146 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " 148 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d "
147 "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " 149 "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d "
148 "followtarget=%d spawnflags=%d hacked=%d npcstate=%d", 150 "followtarget=%d spawnflags=%d hacked=%d npcstate=%d "
151 "takedamage=%d",
149 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, 152 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate,
150 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, 153 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity,
151 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, 154 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget,
152 g_srv_spawnflags, g_srv_hacked, g_srv_npcstate); 155 g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage);
153 fail_logged = false; 156 fail_logged = false;
154 } else if (!fail_logged) { 157 } else if (!fail_logged) {
155 fail_logged = true; 158 fail_logged = true;
@@ -268,6 +271,18 @@ int server_npc_state(int idx) {
268 return *(int *)((uintptr_t)base + g_srv_npcstate); 271 return *(int *)((uintptr_t)base + g_srv_npcstate);
269} 272}
270 273
274// TAKEDAMAGE: 0 = DAMAGE_NO, 1 = DAMAGE_EVENTS_ONLY, 2 = DAMAGE_YES,
275// 3 = DAMAGE_AIM. 0/1 cannot be damaged at all.
276int server_npc_takedamage(int idx) {
277 if (!g_srv_ready || g_srv_takedamage < 0)
278 return -1;
279 void *base = nullptr;
280 server_entity_health(idx, nullptr, nullptr, &base);
281 if (!base)
282 return -1;
283 return *(int *)((uintptr_t)base + g_srv_takedamage);
284}
285
271bool server_npc_has_follow_target(int idx) { 286bool server_npc_has_follow_target(int idx) {
272 if (!g_srv_ready || g_srv_followtarget < 0) 287 if (!g_srv_ready || g_srv_followtarget < 0)
273 return false; 288 return false;