diff options
| -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; |
