summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-03 23:36:58 +0100
committersalmonik <salmonik@tuta.io>2026-11-03 23:36:58 +0100
commit5ee8eb4a2b96b4a9916f85ddec5b43ae24ca93d1 (patch)
treef618c930aa6bcd51f396598872194943c442b172
parentWorld: add prop tint and fog override (diff)
downloadhl2h-5ee8eb4a2b96b4a9916f85ddec5b43ae24ca93d1.tar.gz
hl2h-5ee8eb4a2b96b4a9916f85ddec5b43ae24ca93d1.zip
AI: treat scripted NPCs as neutral; Lamarr neutral
Read the server m_NPCState and mark NPC_STATE_SCRIPT actors neutral so cutscene NPCs and scripted pets are not aimbot targets and draw in the neutral colour. Add Lamarr (Kleiner's pet headcrab) to the neutral model list as requested. Also log state changes in the NPC debug line and resolve/log m_NPCState in the server datamap summary.
-rw-r--r--CONTEXT.md9
-rw-r--r--src/esp.cpp41
-rw-r--r--src/hl2h.h1
-rw-r--r--src/server.cpp18
4 files changed, 54 insertions, 15 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 1d365e8..741050d 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -174,6 +174,15 @@ vocabulary / decisions.
174- Bugbait-controlled antlions are also treated as friendly when their server 174- Bugbait-controlled antlions are also treated as friendly when their server
175 `m_hFollowTarget` is set, covering antlions that spawned before the global 175 `m_hFollowTarget` is set, covering antlions that spawned before the global
176 `antlion_allied` state changed. 176 `antlion_allied` state changed.
177- Stock NPCs have no `m_Relationship` rows at all: their disposition comes from
178 the global `CHL2GameRules::InitDefaultRelationships()` table indexed by
179 `Classify()` (e.g. `CLASS_PLAYER_ALLY -> D_LI`, `CLASS_HEADCRAB -> D_HT`).
180 That table is not stored per entity, so `relation=-1` is normal and the
181 model heuristic is the fallback for those NPCs (Kleiner, Combine, ...).
182- `m_NPCState` (`FIELD_INTEGER`, server datamap) is read as well:
183 `NPC_STATE_SCRIPT` (4) marks scripted/passive actors as neutral, so they are
184 not aimbot targets and draw with the neutral colour. Lamarr
185 (`models/Lamarr.mdl`) is additionally listed as neutral by model.
177 186
178## Bones / ESP 187## Bones / ESP
179 188
diff --git a/src/esp.cpp b/src/esp.cpp
index 405dea7..50d7b5d 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -139,8 +139,10 @@ static bool model_is_friendly(const char *m) {
139 139
140// harmless wildlife that also happens to be CAI_BaseNPC 140// harmless wildlife that also happens to be CAI_BaseNPC
141static bool model_is_neutral(const char *m) { 141static bool model_is_neutral(const char *m) {
142 // Lamarr is Kleiner's pet headcrab: scripted and harmless, and the user
143 // wants her shown neutral rather than hostile.
142 static const char *patterns[] = {"crow", "gull", "pigeon", "seagull", 144 static const char *patterns[] = {"crow", "gull", "pigeon", "seagull",
143 "bird", nullptr}; 145 "bird", "Lamarr", nullptr};
144 if (!m) return false; 146 if (!m) return false;
145 for (int i = 0; patterns[i]; i++) 147 for (int i = 0; patterns[i]; i++)
146 if (strstr(m, patterns[i])) 148 if (strstr(m, patterns[i]))
@@ -155,42 +157,55 @@ static bool model_is_floor_turret(const char *m) {
155static bool npc_is_friendly(int idx, const char *model, int local_idx, 157static bool npc_is_friendly(int idx, const char *model, int local_idx,
156 bool &neutral) { 158 bool &neutral) {
157 int rel = server_npc_relation(idx, local_idx); 159 int rel = server_npc_relation(idx, local_idx);
160 int state = server_npc_state(idx);
158 int turret = -1; 161 int turret = -1;
159 // Citizen-built and Alyx-hacked turrets never add an m_Relationship row; 162 // Citizen-built and Alyx-hacked turrets never add an m_Relationship row;
160 // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state. 163 // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state.
161 if (model_is_floor_turret(model)) 164 if (model_is_floor_turret(model))
162 turret = server_npc_turret_flags(idx); 165 turret = server_npc_turret_flags(idx);
163 // Debug-only: relation values are the authoritative answer and are the 166 // Debug-only: relation/state changes are the authoritative signals and the
164 // first thing to check when a friendly NPC still looks hostile. Logged only 167 // first thing to check when a friendly NPC still looks hostile.
165 // when an entity's relation changes, so this does not flood the log.
166 static int rel_cache[2048]; 168 static int rel_cache[2048];
167 static bool rel_cache_init = false; 169 static int state_cache[2048];
168 if (!rel_cache_init) { 170 static bool cache_init = false;
171 if (!cache_init) {
169 for (int i = 0; i < 2048; i++) 172 for (int i = 0; i < 2048; i++)
170 rel_cache[i] = -999; 173 rel_cache[i] = state_cache[i] = -999;
171 rel_cache_init = true; 174 cache_init = true;
172 } 175 }
173 if (idx > 0 && idx < 2048 && rel_cache[idx] != rel) { 176 if (idx > 0 && idx < 2048 &&
177 (rel_cache[idx] != rel || state_cache[idx] != state)) {
174 rel_cache[idx] = rel; 178 rel_cache[idx] = rel;
175 logd("npc %d (%s): relation=%d followtarget=%d turret=%d local=%d", idx, 179 state_cache[idx] = state;
176 model ? model : "?", rel, 180 logd("npc %d (%s): relation=%d state=%d followtarget=%d turret=%d "
181 "local=%d", idx, model ? model : "?", rel, state,
177 (int)server_npc_has_follow_target(idx), turret, local_idx); 182 (int)server_npc_has_follow_target(idx), turret, local_idx);
178 } 183 }
179 if (rel == 3) { // D_LI 184 if (rel == 3) { // D_LI
180 neutral = false; 185 neutral = false;
181 return true; 186 return true;
182 } 187 }
188 if (turret > 0) { // citizen-built / Alyx-hacked
189 neutral = false;
190 return true;
191 }
183 if (rel == 4) { // D_NU 192 if (rel == 4) { // D_NU
184 neutral = true; 193 neutral = true;
185 return false; 194 return false;
186 } 195 }
196 // NPC_STATE_SCRIPT = 4: cutscene actors and scripted pets are passive even
197 // when their class relationship says hate (Lamarr, scripted Combine, ...).
198 if (state == 4) {
199 neutral = true;
200 return false;
201 }
187 if (rel == 1 || rel == 2) { // D_HT / D_FR 202 if (rel == 1 || rel == 2) { // D_HT / D_FR
188 neutral = false; 203 neutral = false;
189 return false; 204 return false;
190 } 205 }
191 if (turret >= 0) { 206 if (turret >= 0) { // ordinary floor turret
192 neutral = false; 207 neutral = false;
193 return turret != 0; 208 return false;
194 } 209 }
195 // Bugbait-controlled antlions follow a target even when their default 210 // Bugbait-controlled antlions follow a target even when their default
196 // relationship was never rewritten. 211 // relationship was never rewritten.
diff --git a/src/hl2h.h b/src/hl2h.h
index 6f23221..877afac 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -266,6 +266,7 @@ bool server_set_velocity_z(int idx, float vz);
266int server_active_weapon(int idx); 266int server_active_weapon(int idx);
267int server_npc_relation(int idx, int local_idx); 267int server_npc_relation(int idx, int local_idx);
268bool server_npc_has_follow_target(int idx); 268bool server_npc_has_follow_target(int idx);
269int server_npc_state(int idx);
269int server_npc_turret_flags(int idx); 270int server_npc_turret_flags(int idx);
270int server_model_index(void *base); 271int server_model_index(void *base);
271void find_modules(); 272void find_modules();
diff --git a/src/server.cpp b/src/server.cpp
index 19be144..c2864c8 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -50,6 +50,7 @@ static int g_srv_relationship = -1;
50static int g_srv_followtarget = -1; 50static int g_srv_followtarget = -1;
51static int g_srv_spawnflags = -1; 51static int g_srv_spawnflags = -1;
52static int g_srv_hacked = -1; 52static int g_srv_hacked = -1;
53static int g_srv_npcstate = -1;
53static bool g_srv_ready = false; 54static bool g_srv_ready = false;
54static bool g_srv_checked = false; 55static bool g_srv_checked = false;
55 56
@@ -139,15 +140,16 @@ bool resolve_server_netvars() {
139 // CBaseEntity::m_spawnflags is a DEFINE_KEYFIELD at a low offset. 140 // 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_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4);
141 g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN); 142 g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN);
143 g_srv_npcstate = datamap_offset("m_NPCState", FIELD_INTEGER);
142 g_srv_ready = g_srv_health > 0; 144 g_srv_ready = g_srv_health > 0;
143 if (g_srv_ready) { 145 if (g_srv_ready) {
144 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " 146 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d "
145 "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d " 147 "modelindex=%d absvel=%d vel=%d activeweapon=%d relationship=%d "
146 "followtarget=%d spawnflags=%d hacked=%d", 148 "followtarget=%d spawnflags=%d hacked=%d npcstate=%d",
147 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, 149 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate,
148 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, 150 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity,
149 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, 151 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget,
150 g_srv_spawnflags, g_srv_hacked); 152 g_srv_spawnflags, g_srv_hacked, g_srv_npcstate);
151 fail_logged = false; 153 fail_logged = false;
152 } else if (!fail_logged) { 154 } else if (!fail_logged) {
153 fail_logged = true; 155 fail_logged = true;
@@ -254,6 +256,18 @@ int server_npc_relation(int idx, int local_idx) {
254 return class_relation; 256 return class_relation;
255} 257}
256 258
259// NPC_STATE_SCRIPT = 4 (ai_npcstate.h). Scripted NPCs are passive: cutscene
260// actors, Kleiner's Lamarr, scripted soldiers standing down, and so on.
261int server_npc_state(int idx) {
262 if (!g_srv_ready || g_srv_npcstate < 0)
263 return -1;
264 void *base = nullptr;
265 server_entity_health(idx, nullptr, nullptr, &base);
266 if (!base)
267 return -1;
268 return *(int *)((uintptr_t)base + g_srv_npcstate);
269}
270
257bool server_npc_has_follow_target(int idx) { 271bool server_npc_has_follow_target(int idx) {
258 if (!g_srv_ready || g_srv_followtarget < 0) 272 if (!g_srv_ready || g_srv_followtarget < 0)
259 return false; 273 return false;