From 1f7c91905f69baba0aca8265c84deaba2ddac98a Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 3 Nov 2026 23:21:13 +0100 Subject: World: add world/sky material colour modulation Port Amalgam's modulation approach: obtain IMaterialSystem from the already loaded materialsystem.so (VMaterialSystem080) and walk materials with FirstMaterial/NextMaterial/GetMaterial. ColorModulate the "World textures" group (sky names excluded) and the "SkyBox textures"/sky-named group. The walk runs only when the enabled groups or colours change, restores white when disabled, and re-applies after level changes. Before touching materials, GetNumMaterials() and the first material's name/group are sanity-checked so a wrong vtable slot logs and aborts instead of walking garbage. GUI: "World tint" card with world/sky toggles and RGB sliders; config section [worldtint]. --- src/gui.cpp | 20 +++++ src/hl2h.cpp | 1 + src/hl2h.h | 10 ++- src/menu.cpp | 45 ++++++++++- src/world.cpp | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 src/world.cpp (limited to 'src') diff --git a/src/gui.cpp b/src/gui.cpp index 3f1651d..9d27a09 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -375,6 +375,26 @@ static void build_cards() { add_card(2, &g_fullbright, "Fullbright", "Ignore world lighting so dark areas are visible.", true); + int tint = add_card(2, &g_world_tint, "World tint", + "Multiply world and skybox material colour. " + "Independent of Fullbright lighting.", true); + add_toggle(tint, "World brushes", + "Tint world textures (sky and water excluded from this part).", + &g_tint_world_on); + add_slider(tint, "World red", "Red channel of the world tint (0-255).", 0, + 255, &g_tint_world_rgb[0]); + add_slider(tint, "World green", "Green channel of the world tint (0-255).", 0, + 255, &g_tint_world_rgb[1]); + add_slider(tint, "World blue", "Blue channel of the world tint (0-255).", 0, + 255, &g_tint_world_rgb[2]); + add_toggle(tint, "Skybox", "Tint the skybox textures.", &g_tint_sky_on); + add_slider(tint, "Sky red", "Red channel of the sky tint (0-255).", 0, 255, + &g_tint_sky_rgb[0]); + add_slider(tint, "Sky green", "Green channel of the sky tint (0-255).", 0, + 255, &g_tint_sky_rgb[1]); + add_slider(tint, "Sky blue", "Blue channel of the sky tint (0-255).", 0, 255, + &g_tint_sky_rgb[2]); + int spread = add_card(3, &g_spread, "Spread control", "Choose a server shot seed that tightens the cone.", true); diff --git a/src/hl2h.cpp b/src/hl2h.cpp index 375cd5a..dfe4e57 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -584,6 +584,7 @@ static void do_move(int seq, float frametime) { aimbot_reset_lock(); silentaim_reset_lock(); fullbright_invalidate(); + world_tint_invalidate(); } if (feature_active(FEAT_SPREAD)) { diff --git a/src/hl2h.h b/src/hl2h.h index 2824d3c..e90ba91 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -178,7 +178,8 @@ enum FeatureId { FEAT_SPREAD = 8, FEAT_ITEM_ESP = 9, FEAT_FULLBRIGHT = 10, - FEAT_COUNT = 11 + FEAT_WORLD_TINT = 11, + FEAT_COUNT = 12 }; enum EspOption { ESP_OPT_SHOW_NEUTRAL = 0, @@ -210,7 +211,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_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright, + g_world_tint; +extern bool g_tint_world_on, g_tint_sky_on; +extern int g_tint_world_rgb[3], g_tint_sky_rgb[3]; extern bool g_bhop_autostrafe; extern bool g_jumpbug_assist; extern bool g_spread_no_spread; @@ -248,6 +252,8 @@ void resolve_netvars(); bool cvar_set_int(const char *name, int value); void fullbright_update(); void fullbright_invalidate(); +void world_tint_update(); +void world_tint_invalidate(); bool resolve_server_netvars(); int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); bool server_set_velocity_z(int idx, float vz); diff --git a/src/menu.cpp b/src/menu.cpp index 9a249f8..6a7ffe5 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -46,6 +46,11 @@ FeatureState g_triggerbot{true, 603}; // default Mouse4 FeatureState g_silentaim{true, 602}; // default Mouse3 FeatureState g_item_esp{true, 0}; // valuable item tags, always-on by default FeatureState g_fullbright{false, 0}; // mat_fullbright world lighting +FeatureState g_world_tint{false, 0}; // material colour modulation +bool g_tint_world_on = true; +bool g_tint_sky_on = true; +int g_tint_world_rgb[3] = {255, 255, 255}; +int g_tint_sky_rgb[3] = {255, 255, 255}; bool g_item_health = true; bool g_item_ammo = true; bool g_item_armor = true; @@ -187,6 +192,12 @@ void config_save() { g_silentaim.bind, g_silent_fov); fprintf(f, "[fullbright]\nactive=%d\nbind=%d\n", g_fullbright.active, g_fullbright.bind); + fprintf(f, "[worldtint]\nactive=%d\nbind=%d\nworld=%d\nsky=%d\n" + "wr=%d\nwg=%d\nwb=%d\nsr=%d\nsg=%d\nsb=%d\n", + g_world_tint.active, g_world_tint.bind, (int)g_tint_world_on, + (int)g_tint_sky_on, g_tint_world_rgb[0], g_tint_world_rgb[1], + g_tint_world_rgb[2], g_tint_sky_rgb[0], g_tint_sky_rgb[1], + g_tint_sky_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", g_item_esp.active, g_item_esp.bind, (int)g_item_health, @@ -220,7 +231,8 @@ static bool config_load() { char line[256]; FeatureState *cur = nullptr; bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false, - in_jumpbug = false, in_spread = false, in_items = false; + in_jumpbug = false, in_spread = false, in_items = false, + in_tint = false; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { @@ -240,6 +252,8 @@ static bool config_load() { cur = &g_item_esp; else if (strstr(line, "[fullbright]")) cur = &g_fullbright; + else if (strstr(line, "[worldtint]")) + cur = &g_world_tint; else if (strstr(line, "[aimbot]")) cur = &g_aimbot; else if (strstr(line, "[triggerbot]")) @@ -255,6 +269,7 @@ static bool config_load() { in_jumpbug = (cur == &g_jumpbug); in_spread = (cur == &g_spread); in_items = (cur == &g_item_esp); + in_tint = (cur == &g_world_tint); } else if (cur) { char *eq = strchr(line, '='); if (!eq) @@ -325,6 +340,25 @@ static bool config_load() { if (v > 180) v = 180; g_silent_fov = v; } + if (in_tint) { + if (strcmp(key, "world") == 0) + g_tint_world_on = atoi(val) != 0; + else if (strcmp(key, "sky") == 0) + g_tint_sky_on = atoi(val) != 0; + else if (strcmp(key, "wr") == 0 || strcmp(key, "wg") == 0 || + strcmp(key, "wb") == 0) { + int v = atoi(val); + if (v < 0) v = 0; + if (v > 255) v = 255; + g_tint_world_rgb[key[1] == 'r' ? 0 : key[1] == 'g' ? 1 : 2] = v; + } else if (strcmp(key, "sr") == 0 || strcmp(key, "sg") == 0 || + strcmp(key, "sb") == 0) { + int v = atoi(val); + if (v < 0) v = 0; + if (v > 255) v = 255; + g_tint_sky_rgb[key[1] == 'r' ? 0 : key[1] == 'g' ? 1 : 2] = v; + } + } if (in_esp) { if (strcmp(key, "neutral") == 0) g_esp_opt[ESP_OPT_SHOW_NEUTRAL] = atoi(val) != 0; @@ -378,6 +412,11 @@ void config_reset_defaults() { g_esp_opt[i] = true; g_item_esp = {true, 0}; g_fullbright = {false, 0}; + g_world_tint = {false, 0}; + g_tint_world_on = true; + g_tint_sky_on = true; + g_tint_world_rgb[0] = g_tint_world_rgb[1] = g_tint_world_rgb[2] = 255; + g_tint_sky_rgb[0] = g_tint_sky_rgb[1] = g_tint_sky_rgb[2] = 255; g_item_health = true; g_item_ammo = true; g_item_armor = true; @@ -495,7 +534,7 @@ 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_fullbright, &g_world_tint}; if (feature < 0 || feature >= FEAT_COUNT) return nullptr; return feats[feature]; @@ -701,6 +740,7 @@ void fullbright_update() { void outer_draw() { poll_keyboard(); fullbright_update(); + world_tint_update(); gui_handle_input(); // HL2H_DRAW_TEST=1 draws a probe rect without needing the GUI open static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr; @@ -721,6 +761,7 @@ void outer_draw() { if (le != last_local) { last_local = le; font_reset = true; + world_tint_invalidate(); } } if (font_reset && g_font) { diff --git a/src/world.cpp b/src/world.cpp new file mode 100644 index 0000000..eeffb99 --- /dev/null +++ b/src/world.cpp @@ -0,0 +1,248 @@ +// world.cpp — world/sky material colour modulation. +// +// Same mechanism Amalgam uses for its "World modulation": walk every material +// in the material system and call IMaterial::ColorModulate on the ones whose +// texture group is the world (or the skybox). This multiplies the material's +// albedo; it does not touch lightmaps, so it is cheap and does not recompile +// shaders. +// +// HL2's materialsystem.so registers "VMaterialSystem080" (the SDK 2013 name +// for the modern layout) and exports CreateInterface, so the interface is +// obtained exactly like VClient017/VGUI_Surface030 elsewhere in this cheat. +// +// Vtable slots are taken from the 2013 SDK headers: +// IMaterialSystem: FirstMaterial=73 NextMaterial=74 InvalidMaterial=75 +// GetMaterial=76 GetNumMaterials=77 +// IMaterial: GetName=0 GetTextureGroupName=1 ColorModulate=28 + +#include "hl2h.h" +#include +#include +#include +#include + +namespace { + +constexpr int MATSYS_FirstMaterial = 73; +constexpr int MATSYS_NextMaterial = 74; +constexpr int MATSYS_InvalidMaterial = 75; +constexpr int MATSYS_GetMaterial = 76; +constexpr int MATSYS_GetNumMaterials = 77; + +constexpr int MAT_GetName = 0; +constexpr int MAT_GetTextureGroupName = 1; +constexpr int MAT_ColorModulate = 28; + +constexpr const char *GROUP_WORLD = "World textures"; +constexpr const char *GROUP_SKYBOX = "SkyBox textures"; + +void *g_matsys = nullptr; +bool g_matsys_tried = false; +bool g_matsys_failed = false; +bool g_matsys_logged = false; + +// Last state handed to the material system. Colour changes are applied when +// this differs from the desired state, so the all-material walk runs only on +// toggles/colour edits/level changes, never every frame. +struct Tint { + bool valid; + int world_r, world_g, world_b; + int sky_r, sky_g, sky_b; +}; +Tint g_applied{false, 255, 255, 255, 255, 255, 255}; + +struct MatCalls { + 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 *); + using GroupFn = const char *(*)(void *); + using ModulateFn = void (*)(void *, float, float, float); +}; + +// Reject obviously bogus strings returned by a mis-mapped vtable slot instead +// of walking thousands of materials with garbage pointers. +bool plausible_name(const char *s) { + if (!s || !*s) + return false; + size_t n = strnlen(s, 512); + if (n == 0 || n >= 512) + return false; + for (size_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)s[i]; + if (c < 0x20 || c == 0x7f) + return false; + } + return true; +} + +std::string find_loaded_module(const char *needle) { + FILE *maps = fopen("/proc/self/maps", "r"); + if (!maps) + return {}; + char line[1024]; + std::string found; + while (fgets(line, sizeof(line), maps)) { + char *p = strchr(line, '/'); + if (!p) + continue; + size_t pl = strlen(p); + while (pl && (p[pl - 1] == '\n' || p[pl - 1] == '\r')) + p[--pl] = 0; + if (!strstr(p, needle) || !strstr(p, ".so")) + continue; + found = p; + if (!strstr(p, "(deleted)")) + break; + } + fclose(maps); + return found; +} + +void *material_system() { + if (g_matsys_tried) + return g_matsys; + g_matsys_tried = true; + + std::string path = find_loaded_module("materialsystem"); + void *h = nullptr; + if (!path.empty()) + h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + // The engine always dlopens the module by name; NOLOAD only attaches to the + // already-mapped copy, so we never spawn a second material system. + if (!h) + h = dlopen("materialsystem.so", RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + if (!h) { + g_matsys_failed = true; + logf("tint: materialsystem.so not loaded"); + return nullptr; + } + auto cf = (CreateInterfaceFn)dlsym(h, "CreateInterface"); + if (!cf) { + g_matsys_failed = true; + logf("tint: materialsystem CreateInterface missing"); + return nullptr; + } + static const char *names[] = {"VMaterialSystem080", "VMaterialSystem082", + "VMaterialSystem081", nullptr}; + for (const char **n = names; *n; n++) { + int ret = 0; + void *ms = cf(*n, &ret); + if (!ms) + continue; + g_matsys = ms; + logf("tint: %s = %p via %s", *n, ms, + path.empty() ? "materialsystem.so" : path.c_str()); + return ms; + } + g_matsys_failed = true; + logf("tint: material system interface not exposed"); + return nullptr; +} + +// Push one colour pair over all materials. Groups that are off get white so a +// previously tinted world is restored. +bool apply_tint(int world_r, int world_g, int world_b, int sky_r, int sky_g, + int sky_b) { + void *ms = material_system(); + if (!ms) + return false; + + auto invalid = vfunc(ms, MATSYS_InvalidMaterial); + auto first = vfunc(ms, MATSYS_FirstMaterial); + auto next = vfunc(ms, MATSYS_NextMaterial); + auto get = vfunc(ms, MATSYS_GetMaterial); + auto count = vfunc(ms, MATSYS_GetNumMaterials); + + int total = count(ms); + if (total <= 0 || total > 200000) { + logf("tint: GetNumMaterials() = %d (unexpected; disabled)", total); + g_matsys_failed = true; + toastf(TOAST_ERROR, "World tint: material system layout mismatch"); + return false; + } + + float wr = world_r / 255.0f, wg = world_g / 255.0f, wb = world_b / 255.0f; + float sr = sky_r / 255.0f, sg = sky_g / 255.0f, sb = sky_b / 255.0f; + + unsigned short inv = invalid(ms); + int seen = 0, world_hits = 0, sky_hits = 0; + for (unsigned short h = first(ms); h != inv; h = next(ms, h)) { + if (++seen > total + 16) { + logf("tint: material walk runaway (%d > %d); stopped", seen, total); + break; + } + void *mat = get(ms, h); + if (!mat) + continue; + auto name_fn = vfunc(mat, MAT_GetName); + auto group_fn = vfunc(mat, MAT_GetTextureGroupName); + auto mod_fn = vfunc(mat, MAT_ColorModulate); + const char *name = name_fn(mat); + const char *group = group_fn(mat); + if (!plausible_name(name) || !plausible_name(group)) + continue; + // The first material's name also validates the IMaterial vtable mapping + // before we poke any colour state. + if (!g_matsys_logged) { + g_matsys_logged = true; + logf("tint: first material \"%s\" group \"%s\"", name, group); + } + bool is_world = strncmp(group, GROUP_WORLD, strlen(GROUP_WORLD)) == 0; + bool is_sky = strncmp(group, GROUP_SKYBOX, strlen(GROUP_SKYBOX)) == 0 || + strstr(name, "sky") != nullptr; + if (is_world && !strstr(name, "sky")) { + mod_fn(mat, wr, wg, wb); + world_hits++; + } else if (is_sky) { + mod_fn(mat, sr, sg, sb); + sky_hits++; + } + } + logf("tint: applied world=%d(%d,%d,%d) sky=%d(%d,%d,%d) of %d materials", + world_hits, world_r, world_g, world_b, sky_hits, sky_r, sky_g, sky_b, + total); + return true; +} + +} // namespace + +void world_tint_invalidate() { g_applied.valid = false; } + +void world_tint_update() { + bool on = feature_active(FEAT_WORLD_TINT); + bool world_on = on && g_tint_world_on; + bool sky_on = on && g_tint_sky_on; + + int wr = world_on ? g_tint_world_rgb[0] : 255; + int wg = world_on ? g_tint_world_rgb[1] : 255; + int wb = world_on ? g_tint_world_rgb[2] : 255; + int sr = sky_on ? g_tint_sky_rgb[0] : 255; + int sg = sky_on ? g_tint_sky_rgb[1] : 255; + int sb = sky_on ? g_tint_sky_rgb[2] : 255; + + bool white = wr == 255 && wg == 255 && wb == 255 && sr == 255 && + sg == 255 && sb == 255; + if (g_applied.valid && g_applied.world_r == wr && g_applied.world_g == wg && + g_applied.world_b == wb && g_applied.sky_r == sr && + g_applied.sky_g == sg && g_applied.sky_b == sb) + return; + // Nothing was ever tinted and nothing is requested: no need to walk the + // material list just to write white over white. + if (!g_applied.valid && white) { + g_applied = {true, wr, wg, wb, sr, sg, sb}; + return; + } + if (g_matsys_failed && !world_on && !sky_on) { + g_applied = {true, wr, wg, wb, sr, sg, sb}; + return; + } + if (!apply_tint(wr, wg, wb, sr, sg, sb)) { + if (g_matsys_failed) + g_applied = {true, wr, wg, wb, sr, sg, sb}; + return; + } + g_applied = {true, wr, wg, wb, sr, sg, sb}; +} -- cgit v1.2.3