From 094ce7621e920276dc0e658cd43ca73db848f2f3 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 3 Nov 2026 22:53:05 +0100 Subject: 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. --- src/esp.cpp | 45 +++++++++++++++++++++++++++++++++++------ src/hl2h.h | 2 ++ src/server.cpp | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 101 insertions(+), 10 deletions(-) (limited to 'src') 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) { return false; } +static bool npc_is_friendly(int idx, const char *model, int local_idx, + bool &neutral) { + int rel = server_npc_relation(idx, local_idx); + if (rel == 3) { // D_LI + neutral = false; + return true; + } + if (rel == 4) { // D_NU + neutral = true; + return false; + } + if (rel == 1 || rel == 2) { // D_HT / D_FR + neutral = false; + return false; + } + // Bugbait-controlled antlions follow a target even when their default + // relationship was never rewritten. + if (model && strstr(model, "antlion") && + server_npc_has_follow_target(idx)) { + neutral = false; + return true; + } + bool friendly = model_is_friendly(model); + neutral = model_is_neutral(model); + return friendly; +} + + static void short_name(const char *model, char *out, size_t outn) { if (!model) { snprintf(out, outn, "NPC"); return; } const char *base = strrchr(model, '/'); @@ -259,8 +287,9 @@ static void esp_collect(int local_idx) { t.health = hp > 0 ? (float)hp : 0.0f; t.maxhealth = maxhp > 0 ? (float)maxhp : 100.0f; t.show_health = hp > 0; - t.friendly = model_is_friendly(model); - t.neutral = model_is_neutral(model); + bool neutral = false; + t.friendly = npc_is_friendly(idx, model, local_idx, neutral); + t.neutral = neutral; short_name(model, t.name, sizeof(t.name)); if (npc && !model) snprintf(t.name, sizeof(t.name), "NPC"); } @@ -527,7 +556,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, if (life != LIFE_ALIVE) return false; const char *model = entity_model_name(ent); - if (model_is_friendly(model) || model_is_neutral(model)) + bool neutral = false; + if (npc_is_friendly(idx, model, local, neutral) || neutral) return false; if (!bones_update(idx, ent)) return false; @@ -605,7 +635,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask, if (life != LIFE_ALIVE) continue; const char *model = entity_model_name(ent); - if (model_is_friendly(model) || model_is_neutral(model)) + bool neutral = false; + if (npc_is_friendly(idx, model, local, neutral) || neutral) continue; float maxz = g_net.maxs >= 0 ? *(float *)(e + g_net.maxs + 8) : 72.0f; @@ -990,7 +1021,8 @@ bool melee_aimbot_apply(CUserCmd *cmd) { if (life != LIFE_ALIVE) continue; const char *model = entity_model_name(ent); - if (model_is_friendly(model) || model_is_neutral(model)) + bool neutral = false; + if (npc_is_friendly(idx, model, local, neutral) || neutral) continue; Vector origin{*(float *)(e + g_net.origin), @@ -1320,7 +1352,8 @@ bool triggerbot_apply(CUserCmd *cmd) { if (life != LIFE_ALIVE) return false; const char *model = entity_model_name(hit); - if (model_is_friendly(model) || model_is_neutral(model)) + bool neutral = false; + if (npc_is_friendly(idx, model, local, neutral) || neutral) return false; cmd->buttons |= IN_ATTACK; static int logs = 0; diff --git a/src/hl2h.h b/src/hl2h.h index e441167..414e061 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -252,6 +252,8 @@ bool resolve_server_netvars(); int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); bool server_set_velocity_z(int idx, float vz); 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_model_index(void *base); void find_modules(); 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 }; // typedescription_t (32-bit): fieldType@0, fieldName@4, fieldOffset[0]@8, // fieldOffset[1]@0xC, fieldSize@0x10, flags@0x12, externalName@0x14 -enum { FIELD_INTEGER = 5, FIELD_CHARACTER = 8 }; +enum { + FIELD_INTEGER = 5, + FIELD_CHARACTER = 8, + FIELD_EMBEDDED = 10, + FIELD_EHANDLE = 13 +}; int g_srv_health = -1; int g_srv_maxhealth = -1; @@ -34,6 +39,8 @@ int g_srv_modelindex = -1; int g_srv_absvelocity = -1; int g_srv_velocity = -1; int g_srv_activeweapon = -1; +static int g_srv_relationship = -1; +static int g_srv_followtarget = -1; static bool g_srv_ready = false; static bool g_srv_checked = false; @@ -110,14 +117,17 @@ bool resolve_server_netvars() { g_srv_modelindex = datamap_offset("m_nModelIndex", 7); // FIELD_SHORT g_srv_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR - g_srv_activeweapon = datamap_offset("m_hActiveWeapon", 13); // FIELD_EHANDLE + g_srv_activeweapon = datamap_offset("m_hActiveWeapon", FIELD_EHANDLE); + g_srv_relationship = datamap_offset("m_Relationship", FIELD_EMBEDDED); + g_srv_followtarget = datamap_offset("m_hFollowTarget", FIELD_EHANDLE); 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", + "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " + "followtarget=%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_activeweapon, g_srv_relationship, g_srv_followtarget); fail_logged = false; } else if (!fail_logged) { fail_logged = true; @@ -189,6 +199,52 @@ int server_model_index(void *base) { // networked m_vecVelocity directly is deliberately avoided because the server // datamap scan can find a same-named field on another class first. The movement // code syncs m_vecVelocity through SetAbsVelocity after the command. +// Server-side AI relationship (D_ER=0, D_HT=1, D_FR=2, D_LI=3, D_NU=4). +// m_Relationship is a CUtlVector; each entry is +// EHANDLE entity, Class_T classType, Disposition_t disposition, int priority. +int server_npc_relation(int idx, int local_idx) { + if (!g_srv_ready || g_srv_relationship < 0 || local_idx <= 0) + return -1; + void *base = nullptr; + server_entity_health(idx, nullptr, nullptr, &base); + if (!base) + return -1; + uintptr_t rel = (uintptr_t)base + g_srv_relationship; + int count = *(int *)(rel + 12); + if (count <= 0 || count > 64) + return -1; + uintptr_t elems = *(uintptr_t *)(rel + 16); + if (!elems) + elems = *(uintptr_t *)(rel + 0); + if (!elems || (elems & 3)) + return -1; + int class_relation = -1; + for (int i = 0; i < count; i++) { + uintptr_t e = elems + (uintptr_t)i * 16; + uint32_t h = *(uint32_t *)(e + 0); + int class_type = *(int *)(e + 4); + int disposition = *(int *)(e + 8); + if (h != 0 && h != 0xFFFFFFFFu) { + if ((int)(h & 0x7FF) == local_idx) + return disposition; + } else if (class_type == 1) { // CLASS_PLAYER + class_relation = disposition; + } + } + return class_relation; +} + +bool server_npc_has_follow_target(int idx) { + if (!g_srv_ready || g_srv_followtarget < 0) + return false; + void *base = nullptr; + server_entity_health(idx, nullptr, nullptr, &base); + if (!base) + return false; + uint32_t h = *(uint32_t *)((uintptr_t)base + g_srv_followtarget); + return h != 0 && h != 0xFFFFFFFFu; +} + bool server_set_velocity_z(int idx, float vz) { if (!g_srv_ready || !g_iface.engineserver || g_srv_absvelocity <= 0) return false; -- cgit v1.2.3