summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 17:51:20 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 17:51:20 +0100
commit49df428d95a7c443d418d5b7e2a93af599b54f76 (patch)
tree604e64048aacc5e2b86f441e03e5e5ab8600bef2
parentItems: skip the CNetworkArray length header for m_iAmmo/m_hMyWeapons (diff)
downloadhl2h-49df428d95a7c443d418d5b7e2a93af599b54f76.tar.gz
hl2h-49df428d95a7c443d418d5b7e2a93af599b54f76.zip
Add chams
- Hook IVModelRender::DrawModelExecute (VEngineModel016 slot 19) and force models/debug/debugwhite for NPC models, identified by ModelRenderInfo_t::entity_index. - Two passes: MATERIAL_VAR_IGNOREZ through walls, then normal depth, with IMaterial::ColorModulate for the configured colour; restore the shared material's flag/colour afterwards. - Material lookup via the IMaterialSystem list, FindMaterial fallback. - New Chams feature/card with Through walls and colour; [chams] config. - Remove the temporary ammo/projectile/fuse diagnostics.
-rw-r--r--CONTEXT.md19
-rw-r--r--src/esp.cpp41
-rw-r--r--src/gui.cpp7
-rw-r--r--src/hl2h.cpp4
-rw-r--r--src/hl2h.h11
-rw-r--r--src/menu.cpp30
-rw-r--r--src/netvars.cpp31
-rw-r--r--src/server.cpp9
-rw-r--r--src/studio.cpp98
-rw-r--r--src/world.cpp47
10 files changed, 224 insertions, 73 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 92094bc..1fe24a0 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -278,6 +278,25 @@ vocabulary / decisions.
278 visible corner. Each edge is clipped at the camera near plane and then to the 278 visible corner. Each edge is clipped at the camera near plane and then to the
279 screen, so a box straddling the camera still draws its visible portions. 279 screen, so a box straddling the camera still draws its visible portions.
280 280
281## Chams
282
283- Hooks `IVModelRender::DrawModelExecute` (slot 19 in `VEngineModel016`).
284 The model's entity is `ModelRenderInfo_t::entity_index` (32-bit offset 0x30);
285 only entities for which `esp_is_npc_index` says the client class is an NPC
286 are affected.
287- Uses `IVModelRender::ForcedMaterialOverride` (slot 1) with
288 `models/debug/debugwhite` and draws twice: first with
289 `MATERIAL_VAR_IGNOREZ` (1 << 15) set through `IMaterial::SetMaterialVarFlag`
290 (slot 29) for the through-wall pass, then without it. `IMaterial::
291 ColorModulate` (slot 28) applies the configured colour. The shared material's
292 flag and colour are restored to (false, white) after the draw.
293- Material lookup walks the `IMaterialSystem` list (`world_find_material`), with
294 `FindMaterial` (slot 71) as the fallback for a not-yet-resident material.
295- Feature is off by default; `[chams] active/bind/walls/r/g/b`, with a
296 "Through walls" toggle and colour picker in the Visuals card.
297- This is a flat material override, not stencil outlines; depth handling is
298 the simple ignorez/normal two-pass.
299
281## Bones / ESP 300## Bones / ESP
282 301
283- `SetupBones(BONE_USED_BY_ANYTHING)` with engine `Time()`; studio header via 302- `SetupBones(BONE_USED_BY_ANYTHING)` with engine `Time()`; studio header via
diff --git a/src/esp.cpp b/src/esp.cpp
index a12cf9c..b50dd87 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -163,6 +163,16 @@ static bool is_npc_class(const char *name) {
163 strstr(name, "BaseHelicopter") != nullptr; 163 strstr(name, "BaseHelicopter") != nullptr;
164} 164}
165 165
166// Used by the chams hook to decide whether a model-render entity is an NPC.
167bool esp_is_npc_index(int idx) {
168 if (!g_iface.entitylist || idx <= 0 || idx > 8192)
169 return false;
170 void *net = entity_networkable(idx);
171 if (!net)
172 return false;
173 return is_npc_class(entity_class_name(net, nullptr));
174}
175
166static bool model_is_friendly(const char *m) { 176static bool model_is_friendly(const char *m) {
167 static const char *patterns[] = { 177 static const char *patterns[] = {
168 "citizen", "humans/", "male_", "female_", "barney", "alyx", 178 "citizen", "humans/", "male_", "female_", "barney", "alyx",
@@ -2796,23 +2806,6 @@ void esp_draw_items() {
2796 if (g_item_usable) 2806 if (g_item_usable)
2797 local_vitals(vit_hp, vit_max, vit_armor); 2807 local_vitals(vit_hp, vit_max, vit_armor);
2798 2808
2799 static bool ammo_logged = false;
2800 if (!ammo_logged) {
2801 ammo_logged = true;
2802 void *le = entitylist_get(local);
2803 char buf[512] = {};
2804 int used = 0;
2805 for (int t = 0; t < 12 && used < (int)sizeof(buf) - 24; t++) {
2806 int w = snprintf(buf + used, sizeof(buf) - (size_t)used, "%d=%d/%d ", t,
2807 local_ammo(le, t), ammo_max(t));
2808 if (w < 0)
2809 break;
2810 used += w;
2811 }
2812 logf("items: ammo current/max %s(usable=%d my_weapons=%d ammo=%d)", buf,
2813 (int)g_item_usable, g_net.my_weapons, g_net.ammo);
2814 }
2815
2816 for (int i = 0; i < g_nitems; i++) { 2809 for (int i = 0; i < g_nitems; i++) {
2817 const ItemTarget &t = g_item_targets[i]; 2810 const ItemTarget &t = g_item_targets[i];
2818 if (!item_kind_enabled(t.kind)) 2811 if (!item_kind_enabled(t.kind))
@@ -2830,12 +2823,6 @@ void esp_draw_items() {
2830 if (t.kind == IK_AMMO && t.ammo_type >= 0) { 2823 if (t.kind == IK_AMMO && t.ammo_type >= 0) {
2831 int maxa = ammo_max(t.ammo_type); 2824 int maxa = ammo_max(t.ammo_type);
2832 int cura = local_ammo(entitylist_get(local), t.ammo_type); 2825 int cura = local_ammo(entitylist_get(local), t.ammo_type);
2833 static int flt_logs = 0;
2834 if (flt_logs < 24) {
2835 flt_logs++;
2836 logf("items: ammo %s type=%d cur=%d max=%d", t.label, t.ammo_type,
2837 cura, maxa);
2838 }
2839 if (maxa > 0 && cura >= 0 && cura >= maxa) 2826 if (maxa > 0 && cura >= 0 && cura >= maxa)
2840 continue; 2827 continue;
2841 } 2828 }
@@ -3021,14 +3008,6 @@ static void projs_collect(int local_idx) {
3021 p.fuse = -1.0f; 3008 p.fuse = -1.0f;
3022 } 3009 }
3023 } 3010 }
3024 static int logs = 0;
3025 if (logs < 8) {
3026 logs++;
3027 logf("projectile: idx=%d cls=%s model=%s kind=%s det=%.2f cur=%.2f "
3028 "fuse=%.2f radius=%.0f",
3029 idx, cls ? cls : "?", model, proj_name(kind), det, curtime, p.fuse,
3030 p.radius);
3031 }
3032 } 3011 }
3033} 3012}
3034 3013
diff --git a/src/gui.cpp b/src/gui.cpp
index 2f832a2..a38f537 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -423,6 +423,13 @@ static void build_cards() {
423 add_toggle(proj, "Blast radius", 423 add_toggle(proj, "Blast radius",
424 "Draw the projectile's damage radius on the ground.", &g_proj_radius); 424 "Draw the projectile's damage radius on the ground.", &g_proj_radius);
425 425
426 int chams = add_card(2, &g_chams, "Chams",
427 "Flat-colour NPC models.", true);
428 add_toggle(chams, "Through walls",
429 "Draw a second pass with the depth test disabled.",
430 &g_chams_walls);
431 add_color(chams, "Colour", "Chams colour.", g_chams_rgb);
432
426 int view = add_card(2, nullptr, "Camera / display", 433 int view = add_card(2, nullptr, "Camera / display",
427 "Camera FOV, viewmodel FOV and HDR brightness.", true); 434 "Camera FOV, viewmodel FOV and HDR brightness.", true);
428 add_slider(view, "Camera FOV", 435 add_slider(view, "Camera FOV",
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index 703a582..067b11f 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -1148,6 +1148,7 @@ static void *setup_thread(void *) {
1148 g_iface.engineserver = ef("VEngineServer021", nullptr); 1148 g_iface.engineserver = ef("VEngineServer021", nullptr);
1149 g_iface.debugoverlay = ef("VDebugOverlay003", nullptr); 1149 g_iface.debugoverlay = ef("VDebugOverlay003", nullptr);
1150 g_iface.enginetrace = ef("EngineTraceClient003", nullptr); 1150 g_iface.enginetrace = ef("EngineTraceClient003", nullptr);
1151 g_iface.modelrender = ef("VEngineModel016", nullptr);
1151 static const char *cvar_names[] = {"VEngineCvar004", "VEngineCvar007", 1152 static const char *cvar_names[] = {"VEngineCvar004", "VEngineCvar007",
1152 "VEngineCvar003", "IVEngineCvar004", 1153 "VEngineCvar003", "IVEngineCvar004",
1153 nullptr}; 1154 nullptr};
@@ -1178,6 +1179,9 @@ static void *setup_thread(void *) {
1178 "enginetrace: %p (EngineTraceClient003)", 1179 "enginetrace: %p (EngineTraceClient003)",
1179 g_iface.engineserver, g_iface.debugoverlay, g_iface.enginetrace); 1180 g_iface.engineserver, g_iface.debugoverlay, g_iface.enginetrace);
1180 logf("cvar: %p (VEngineCvar004)", g_iface.cvar); 1181 logf("cvar: %p (VEngineCvar004)", g_iface.cvar);
1182 logf("modelrender: %p (VEngineModel016)", g_iface.modelrender);
1183
1184 chams_init();
1181 1185
1182 resolve_netvars(); 1186 resolve_netvars();
1183 if (!g_net.ok()) 1187 if (!g_net.ok())
diff --git a/src/hl2h.h b/src/hl2h.h
index d8927c7..55fac0d 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -68,6 +68,7 @@ struct GameInterfaces {
68 void *debugoverlay = nullptr; // IVDebugOverlay003 (engine-accurate world->screen) 68 void *debugoverlay = nullptr; // IVDebugOverlay003 (engine-accurate world->screen)
69 void *enginetrace = nullptr; // EngineTraceClient003 (world vischeck) 69 void *enginetrace = nullptr; // EngineTraceClient003 (world vischeck)
70 void *cvar = nullptr; // VEngineCvar004 (mat_fullbright etc.) 70 void *cvar = nullptr; // VEngineCvar004 (mat_fullbright etc.)
71 void *modelrender = nullptr; // VEngineModel016 (chams)
71}; 72};
72extern GameInterfaces g_iface; 73extern GameInterfaces g_iface;
73 74
@@ -189,7 +190,8 @@ enum FeatureId {
189 FEAT_FOG = 12, 190 FEAT_FOG = 12,
190 FEAT_SKYBOX = 13, 191 FEAT_SKYBOX = 13,
191 FEAT_PROJECTILES = 14, 192 FEAT_PROJECTILES = 14,
192 FEAT_COUNT = 15 193 FEAT_CHAMS = 15,
194 FEAT_COUNT = 16
193}; 195};
194enum EspOption { 196enum EspOption {
195 ESP_OPT_SHOW_NEUTRAL = 0, 197 ESP_OPT_SHOW_NEUTRAL = 0,
@@ -226,8 +228,10 @@ struct FeatureState {
226 228
227extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp, 229extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp,
228 g_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright, 230 g_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright,
229 g_world_tint, g_fog, g_skybox, g_projectiles; 231 g_world_tint, g_fog, g_skybox, g_projectiles, g_chams;
230extern bool g_proj_fuse, g_proj_radius; 232extern bool g_proj_fuse, g_proj_radius;
233extern bool g_chams_walls;
234extern int g_chams_rgb[3];
231extern bool g_tint_world_on, g_tint_sky_on, g_tint_prop_on; 235extern bool g_tint_world_on, g_tint_sky_on, g_tint_prop_on;
232extern int g_tint_world_rgb[3], g_tint_sky_rgb[3], g_tint_prop_rgb[3]; 236extern int g_tint_world_rgb[3], g_tint_sky_rgb[3], g_tint_prop_rgb[3];
233extern bool g_fog_skybox, g_fog_disable; 237extern bool g_fog_skybox, g_fog_disable;
@@ -329,6 +333,9 @@ bool aimbot_fov_circle();
329void aimbot_reset_lock(); 333void aimbot_reset_lock();
330void silentaim_reset_lock(); 334void silentaim_reset_lock();
331void *entitylist_get(int idx); 335void *entitylist_get(int idx);
336bool esp_is_npc_index(int idx);
337void *world_find_material(const char *name);
338void chams_init();
332int engine_get_local_player(); 339int engine_get_local_player();
333void draw_text_col(int x, int y, const char *s, int r, int g, int b); 340void draw_text_col(int x, int y, const char *s, int r, int g, int b);
334void draw_text_small_col(int x, int y, const char *s, int r, int g, int b); 341void draw_text_small_col(int x, int y, const char *s, int r, int g, int b);
diff --git a/src/menu.cpp b/src/menu.cpp
index 521bf43..77eb35f 100644
--- a/src/menu.cpp
+++ b/src/menu.cpp
@@ -63,6 +63,9 @@ FeatureState g_skybox{false, 0}; // skybox.png image override
63FeatureState g_projectiles{true, 0}; // grenade / rocket ESP 63FeatureState g_projectiles{true, 0}; // grenade / rocket ESP
64bool g_proj_fuse = true; 64bool g_proj_fuse = true;
65bool g_proj_radius = true; 65bool g_proj_radius = true;
66FeatureState g_chams{false, 0}; // flat model material, optional through walls
67bool g_chams_walls = true;
68int g_chams_rgb[3] = {255, 60, 60};
66bool g_item_health = true; 69bool g_item_health = true;
67bool g_item_ammo = true; 70bool g_item_ammo = true;
68bool g_item_armor = true; 71bool g_item_armor = true;
@@ -234,6 +237,9 @@ void config_save() {
234 fprintf(f, "[projectiles]\nactive=%d\nbind=%d\nfuse=%d\nradius=%d\n", 237 fprintf(f, "[projectiles]\nactive=%d\nbind=%d\nfuse=%d\nradius=%d\n",
235 g_projectiles.active, g_projectiles.bind, (int)g_proj_fuse, 238 g_projectiles.active, g_projectiles.bind, (int)g_proj_fuse,
236 (int)g_proj_radius); 239 (int)g_proj_radius);
240 fprintf(f, "[chams]\nactive=%d\nbind=%d\nwalls=%d\nr=%d\ng=%d\nb=%d\n",
241 g_chams.active, g_chams.bind, (int)g_chams_walls, g_chams_rgb[0],
242 g_chams_rgb[1], g_chams_rgb[2]);
237 fprintf(f, "[items]\nactive=%d\nbind=%d\nhealth=%d\nammo=%d\n" 243 fprintf(f, "[items]\nactive=%d\nbind=%d\nhealth=%d\nammo=%d\n"
238 "armor=%d\nweapons=%d\ncrates=%d\ndistance=%d\nmax_dist=%d\n" 244 "armor=%d\nweapons=%d\ncrates=%d\ndistance=%d\nmax_dist=%d\n"
239 "usable=%d\n", 245 "usable=%d\n",
@@ -284,7 +290,7 @@ static bool config_load() {
284 bool in_esp = false, in_bhop = false, in_aim = false, 290 bool in_esp = false, in_bhop = false, in_aim = false,
285 in_jumpbug = false, in_spread = false, in_items = false, 291 in_jumpbug = false, in_spread = false, in_items = false,
286 in_tint = false, in_fog = false, in_view = false, in_movement = false, 292 in_tint = false, in_fog = false, in_view = false, in_movement = false,
287 in_proj = false; 293 in_proj = false, in_chams = false;
288 while (fgets(line, sizeof(line), f)) { 294 while (fgets(line, sizeof(line), f)) {
289 line[strcspn(line, "\r\n")] = 0; 295 line[strcspn(line, "\r\n")] = 0;
290 if (line[0] == '[') { 296 if (line[0] == '[') {
@@ -312,6 +318,8 @@ static bool config_load() {
312 cur = &g_skybox; 318 cur = &g_skybox;
313 else if (strstr(line, "[projectiles]")) 319 else if (strstr(line, "[projectiles]"))
314 cur = &g_projectiles; 320 cur = &g_projectiles;
321 else if (strstr(line, "[chams]"))
322 cur = &g_chams;
315 else if (strstr(line, "[aimbot]")) 323 else if (strstr(line, "[aimbot]"))
316 cur = &g_aimbot; 324 cur = &g_aimbot;
317 else if (strstr(line, "[triggerbot]")) 325 else if (strstr(line, "[triggerbot]"))
@@ -335,6 +343,7 @@ static bool config_load() {
335 in_view = strstr(line, "[view]") != nullptr; 343 in_view = strstr(line, "[view]") != nullptr;
336 in_movement = strstr(line, "[movement]") != nullptr; 344 in_movement = strstr(line, "[movement]") != nullptr;
337 in_proj = (cur == &g_projectiles); 345 in_proj = (cur == &g_projectiles);
346 in_chams = (cur == &g_chams);
338 } else if (cur || in_view || in_movement) { 347 } else if (cur || in_view || in_movement) {
339 char *eq = strchr(line, '='); 348 char *eq = strchr(line, '=');
340 if (!eq) 349 if (!eq)
@@ -381,6 +390,17 @@ static bool config_load() {
381 else if (strcmp(key, "radius") == 0) 390 else if (strcmp(key, "radius") == 0)
382 g_proj_radius = atoi(val) != 0; 391 g_proj_radius = atoi(val) != 0;
383 } 392 }
393 if (in_chams) {
394 if (strcmp(key, "walls") == 0)
395 g_chams_walls = atoi(val) != 0;
396 else if (strcmp(key, "r") == 0 || strcmp(key, "g") == 0 ||
397 strcmp(key, "b") == 0) {
398 int v = atoi(val);
399 if (v < 0) v = 0;
400 if (v > 255) v = 255;
401 g_chams_rgb[key[0] == 'r' ? 0 : key[0] == 'g' ? 1 : 2] = v;
402 }
403 }
384 if (in_aim) { 404 if (in_aim) {
385 if (strcmp(key, "smooth") == 0) { 405 if (strcmp(key, "smooth") == 0) {
386 int v = atoi(val); 406 int v = atoi(val);
@@ -605,6 +625,11 @@ void config_reset_defaults() {
605 g_projectiles = {true, 0}; 625 g_projectiles = {true, 0};
606 g_proj_fuse = true; 626 g_proj_fuse = true;
607 g_proj_radius = true; 627 g_proj_radius = true;
628 g_chams = {false, 0};
629 g_chams_walls = true;
630 g_chams_rgb[0] = 255;
631 g_chams_rgb[1] = 60;
632 g_chams_rgb[2] = 60;
608 g_item_health = true; 633 g_item_health = true;
609 g_item_ammo = true; 634 g_item_ammo = true;
610 g_item_armor = true; 635 g_item_armor = true;
@@ -738,7 +763,8 @@ static FeatureState *feature_state(int feature) {
738 static FeatureState *feats[FEAT_COUNT] = { 763 static FeatureState *feats[FEAT_COUNT] = {
739 &g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot, 764 &g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot,
740 &g_silentaim, &g_jumpbug, &g_melee, &g_spread, &g_item_esp, 765 &g_silentaim, &g_jumpbug, &g_melee, &g_spread, &g_item_esp,
741 &g_fullbright, &g_world_tint, &g_fog, &g_skybox, &g_projectiles}; 766 &g_fullbright, &g_world_tint, &g_fog, &g_skybox, &g_projectiles,
767 &g_chams};
742 if (feature < 0 || feature >= FEAT_COUNT) 768 if (feature < 0 || feature >= FEAT_COUNT)
743 return nullptr; 769 return nullptr;
744 return feats[feature]; 770 return feats[feature];
diff --git a/src/netvars.cpp b/src/netvars.cpp
index 6a34ee5..e22ff38 100644
--- a/src/netvars.cpp
+++ b/src/netvars.cpp
@@ -61,15 +61,8 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
61 uintptr_t ptable = *(uintptr_t *)(prop + 0x28); 61 uintptr_t ptable = *(uintptr_t *)(prop + 0x28);
62 62
63 char pname2[64]; 63 char pname2[64];
64 if (!module_string(pname, pname2, sizeof(pname2))) { 64 if (!module_string(pname, pname2, sizeof(pname2)))
65 static int bad_names = 0;
66 if (bad_names < 8) {
67 bad_names++;
68 logf("netvar: unreadable prop name %#lx in %s", (unsigned long)pname,
69 tname);
70 }
71 continue; 65 continue;
72 }
73 66
74 if (dump) 67 if (dump)
75 logd(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2, 68 logd(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2,
@@ -90,13 +83,6 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
90 g_net.my_weapons = off; 83 g_net.my_weapons = off;
91 logf("netvar: m_hMyWeapons prop=%s poff=%d elem=%p -> %d", pname2, 84 logf("netvar: m_hMyWeapons prop=%s poff=%d elem=%p -> %d", pname2,
92 poff, (void *)elem, off); 85 poff, (void *)elem, off);
93 } else {
94 static int bad = 0;
95 if (bad < 8) {
96 bad++;
97 logf("netvar: m_hMyWeapons prop=%s rejected poff=%d elem=%p off=%d",
98 pname2, poff, (void *)elem, off);
99 }
100 } 86 }
101 continue; 87 continue;
102 } 88 }
@@ -112,13 +98,6 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
112 g_net.ammo = off; 98 g_net.ammo = off;
113 logf("netvar: m_iAmmo prop=%s poff=%d elem=%p -> %d", pname2, poff, 99 logf("netvar: m_iAmmo prop=%s poff=%d elem=%p -> %d", pname2, poff,
114 (void *)elem, off); 100 (void *)elem, off);
115 } else {
116 static int bad = 0;
117 if (bad < 8) {
118 bad++;
119 logf("netvar: m_iAmmo prop=%s rejected poff=%d elem=%p off=%d",
120 pname2, poff, (void *)elem, off);
121 }
122 } 101 }
123 continue; 102 continue;
124 } 103 }
@@ -126,14 +105,6 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
126 collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1); 105 collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1);
127 continue; 106 continue;
128 } 107 }
129 if (parray) {
130 static int alogs = 0;
131 if (alogs < 16) {
132 alogs++;
133 logf("netvar array %s poff=%d eoff=%d base=%d", pname2, poff,
134 *(int32_t *)(parray + 0x2C), (int)base_off);
135 }
136 }
137 108
138 if (strcmp(pname2, "m_lifeState") == 0 && g_net.lifestate < 0) { 109 if (strcmp(pname2, "m_lifeState") == 0 && g_net.lifestate < 0) {
139 g_net.lifestate = (int)(base_off + (uint32_t)poff); 110 g_net.lifestate = (int)(base_off + (uint32_t)poff);
diff --git a/src/server.cpp b/src/server.cpp
index 09d198a..bae355e 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -462,14 +462,7 @@ float server_grenade_detonate(int idx) {
462 server_entity_health(idx, nullptr, nullptr, &base); 462 server_entity_health(idx, nullptr, nullptr, &base);
463 if (!base) 463 if (!base)
464 return -1.0f; 464 return -1.0f;
465 float v = *(float *)((uintptr_t)base + g_srv_detonate); 465 return *(float *)((uintptr_t)base + g_srv_detonate);
466 static int logs = 0;
467 if (logs < 8) {
468 logs++;
469 logf("grenade fuse: idx=%d base=%p off=%d value=%.2f", idx, base,
470 g_srv_detonate, v);
471 }
472 return v;
473} 466}
474 467
475float server_grenade_radius(int idx) { 468float server_grenade_radius(int idx) {
diff --git a/src/studio.cpp b/src/studio.cpp
index 23be97f..1b7b77d 100644
--- a/src/studio.cpp
+++ b/src/studio.cpp
@@ -181,8 +181,106 @@ std::string loaded_module_path(const char *needle) {
181 return found; 181 return found;
182} 182}
183 183
184// ---------------------------------------------------------------------------
185// chams: force a flat material on NPC draws
186// ---------------------------------------------------------------------------
187//
188// IVModelRender::DrawModelExecute is called once per model. For NPC entities
189// we force models/debug/debugwhite, optionally with MATERIAL_VAR_IGNOREZ for
190// the through-wall pass, and draw twice. IVModelRender::ForcedMaterialOverride
191// is slot 1; DrawModelExecute is slot 19 in VEngineModel016.
192
193using ModelExecFn = void (*)(void *, const void *, const void *, void *);
194using ForceOverrideFn = void (*)(void *, void *, int);
195
196void *g_modelrender = nullptr;
197ModelExecFn g_orig_model_exec = nullptr;
198ForceOverrideFn g_force_override = nullptr;
199void *g_chams_mat = nullptr;
200bool g_chams_mat_tried = false;
201
202void *chams_material() {
203 if (g_chams_mat_tried)
204 return g_chams_mat;
205 g_chams_mat_tried = true;
206 g_chams_mat = world_find_material("models/debug/debugwhite");
207 if (g_chams_mat)
208 logf("chams: material models/debug/debugwhite = %p", g_chams_mat);
209 else
210 logf("chams: models/debug/debugwhite not found");
211 return g_chams_mat;
212}
213
214void chams_apply(void *mat, bool ignorez) {
215 using ColorFn = void (*)(void *, float, float, float);
216 using FlagFn = void (*)(void *, int, bool);
217 vfunc<ColorFn>(mat, 28)(mat, (float)g_chams_rgb[0] / 255.0f,
218 (float)g_chams_rgb[1] / 255.0f,
219 (float)g_chams_rgb[2] / 255.0f);
220 vfunc<FlagFn>(mat, 29)(mat, 1 << 15 /* MATERIAL_VAR_IGNOREZ */, ignorez);
221}
222
223void hkdModelExecute(void *self, const void *state, const void *info,
224 void *bone) {
225 if (!g_orig_model_exec)
226 return;
227 if (feature_active(FEAT_CHAMS) && info && g_force_override) {
228 // ModelRenderInfo_t::entity_index is at 0x30 on 32-bit.
229 int idx = *(const int *)((const char *)info + 0x30);
230 if (idx > 0 && idx <= 8192 && esp_is_npc_index(idx)) {
231 void *mat = chams_material();
232 if (mat) {
233 if (g_chams_walls) {
234 chams_apply(mat, true);
235 g_force_override(self, mat, 0 /* OVERRIDE_NORMAL */);
236 g_orig_model_exec(self, state, info, bone);
237 }
238 chams_apply(mat, false);
239 g_force_override(self, mat, 0);
240 g_orig_model_exec(self, state, info, bone);
241 g_force_override(self, nullptr, 0);
242 // debugwhite is shared: restore colour and depth behaviour.
243 using ColorFn = void (*)(void *, float, float, float);
244 using FlagFn = void (*)(void *, int, bool);
245 vfunc<FlagFn>(mat, 29)(mat, 1 << 15, false);
246 vfunc<ColorFn>(mat, 28)(mat, 1.0f, 1.0f, 1.0f);
247 return;
248 }
249 }
250 }
251 g_orig_model_exec(self, state, info, bone);
252}
253
184} // namespace 254} // namespace
185 255
256void chams_init() {
257 if (g_modelrender || !g_iface.modelrender) {
258 if (!g_iface.modelrender)
259 logf("chams: VEngineModel016 unavailable");
260 return;
261 }
262 void **vt = *(void ***)g_iface.modelrender;
263 if (!vt) {
264 logf("chams: model render has no vtable");
265 return;
266 }
267 g_force_override = (ForceOverrideFn)vt[1];
268 g_orig_model_exec = (ModelExecFn)vt[19];
269 if (!g_engine.contains((uintptr_t)g_force_override) ||
270 !g_engine.contains((uintptr_t)g_orig_model_exec)) {
271 logf("chams: implausible vtable entries override=%p draw=%p",
272 (void *)g_force_override, (void *)g_orig_model_exec);
273 return;
274 }
275 static void *nvt[32];
276 memcpy(nvt, vt, sizeof(nvt));
277 nvt[19] = (void *)&hkdModelExecute;
278 g_modelrender = g_iface.modelrender;
279 *(void ***)g_modelrender = nvt;
280 logf("chams: IVModelRender %p hooked (draw=%p override=%p)", g_modelrender,
281 (void *)g_orig_model_exec, (void *)g_force_override);
282}
283
186void studio_props_init() { 284void studio_props_init() {
187 if (g_install_tried) 285 if (g_install_tried)
188 return; 286 return;
diff --git a/src/world.cpp b/src/world.cpp
index 2fa1fc4..ab0c134 100644
--- a/src/world.cpp
+++ b/src/world.cpp
@@ -999,3 +999,50 @@ void skybox_update() {
999 g_sky_retry_at = now + 1.0; 999 g_sky_retry_at = now + 1.0;
1000 } 1000 }
1001} 1001}
1002
1003// Find a loaded material by its full name (e.g. "models/debug/debugwhite").
1004// Used by chams; walks the material list like the tint code and falls back to
1005// FindMaterial if the material is not currently resident.
1006void *world_find_material(const char *name) {
1007 if (!name || !*name)
1008 return nullptr;
1009 void *ms = material_system();
1010 if (!ms)
1011 return nullptr;
1012 using FirstFn = unsigned short (*)(void *);
1013 using NextFn = unsigned short (*)(void *, unsigned short);
1014 using InvalidFn = unsigned short (*)(void *);
1015 using GetFn = void *(*)(void *, unsigned short);
1016 using CountFn = int (*)(void *);
1017 using NameFn = const char *(*)(void *);
1018 FirstFn first = (FirstFn)vfunc<FirstFn>(ms, 73);
1019 NextFn next = (NextFn)vfunc<NextFn>(ms, 74);
1020 InvalidFn invalid = (InvalidFn)vfunc<InvalidFn>(ms, 75);
1021 GetFn get = (GetFn)vfunc<GetFn>(ms, 76);
1022 CountFn count = (CountFn)vfunc<CountFn>(ms, 77);
1023 int total = count ? count(ms) : 0;
1024 if (total > 0 && total < 200000) {
1025 unsigned short h = first(ms);
1026 unsigned short inv = invalid(ms);
1027 for (int i = 0; i < total && h != inv; i++) {
1028 void *mat = get(ms, h);
1029 if (mat) {
1030 NameFn mat_name = (NameFn)vfunc<NameFn>(mat, 0);
1031 const char *n = mat_name(mat);
1032 if (n && strcmp(n, name) == 0)
1033 return mat;
1034 }
1035 h = next(ms, h);
1036 }
1037 }
1038 // Not resident yet: let the material system load it. FindMaterial is slot
1039 // 71 (IsMaterialLoaded is 72, FirstMaterial 73).
1040 using FindFn = void *(*)(void *, const char *, const char *, bool,
1041 const char *);
1042 void *mat = vfunc<FindFn>(ms, 71)(ms, name, "Model textures", false, nullptr);
1043 if (!mat)
1044 return nullptr;
1045 NameFn mat_name = (NameFn)vfunc<NameFn>(mat, 0);
1046 const char *n = mat_name(mat);
1047 return (n && strcmp(n, name) == 0) ? mat : nullptr;
1048}