From b9d5965c0366fd7777c3627b9fbf89c8526ac0ec Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 01:13:09 +0100 Subject: World tint: colour dynamic props through m_clrRender Prop model materials (VertexLitGeneric) ignore IMaterial::ColorModulate: the engine feeds StudioRender the per-entity render colour (m_clrRender) on every draw, so the material-side tint never showed on crates and other props. Resolve m_clrRender from DT_BaseEntity and, every 10 ms while World tint and Props are enabled, write the prop colour into every entity whose model is under models/props. Restore white once when disabled. --- src/esp.cpp | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ src/hl2h.h | 2 ++ src/menu.cpp | 1 + src/netvars.cpp | 6 ++++-- 4 files changed, 56 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/esp.cpp b/src/esp.cpp index 50d7b5d..9d041b0 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -1405,6 +1405,55 @@ bool triggerbot_apply(CUserCmd *cmd) { return false; } +// Tints dynamic props by writing their client render colour. StudioRender +// uses m_clrRender as the per-model colour modulation, which is why changing +// the prop MATERIAL colour had no effect. +void prop_tint_update() { + static bool applied = false; + static bool logged_missing = false; + static bool logged_touched = false; + bool on = feature_active(FEAT_WORLD_TINT) && g_tint_prop_on; + if (!on && !applied) + return; + if (!g_iface.entitylist || g_net.clr_render < 0) { + if (on && !logged_missing) { + logged_missing = true; + logf("prop tint: m_clrRender unavailable"); + } + return; + } + static double last = 0.0; + double now = esp_now(); + if (on && now - last < 0.01) + return; + last = now; + + using Fn = int (*)(void *); + int hi = vfunc(g_iface.entitylist, 6)(g_iface.entitylist); + if (hi < 0 || hi > 4096) + hi = 4096; + uint32_t col = + on ? ((uint32_t)g_tint_prop_rgb[0] | ((uint32_t)g_tint_prop_rgb[1] << 8) | + ((uint32_t)g_tint_prop_rgb[2] << 16) | (255u << 24)) + : 0xFFFFFFFFu; + int touched = 0; + for (int idx = 1; idx <= hi; idx++) { + void *ent = entitylist_get(idx); + if (!ent) + continue; + const char *model = entity_model_name(ent); + if (!model || strncmp(model, "models/props", 12) != 0) + continue; + *(uint32_t *)((uintptr_t)ent + (uintptr_t)g_net.clr_render) = col; + touched++; + } + applied = on; + if (on && touched > 0 && !logged_touched) { + logged_touched = true; + logf("prop tint: %d prop entities", touched); + } +} + void esp_draw() { if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return; if (!g_scr_w) { diff --git a/src/hl2h.h b/src/hl2h.h index fd7a863..779a241 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -96,6 +96,7 @@ struct NetVars { int activeweapon = -1; // client m_hActiveWeapon handle int owner = -1; // client m_hOwnerEntity handle int weapon_owner = -1; // client m_hOwner handle (C_BaseCombatWeapon) + int clr_render = -1; // client m_clrRender (entity render colour) // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100. @@ -296,6 +297,7 @@ int engine_get_local_player(); void draw_text_col(int x, int y, const char *s, int r, int g, int b); void esp_draw(); void esp_draw_items(); +void prop_tint_update(); bool world_to_screen(const Vector &w, float &sx, float &sy); void set_silent_restore(const QAngle &angles); bool is_visible(const Vector &eye, const Vector &point, diff --git a/src/menu.cpp b/src/menu.cpp index 7ffc84a..0ce6c05 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -815,6 +815,7 @@ void outer_draw() { poll_keyboard(); fullbright_update(); world_tint_update(); + prop_tint_update(); fog_update(); gui_handle_input(); // HL2H_DRAW_TEST=1 draws a probe rect without needing the GUI open diff --git a/src/netvars.cpp b/src/netvars.cpp index 238af70..4578e52 100644 --- a/src/netvars.cpp +++ b/src/netvars.cpp @@ -92,6 +92,8 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { } else if (strcmp(pname2, "m_hActiveWeapon") == 0 && g_net.activeweapon < 0) { g_net.activeweapon = (int)(base_off + (uint32_t)poff); + } else if (strcmp(pname2, "m_clrRender") == 0 && g_net.clr_render < 0) { + g_net.clr_render = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) { g_net.teamnum = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) { @@ -273,9 +275,9 @@ void resolve_netvars() { logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d " "modelindex=%d absvelocity=%d activeweapon=%d owner=%d " - "weapon_owner=%d", + "weapon_owner=%d clr_render=%d", g_net.flags, g_net.velocity, g_net.movetype, g_net.health, g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex, - g_net.absvelocity, g_net.activeweapon); + g_net.absvelocity, g_net.activeweapon, g_net.clr_render); } -- cgit v1.2.3