diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-03 23:36:58 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-03 23:36:58 +0100 |
| commit | 5ee8eb4a2b96b4a9916f85ddec5b43ae24ca93d1 (patch) | |
| tree | f618c930aa6bcd51f396598872194943c442b172 | |
| parent | World: add prop tint and fog override (diff) | |
| download | hl2h-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.md | 9 | ||||
| -rw-r--r-- | src/esp.cpp | 41 | ||||
| -rw-r--r-- | src/hl2h.h | 1 | ||||
| -rw-r--r-- | src/server.cpp | 18 |
4 files changed, 54 insertions, 15 deletions
| @@ -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 |
| 141 | static bool model_is_neutral(const char *m) { | 141 | static 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) { | |||
| 155 | static bool npc_is_friendly(int idx, const char *model, int local_idx, | 157 | static 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. |
| @@ -266,6 +266,7 @@ bool server_set_velocity_z(int idx, float vz); | |||
| 266 | int server_active_weapon(int idx); | 266 | int server_active_weapon(int idx); |
| 267 | int server_npc_relation(int idx, int local_idx); | 267 | int server_npc_relation(int idx, int local_idx); |
| 268 | bool server_npc_has_follow_target(int idx); | 268 | bool server_npc_has_follow_target(int idx); |
| 269 | int server_npc_state(int idx); | ||
| 269 | int server_npc_turret_flags(int idx); | 270 | int server_npc_turret_flags(int idx); |
| 270 | int server_model_index(void *base); | 271 | int server_model_index(void *base); |
| 271 | void find_modules(); | 272 | void 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; | |||
| 50 | static int g_srv_followtarget = -1; | 50 | static int g_srv_followtarget = -1; |
| 51 | static int g_srv_spawnflags = -1; | 51 | static int g_srv_spawnflags = -1; |
| 52 | static int g_srv_hacked = -1; | 52 | static int g_srv_hacked = -1; |
| 53 | static int g_srv_npcstate = -1; | ||
| 53 | static bool g_srv_ready = false; | 54 | static bool g_srv_ready = false; |
| 54 | static bool g_srv_checked = false; | 55 | static 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. | ||
| 261 | int 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 | |||
| 257 | bool server_npc_has_follow_target(int idx) { | 271 | bool 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; |
