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 /src/server.cpp | |
| 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.
Diffstat (limited to 'src/server.cpp')
| -rw-r--r-- | src/server.cpp | 56 |
1 files changed, 51 insertions, 5 deletions
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; |
