From edba65625dd5823705619a2e682a99d6ad91ae8b Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 3 Nov 2026 23:32:16 +0100 Subject: World: add prop tint and fog override - World tint now also colours prop materials (models/props*) with their own colour, covering static props and prop_physics while leaving NPC, player and weapon materials untouched. - New Fog card: fog_override colour/start/end via ClientCmd, optional skybox fog, and a Remove fog mode. Restores the map fog (fog_override 0) when disabled and re-applies after level changes. - Config sections [worldtint] prop/colour keys and [fog]. --- CONTEXT.md | 26 +++++++++++-- src/gui.cpp | 18 +++++++++ src/hl2h.cpp | 17 ++++++--- src/hl2h.h | 14 +++++-- src/menu.cpp | 72 ++++++++++++++++++++++++++++++++--- src/world.cpp | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++------- 6 files changed, 236 insertions(+), 31 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index b48100b..1d365e8 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -316,8 +316,11 @@ vocabulary / decisions. already-loaded `materialsystem.so` and walk every material with `FirstMaterial` / `NextMaterial` / `GetMaterial`. - `IMaterial::ColorModulate(r,g,b)` is applied to materials whose - `GetTextureGroupName()` is `World textures` (sky names excluded) and to - `SkyBox textures` (or names containing `sky`). Lightmaps are not touched. + `GetTextureGroupName()` is `World textures` (sky names excluded), to + `SkyBox textures` (or names containing `sky`), and to prop materials whose + name starts with `models/props` (static props and prop_physics share those + model materials; NPC/player/weapon materials are excluded). Lightmaps are + not touched. - Slot map (2013 SDK layout, registered under `VMaterialSystem080`): `IMaterialSystem` 73/74/75/76/77 = First/Next/Invalid/Get/GetNumMaterials; `IMaterial` 0/1/28 = GetName/GetTextureGroupName/ColorModulate. @@ -327,7 +330,24 @@ vocabulary / decisions. - Validation before any walk: `GetNumMaterials()` must return 1..200000; the first material's name/group must be printable strings. A mismatch logs and toasts instead of poking garbage. -- Saved as `[worldtint] active/bind/world/sky/wr..wb/sr..sb`. +- Saved as `[worldtint] active/bind/world/sky/prop/wr..pb`. +- Particle tint is deliberately not implemented: HL2's old particle manager + draws per-vertex coloured IMesh quads, and SpriteCard/TF2-style particles + take colour from per-particle vertex data, so `IMaterial::ColorModulate` + does nothing for them. It needs a hook in the client particle render path. + +### Fog + +- `src/world.cpp` `fog_update()`, driven by the `fog_override` engine command + family through `IVEngineClient::ClientCmd` (same safe path as fullbright). + No render hooks. +- Enabled: `fog_override 1`, `fog_enable 1`, `fog_color`, `fog_start`, + `fog_end`, plus `fog_enableskybox` / `fog_colorskybox` / + `fog_startskybox` / `fog_endskybox` when skybox fog is on. + "Remove fog" issues `fog_enable 0` instead. +- Disabled: `fog_override 0` restores the map's own fog controller. +- End is clamped above Start. Re-applied on level change via + `fog_invalidate()`. Saved as `[fog] active/bind/r/g/b/start/end/skybox/disable`. ## Logging diff --git a/src/gui.cpp b/src/gui.cpp index 9981f25..aa47868 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -401,6 +401,24 @@ static void build_cards() { add_toggle(tint, "Skybox", "Tint the skybox textures.", &g_tint_sky_on); add_color(tint, "Sky colour", "Click the swatch to open the picker.", g_tint_sky_rgb); + add_toggle(tint, "Props", + "Tint model materials under models/props (map props, static " + "and physics).", + &g_tint_prop_on); + add_color(tint, "Prop colour", "Click the swatch to open the picker.", + g_tint_prop_rgb); + + int fog = add_card(2, &g_fog, "Fog", + "Override the map's fog colour and distances.", true); + add_color(fog, "Colour", "Click the swatch to open the picker.", g_fog_rgb); + add_slider(fog, "Start", "Distance in units where fog starts.", 0, 8000, + &g_fog_start); + add_slider(fog, "End", "Distance in units where fog is fully opaque.", 0, + 20000, &g_fog_end); + add_toggle(fog, "Skybox fog", "Apply the same fog to the 3D skybox.", + &g_fog_skybox); + add_toggle(fog, "Remove fog", + "Disable fog entirely instead of recolouring it.", &g_fog_disable); int spread = add_card(3, &g_spread, "Spread control", "Choose a server shot seed that tightens the cone.", diff --git a/src/hl2h.cpp b/src/hl2h.cpp index dfe4e57..3f1acb4 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -130,17 +130,23 @@ static int g_modified_logged = 0; // ConCommandBase has two dtor slots, then IsCommand, IsFlagSet, AddFlags, // GetName, GetHelpText, IsRegistered, GetDLLIdentifier; ConVar then adds // SetValue(const char*), SetValue(float), SetValue(int) -> slot 11. +bool console_cmd(const char *cmd) { + if (!cmd || !*cmd || !g_iface.engine) + return false; + using CmdFn = void (*)(void *, const char *); + vfunc(g_iface.engine, 7)(g_iface.engine, cmd); + logd("cmd: %s", cmd); + return true; +} + bool cvar_set_int(const char *name, int value) { - if (!name || !g_iface.engine) + if (!name) return false; // Use the stable IVEngineClient ClientCmd slot rather than poking ConVar's // vtable; the installed VEngineCvar004 layout differs enough to crash. char cmd[96]; snprintf(cmd, sizeof(cmd), "%s %d", name, value); - using CmdFn = void (*)(void *, const char *); - vfunc(g_iface.engine, 7)(g_iface.engine, cmd); - logd("cvar: command '%s'", cmd); - return true; + return console_cmd(cmd); } void note_modified(const CUserCmd *cmd, int flags, const float *vel) { @@ -585,6 +591,7 @@ static void do_move(int seq, float frametime) { silentaim_reset_lock(); fullbright_invalidate(); world_tint_invalidate(); + fog_invalidate(); } if (feature_active(FEAT_SPREAD)) { diff --git a/src/hl2h.h b/src/hl2h.h index e90ba91..6f23221 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -179,7 +179,8 @@ enum FeatureId { FEAT_ITEM_ESP = 9, FEAT_FULLBRIGHT = 10, FEAT_WORLD_TINT = 11, - FEAT_COUNT = 12 + FEAT_FOG = 12, + FEAT_COUNT = 13 }; enum EspOption { ESP_OPT_SHOW_NEUTRAL = 0, @@ -212,9 +213,11 @@ 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; -extern bool g_tint_world_on, g_tint_sky_on; -extern int g_tint_world_rgb[3], g_tint_sky_rgb[3]; + g_world_tint, g_fog; +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; +extern int g_fog_rgb[3], g_fog_start, g_fog_end; extern bool g_bhop_autostrafe; extern bool g_jumpbug_assist; extern bool g_spread_no_spread; @@ -249,11 +252,14 @@ void fix_verified_crc(int seq, uint32_t crc); uint32_t usercmd_checksum(const CUserCmd *c); bool find_input_global(); void resolve_netvars(); +bool console_cmd(const char *cmd); bool cvar_set_int(const char *name, int value); void fullbright_update(); void fullbright_invalidate(); void world_tint_update(); void world_tint_invalidate(); +void fog_update(); +void fog_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 6a7ffe5..237502b 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -49,8 +49,16 @@ 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; +bool g_tint_prop_on = true; int g_tint_world_rgb[3] = {255, 255, 255}; int g_tint_sky_rgb[3] = {255, 255, 255}; +int g_tint_prop_rgb[3] = {255, 255, 255}; +FeatureState g_fog{false, 0}; // fog_override colour/distance +bool g_fog_skybox = true; +bool g_fog_disable = false; +int g_fog_rgb[3] = {160, 170, 190}; +int g_fog_start = 200; +int g_fog_end = 3000; bool g_item_health = true; bool g_item_ammo = true; bool g_item_armor = true; @@ -193,11 +201,17 @@ void config_save() { 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", + "prop=%d\nwr=%d\nwg=%d\nwb=%d\nsr=%d\nsg=%d\nsb=%d\n" + "pr=%d\npg=%d\npb=%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]); + (int)g_tint_sky_on, (int)g_tint_prop_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], g_tint_prop_rgb[0], + g_tint_prop_rgb[1], g_tint_prop_rgb[2]); + fprintf(f, "[fog]\nactive=%d\nbind=%d\nr=%d\ng=%d\nb=%d\n" + "start=%d\nend=%d\nskybox=%d\ndisable=%d\n", + g_fog.active, g_fog.bind, g_fog_rgb[0], g_fog_rgb[1], g_fog_rgb[2], + g_fog_start, g_fog_end, (int)g_fog_skybox, (int)g_fog_disable); 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, @@ -232,7 +246,7 @@ static bool config_load() { 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_tint = false; + in_tint = false, in_fog = false; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { @@ -254,6 +268,8 @@ static bool config_load() { cur = &g_fullbright; else if (strstr(line, "[worldtint]")) cur = &g_world_tint; + else if (strstr(line, "[fog]")) + cur = &g_fog; else if (strstr(line, "[aimbot]")) cur = &g_aimbot; else if (strstr(line, "[triggerbot]")) @@ -270,6 +286,7 @@ static bool config_load() { in_spread = (cur == &g_spread); in_items = (cur == &g_item_esp); in_tint = (cur == &g_world_tint); + in_fog = (cur == &g_fog); } else if (cur) { char *eq = strchr(line, '='); if (!eq) @@ -345,6 +362,8 @@ static bool config_load() { g_tint_world_on = atoi(val) != 0; else if (strcmp(key, "sky") == 0) g_tint_sky_on = atoi(val) != 0; + else if (strcmp(key, "prop") == 0) + g_tint_prop_on = atoi(val) != 0; else if (strcmp(key, "wr") == 0 || strcmp(key, "wg") == 0 || strcmp(key, "wb") == 0) { int v = atoi(val); @@ -357,6 +376,35 @@ static bool config_load() { if (v < 0) v = 0; if (v > 255) v = 255; g_tint_sky_rgb[key[1] == 'r' ? 0 : key[1] == 'g' ? 1 : 2] = v; + } else if (strcmp(key, "pr") == 0 || strcmp(key, "pg") == 0 || + strcmp(key, "pb") == 0) { + int v = atoi(val); + if (v < 0) v = 0; + if (v > 255) v = 255; + g_tint_prop_rgb[key[1] == 'r' ? 0 : key[1] == 'g' ? 1 : 2] = v; + } + } + if (in_fog) { + 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_fog_rgb[key[0] == 'r' ? 0 : key[0] == 'g' ? 1 : 2] = v; + } else if (strcmp(key, "start") == 0) { + int v = atoi(val); + if (v < 0) v = 0; + if (v > 8000) v = 8000; + g_fog_start = v; + } else if (strcmp(key, "end") == 0) { + int v = atoi(val); + if (v < 0) v = 0; + if (v > 20000) v = 20000; + g_fog_end = v; + } else if (strcmp(key, "skybox") == 0) { + g_fog_skybox = atoi(val) != 0; + } else if (strcmp(key, "disable") == 0) { + g_fog_disable = atoi(val) != 0; } } if (in_esp) { @@ -415,8 +463,18 @@ void config_reset_defaults() { g_world_tint = {false, 0}; g_tint_world_on = true; g_tint_sky_on = true; + g_tint_prop_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_tint_prop_rgb[0] = g_tint_prop_rgb[1] = g_tint_prop_rgb[2] = 255; + g_fog = {false, 0}; + g_fog_skybox = true; + g_fog_disable = false; + g_fog_rgb[0] = 160; + g_fog_rgb[1] = 170; + g_fog_rgb[2] = 190; + g_fog_start = 200; + g_fog_end = 3000; g_item_health = true; g_item_ammo = true; g_item_armor = true; @@ -534,7 +592,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_world_tint}; + &g_fullbright, &g_world_tint, &g_fog}; if (feature < 0 || feature >= FEAT_COUNT) return nullptr; return feats[feature]; @@ -741,6 +799,7 @@ void outer_draw() { poll_keyboard(); fullbright_update(); world_tint_update(); + fog_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; @@ -762,6 +821,7 @@ void outer_draw() { last_local = le; font_reset = true; world_tint_invalidate(); + fog_invalidate(); } } if (font_reset && g_font) { diff --git a/src/world.cpp b/src/world.cpp index eeffb99..2333829 100644 --- a/src/world.cpp +++ b/src/world.cpp @@ -48,8 +48,16 @@ struct Tint { bool valid; int world_r, world_g, world_b; int sky_r, sky_g, sky_b; + int prop_r, prop_g, prop_b; }; -Tint g_applied{false, 255, 255, 255, 255, 255, 255}; +Tint g_applied{false, 255, 255, 255, 255, 255, 255, 255, 255, 255}; + +// Static props and prop_physics share their model materials; every stock HL2 +// map prop lives under models/props*, which keeps NPC/player/weapon materials +// out of the prop pass. +bool is_prop_material(const char *name) { + return name && strncmp(name, "models/props", 12) == 0; +} struct MatCalls { using FirstFn = unsigned short (*)(void *); @@ -145,7 +153,7 @@ void *material_system() { // 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) { + int sky_b, int prop_r, int prop_g, int prop_b) { void *ms = material_system(); if (!ms) return false; @@ -166,9 +174,10 @@ bool apply_tint(int world_r, int world_g, int world_b, int sky_r, int sky_g, 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; + float pr = prop_r / 255.0f, pg = prop_g / 255.0f, pb = prop_b / 255.0f; unsigned short inv = invalid(ms); - int seen = 0, world_hits = 0, sky_hits = 0; + int seen = 0, world_hits = 0, sky_hits = 0, prop_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); @@ -199,11 +208,15 @@ bool apply_tint(int world_r, int world_g, int world_b, int sky_r, int sky_g, } else if (is_sky) { mod_fn(mat, sr, sg, sb); sky_hits++; + } else if (is_prop_material(name)) { + mod_fn(mat, pr, pg, pb); + prop_hits++; } } - logf("tint: applied world=%d(%d,%d,%d) sky=%d(%d,%d,%d) of %d materials", + logf("tint: applied world=%d(%d,%d,%d) sky=%d(%d,%d,%d) props=%d(%d,%d,%d) " + "of %d materials", world_hits, world_r, world_g, world_b, sky_hits, sky_r, sky_g, sky_b, - total); + prop_hits, prop_r, prop_g, prop_b, total); return true; } @@ -215,6 +228,7 @@ 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; + bool prop_on = on && g_tint_prop_on; int wr = world_on ? g_tint_world_rgb[0] : 255; int wg = world_on ? g_tint_world_rgb[1] : 255; @@ -222,27 +236,107 @@ void world_tint_update() { 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; + int pr = prop_on ? g_tint_prop_rgb[0] : 255; + int pg = prop_on ? g_tint_prop_rgb[1] : 255; + int pb = prop_on ? g_tint_prop_rgb[2] : 255; bool white = wr == 255 && wg == 255 && wb == 255 && sr == 255 && - sg == 255 && sb == 255; + sg == 255 && sb == 255 && pr == 255 && pg == 255 && pb == 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) + g_applied.sky_g == sg && g_applied.sky_b == sb && + g_applied.prop_r == pr && g_applied.prop_g == pg && + g_applied.prop_b == pb) 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}; + g_applied = {true, wr, wg, wb, sr, sg, sb, pr, pg, pb}; return; } - if (g_matsys_failed && !world_on && !sky_on) { - g_applied = {true, wr, wg, wb, sr, sg, sb}; + if (g_matsys_failed && !world_on && !sky_on && !prop_on) { + g_applied = {true, wr, wg, wb, sr, sg, sb, pr, pg, pb}; return; } - if (!apply_tint(wr, wg, wb, sr, sg, sb)) { + if (!apply_tint(wr, wg, wb, sr, sg, sb, pr, pg, pb)) { if (g_matsys_failed) - g_applied = {true, wr, wg, wb, sr, sg, sb}; + g_applied = {true, wr, wg, wb, sr, sg, sb, pr, pg, pb}; return; } - g_applied = {true, wr, wg, wb, sr, sg, sb}; + g_applied = {true, wr, wg, wb, sr, sg, sb, pr, pg, pb}; +} + +// --------------------------------------------------------------------------- +// Fog override (engine console commands; no render hooks) +// --------------------------------------------------------------------------- + +namespace { + +struct FogState { + bool valid; + bool on; + bool disable; + int r, g, b; + int start, end; + bool skybox; +}; +FogState g_fog_applied{false, false, false, 0, 0, 0, 0, 0, false}; + +} // namespace + +void fog_invalidate() { g_fog_applied.valid = false; } + +void fog_update() { + bool on = feature_active(FEAT_FOG); + bool disable = on && g_fog_disable; + int r = g_fog_rgb[0], g = g_fog_rgb[1], b = g_fog_rgb[2]; + int start = g_fog_start; + int end = g_fog_end; + if (end <= start) + end = start + 1; + bool skybox = on && g_fog_skybox; + + if (g_fog_applied.valid && g_fog_applied.on == on && + g_fog_applied.disable == disable && g_fog_applied.r == r && + g_fog_applied.g == g && g_fog_applied.b == b && + g_fog_applied.start == start && g_fog_applied.end == end && + g_fog_applied.skybox == skybox) + return; + + if (!on) { + if (g_fog_applied.valid && g_fog_applied.on) + console_cmd("fog_override 0"); + g_fog_applied = {true, false, false, 0, 0, 0, 0, 0, false}; + return; + } + + console_cmd("fog_override 1"); + char cmd[128]; + if (disable) { + console_cmd("fog_enable 0"); + if (skybox) + console_cmd("fog_enableskybox 0"); + } else { + console_cmd("fog_enable 1"); + snprintf(cmd, sizeof(cmd), "fog_color %d %d %d", r, g, b); + console_cmd(cmd); + snprintf(cmd, sizeof(cmd), "fog_start %d", start); + console_cmd(cmd); + snprintf(cmd, sizeof(cmd), "fog_end %d", end); + console_cmd(cmd); + if (skybox) { + console_cmd("fog_enableskybox 1"); + snprintf(cmd, sizeof(cmd), "fog_colorskybox %d %d %d", r, g, b); + console_cmd(cmd); + snprintf(cmd, sizeof(cmd), "fog_startskybox %d", start); + console_cmd(cmd); + snprintf(cmd, sizeof(cmd), "fog_endskybox %d", end); + console_cmd(cmd); + } else { + console_cmd("fog_enableskybox 0"); + } + } + g_fog_applied = {true, on, disable, r, g, b, start, end, skybox}; + logf("fog: %s colour=(%d,%d,%d) start=%d end=%d skybox=%d", disable ? "off" + : "on", r, g, b, start, end, (int)skybox); } -- cgit v1.2.3