summaryrefslogtreecommitdiffstats
path: root/src/server.cpp
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-03 23:07:40 +0100
committersalmonik <salmonik@tuta.io>2026-11-03 23:07:40 +0100
commit3249caf17de0dcab005398995ef3e611b3f81131 (patch)
tree159ed49a5010d330e52322572026e0bcd046717a /src/server.cpp
parentAI: debug-log NPC relation changes (diff)
downloadhl2h-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 '')
-rw-r--r--src/server.cpp56
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
28enum { 28enum {
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.
40static const int TURRET_FLAG_CITIZEN = 0x200;
41
35int g_srv_health = -1; 42int g_srv_health = -1;
36int g_srv_maxhealth = -1; 43int g_srv_maxhealth = -1;
37int g_srv_lifestate = -1; 44int g_srv_lifestate = -1;
@@ -41,6 +48,8 @@ int g_srv_velocity = -1;
41int g_srv_activeweapon = -1; 48int g_srv_activeweapon = -1;
42static int g_srv_relationship = -1; 49static int g_srv_relationship = -1;
43static int g_srv_followtarget = -1; 50static int g_srv_followtarget = -1;
51static int g_srv_spawnflags = -1;
52static int g_srv_hacked = -1;
44static bool g_srv_ready = false; 53static bool g_srv_ready = false;
45static bool g_srv_checked = false; 54static 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).
61static 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.
72static 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.
272int 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
248bool server_set_velocity_z(int idx, float vz) { 294bool 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;