From 3249caf17de0dcab005398995ef3e611b3f81131 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 3 Nov 2026 23:07:40 +0100 Subject: AI: fix relationship lookup; classify citizen/hacked turrets m_Relationship is DEFINE_UTLVECTOR (FIELD_CUSTOM), not FIELD_EMBEDDED, so the datamap scan always returned -1 and every NPC fell back to the model heuristic. Resolve it as FIELD_CUSTOM with an embedded fallback. Floor turrets never add relationship rows when friendly: CNPC_FloorTurret overrides Classify() to CLASS_PLAYER_ALLY for citizen-built (SF_FLOOR_TURRET_CITIZEN) and Alyx-hacked turrets. Read m_spawnflags and m_bHackedByAlyx from the server instead. Widen the datamap offset window (0x4..0x4000) for derived-class fields and log the new offsets plus turret state in the NPC debug line. --- src/esp.cpp | 17 +++++++++++++++-- src/hl2h.h | 1 + src/server.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 67 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/esp.cpp b/src/esp.cpp index b09588f..40e00f6 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -148,9 +148,18 @@ static bool model_is_neutral(const char *m) { return false; } +static bool model_is_floor_turret(const char *m) { + return m && strstr(m, "floor_turret"); +} + static bool npc_is_friendly(int idx, const char *model, int local_idx, bool &neutral) { int rel = server_npc_relation(idx, local_idx); + int turret = -1; + // Citizen-built and Alyx-hacked turrets never add an m_Relationship row; + // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state. + if (model_is_floor_turret(model)) + turret = server_npc_turret_flags(idx); // Debug-only: relation values are the authoritative answer and are the // first thing to check when a friendly NPC still looks hostile. Logged only // when an entity's relation changes, so this does not flood the log. @@ -163,9 +172,9 @@ static bool npc_is_friendly(int idx, const char *model, int local_idx, } if (idx > 0 && idx < 2048 && rel_cache[idx] != rel) { rel_cache[idx] = rel; - logd("npc %d (%s): relation=%d followtarget=%d local=%d", idx, + logd("npc %d (%s): relation=%d followtarget=%d turret=%d local=%d", idx, model ? model : "?", rel, - (int)server_npc_has_follow_target(idx), local_idx); + (int)server_npc_has_follow_target(idx), turret, local_idx); } if (rel == 3) { // D_LI neutral = false; @@ -179,6 +188,10 @@ static bool npc_is_friendly(int idx, const char *model, int local_idx, neutral = false; return false; } + if (turret >= 0) { + neutral = false; + return turret != 0; + } // Bugbait-controlled antlions follow a target even when their default // relationship was never rewritten. if (model && strstr(model, "antlion") && diff --git a/src/hl2h.h b/src/hl2h.h index 414e061..2824d3c 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -254,6 +254,7 @@ 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_npc_turret_flags(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 4702c44..19be144 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -27,11 +27,18 @@ enum { EDICT_UNKNOWN_OFF = 0x0C }; // fieldOffset[1]@0xC, fieldSize@0x10, flags@0x12, externalName@0x14 enum { FIELD_INTEGER = 5, + FIELD_BOOLEAN = 6, FIELD_CHARACTER = 8, FIELD_EMBEDDED = 10, + FIELD_CUSTOM = 11, FIELD_EHANDLE = 13 }; +// SF_FLOOR_TURRET_CITIZEN (npc_turret_floor.h). Citizen-built turrets +// Classify() as CLASS_PLAYER_ALLY, i.e. they are friendly without ever adding +// an entry to m_Relationship. +static const int TURRET_FLAG_CITIZEN = 0x200; + int g_srv_health = -1; int g_srv_maxhealth = -1; int g_srv_lifestate = -1; @@ -41,6 +48,8 @@ int g_srv_velocity = -1; int g_srv_activeweapon = -1; static int g_srv_relationship = -1; static int g_srv_followtarget = -1; +static int g_srv_spawnflags = -1; +static int g_srv_hacked = -1; static bool g_srv_ready = false; static bool g_srv_checked = false; @@ -58,7 +67,10 @@ static uintptr_t find_string(const char *s) { // Find the typedescription_t whose fieldName points at `name` and read its // fieldOffset. type filters false positives (m_iHealth is FIELD_INTEGER). -static int datamap_offset(const char *name, int want_type) { +// max_off is generous on purpose: derived-class fields (m_bHackedByAlyx, +// m_hFollowTarget) live well past the base class size. +static int datamap_offset(const char *name, int want_type, int min_off = 0x40, + int max_off = 0x4000) { uintptr_t str = find_string(name); if (!str) return -1; @@ -75,7 +87,7 @@ static int datamap_offset(const char *name, int want_type) { uintptr_t ext = *(const uintptr_t *)(e + 0x14); if (ftype != want_type) continue; - if (foff < 0x40 || foff > 0x2000) + if (foff < min_off || foff > max_off) continue; if (fsize < 1 || fsize > 64) continue; @@ -118,16 +130,24 @@ bool resolve_server_netvars() { 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", FIELD_EHANDLE); - g_srv_relationship = datamap_offset("m_Relationship", FIELD_EMBEDDED); + // m_Relationship is DEFINE_UTLVECTOR, i.e. FIELD_CUSTOM (older builds used + // FIELD_EMBEDDED), never FIELD_EMBEDDED in the modern SDK. + g_srv_relationship = datamap_offset("m_Relationship", FIELD_CUSTOM); + if (g_srv_relationship < 0) + g_srv_relationship = datamap_offset("m_Relationship", FIELD_EMBEDDED); g_srv_followtarget = datamap_offset("m_hFollowTarget", FIELD_EHANDLE); + // CBaseEntity::m_spawnflags is a DEFINE_KEYFIELD at a low offset. + g_srv_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4); + g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN); 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", + "followtarget=%d spawnflags=%d hacked=%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_activeweapon, g_srv_relationship, g_srv_followtarget, + g_srv_spawnflags, g_srv_hacked); fail_logged = false; } else if (!fail_logged) { fail_logged = true; @@ -245,6 +265,32 @@ bool server_npc_has_follow_target(int idx) { return h != 0 && h != 0xFFFFFFFFu; } +// Turret friendliness that never appears in m_Relationship: CNPC_FloorTurret's +// Classify() reports CLASS_PLAYER_ALLY for citizen-built (SF_FLOOR_TURRET_CITIZEN) +// and Alyx-hacked turrets, which makes the default class table D_LI toward the +// player. Returns -1 if unknown, else bit0 = citizen, bit1 = hacked. +int server_npc_turret_flags(int idx) { + if (!g_srv_ready || (g_srv_spawnflags < 0 && g_srv_hacked < 0)) + return -1; + void *base = nullptr; + server_entity_health(idx, nullptr, nullptr, &base); + if (!base) + return -1; + uintptr_t b = (uintptr_t)base; + int flags = -1; + int hacked = -1; + if (g_srv_spawnflags > 0) + flags = *(int *)(b + g_srv_spawnflags); + if (g_srv_hacked > 0) + hacked = (int)*(unsigned char *)(b + g_srv_hacked); + int out = 0; + if (flags > 0 && (flags & TURRET_FLAG_CITIZEN)) + out |= 1; + if (hacked > 0) + out |= 2; + return out; +} + 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