diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-03 22:53:05 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-03 22:53:05 +0100 |
| commit | 094ce7621e920276dc0e658cd43ca73db848f2f3 (patch) | |
| tree | 5c191fe2aa69fb7d59229c19091f12dfe208ce56 /src | |
| parent | Fix undefined fullbright symbols; link with --no-undefined (diff) | |
| download | hl2h-094ce7621e920276dc0e658cd43ca73db848f2f3.tar.gz hl2h-094ce7621e920276dc0e658cd43ca73db848f2f3.zip | |
AI: use server relationships to classify NPC friendliness
- Read the server NPC's m_Relationship CUtlVector (entity row for the local
player first, then the CLASS_PLAYER row): D_LI friendly, D_NU neutral,
D_HT/D_FR hostile.
- Use it for ESP colours and aimbot/silent-aim/melee target filtering, with
the existing model heuristic as a fallback.
- Treat bugbait-controlled antlions as friendly when their server
m_hFollowTarget is set.
- Resolve m_Relationship/m_hFollowTarget datamap offsets and log them.
Diffstat (limited to 'src')
| -rw-r--r-- | src/esp.cpp | 45 | ||||
| -rw-r--r-- | src/hl2h.h | 2 | ||||
| -rw-r--r-- | src/server.cpp | 64 |
3 files changed, 101 insertions, 10 deletions
diff --git a/src/esp.cpp b/src/esp.cpp index 7c3f1bc..0966ff5 100644 --- a/src/esp.cpp +++ b/src/esp.cpp | |||
| @@ -148,6 +148,34 @@ static bool model_is_neutral(const char *m) { | |||
| 148 | return false; | 148 | return false; |
| 149 | } | 149 | } |
| 150 | 150 | ||
| 151 | static bool npc_is_friendly(int idx, const char *model, int local_idx, | ||
| 152 | bool &neutral) { | ||
| 153 | int rel = server_npc_relation(idx, local_idx); | ||
| 154 | if (rel == 3) { // D_LI | ||
| 155 | neutral = false; | ||
| 156 | return true; | ||
| 157 | } | ||
| 158 | if (rel == 4) { // D_NU | ||
| 159 | neutral = true; | ||
| 160 | return false; | ||
| 161 | } | ||
| 162 | if (rel == 1 || rel == 2) { // D_HT / D_FR | ||
| 163 | neutral = false; | ||
| 164 | return false; | ||
| 165 | } | ||
| 166 | // Bugbait-controlled antlions follow a target even when their default | ||
| 167 | // relationship was never rewritten. | ||
| 168 | if (model && strstr(model, "antlion") && | ||
| 169 | server_npc_has_follow_target(idx)) { | ||
| 170 | neutral = false; | ||
| 171 | return true; | ||
| 172 | } | ||
| 173 | bool friendly = model_is_friendly(model); | ||
| 174 | neutral = model_is_neutral(model); | ||
| 175 | return friendly; | ||
| 176 | } | ||
| 177 | |||
| 178 | |||
| 151 | static void short_name(const char *model, char *out, size_t outn) { | 179 | static void short_name(const char *model, char *out, size_t outn) { |
| 152 | if (!model) { snprintf(out, outn, "NPC"); return; } | 180 | if (!model) { snprintf(out, outn, "NPC"); return; } |
| 153 | const char *base = strrchr(model, '/'); | 181 | const char *base = strrchr(model, '/'); |
| @@ -259,8 +287,9 @@ static void esp_collect(int local_idx) { | |||
| 259 | t.health = hp > 0 ? (float)hp : 0.0f; | 287 | t.health = hp > 0 ? (float)hp : 0.0f; |
| 260 | t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; | 288 | t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; |
| 261 | t.show_health = hp > 0; | 289 | t.show_health = hp > 0; |
| 262 | t.friendly = model_is_friendly(model); | 290 | bool neutral = false; |
| 263 | t.neutral = model_is_neutral(model); | 291 | t.friendly = npc_is_friendly(idx, model, local_idx, neutral); |
| 292 | t.neutral = neutral; | ||
| 264 | short_name(model, t.name, sizeof(t.name)); | 293 | short_name(model, t.name, sizeof(t.name)); |
| 265 | if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); | 294 | if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); |
| 266 | } | 295 | } |
| @@ -527,7 +556,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, | |||
| 527 | if (life != LIFE_ALIVE) | 556 | if (life != LIFE_ALIVE) |
| 528 | return false; | 557 | return false; |
| 529 | const char *model = entity_model_name(ent); | 558 | const char *model = entity_model_name(ent); |
| 530 | if (model_is_friendly(model) || model_is_neutral(model)) | 559 | bool neutral = false; |
| 560 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 531 | return false; | 561 | return false; |
| 532 | if (!bones_update(idx, ent)) | 562 | if (!bones_update(idx, ent)) |
| 533 | return false; | 563 | return false; |
| @@ -605,7 +635,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, | |||
| 605 | if (life != LIFE_ALIVE) | 635 | if (life != LIFE_ALIVE) |
| 606 | continue; | 636 | continue; |
| 607 | const char *model = entity_model_name(ent); | 637 | const char *model = entity_model_name(ent); |
| 608 | if (model_is_friendly(model) || model_is_neutral(model)) | 638 | bool neutral = false; |
| 639 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 609 | continue; | 640 | continue; |
| 610 | 641 | ||
| 611 | float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; | 642 | float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; |
| @@ -990,7 +1021,8 @@ bool melee_aimbot_apply(CUserCmd *cmd) { | |||
| 990 | if (life != LIFE_ALIVE) | 1021 | if (life != LIFE_ALIVE) |
| 991 | continue; | 1022 | continue; |
| 992 | const char *model = entity_model_name(ent); | 1023 | const char *model = entity_model_name(ent); |
| 993 | if (model_is_friendly(model) || model_is_neutral(model)) | 1024 | bool neutral = false; |
| 1025 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 994 | continue; | 1026 | continue; |
| 995 | 1027 | ||
| 996 | Vector origin{*(float *)(e + g_net.origin), | 1028 | Vector origin{*(float *)(e + g_net.origin), |
| @@ -1320,7 +1352,8 @@ bool triggerbot_apply(CUserCmd *cmd) { | |||
| 1320 | if (life != LIFE_ALIVE) | 1352 | if (life != LIFE_ALIVE) |
| 1321 | return false; | 1353 | return false; |
| 1322 | const char *model = entity_model_name(hit); | 1354 | const char *model = entity_model_name(hit); |
| 1323 | if (model_is_friendly(model) || model_is_neutral(model)) | 1355 | bool neutral = false; |
| 1356 | if (npc_is_friendly(idx, model, local, neutral) || neutral) | ||
| 1324 | return false; | 1357 | return false; |
| 1325 | cmd->buttons |= IN_ATTACK; | 1358 | cmd->buttons |= IN_ATTACK; |
| 1326 | static int logs = 0; | 1359 | static int logs = 0; |
| @@ -252,6 +252,8 @@ bool resolve_server_netvars(); | |||
| 252 | int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); | 252 | int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); |
| 253 | bool server_set_velocity_z(int idx, float vz); | 253 | bool server_set_velocity_z(int idx, float vz); |
| 254 | int server_active_weapon(int idx); | 254 | int server_active_weapon(int idx); |
| 255 | int server_npc_relation(int idx, int local_idx); | ||
| 256 | bool server_npc_has_follow_target(int idx); | ||
| 255 | int server_model_index(void *base); | 257 | int server_model_index(void *base); |
| 256 | void find_modules(); | 258 | void find_modules(); |
| 257 | void init_conmsg(); | 259 | void init_conmsg(); |
diff --git a/src/server.cpp b/src/server.cpp index 5599227..4702c44 100644 --- a/src/server.cpp +++ b/src/server.cpp | |||
| @@ -25,7 +25,12 @@ enum { EDICT_UNKNOWN_OFF = 0x0C }; | |||
| 25 | 25 | ||
| 26 | // typedescription_t (32-bit): fieldType@0, fieldName@4, fieldOffset[0]@8, | 26 | // typedescription_t (32-bit): fieldType@0, fieldName@4, fieldOffset[0]@8, |
| 27 | // fieldOffset[1]@0xC, fieldSize@0x10, flags@0x12, externalName@0x14 | 27 | // fieldOffset[1]@0xC, fieldSize@0x10, flags@0x12, externalName@0x14 |
| 28 | enum { FIELD_INTEGER = 5, FIELD_CHARACTER = 8 }; | 28 | enum { |
| 29 | FIELD_INTEGER = 5, | ||
| 30 | FIELD_CHARACTER = 8, | ||
| 31 | FIELD_EMBEDDED = 10, | ||
| 32 | FIELD_EHANDLE = 13 | ||
| 33 | }; | ||
| 29 | 34 | ||
| 30 | int g_srv_health = -1; | 35 | int g_srv_health = -1; |
| 31 | int g_srv_maxhealth = -1; | 36 | int g_srv_maxhealth = -1; |
| @@ -34,6 +39,8 @@ int g_srv_modelindex = -1; | |||
| 34 | int g_srv_absvelocity = -1; | 39 | int g_srv_absvelocity = -1; |
| 35 | int g_srv_velocity = -1; | 40 | int g_srv_velocity = -1; |
| 36 | int g_srv_activeweapon = -1; | 41 | int g_srv_activeweapon = -1; |
| 42 | static int g_srv_relationship = -1; | ||
| 43 | static int g_srv_followtarget = -1; | ||
| 37 | static bool g_srv_ready = false; | 44 | static bool g_srv_ready = false; |
| 38 | static bool g_srv_checked = false; | 45 | static bool g_srv_checked = false; |
| 39 | 46 | ||
| @@ -110,14 +117,17 @@ bool resolve_server_netvars() { | |||
| 110 | g_srv_modelindex = datamap_offset("m_nModelIndex", 7); // FIELD_SHORT | 117 | g_srv_modelindex = datamap_offset("m_nModelIndex", 7); // FIELD_SHORT |
| 111 | g_srv_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR | 118 | g_srv_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR |
| 112 | g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR | 119 | g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR |
| 113 | g_srv_activeweapon = datamap_offset("m_hActiveWeapon", 13); // FIELD_EHANDLE | 120 | g_srv_activeweapon = datamap_offset("m_hActiveWeapon", FIELD_EHANDLE); |
| 121 | g_srv_relationship = datamap_offset("m_Relationship", FIELD_EMBEDDED); | ||
| 122 | g_srv_followtarget = datamap_offset("m_hFollowTarget", FIELD_EHANDLE); | ||
| 114 | g_srv_ready = g_srv_health > 0; | 123 | g_srv_ready = g_srv_health > 0; |
| 115 | if (g_srv_ready) { | 124 | if (g_srv_ready) { |
| 116 | logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " | 125 | logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " |
| 117 | "modelindex=%d absvel=%d vel=%d activeweapon=%d", | 126 | "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " |
| 127 | "followtarget=%d", | ||
| 118 | g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, | 128 | g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, |
| 119 | g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, | 129 | g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, |
| 120 | g_srv_activeweapon); | 130 | g_srv_activeweapon, g_srv_relationship, g_srv_followtarget); |
| 121 | fail_logged = false; | 131 | fail_logged = false; |
| 122 | } else if (!fail_logged) { | 132 | } else if (!fail_logged) { |
| 123 | fail_logged = true; | 133 | fail_logged = true; |
| @@ -189,6 +199,52 @@ int server_model_index(void *base) { | |||
| 189 | // networked m_vecVelocity directly is deliberately avoided because the server | 199 | // networked m_vecVelocity directly is deliberately avoided because the server |
| 190 | // datamap scan can find a same-named field on another class first. The movement | 200 | // datamap scan can find a same-named field on another class first. The movement |
| 191 | // code syncs m_vecVelocity through SetAbsVelocity after the command. | 201 | // code syncs m_vecVelocity through SetAbsVelocity after the command. |
| 202 | // Server-side AI relationship (D_ER=0, D_HT=1, D_FR=2, D_LI=3, D_NU=4). | ||
| 203 | // m_Relationship is a CUtlVector<Relationship_t>; each entry is | ||
| 204 | // EHANDLE entity, Class_T classType, Disposition_t disposition, int priority. | ||
| 205 | int server_npc_relation(int idx, int local_idx) { | ||
| 206 | if (!g_srv_ready || g_srv_relationship < 0 || local_idx <= 0) | ||
| 207 | return -1; | ||
| 208 | void *base = nullptr; | ||
| 209 | server_entity_health(idx, nullptr, nullptr, &base); | ||
| 210 | if (!base) | ||
| 211 | return -1; | ||
| 212 | uintptr_t rel = (uintptr_t)base + g_srv_relationship; | ||
| 213 | int count = *(int *)(rel + 12); | ||
| 214 | if (count <= 0 || count > 64) | ||
| 215 | return -1; | ||
| 216 | uintptr_t elems = *(uintptr_t *)(rel + 16); | ||
| 217 | if (!elems) | ||
| 218 | elems = *(uintptr_t *)(rel + 0); | ||
| 219 | if (!elems || (elems & 3)) | ||
| 220 | return -1; | ||
| 221 | int class_relation = -1; | ||
| 222 | for (int i = 0; i < count; i++) { | ||
| 223 | uintptr_t e = elems + (uintptr_t)i * 16; | ||
| 224 | uint32_t h = *(uint32_t *)(e + 0); | ||
| 225 | int class_type = *(int *)(e + 4); | ||
| 226 | int disposition = *(int *)(e + 8); | ||
| 227 | if (h != 0 && h != 0xFFFFFFFFu) { | ||
| 228 | if ((int)(h & 0x7FF) == local_idx) | ||
| 229 | return disposition; | ||
| 230 | } else if (class_type == 1) { // CLASS_PLAYER | ||
| 231 | class_relation = disposition; | ||
| 232 | } | ||
| 233 | } | ||
| 234 | return class_relation; | ||
| 235 | } | ||
| 236 | |||
| 237 | bool server_npc_has_follow_target(int idx) { | ||
| 238 | if (!g_srv_ready || g_srv_followtarget < 0) | ||
| 239 | return false; | ||
| 240 | void *base = nullptr; | ||
| 241 | server_entity_health(idx, nullptr, nullptr, &base); | ||
| 242 | if (!base) | ||
| 243 | return false; | ||
| 244 | uint32_t h = *(uint32_t *)((uintptr_t)base + g_srv_followtarget); | ||
| 245 | return h != 0 && h != 0xFFFFFFFFu; | ||
| 246 | } | ||
| 247 | |||
| 192 | bool server_set_velocity_z(int idx, float vz) { | 248 | bool server_set_velocity_z(int idx, float vz) { |
| 193 | if (!g_srv_ready || !g_iface.engineserver || g_srv_absvelocity <= 0) | 249 | if (!g_srv_ready || !g_iface.engineserver || g_srv_absvelocity <= 0) |
| 194 | return false; | 250 | return false; |
