// 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}; }