diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-03 23:07:40 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-03 23:07:40 +0100 |
| commit | 3249caf17de0dcab005398995ef3e611b3f81131 (patch) | |
| tree | 159ed49a5010d330e52322572026e0bcd046717a | |
| parent | AI: debug-log NPC relation changes (diff) | |
| download | hl2h-3249caf17de0dcab005398995ef3e611b3f81131.tar.gz hl2h-3249caf17de0dcab005398995ef3e611b3f81131.zip | |
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.
| -rw-r--r-- | CONTEXT.md | 7 | ||||
| -rw-r--r-- | src/esp.cpp | 17 | ||||
| -rw-r--r-- | src/hl2h.h | 1 | ||||
| -rw-r--r-- | src/server.cpp | 56 |
4 files changed, 74 insertions, 7 deletions
| @@ -157,6 +157,13 @@ vocabulary / decisions. | |||
| 157 | - This makes map-scripted friendly turrets and other relationship overrides | 157 | - This makes map-scripted friendly turrets and other relationship overrides |
| 158 | dynamic instead of model-name guesses. When no row exists, the old model | 158 | dynamic instead of model-name guesses. When no row exists, the old model |
| 159 | heuristic is used. | 159 | heuristic is used. |
| 160 | - `m_Relationship` is declared with `DEFINE_UTLVECTOR`, so its datamap entry | ||
| 161 | is `FIELD_CUSTOM` (not `FIELD_EMBEDDED`). Resolving it as embedded silently | ||
| 162 | returned `-1` for every NPC. | ||
| 163 | - Floor turrets are special: citizen-built turrets (`SF_FLOOR_TURRET_CITIZEN`, | ||
| 164 | spawnflags `0x200`) and Alyx-hacked turrets (`m_bHackedByAlyx`) report | ||
| 165 | `CLASS_PLAYER_ALLY` from their overridden `Classify()` and never add a | ||
| 166 | relationship row. They are classified from those server fields instead. | ||
| 160 | - Bugbait-controlled antlions are also treated as friendly when their server | 167 | - Bugbait-controlled antlions are also treated as friendly when their server |
| 161 | `m_hFollowTarget` is set, covering antlions that spawned before the global | 168 | `m_hFollowTarget` is set, covering antlions that spawned before the global |
| 162 | `antlion_allied` state changed. | 169 | `antlion_allied` state changed. |
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) { | |||
| 148 | return false; | 148 | return false; |
| 149 | } | 149 | } |
| 150 | 150 | ||
| 151 | static bool model_is_floor_turret(const char *m) { | ||
| 152 | return m && strstr(m, "floor_turret"); | ||
| 153 | } | ||
| 154 | |||
| 151 | static bool npc_is_friendly(int idx, const char *model, int local_idx, | 155 | static bool npc_is_friendly(int idx, const char *model, int local_idx, |
| 152 | bool &neutral) { | 156 | bool &neutral) { |
| 153 | int rel = server_npc_relation(idx, local_idx); | 157 | int rel = server_npc_relation(idx, local_idx); |
| 158 | int turret = -1; | ||
| 159 | // Citizen-built and Alyx-hacked turrets never add an m_Relationship row; | ||
| 160 | // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state. | ||
| 161 | if (model_is_floor_turret(model)) | ||
| 162 | turret = server_npc_turret_flags(idx); | ||
| 154 | // Debug-only: relation values are the authoritative answer and are the | 163 | // Debug-only: relation values are the authoritative answer and are the |
| 155 | // first thing to check when a friendly NPC still looks hostile. Logged only | 164 | // first thing to check when a friendly NPC still looks hostile. Logged only |
| 156 | // when an entity's relation changes, so this does not flood the log. | 165 | // 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, | |||
| 163 | } | 172 | } |
| 164 | if (idx > 0 && idx < 2048 && rel_cache[idx] != rel) { | 173 | if (idx > 0 && idx < 2048 && rel_cache[idx] != rel) { |
| 165 | rel_cache[idx] = rel; | 174 | rel_cache[idx] = rel; |
| 166 | logd("npc %d (%s): relation=%d followtarget=%d local=%d", idx, | 175 | logd("npc %d (%s): relation=%d followtarget=%d turret=%d local=%d", idx, |
| 167 | model ? model : "?", rel, | 176 | model ? model : "?", rel, |
| 168 | (int)server_npc_has_follow_target(idx), local_idx); | 177 | (int)server_npc_has_follow_target(idx), turret, local_idx); |
| 169 | } | 178 | } |
| 170 | if (rel == 3) { // D_LI | 179 | if (rel == 3) { // D_LI |
| 171 | neutral = false; | 180 | neutral = false; |
| @@ -179,6 +188,10 @@ static bool npc_is_friendly(int idx, const char *model, int local_idx, | |||
| 179 | neutral = false; | 188 | neutral = false; |
| 180 | return false; | 189 | return false; |
| 181 | } | 190 | } |
| 191 | if (turret >= 0) { | ||
| 192 | neutral = false; | ||
| 193 | return turret != 0; | ||
| 194 | } | ||
| 182 | // Bugbait-controlled antlions follow a target even when their default | 195 | // Bugbait-controlled antlions follow a target even when their default |
| 183 | // relationship was never rewritten. | 196 | // relationship was never rewritten. |
| 184 | if (model && strstr(model, "antlion") && | 197 | if (model && strstr(model, "antlion") && |
| @@ -254,6 +254,7 @@ 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); | 255 | int server_npc_relation(int idx, int local_idx); |
| 256 | bool server_npc_has_follow_target(int idx); | 256 | bool server_npc_has_follow_target(int idx); |
| 257 | int server_npc_turret_flags(int idx); | ||
| 257 | int server_model_index(void *base); | 258 | int server_model_index(void *base); |
| 258 | void find_modules(); | 259 | void find_modules(); |
| 259 | void init_conmsg(); | 260 | 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 }; | |||
| 27 | // fieldOffset[1]@0xC, fieldSize@0x10, flags@0x12, externalName@0x14 | 27 | // fieldOffset[1]@0xC, fieldSize@0x10, flags@0x12, externalName@0x14 |
| 28 | enum { | 28 | enum { |
| 29 | FIELD_INTEGER = 5, | 29 | FIELD_INTEGER = 5, |
| 30 | FIELD_BOOLEAN = 6, | ||
| 30 | FIELD_CHARACTER = 8, | 31 | FIELD_CHARACTER = 8, |
| 31 | FIELD_EMBEDDED = 10, | 32 | FIELD_EMBEDDED = 10, |
| 33 | FIELD_CUSTOM = 11, | ||
| 32 | FIELD_EHANDLE = 13 | 34 | FIELD_EHANDLE = 13 |
| 33 | }; | 35 | }; |
| 34 | 36 | ||
| 37 | // SF_FLOOR_TURRET_CITIZEN (npc_turret_floor.h). Citizen-built turrets | ||
| 38 | // Classify() as CLASS_PLAYER_ALLY, i.e. they are friendly without ever adding | ||
| 39 | // an entry to m_Relationship. | ||
| 40 | static const int TURRET_FLAG_CITIZEN = 0x200; | ||
| 41 | |||
| 35 | int g_srv_health = -1; | 42 | int g_srv_health = -1; |
| 36 | int g_srv_maxhealth = -1; | 43 | int g_srv_maxhealth = -1; |
| 37 | int g_srv_lifestate = -1; | 44 | int g_srv_lifestate = -1; |
| @@ -41,6 +48,8 @@ int g_srv_velocity = -1; | |||
| 41 | int g_srv_activeweapon = -1; | 48 | int g_srv_activeweapon = -1; |
| 42 | static int g_srv_relationship = -1; | 49 | static int g_srv_relationship = -1; |
| 43 | static int g_srv_followtarget = -1; | 50 | static int g_srv_followtarget = -1; |
| 51 | static int g_srv_spawnflags = -1; | ||
| 52 | static int g_srv_hacked = -1; | ||
| 44 | static bool g_srv_ready = false; | 53 | static bool g_srv_ready = false; |
| 45 | static bool g_srv_checked = false; | 54 | static bool g_srv_checked = false; |
| 46 | 55 | ||
| @@ -58,7 +67,10 @@ static uintptr_t find_string(const char *s) { | |||
| 58 | 67 | ||
| 59 | // Find the typedescription_t whose fieldName points at `name` and read its | 68 | // Find the typedescription_t whose fieldName points at `name` and read its |
| 60 | // fieldOffset. type filters false positives (m_iHealth is FIELD_INTEGER). | 69 | // fieldOffset. type filters false positives (m_iHealth is FIELD_INTEGER). |
| 61 | static int datamap_offset(const char *name, int want_type) { | 70 | // max_off is generous on purpose: derived-class fields (m_bHackedByAlyx, |
| 71 | // m_hFollowTarget) live well past the base class size. | ||
| 72 | static int datamap_offset(const char *name, int want_type, int min_off = 0x40, | ||
| 73 | int max_off = 0x4000) { | ||
| 62 | uintptr_t str = find_string(name); | 74 | uintptr_t str = find_string(name); |
| 63 | if (!str) | 75 | if (!str) |
| 64 | return -1; | 76 | return -1; |
| @@ -75,7 +87,7 @@ static int datamap_offset(const char *name, int want_type) { | |||
| 75 | uintptr_t ext = *(const uintptr_t *)(e + 0x14); | 87 | uintptr_t ext = *(const uintptr_t *)(e + 0x14); |
| 76 | if (ftype != want_type) | 88 | if (ftype != want_type) |
| 77 | continue; | 89 | continue; |
| 78 | if (foff < 0x40 || foff > 0x2000) | 90 | if (foff < min_off || foff > max_off) |
| 79 | continue; | 91 | continue; |
| 80 | if (fsize < 1 || fsize > 64) | 92 | if (fsize < 1 || fsize > 64) |
| 81 | continue; | 93 | continue; |
| @@ -118,16 +130,24 @@ bool resolve_server_netvars() { | |||
| 118 | g_srv_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR | 130 | g_srv_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR |
| 119 | g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR | 131 | g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR |
| 120 | g_srv_activeweapon = datamap_offset("m_hActiveWeapon", FIELD_EHANDLE); | 132 | g_srv_activeweapon = datamap_offset("m_hActiveWeapon", FIELD_EHANDLE); |
| 121 | g_srv_relationship = datamap_offset("m_Relationship", FIELD_EMBEDDED); | 133 | // m_Relationship is DEFINE_UTLVECTOR, i.e. FIELD_CUSTOM (older builds used |
| 134 | // FIELD_EMBEDDED), never FIELD_EMBEDDED in the modern SDK. | ||
| 135 | g_srv_relationship = datamap_offset("m_Relationship", FIELD_CUSTOM); | ||
| 136 | if (g_srv_relationship < 0) | ||
| 137 | g_srv_relationship = datamap_offset("m_Relationship", FIELD_EMBEDDED); | ||
| 122 | g_srv_followtarget = datamap_offset("m_hFollowTarget", FIELD_EHANDLE); | 138 | g_srv_followtarget = datamap_offset("m_hFollowTarget", FIELD_EHANDLE); |
| 139 | // CBaseEntity::m_spawnflags is a DEFINE_KEYFIELD at a low offset. | ||
| 140 | g_srv_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4); | ||
| 141 | g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN); | ||
| 123 | g_srv_ready = g_srv_health > 0; | 142 | g_srv_ready = g_srv_health > 0; |
| 124 | if (g_srv_ready) { | 143 | if (g_srv_ready) { |
| 125 | logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " | 144 | logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " |
| 126 | "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " | 145 | "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " |
| 127 | "followtarget=%d", | 146 | "followtarget=%d spawnflags=%d hacked=%d", |
| 128 | g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, | 147 | g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, |
| 129 | g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, | 148 | g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, |
| 130 | g_srv_activeweapon, g_srv_relationship, g_srv_followtarget); | 149 | g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, |
| 150 | g_srv_spawnflags, g_srv_hacked); | ||
| 131 | fail_logged = false; | 151 | fail_logged = false; |
| 132 | } else if (!fail_logged) { | 152 | } else if (!fail_logged) { |
| 133 | fail_logged = true; | 153 | fail_logged = true; |
| @@ -245,6 +265,32 @@ bool server_npc_has_follow_target(int idx) { | |||
| 245 | return h != 0 && h != 0xFFFFFFFFu; | 265 | return h != 0 && h != 0xFFFFFFFFu; |
| 246 | } | 266 | } |
| 247 | 267 | ||
| 268 | // Turret friendliness that never appears in m_Relationship: CNPC_FloorTurret's | ||
| 269 | // Classify() reports CLASS_PLAYER_ALLY for citizen-built (SF_FLOOR_TURRET_CITIZEN) | ||
| 270 | // and Alyx-hacked turrets, which makes the default class table D_LI toward the | ||
| 271 | // player. Returns -1 if unknown, else bit0 = citizen, bit1 = hacked. | ||
| 272 | int server_npc_turret_flags(int idx) { | ||
| 273 | if (!g_srv_ready || (g_srv_spawnflags < 0 && g_srv_hacked < 0)) | ||
| 274 | return -1; | ||
| 275 | void *base = nullptr; | ||
| 276 | server_entity_health(idx, nullptr, nullptr, &base); | ||
| 277 | if (!base) | ||
| 278 | return -1; | ||
| 279 | uintptr_t b = (uintptr_t)base; | ||
| 280 | int flags = -1; | ||
| 281 | int hacked = -1; | ||
| 282 | if (g_srv_spawnflags > 0) | ||
| 283 | flags = *(int *)(b + g_srv_spawnflags); | ||
| 284 | if (g_srv_hacked > 0) | ||
| 285 | hacked = (int)*(unsigned char *)(b + g_srv_hacked); | ||
| 286 | int out = 0; | ||
| 287 | if (flags > 0 && (flags & TURRET_FLAG_CITIZEN)) | ||
| 288 | out |= 1; | ||
| 289 | if (hacked > 0) | ||
| 290 | out |= 2; | ||
| 291 | return out; | ||
| 292 | } | ||
| 293 | |||
| 248 | bool server_set_velocity_z(int idx, float vz) { | 294 | bool server_set_velocity_z(int idx, float vz) { |
| 249 | if (!g_srv_ready || !g_iface.engineserver || g_srv_absvelocity <= 0) | 295 | if (!g_srv_ready || !g_iface.engineserver || g_srv_absvelocity <= 0) |
| 250 | return false; | 296 | return false; |
