diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-06 16:25:48 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-06 16:25:48 +0100 |
| commit | c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1 (patch) | |
| tree | 2da18f9d243d898fb218f36a4508e0d309782d5e /src/entity_query.cpp | |
| parent | ESP: extract item ESP into its own translation unit (diff) | |
| download | hl2h-c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1.tar.gz hl2h-c43cb2e42a5ac1cc09f83cd72fa7e568c9ef37e1.zip | |
ESP: finish the split into entity, combat, item and projectile units
esp.cpp keeps the scan and drawing (~1000 lines, down from 3665). Entity
lookups and NPC classification move to entity_query.cpp together with the
per-map caches and the session reset; combat (aimbot, silent aim, melee,
spread, triggerbot) moves to combat.cpp. Shared helpers are declared in
esp_internal.h.
Diffstat (limited to 'src/entity_query.cpp')
| -rw-r--r-- | src/entity_query.cpp | 460 |
1 files changed, 460 insertions, 0 deletions
diff --git a/src/entity_query.cpp b/src/entity_query.cpp new file mode 100644 index 0000000..4697bf2 --- /dev/null +++ b/src/entity_query.cpp | |||
| @@ -0,0 +1,460 @@ | |||
| 1 | // entity_query.cpp — client entity lookups, NPC classification, per-map caches. | ||
| 2 | #include "esp_internal.h" | ||
| 3 | #include "hl2h.h" | ||
| 4 | #include <cmath> | ||
| 5 | #include <cstring> | ||
| 6 | |||
| 7 | using GetNetFn = void *(*)(void *, int); | ||
| 8 | using GetCCFn = void *(*)(void *); | ||
| 9 | using GetModelFn = const void *(*)(void *, int); | ||
| 10 | using GetModelNameFn = const char *(*)(void *, const void *); | ||
| 11 | |||
| 12 | // Listen-server sanity latch. Only ever set once per map (esp_reset_session), | ||
| 13 | // and only after both model indices are valid: latching on the transient | ||
| 14 | // cli_model == 0 at map load disabled server health for the whole process. | ||
| 15 | bool g_srv_checked = false; | ||
| 16 | bool g_srv_trusted = false; | ||
| 17 | |||
| 18 | // Per-map caches. The ESP resolves a model name for every entity on every scan | ||
| 19 | // and the chams hook asks for a relation on every model draw; both are virtual | ||
| 20 | // call chains into the engine plus server datamap reads, so memoise them. | ||
| 21 | static constexpr int MODEL_CACHE = 16384; | ||
| 22 | static const char *g_model_name_cache[MODEL_CACHE]; | ||
| 23 | static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail | ||
| 24 | |||
| 25 | // Direct-mapped entity pointer -> index map, filled by esp_collect. The | ||
| 26 | // triggerbot traces a ray and gets a pointer back, and used to find its index | ||
| 27 | // by walking up to 4096 entity-list slots on every fire tick. | ||
| 28 | static constexpr int ENTPTR_CACHE = 4096; | ||
| 29 | static void *g_entptr_key[ENTPTR_CACHE]; | ||
| 30 | static int g_entptr_idx[ENTPTR_CACHE]; | ||
| 31 | |||
| 32 | void esp_reset_session() { | ||
| 33 | g_srv_checked = false; | ||
| 34 | g_srv_trusted = false; | ||
| 35 | memset(g_model_name_cache, 0, sizeof(g_model_name_cache)); | ||
| 36 | memset(g_model_state_cache, 0, sizeof(g_model_state_cache)); | ||
| 37 | memset(g_entptr_key, 0, sizeof(g_entptr_key)); | ||
| 38 | memset(g_entptr_idx, 0, sizeof(g_entptr_idx)); | ||
| 39 | } | ||
| 40 | |||
| 41 | void *entity_networkable(int idx) { | ||
| 42 | return vfunc<GetNetFn>(g_iface.entitylist, 0)(g_iface.entitylist, idx); | ||
| 43 | } | ||
| 44 | |||
| 45 | // IClientNetworkable slot 2 = GetClientClass(); ClientClass::m_pNetworkName @ +8. | ||
| 46 | const char *entity_class_name(void *net, void **out_cc) { | ||
| 47 | if (!net) return nullptr; | ||
| 48 | void *cc = vfunc<GetCCFn>(net, 2)(net); | ||
| 49 | if (out_cc) *out_cc = cc; | ||
| 50 | return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; | ||
| 51 | } | ||
| 52 | |||
| 53 | // IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant | ||
| 54 | // 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK, | ||
| 55 | // so the slot is validated once against the local player, which can never be | ||
| 56 | // dormant: a wrong slot disables the check instead of misreporting entities. | ||
| 57 | // Dormant entities keep their last networked origin, so drawing or aiming at | ||
| 58 | // them produces ghosts at stale positions. | ||
| 59 | static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled | ||
| 60 | |||
| 61 | static void dormant_selftest() { | ||
| 62 | // Leave the state untested (0) until a local player exists: the first call | ||
| 63 | // can come from the main menu, and latching "disabled" there would turn the | ||
| 64 | // check off for the whole session. | ||
| 65 | if (!g_iface.entitylist) | ||
| 66 | return; | ||
| 67 | int local = engine_get_local_player(); | ||
| 68 | if (local <= 0) | ||
| 69 | return; | ||
| 70 | void *net = entity_networkable(local); | ||
| 71 | if (!net) | ||
| 72 | return; | ||
| 73 | void **vt = *(void ***)net; | ||
| 74 | if (!vt || !mod_client().contains((uintptr_t)vt[8])) { | ||
| 75 | g_dormant_state = -1; | ||
| 76 | logf("dormant check disabled: slot 8 outside client module"); | ||
| 77 | return; | ||
| 78 | } | ||
| 79 | using DormantFn = unsigned char (*)(void *); | ||
| 80 | if (vfunc<DormantFn>(net, 8)(net) != 0) { | ||
| 81 | g_dormant_state = -1; | ||
| 82 | logf("dormant check disabled: local player reported dormant"); | ||
| 83 | return; | ||
| 84 | } | ||
| 85 | g_dormant_state = 1; | ||
| 86 | logd("dormant check enabled"); | ||
| 87 | } | ||
| 88 | |||
| 89 | bool entity_is_dormant(void *net) { | ||
| 90 | if (!net) | ||
| 91 | return false; | ||
| 92 | if (g_dormant_state == 0) | ||
| 93 | dormant_selftest(); | ||
| 94 | if (g_dormant_state != 1) | ||
| 95 | return false; | ||
| 96 | using DormantFn = unsigned char (*)(void *); | ||
| 97 | return vfunc<DormantFn>(net, 8)(net) != 0; | ||
| 98 | } | ||
| 99 | |||
| 100 | // Entity pointer -> index, filled by the entity scans. The triggerbot gets a | ||
| 101 | // pointer back from the trace and needs the index; the alternative is walking | ||
| 102 | // up to 4096 entity-list slots on every fire tick. | ||
| 103 | int entptr_lookup(void *ent) { | ||
| 104 | if (!ent) | ||
| 105 | return -1; | ||
| 106 | unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); | ||
| 107 | return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1; | ||
| 108 | } | ||
| 109 | |||
| 110 | void entptr_store(void *ent, int idx) { | ||
| 111 | if (!ent || idx <= 0) | ||
| 112 | return; | ||
| 113 | unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1)); | ||
| 114 | g_entptr_key[h] = ent; | ||
| 115 | g_entptr_idx[h] = idx; | ||
| 116 | } | ||
| 117 | |||
| 118 | // m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. | ||
| 119 | // Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. | ||
| 120 | const char *model_name_from_index(int modelindex) { | ||
| 121 | if (!g_iface.modelinfo || modelindex <= 0) | ||
| 122 | return nullptr; | ||
| 123 | if (modelindex < MODEL_CACHE) { | ||
| 124 | if (g_model_state_cache[modelindex] == 1) | ||
| 125 | return g_model_name_cache[modelindex]; | ||
| 126 | if (g_model_state_cache[modelindex] == 2) | ||
| 127 | return nullptr; | ||
| 128 | } | ||
| 129 | const void *model = | ||
| 130 | vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); | ||
| 131 | if (!model) { | ||
| 132 | if (modelindex < MODEL_CACHE) | ||
| 133 | g_model_state_cache[modelindex] = 2; | ||
| 134 | return nullptr; | ||
| 135 | } | ||
| 136 | const char *name = | ||
| 137 | vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model); | ||
| 138 | if (modelindex < MODEL_CACHE) { | ||
| 139 | g_model_name_cache[modelindex] = name; | ||
| 140 | g_model_state_cache[modelindex] = name ? 1 : 2; | ||
| 141 | } | ||
| 142 | return name; | ||
| 143 | } | ||
| 144 | |||
| 145 | const char *entity_model_name(void *ent) { | ||
| 146 | if (!g_iface.modelinfo || g_net.modelindex < 0) return nullptr; | ||
| 147 | int modelindex = *(int *)((uintptr_t)ent + g_net.modelindex); | ||
| 148 | return model_name_from_index(modelindex); | ||
| 149 | } | ||
| 150 | |||
| 151 | // Live origin for a cached entity index. Item/projectile discovery is throttled | ||
| 152 | // to 10 Hz (it walks the whole entity list), but the markers must follow the | ||
| 153 | // world at render rate; refreshing one netvar read per cached target is cheap. | ||
| 154 | bool entity_origin(void *ent, Vector &out) { | ||
| 155 | if (!ent || g_net.origin < 0) | ||
| 156 | return false; | ||
| 157 | uintptr_t e = (uintptr_t)ent; | ||
| 158 | out.x = *(float *)(e + g_net.origin); | ||
| 159 | out.y = *(float *)(e + g_net.origin + 4); | ||
| 160 | out.z = *(float *)(e + g_net.origin + 8); | ||
| 161 | return true; | ||
| 162 | } | ||
| 163 | |||
| 164 | // HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all | ||
| 165 | // use the single client class "CAI_BaseNPC" — only special NPCs register their | ||
| 166 | // own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). | ||
| 167 | bool is_npc_class(const char *name) { | ||
| 168 | if (!name) return false; | ||
| 169 | return strstr(name, "NPC") != nullptr || | ||
| 170 | strstr(name, "BaseHelicopter") != nullptr; | ||
| 171 | } | ||
| 172 | |||
| 173 | // Used by the chams hook to decide whether a model-render entity is an NPC. | ||
| 174 | bool esp_is_npc_index(int idx) { | ||
| 175 | if (!g_iface.entitylist || idx <= 0 || idx > 8192) | ||
| 176 | return false; | ||
| 177 | void *net = entity_networkable(idx); | ||
| 178 | if (!net) | ||
| 179 | return false; | ||
| 180 | return is_npc_class(entity_class_name(net, nullptr)); | ||
| 181 | } | ||
| 182 | |||
| 183 | bool esp_is_viewmodel_index(int idx) { | ||
| 184 | if (!g_iface.entitylist || idx <= 0 || idx > 8192) | ||
| 185 | return false; | ||
| 186 | void *ent = entitylist_get(idx); | ||
| 187 | if (!ent) | ||
| 188 | return false; | ||
| 189 | void *net = entity_networkable(idx); | ||
| 190 | const char *cls = net ? entity_class_name(net, nullptr) : nullptr; | ||
| 191 | if (cls && strcasestr(cls, "viewmodel")) | ||
| 192 | return true; | ||
| 193 | const char *model = entity_model_name(ent); | ||
| 194 | return model && (strstr(model, "/v_") || strstr(model, "\\v_")); | ||
| 195 | } | ||
| 196 | |||
| 197 | // 0 = hostile, 1 = neutral, 2 = friendly, -1 = not an NPC. | ||
| 198 | bool npc_is_friendly(int idx, const char *model, int local_idx, | ||
| 199 | bool &neutral); | ||
| 200 | int esp_npc_relation(int idx) { | ||
| 201 | // Called from the chams hook up to three times per entity per frame (visible, | ||
| 202 | // through-wall, wireframe pass) and the work behind it is a chain of server | ||
| 203 | // edict/datamap reads. A short TTL cache keeps that out of the render path. | ||
| 204 | struct RelCache { | ||
| 205 | int idx; | ||
| 206 | double at; | ||
| 207 | int val; | ||
| 208 | }; | ||
| 209 | static RelCache cache[256]; | ||
| 210 | double now = esp_now(); | ||
| 211 | RelCache &c = cache[(unsigned)idx & 255]; | ||
| 212 | if (c.idx == idx && now - c.at < 0.05) | ||
| 213 | return c.val; | ||
| 214 | |||
| 215 | int val = -1; | ||
| 216 | if (esp_is_npc_index(idx)) { | ||
| 217 | int local = engine_get_local_player(); | ||
| 218 | if (local <= 0) { | ||
| 219 | val = 0; | ||
| 220 | } else { | ||
| 221 | void *ent = entitylist_get(idx); | ||
| 222 | const char *model = ent ? entity_model_name(ent) : nullptr; | ||
| 223 | bool neutral = false; | ||
| 224 | bool friendly = npc_is_friendly(idx, model, local, neutral); | ||
| 225 | val = neutral ? 1 : friendly ? 2 : 0; | ||
| 226 | } | ||
| 227 | } | ||
| 228 | c = {idx, now, val}; | ||
| 229 | return val; | ||
| 230 | } | ||
| 231 | |||
| 232 | static bool model_is_friendly(const char *m) { | ||
| 233 | static const char *patterns[] = { | ||
| 234 | "citizen", "humans/", "male_", "female_", "barney", "alyx", | ||
| 235 | "alex", "scientist", "kleiner", "eli", "mossman", "vortigaunt", | ||
| 236 | "dog", "grigori", "monk", "odessa", "gman", nullptr}; | ||
| 237 | if (!m) return false; | ||
| 238 | for (int i = 0; patterns[i]; i++) | ||
| 239 | if (strstr(m, patterns[i])) | ||
| 240 | return true; | ||
| 241 | return false; | ||
| 242 | } | ||
| 243 | |||
| 244 | // harmless wildlife that also happens to be CAI_BaseNPC | ||
| 245 | static bool model_is_neutral(const char *m) { | ||
| 246 | // Lamarr is Kleiner's pet headcrab: scripted and harmless, and the user | ||
| 247 | // wants her shown neutral rather than hostile. | ||
| 248 | static const char *patterns[] = {"crow", "gull", "pigeon", "seagull", | ||
| 249 | "bird", "Lamarr", nullptr}; | ||
| 250 | if (!m) return false; | ||
| 251 | for (int i = 0; patterns[i]; i++) | ||
| 252 | if (strstr(m, patterns[i])) | ||
| 253 | return true; | ||
| 254 | return false; | ||
| 255 | } | ||
| 256 | |||
| 257 | static bool model_is_floor_turret(const char *m) { | ||
| 258 | return m && strstr(m, "floor_turret"); | ||
| 259 | } | ||
| 260 | |||
| 261 | static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx, | ||
| 262 | bool &neutral) { | ||
| 263 | int rel = server_npc_relation(idx, local_idx); | ||
| 264 | int state = server_npc_state(idx); | ||
| 265 | int turret = -1; | ||
| 266 | // Citizen-built and Alyx-hacked turrets never add an m_Relationship row; | ||
| 267 | // their Classify() (CLASS_PLAYER_ALLY) comes from server runtime state. | ||
| 268 | if (model_is_floor_turret(model)) | ||
| 269 | turret = server_npc_turret_flags(idx); | ||
| 270 | // Debug-only: relation/state changes are the authoritative signals and the | ||
| 271 | // first thing to check when a friendly NPC still looks hostile. | ||
| 272 | static int rel_cache[2048]; | ||
| 273 | static int state_cache[2048]; | ||
| 274 | static bool cache_init = false; | ||
| 275 | if (!cache_init) { | ||
| 276 | for (int i = 0; i < 2048; i++) | ||
| 277 | rel_cache[i] = state_cache[i] = -999; | ||
| 278 | cache_init = true; | ||
| 279 | } | ||
| 280 | if (idx > 0 && idx < 2048 && | ||
| 281 | (rel_cache[idx] != rel || state_cache[idx] != state)) { | ||
| 282 | rel_cache[idx] = rel; | ||
| 283 | state_cache[idx] = state; | ||
| 284 | logd("npc %d (%s): relation=%d state=%d followtarget=%d turret=%d " | ||
| 285 | "local=%d", idx, model ? model : "?", rel, state, | ||
| 286 | (int)server_npc_has_follow_target(idx), turret, local_idx); | ||
| 287 | } | ||
| 288 | if (rel == 3) { // D_LI | ||
| 289 | neutral = false; | ||
| 290 | return true; | ||
| 291 | } | ||
| 292 | if (turret > 0) { // citizen-built / Alyx-hacked | ||
| 293 | neutral = false; | ||
| 294 | return true; | ||
| 295 | } | ||
| 296 | if (rel == 4) { // D_NU | ||
| 297 | neutral = true; | ||
| 298 | return false; | ||
| 299 | } | ||
| 300 | // NPC_STATE_SCRIPT = 4: cutscene actors and scripted pets are passive even | ||
| 301 | // when their class relationship says hate (Lamarr, scripted Combine, ...). | ||
| 302 | if (state == 4) { | ||
| 303 | neutral = true; | ||
| 304 | return false; | ||
| 305 | } | ||
| 306 | if (rel == 1 || rel == 2) { // D_HT / D_FR | ||
| 307 | neutral = false; | ||
| 308 | return false; | ||
| 309 | } | ||
| 310 | if (turret >= 0) { // ordinary floor turret | ||
| 311 | neutral = false; | ||
| 312 | return false; | ||
| 313 | } | ||
| 314 | // Bugbait-controlled antlions follow a target even when their default | ||
| 315 | // relationship was never rewritten. | ||
| 316 | if (model && strstr(model, "antlion") && | ||
| 317 | server_npc_has_follow_target(idx)) { | ||
| 318 | neutral = false; | ||
| 319 | return true; | ||
| 320 | } | ||
| 321 | bool friendly = model_is_friendly(model); | ||
| 322 | neutral = model_is_neutral(model); | ||
| 323 | return friendly; | ||
| 324 | } | ||
| 325 | |||
| 326 | // Friendly/neutral state changes slowly (hacked turrets, bugbait), while the | ||
| 327 | // entity scan and the chams hook ask for it constantly. Cache it briefly so the | ||
| 328 | // server lookups happen once per NPC per window instead of per call. | ||
| 329 | bool npc_is_friendly(int idx, const char *model, int local_idx, | ||
| 330 | bool &neutral) { | ||
| 331 | struct Cache { | ||
| 332 | int idx; | ||
| 333 | double at; | ||
| 334 | bool friendly; | ||
| 335 | bool neutral; | ||
| 336 | }; | ||
| 337 | static Cache cache[256]; | ||
| 338 | double now = esp_now(); | ||
| 339 | Cache &c = cache[(unsigned)idx & 255]; | ||
| 340 | if (c.idx == idx && now - c.at < 0.10) { | ||
| 341 | neutral = c.neutral; | ||
| 342 | return c.friendly; | ||
| 343 | } | ||
| 344 | bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral); | ||
| 345 | c = {idx, now, f, neutral}; | ||
| 346 | return f; | ||
| 347 | } | ||
| 348 | |||
| 349 | |||
| 350 | static bool model_has(const char *model, const char *needle) { | ||
| 351 | return model && strcasestr(model, needle) != nullptr; | ||
| 352 | } | ||
| 353 | |||
| 354 | // Server m_takedamage first (authoritative immunity), then the HL2 damage | ||
| 355 | // rules for units whose OnTakeDamage filters specific damage types. | ||
| 356 | static int npc_damage_class_uncached(int idx, const char *model) { | ||
| 357 | int td = server_npc_takedamage(idx); | ||
| 358 | if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY | ||
| 359 | return NPC_DMG_IMMUNE; | ||
| 360 | if (model_has(model, "gunship") || model_has(model, "combine_strider") || | ||
| 361 | model_has(model, "dropship") || model_has(model, "helicopter") || | ||
| 362 | model_has(model, "chopper")) | ||
| 363 | return NPC_DMG_ARMORED; | ||
| 364 | if (model_has(model, "headcrab") || model_has(model, "antlion") || | ||
| 365 | model_has(model, "manhack")) | ||
| 366 | return NPC_DMG_VULNERABLE; | ||
| 367 | return NPC_DMG_NORMAL; | ||
| 368 | } | ||
| 369 | |||
| 370 | // Damage class depends on m_takedamage (a server read) plus model string | ||
| 371 | // matching. It changes even more rarely than the relation, so cache it. | ||
| 372 | int npc_damage_class(int idx, const char *model) { | ||
| 373 | struct Cache { | ||
| 374 | int idx; | ||
| 375 | double at; | ||
| 376 | int val; | ||
| 377 | }; | ||
| 378 | static Cache cache[256]; | ||
| 379 | double now = esp_now(); | ||
| 380 | Cache &c = cache[(unsigned)idx & 255]; | ||
| 381 | if (c.idx == idx && now - c.at < 0.25) | ||
| 382 | return c.val; | ||
| 383 | int v = npc_damage_class_uncached(idx, model); | ||
| 384 | c = {idx, now, v}; | ||
| 385 | return v; | ||
| 386 | } | ||
| 387 | |||
| 388 | bool active_weapon_contains(void *local_ent, const char *needle); | ||
| 389 | |||
| 390 | // Armoured units need blast damage; the RPG is the only stock weapon that | ||
| 391 | // passes their OnTakeDamage filters. Immune units can never be damaged. | ||
| 392 | bool npc_can_be_damaged(int idx, const char *model) { | ||
| 393 | int dc = npc_damage_class(idx, model); | ||
| 394 | if (dc == NPC_DMG_IMMUNE) | ||
| 395 | return false; | ||
| 396 | if (dc != NPC_DMG_ARMORED) | ||
| 397 | return true; | ||
| 398 | int local = engine_get_local_player(); | ||
| 399 | void *le = local > 0 ? entitylist_get(local) : nullptr; | ||
| 400 | return active_weapon_contains(le, "rocket_launcher") || | ||
| 401 | active_weapon_contains(le, "rpg"); | ||
| 402 | } | ||
| 403 | |||
| 404 | void short_name(const char *model, char *out, size_t outn) { | ||
| 405 | if (!model) { snprintf(out, outn, "NPC"); return; } | ||
| 406 | const char *base = strrchr(model, '/'); | ||
| 407 | base = base ? base + 1 : model; | ||
| 408 | size_t n = strcspn(base, "."); | ||
| 409 | if (n >= outn) n = outn - 1; | ||
| 410 | memcpy(out, base, n); | ||
| 411 | out[n] = 0; | ||
| 412 | } | ||
| 413 | |||
| 414 | // Enemy weapon label. NPC weapons are server entities, so read the server | ||
| 415 | // model index first and fall back to the client class name. | ||
| 416 | void weapon_short_name(int idx, char *out, size_t outn) { | ||
| 417 | out[0] = 0; | ||
| 418 | int wi = server_active_weapon(idx); | ||
| 419 | if (wi <= 0) | ||
| 420 | return; | ||
| 421 | const char *name = nullptr; | ||
| 422 | void *base = server_entity_base(wi); | ||
| 423 | int mi = base ? server_model_index(base) : -1; | ||
| 424 | if (mi > 0) | ||
| 425 | name = model_name_from_index(mi); | ||
| 426 | if (!name) { | ||
| 427 | void *wep = entitylist_get(wi); | ||
| 428 | if (wep) { | ||
| 429 | void *net = entity_networkable(wi); | ||
| 430 | name = entity_class_name(net, nullptr); | ||
| 431 | } | ||
| 432 | } | ||
| 433 | if (!name) | ||
| 434 | name = server_entity_classname(wi); | ||
| 435 | if (!name) | ||
| 436 | return; | ||
| 437 | static const struct { | ||
| 438 | const char *needle; | ||
| 439 | const char *label; | ||
| 440 | } map[] = { | ||
| 441 | {"smg1", "SMG1"}, {"irifle", "AR2"}, {"ar2", "AR2"}, | ||
| 442 | {"shotgun", "Shotgun"}, {"pistol", "Pistol"}, {"357", "357"}, | ||
| 443 | {"crossbow", "Crossbow"}, {"rocket", "RPG"}, {"rpg", "RPG"}, | ||
| 444 | {"grenade", "Grenade"}, {"frag", "Grenade"}, {"crowbar", "Crowbar"}, | ||
| 445 | {"physcannon", "Gravity"}, {"slam", "SLAM"}, {"bugbait", "Bugbait"}, | ||
| 446 | {"manhack", "Manhack"}, {"turret", "Turret"}, {"annabelle", "Rifle"}, | ||
| 447 | {"alyxgun", "Alyx gun"}, {"sniper", "Sniper"}, {"smg", "SMG"}, | ||
| 448 | }; | ||
| 449 | for (size_t i = 0; i < sizeof(map) / sizeof(map[0]); i++) { | ||
| 450 | if (strcasestr(name, map[i].needle)) { | ||
| 451 | snprintf(out, outn, "%s", map[i].label); | ||
| 452 | return; | ||
| 453 | } | ||
| 454 | } | ||
| 455 | // Last resort: the short class/model name. | ||
| 456 | short_name(name, out, outn); | ||
| 457 | } | ||
| 458 | |||
| 459 | // Hostile / neutral / friendly colours come from the ESP colour pickers. | ||
| 460 | |||
