From 49df428d95a7c443d418d5b7e2a93af599b54f76 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 17:51:20 +0100 Subject: 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. --- CONTEXT.md | 19 +++++++++++ src/esp.cpp | 41 ++++++------------------ src/gui.cpp | 7 +++++ src/hl2h.cpp | 4 +++ src/hl2h.h | 11 +++++-- src/menu.cpp | 30 ++++++++++++++++-- src/netvars.cpp | 31 +----------------- src/server.cpp | 9 +----- src/studio.cpp | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/world.cpp | 47 +++++++++++++++++++++++++++ 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. visible corner. Each edge is clipped at the camera near plane and then to the screen, so a box straddling the camera still draws its visible portions. +## Chams + +- Hooks `IVModelRender::DrawModelExecute` (slot 19 in `VEngineModel016`). + The model's entity is `ModelRenderInfo_t::entity_index` (32-bit offset 0x30); + only entities for which `esp_is_npc_index` says the client class is an NPC + are affected. +- Uses `IVModelRender::ForcedMaterialOverride` (slot 1) with + `models/debug/debugwhite` and draws twice: first with + `MATERIAL_VAR_IGNOREZ` (1 << 15) set through `IMaterial::SetMaterialVarFlag` + (slot 29) for the through-wall pass, then without it. `IMaterial:: + ColorModulate` (slot 28) applies the configured colour. The shared material's + flag and colour are restored to (false, white) after the draw. +- Material lookup walks the `IMaterialSystem` list (`world_find_material`), with + `FindMaterial` (slot 71) as the fallback for a not-yet-resident material. +- Feature is off by default; `[chams] active/bind/walls/r/g/b`, with a + "Through walls" toggle and colour picker in the Visuals card. +- This is a flat material override, not stencil outlines; depth handling is + the simple ignorez/normal two-pass. + ## Bones / ESP - `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) { strstr(name, "BaseHelicopter") != nullptr; } +// Used by the chams hook to decide whether a model-render entity is an NPC. +bool esp_is_npc_index(int idx) { + if (!g_iface.entitylist || idx <= 0 || idx > 8192) + return false; + void *net = entity_networkable(idx); + if (!net) + return false; + return is_npc_class(entity_class_name(net, nullptr)); +} + static bool model_is_friendly(const char *m) { static const char *patterns[] = { "citizen", "humans/", "male_", "female_", "barney", "alyx", @@ -2796,23 +2806,6 @@ void esp_draw_items() { if (g_item_usable) local_vitals(vit_hp, vit_max, vit_armor); - static bool ammo_logged = false; - if (!ammo_logged) { - ammo_logged = true; - void *le = entitylist_get(local); - char buf[512] = {}; - int used = 0; - for (int t = 0; t < 12 && used < (int)sizeof(buf) - 24; t++) { - int w = snprintf(buf + used, sizeof(buf) - (size_t)used, "%d=%d/%d ", t, - local_ammo(le, t), ammo_max(t)); - if (w < 0) - break; - used += w; - } - logf("items: ammo current/max %s(usable=%d my_weapons=%d ammo=%d)", buf, - (int)g_item_usable, g_net.my_weapons, g_net.ammo); - } - for (int i = 0; i < g_nitems; i++) { const ItemTarget &t = g_item_targets[i]; if (!item_kind_enabled(t.kind)) @@ -2830,12 +2823,6 @@ void esp_draw_items() { if (t.kind == IK_AMMO && t.ammo_type >= 0) { int maxa = ammo_max(t.ammo_type); int cura = local_ammo(entitylist_get(local), t.ammo_type); - static int flt_logs = 0; - if (flt_logs < 24) { - flt_logs++; - logf("items: ammo %s type=%d cur=%d max=%d", t.label, t.ammo_type, - cura, maxa); - } if (maxa > 0 && cura >= 0 && cura >= maxa) continue; } @@ -3021,14 +3008,6 @@ static void projs_collect(int local_idx) { p.fuse = -1.0f; } } - static int logs = 0; - if (logs < 8) { - logs++; - logf("projectile: idx=%d cls=%s model=%s kind=%s det=%.2f cur=%.2f " - "fuse=%.2f radius=%.0f", - idx, cls ? cls : "?", model, proj_name(kind), det, curtime, p.fuse, - p.radius); - } } } 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() { add_toggle(proj, "Blast radius", "Draw the projectile's damage radius on the ground.", &g_proj_radius); + int chams = add_card(2, &g_chams, "Chams", + "Flat-colour NPC models.", true); + add_toggle(chams, "Through walls", + "Draw a second pass with the depth test disabled.", + &g_chams_walls); + add_color(chams, "Colour", "Chams colour.", g_chams_rgb); + int view = add_card(2, nullptr, "Camera / display", "Camera FOV, viewmodel FOV and HDR brightness.", true); 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 *) { g_iface.engineserver = ef("VEngineServer021", nullptr); g_iface.debugoverlay = ef("VDebugOverlay003", nullptr); g_iface.enginetrace = ef("EngineTraceClient003", nullptr); + g_iface.modelrender = ef("VEngineModel016", nullptr); static const char *cvar_names[] = {"VEngineCvar004", "VEngineCvar007", "VEngineCvar003", "IVEngineCvar004", nullptr}; @@ -1178,6 +1179,9 @@ static void *setup_thread(void *) { "enginetrace: %p (EngineTraceClient003)", g_iface.engineserver, g_iface.debugoverlay, g_iface.enginetrace); logf("cvar: %p (VEngineCvar004)", g_iface.cvar); + logf("modelrender: %p (VEngineModel016)", g_iface.modelrender); + + chams_init(); resolve_netvars(); 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 { void *debugoverlay = nullptr; // IVDebugOverlay003 (engine-accurate world->screen) void *enginetrace = nullptr; // EngineTraceClient003 (world vischeck) void *cvar = nullptr; // VEngineCvar004 (mat_fullbright etc.) + void *modelrender = nullptr; // VEngineModel016 (chams) }; extern GameInterfaces g_iface; @@ -189,7 +190,8 @@ enum FeatureId { FEAT_FOG = 12, FEAT_SKYBOX = 13, FEAT_PROJECTILES = 14, - FEAT_COUNT = 15 + FEAT_CHAMS = 15, + FEAT_COUNT = 16 }; enum EspOption { ESP_OPT_SHOW_NEUTRAL = 0, @@ -226,8 +228,10 @@ struct FeatureState { extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp, g_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright, - g_world_tint, g_fog, g_skybox, g_projectiles; + g_world_tint, g_fog, g_skybox, g_projectiles, g_chams; extern bool g_proj_fuse, g_proj_radius; +extern bool g_chams_walls; +extern int g_chams_rgb[3]; extern bool g_tint_world_on, g_tint_sky_on, g_tint_prop_on; extern int g_tint_world_rgb[3], g_tint_sky_rgb[3], g_tint_prop_rgb[3]; extern bool g_fog_skybox, g_fog_disable; @@ -329,6 +333,9 @@ bool aimbot_fov_circle(); void aimbot_reset_lock(); void silentaim_reset_lock(); void *entitylist_get(int idx); +bool esp_is_npc_index(int idx); +void *world_find_material(const char *name); +void chams_init(); int engine_get_local_player(); void draw_text_col(int x, int y, const char *s, int r, int g, int b); void 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 FeatureState g_projectiles{true, 0}; // grenade / rocket ESP bool g_proj_fuse = true; bool g_proj_radius = true; +FeatureState g_chams{false, 0}; // flat model material, optional through walls +bool g_chams_walls = true; +int g_chams_rgb[3] = {255, 60, 60}; bool g_item_health = true; bool g_item_ammo = true; bool g_item_armor = true; @@ -234,6 +237,9 @@ void config_save() { fprintf(f, "[projectiles]\nactive=%d\nbind=%d\nfuse=%d\nradius=%d\n", g_projectiles.active, g_projectiles.bind, (int)g_proj_fuse, (int)g_proj_radius); + fprintf(f, "[chams]\nactive=%d\nbind=%d\nwalls=%d\nr=%d\ng=%d\nb=%d\n", + g_chams.active, g_chams.bind, (int)g_chams_walls, g_chams_rgb[0], + g_chams_rgb[1], g_chams_rgb[2]); fprintf(f, "[items]\nactive=%d\nbind=%d\nhealth=%d\nammo=%d\n" "armor=%d\nweapons=%d\ncrates=%d\ndistance=%d\nmax_dist=%d\n" "usable=%d\n", @@ -284,7 +290,7 @@ static bool config_load() { bool in_esp = false, in_bhop = false, in_aim = false, in_jumpbug = false, in_spread = false, in_items = false, in_tint = false, in_fog = false, in_view = false, in_movement = false, - in_proj = false; + in_proj = false, in_chams = false; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { @@ -312,6 +318,8 @@ static bool config_load() { cur = &g_skybox; else if (strstr(line, "[projectiles]")) cur = &g_projectiles; + else if (strstr(line, "[chams]")) + cur = &g_chams; else if (strstr(line, "[aimbot]")) cur = &g_aimbot; else if (strstr(line, "[triggerbot]")) @@ -335,6 +343,7 @@ static bool config_load() { in_view = strstr(line, "[view]") != nullptr; in_movement = strstr(line, "[movement]") != nullptr; in_proj = (cur == &g_projectiles); + in_chams = (cur == &g_chams); } else if (cur || in_view || in_movement) { char *eq = strchr(line, '='); if (!eq) @@ -381,6 +390,17 @@ static bool config_load() { else if (strcmp(key, "radius") == 0) g_proj_radius = atoi(val) != 0; } + if (in_chams) { + if (strcmp(key, "walls") == 0) + g_chams_walls = atoi(val) != 0; + else if (strcmp(key, "r") == 0 || strcmp(key, "g") == 0 || + strcmp(key, "b") == 0) { + int v = atoi(val); + if (v < 0) v = 0; + if (v > 255) v = 255; + g_chams_rgb[key[0] == 'r' ? 0 : key[0] == 'g' ? 1 : 2] = v; + } + } if (in_aim) { if (strcmp(key, "smooth") == 0) { int v = atoi(val); @@ -605,6 +625,11 @@ void config_reset_defaults() { g_projectiles = {true, 0}; g_proj_fuse = true; g_proj_radius = true; + g_chams = {false, 0}; + g_chams_walls = true; + g_chams_rgb[0] = 255; + g_chams_rgb[1] = 60; + g_chams_rgb[2] = 60; g_item_health = true; g_item_ammo = true; g_item_armor = true; @@ -738,7 +763,8 @@ static FeatureState *feature_state(int feature) { static FeatureState *feats[FEAT_COUNT] = { &g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot, &g_silentaim, &g_jumpbug, &g_melee, &g_spread, &g_item_esp, - &g_fullbright, &g_world_tint, &g_fog, &g_skybox, &g_projectiles}; + &g_fullbright, &g_world_tint, &g_fog, &g_skybox, &g_projectiles, + &g_chams}; if (feature < 0 || feature >= FEAT_COUNT) return nullptr; 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) { uintptr_t ptable = *(uintptr_t *)(prop + 0x28); char pname2[64]; - if (!module_string(pname, pname2, sizeof(pname2))) { - static int bad_names = 0; - if (bad_names < 8) { - bad_names++; - logf("netvar: unreadable prop name %#lx in %s", (unsigned long)pname, - tname); - } + if (!module_string(pname, pname2, sizeof(pname2))) continue; - } if (dump) 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) { g_net.my_weapons = off; logf("netvar: m_hMyWeapons prop=%s poff=%d elem=%p -> %d", pname2, poff, (void *)elem, off); - } else { - static int bad = 0; - if (bad < 8) { - bad++; - logf("netvar: m_hMyWeapons prop=%s rejected poff=%d elem=%p off=%d", - pname2, poff, (void *)elem, off); - } } continue; } @@ -112,13 +98,6 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { g_net.ammo = off; logf("netvar: m_iAmmo prop=%s poff=%d elem=%p -> %d", pname2, poff, (void *)elem, off); - } else { - static int bad = 0; - if (bad < 8) { - bad++; - logf("netvar: m_iAmmo prop=%s rejected poff=%d elem=%p off=%d", - pname2, poff, (void *)elem, off); - } } continue; } @@ -126,14 +105,6 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1); continue; } - if (parray) { - static int alogs = 0; - if (alogs < 16) { - alogs++; - logf("netvar array %s poff=%d eoff=%d base=%d", pname2, poff, - *(int32_t *)(parray + 0x2C), (int)base_off); - } - } if (strcmp(pname2, "m_lifeState") == 0 && g_net.lifestate < 0) { 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) { server_entity_health(idx, nullptr, nullptr, &base); if (!base) return -1.0f; - float v = *(float *)((uintptr_t)base + g_srv_detonate); - static int logs = 0; - if (logs < 8) { - logs++; - logf("grenade fuse: idx=%d base=%p off=%d value=%.2f", idx, base, - g_srv_detonate, v); - } - return v; + return *(float *)((uintptr_t)base + g_srv_detonate); } float 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) { return found; } +// --------------------------------------------------------------------------- +// chams: force a flat material on NPC draws +// --------------------------------------------------------------------------- +// +// IVModelRender::DrawModelExecute is called once per model. For NPC entities +// we force models/debug/debugwhite, optionally with MATERIAL_VAR_IGNOREZ for +// the through-wall pass, and draw twice. IVModelRender::ForcedMaterialOverride +// is slot 1; DrawModelExecute is slot 19 in VEngineModel016. + +using ModelExecFn = void (*)(void *, const void *, const void *, void *); +using ForceOverrideFn = void (*)(void *, void *, int); + +void *g_modelrender = nullptr; +ModelExecFn g_orig_model_exec = nullptr; +ForceOverrideFn g_force_override = nullptr; +void *g_chams_mat = nullptr; +bool g_chams_mat_tried = false; + +void *chams_material() { + if (g_chams_mat_tried) + return g_chams_mat; + g_chams_mat_tried = true; + g_chams_mat = world_find_material("models/debug/debugwhite"); + if (g_chams_mat) + logf("chams: material models/debug/debugwhite = %p", g_chams_mat); + else + logf("chams: models/debug/debugwhite not found"); + return g_chams_mat; +} + +void chams_apply(void *mat, bool ignorez) { + using ColorFn = void (*)(void *, float, float, float); + using FlagFn = void (*)(void *, int, bool); + vfunc(mat, 28)(mat, (float)g_chams_rgb[0] / 255.0f, + (float)g_chams_rgb[1] / 255.0f, + (float)g_chams_rgb[2] / 255.0f); + vfunc(mat, 29)(mat, 1 << 15 /* MATERIAL_VAR_IGNOREZ */, ignorez); +} + +void hkdModelExecute(void *self, const void *state, const void *info, + void *bone) { + if (!g_orig_model_exec) + return; + if (feature_active(FEAT_CHAMS) && info && g_force_override) { + // ModelRenderInfo_t::entity_index is at 0x30 on 32-bit. + int idx = *(const int *)((const char *)info + 0x30); + if (idx > 0 && idx <= 8192 && esp_is_npc_index(idx)) { + void *mat = chams_material(); + if (mat) { + if (g_chams_walls) { + chams_apply(mat, true); + g_force_override(self, mat, 0 /* OVERRIDE_NORMAL */); + g_orig_model_exec(self, state, info, bone); + } + chams_apply(mat, false); + g_force_override(self, mat, 0); + g_orig_model_exec(self, state, info, bone); + g_force_override(self, nullptr, 0); + // debugwhite is shared: restore colour and depth behaviour. + using ColorFn = void (*)(void *, float, float, float); + using FlagFn = void (*)(void *, int, bool); + vfunc(mat, 29)(mat, 1 << 15, false); + vfunc(mat, 28)(mat, 1.0f, 1.0f, 1.0f); + return; + } + } + } + g_orig_model_exec(self, state, info, bone); +} + } // namespace +void chams_init() { + if (g_modelrender || !g_iface.modelrender) { + if (!g_iface.modelrender) + logf("chams: VEngineModel016 unavailable"); + return; + } + void **vt = *(void ***)g_iface.modelrender; + if (!vt) { + logf("chams: model render has no vtable"); + return; + } + g_force_override = (ForceOverrideFn)vt[1]; + g_orig_model_exec = (ModelExecFn)vt[19]; + if (!g_engine.contains((uintptr_t)g_force_override) || + !g_engine.contains((uintptr_t)g_orig_model_exec)) { + logf("chams: implausible vtable entries override=%p draw=%p", + (void *)g_force_override, (void *)g_orig_model_exec); + return; + } + static void *nvt[32]; + memcpy(nvt, vt, sizeof(nvt)); + nvt[19] = (void *)&hkdModelExecute; + g_modelrender = g_iface.modelrender; + *(void ***)g_modelrender = nvt; + logf("chams: IVModelRender %p hooked (draw=%p override=%p)", g_modelrender, + (void *)g_orig_model_exec, (void *)g_force_override); +} + void studio_props_init() { if (g_install_tried) 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() { g_sky_retry_at = now + 1.0; } } + +// Find a loaded material by its full name (e.g. "models/debug/debugwhite"). +// Used by chams; walks the material list like the tint code and falls back to +// FindMaterial if the material is not currently resident. +void *world_find_material(const char *name) { + if (!name || !*name) + return nullptr; + void *ms = material_system(); + if (!ms) + return nullptr; + using FirstFn = unsigned short (*)(void *); + using NextFn = unsigned short (*)(void *, unsigned short); + using InvalidFn = unsigned short (*)(void *); + using GetFn = void *(*)(void *, unsigned short); + using CountFn = int (*)(void *); + using NameFn = const char *(*)(void *); + FirstFn first = (FirstFn)vfunc(ms, 73); + NextFn next = (NextFn)vfunc(ms, 74); + InvalidFn invalid = (InvalidFn)vfunc(ms, 75); + GetFn get = (GetFn)vfunc(ms, 76); + CountFn count = (CountFn)vfunc(ms, 77); + int total = count ? count(ms) : 0; + if (total > 0 && total < 200000) { + unsigned short h = first(ms); + unsigned short inv = invalid(ms); + for (int i = 0; i < total && h != inv; i++) { + void *mat = get(ms, h); + if (mat) { + NameFn mat_name = (NameFn)vfunc(mat, 0); + const char *n = mat_name(mat); + if (n && strcmp(n, name) == 0) + return mat; + } + h = next(ms, h); + } + } + // Not resident yet: let the material system load it. FindMaterial is slot + // 71 (IsMaterialLoaded is 72, FirstMaterial 73). + using FindFn = void *(*)(void *, const char *, const char *, bool, + const char *); + void *mat = vfunc(ms, 71)(ms, name, "Model textures", false, nullptr); + if (!mat) + return nullptr; + NameFn mat_name = (NameFn)vfunc(mat, 0); + const char *n = mat_name(mat); + return (n && strcmp(n, name) == 0) ? mat : nullptr; +} -- cgit v1.2.3