summaryrefslogtreecommitdiffstats
path: root/src
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 /src
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.
Diffstat (limited to '')
-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
9 files changed, 205 insertions, 73 deletions
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}