// 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 #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; int prop_r, prop_g, prop_b; }; 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) { // Static props often use the brush-style "props/..." materials (e.g. // props/woodcrate001a, LightmappedGeneric), while dynamic props use // models/props_junk/... . return name && (strncmp(name, "models/props", 12) == 0 || strncmp(name, "props/", 6) == 0); } 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, int prop_r, int prop_g, int prop_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; (void)prop_r; (void)prop_g; (void)prop_b; unsigned short inv = invalid(ms); 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); 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++; } else if (is_prop_material(name)) { // Props are tinted through IStudioRender (studio.cpp); material // modulation is overridden per draw by the engine, so leave prop // materials untouched here. prop_hits++; } } 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, prop_hits, prop_r, prop_g, prop_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; 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; 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; 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 && 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.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, pr, pg, pb}; return; } 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, pr, pg, pb)) { if (g_matsys_failed) g_applied = {true, wr, wg, wb, sr, sg, sb, pr, pg, pb}; return; } 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); } // --------------------------------------------------------------------------- // Skybox image (skybox.png from the config directory) // --------------------------------------------------------------------------- namespace { // The game modules export their own (older) libpng symbols, so loading the // system libpng16 makes its internal calls bind to the game's libpng with a // different png_struct layout (crash in png_safe_warning). Decode the PNG // ourselves with zlib instead: no symbol interposition, no extra dependency. using UncompressFn = int (*)(unsigned char *, unsigned long *, const unsigned char *, unsigned long); UncompressFn zlib_uncompress() { static void *lib = nullptr; static bool tried = false; static UncompressFn fn = nullptr; if (!tried) { tried = true; lib = dlopen("libz.so.1", RTLD_LAZY | RTLD_GLOBAL); if (!lib) lib = dlopen("libz.so", RTLD_LAZY | RTLD_GLOBAL); if (lib) fn = (UncompressFn)dlsym(lib, "uncompress"); } return fn; } uint32_t be32(const unsigned char *p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; } int paeth(int a, int b, int c) { int p = a + b - c; int pa = p > a ? p - a : a - p; int pb = p > b ? p - b : b - p; int pc = p > c ? p - c : c - p; if (pa <= pb && pa <= pc) return a; return pb <= pc ? b : c; } bool load_png_rgba(const char *path, std::vector &pixels, int &out_w, int &out_h) { FILE *f = fopen(path, "rb"); if (!f) { logf("skybox: cannot open %s", path); return false; } fseek(f, 0, SEEK_END); long fsz = ftell(f); fseek(f, 0, SEEK_SET); if (fsz <= 0 || fsz > 32 * 1024 * 1024) { fclose(f); logf("skybox: %s has an unsupported file size", path); return false; } std::vector file((size_t)fsz); size_t got = fread(file.data(), 1, file.size(), f); fclose(f); if (got != file.size()) { logf("skybox: short read on %s", path); return false; } static const unsigned char sig[8] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}; if (file.size() < 8 || memcmp(file.data(), sig, 8) != 0) { logf("skybox: %s is not a PNG", path); return false; } uint32_t w = 0, h = 0; int depth = 0, ctype = -1, comp = 0, filt = 0, inter = 0; bool have_ihdr = false; std::vector idat, plte, trns; size_t pos = 8; while (pos + 12 <= file.size()) { uint32_t len = be32(file.data() + pos); pos += 4; const unsigned char *type = file.data() + pos; pos += 4; if ((size_t)len > file.size() - pos - 4) break; const unsigned char *data = file.data() + pos; if (memcmp(type, "IHDR", 4) == 0 && len >= 13) { w = be32(data); h = be32(data + 4); depth = data[8]; ctype = data[9]; comp = data[10]; filt = data[11]; inter = data[12]; have_ihdr = true; } else if (memcmp(type, "PLTE", 4) == 0) { plte.assign(data, data + len); } else if (memcmp(type, "tRNS", 4) == 0) { trns.assign(data, data + len); } else if (memcmp(type, "IDAT", 4) == 0) { idat.insert(idat.end(), data, data + len); } else if (memcmp(type, "IEND", 4) == 0) { break; } pos += (size_t)len + 4; // data + CRC } if (!have_ihdr || w == 0 || h == 0 || w > 8192 || h > 8192) { logf("skybox: %s has an invalid PNG header", path); return false; } if (depth != 8 || comp != 0 || filt != 0 || inter != 0) { logf("skybox: %s: only 8-bit non-interlaced PNGs are supported", path); return false; } int channels = ctype == 0 ? 1 : ctype == 2 ? 3 : ctype == 3 ? 1 : ctype == 4 ? 2 : ctype == 6 ? 4 : 0; if (channels == 0 || idat.empty()) { logf("skybox: %s: unsupported PNG colour type %d", path, ctype); return false; } if (ctype == 3 && plte.size() < 3) { logf("skybox: %s: paletted PNG without a palette", path); return false; } size_t stride = (size_t)w * (size_t)channels; size_t raw_size = (stride + 1) * (size_t)h; std::vector raw(raw_size); unsigned long dest_len = (unsigned long)raw_size; UncompressFn uncompress_fn = zlib_uncompress(); if (!uncompress_fn) { logf("skybox: zlib unavailable"); return false; } if (uncompress_fn(raw.data(), &dest_len, idat.data(), (unsigned long)idat.size()) != 0 || dest_len < raw_size) { logf("skybox: %s: zlib could not inflate the image data", path); return false; } std::vector scan(stride * (size_t)h); int bpp = channels; for (uint32_t y = 0; y < h; y++) { const unsigned char *src = raw.data() + (size_t)y * (stride + 1); unsigned char ft = src[0]; const unsigned char *in = src + 1; unsigned char *out = scan.data() + (size_t)y * stride; const unsigned char *prev = y ? scan.data() + (size_t)(y - 1) * stride : nullptr; for (size_t x = 0; x < stride; x++) { int a = x >= (size_t)bpp ? out[x - (size_t)bpp] : 0; int b = prev ? prev[x] : 0; int c = (prev && x >= (size_t)bpp) ? prev[x - (size_t)bpp] : 0; int v = in[x]; switch (ft) { case 0: break; case 1: v += a; break; case 2: v += b; break; case 3: v += (a + b) >> 1; break; case 4: v += paeth(a, b, c); break; default: logf("skybox: %s: bad PNG row filter %d", path, (int)ft); return false; } out[x] = (unsigned char)(v & 0xff); } } pixels.resize((size_t)w * (size_t)h * 4); for (uint32_t y = 0; y < h; y++) { const unsigned char *in = scan.data() + (size_t)y * stride; unsigned char *o = pixels.data() + (size_t)y * (size_t)w * 4; for (uint32_t x = 0; x < w; x++) { switch (ctype) { case 6: o[0] = in[0]; o[1] = in[1]; o[2] = in[2]; o[3] = in[3]; in += 4; break; case 2: o[0] = in[0]; o[1] = in[1]; o[2] = in[2]; o[3] = 255; in += 3; break; case 0: o[0] = o[1] = o[2] = in[0]; o[3] = 255; in += 1; break; case 4: o[0] = o[1] = o[2] = in[0]; o[3] = in[1]; in += 2; break; case 3: { unsigned idx = in[0]; in += 1; if ((size_t)idx * 3 + 2 < plte.size()) { o[0] = plte[(size_t)idx * 3]; o[1] = plte[(size_t)idx * 3 + 1]; o[2] = plte[(size_t)idx * 3 + 2]; o[3] = (size_t)idx < trns.size() ? trns[idx] : 255; } else { o[0] = o[1] = o[2] = 0; o[3] = 255; } break; } } o += 4; } } // The material system rejects textures whose dimensions are not multiples of // 4 ("Image dimensions must be multiple of 4!" / "LoadTextureFromVTF: VTF // dimensions do not match texture"). Scale to the next power of two; that is // also what the old skybox code expects. { int tw = 1, th = 1; while (tw < (int)w) tw <<= 1; while (th < (int)h) th <<= 1; if (tw > 4096) tw = 4096; if (th > 4096) th = 4096; if (tw != (int)w || th != (int)h) { std::vector scaled((size_t)tw * (size_t)th * 4); for (int y = 0; y < th; y++) { float sy = ((float)y + 0.5f) * (float)h / (float)th - 0.5f; int y0 = (int)sy; float fy = sy - (float)y0; if (y0 < 0) { y0 = 0; fy = 0.0f; } int y1 = y0 + 1 < (int)h ? y0 + 1 : y0; for (int x = 0; x < tw; x++) { float sx = ((float)x + 0.5f) * (float)w / (float)tw - 0.5f; int x0 = (int)sx; float fx = sx - (float)x0; if (x0 < 0) { x0 = 0; fx = 0.0f; } int x1 = x0 + 1 < (int)w ? x0 + 1 : x0; const unsigned char *p00 = pixels.data() + ((size_t)y0 * (size_t)w + (size_t)x0) * 4; const unsigned char *p10 = pixels.data() + ((size_t)y0 * (size_t)w + (size_t)x1) * 4; const unsigned char *p01 = pixels.data() + ((size_t)y1 * (size_t)w + (size_t)x0) * 4; const unsigned char *p11 = pixels.data() + ((size_t)y1 * (size_t)w + (size_t)x1) * 4; unsigned char *o = scaled.data() + ((size_t)y * (size_t)tw + (size_t)x) * 4; for (int c = 0; c < 4; c++) { float v = (1.0f - fy) * ((1.0f - fx) * p00[c] + fx * p10[c]) + fy * ((1.0f - fx) * p01[c] + fx * p11[c]); o[c] = (unsigned char)(v + 0.5f); } } } logf("skybox: resized %dx%d -> %dx%d for the material system", (int)w, (int)h, tw, th); pixels.swap(scaled); w = (uint32_t)tw; h = (uint32_t)th; } } // The material system's procedural upload reaches the sampler with red and // blue exchanged (the sky shader sees the blue background as orange). Swap // them here so the sampled colours match the PNG. for (size_t i = 0; i + 3 < pixels.size(); i += 4) { unsigned char t = pixels[i]; pixels[i] = pixels[i + 2]; pixels[i + 2] = t; } out_w = (int)w; out_h = (int)h; return true; } std::vector g_sky_pixels; int g_sky_px_w = 0, g_sky_px_h = 0; // ITextureRegenerator has two virtuals: RegenerateTextureBits then Release. // This build has no IMaterialSystem::CreateTextureFromBits (its interface stops // at GetDisplayDeviceName), so the pixels are uploaded through a procedural // texture + regenerator instead. class SkyRegenerator { public: virtual void RegenerateTextureBits(void *texture, void *vtf, void *rect) { (void)texture; (void)rect; static bool entered = false; if (!entered) { entered = true; logf("skybox: regenerator entered (vtf=%p, %zu pixels)", vtf, g_sky_pixels.size()); } if (!vtf || g_sky_pixels.empty()) return; using IntFn = int (*)(void *); using PitchFn = int (*)(void *, int); using DataFn = unsigned char *(*)(void *, int, int, int); // IVTFTexture vtable: dtor takes 2 slots, then Init, so Width=15, // Height=16, RowSizeInBytes=19, ImageData(frame,face,mip)=39. int w = vfunc(vtf, 15)(vtf); // IVTFTexture::Width int h = vfunc(vtf, 16)(vtf); // IVTFTexture::Height if (w <= 0 || h <= 0) return; int pitch = vfunc(vtf, 19)(vtf, 0); // RowSizeInBytes(0) unsigned char *dst = vfunc(vtf, 39)(vtf, 0, 0, 0); // ImageData static bool logged_regen = false; if (!logged_regen) { logged_regen = true; logf("skybox: regenerator %dx%d pitch=%d dst=%p first=%02x%02x%02x%02x", w, h, pitch, dst, g_sky_pixels[0], g_sky_pixels[1], g_sky_pixels[2], g_sky_pixels[3]); } if (!dst || pitch <= 0) return; int rows = h < g_sky_px_h ? h : g_sky_px_h; int src_stride = g_sky_px_w * 4; int copy = w * 4; if (copy > src_stride) copy = src_stride; for (int y = 0; y < rows; y++) { const unsigned char *src = g_sky_pixels.data() + (size_t)y * (size_t)src_stride; memcpy(dst + (size_t)y * (size_t)pitch, src, (size_t)copy); } } virtual void Release() {} }; SkyRegenerator g_sky_regenerator; void *g_sky_tex = nullptr; bool g_sky_tex_tried = false; bool g_sky_applied = false; bool g_sky_compensated = false; bool g_sky_reload = false; double g_sky_retry_at = 0.0; bool g_sky_fail_logged = false; struct SkyOrig { std::string material; std::string var; std::string value; // fallback if the original texture pointer is gone void *texture; }; std::vector g_sky_orig; double now_sec() { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; } bool is_sky_material(const char *name, const char *group) { if (!plausible_name(name) || !plausible_name(group)) return false; return strncmp(group, GROUP_SKYBOX, strlen(GROUP_SKYBOX)) == 0 || strncmp(name, "skybox/", 7) == 0; } void record_orig(const char *material, const char *var, const char *value, void *texture) { if (!material || !var) return; for (auto &r : g_sky_orig) { if (r.material == material && r.var == var) return; } g_sky_orig.push_back( {material, var, value ? value : "", texture}); } bool sky_uses_hdr_shader(); int apply_sky_texture() { void *ms = material_system(); if (!ms || !g_sky_tex) return 0; 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) return 0; // HDR sky materials use the "sky"/Sky_HDR_DX9 shader, which treats the // compressed texture as rgb *= alpha, scales by 8, then converts // linear->sRGB. Cancel the 8x with alpha = 1/8 and feed linear values so // the final conversion restores the original sRGB image. Decide this here, // where the materials are definitely loaded, and re-upload if needed. bool hdr = sky_uses_hdr_shader(); if (hdr && !g_sky_compensated && !g_sky_pixels.empty()) { for (size_t i = 0; i + 3 < g_sky_pixels.size(); i += 4) { for (int c = 0; c < 3; c++) { float v = (float)g_sky_pixels[i + (size_t)c] / 255.0f; float lin = (v <= 0.04045f) ? v / 12.92f : powf((v + 0.055f) / 1.055f, 2.4f); g_sky_pixels[i + (size_t)c] = (unsigned char)(lin * 255.0f + 0.5f); } g_sky_pixels[i + 3] = 32; // 1/8 } using SetRegenFn = void (*)(void *, void *); using DownloadFn = void (*)(void *, void *, int); vfunc(g_sky_tex, 12)(g_sky_tex, &g_sky_regenerator); vfunc(g_sky_tex, 13)(g_sky_tex, nullptr, 0); g_sky_compensated = true; logf("skybox: HDR sky shader, using linear colours with 1/8 alpha"); } unsigned short inv = invalid(ms); int seen = 0, hits = 0; for (unsigned short h = first(ms); h != inv; h = next(ms, h)) { if (++seen > total + 16) break; void *mat = get(ms, h); if (!mat) continue; const char *name = vfunc(mat, MAT_GetName)(mat); const char *group = vfunc(mat, MAT_GetTextureGroupName)(mat); if (!is_sky_material(name, group)) continue; using FindVarFn = void *(*)(void *, const char *, bool *, bool); using GetStrFn = const char *(*)(void *); using SetTexFn = void (*)(void *, void *); auto find_var = vfunc(mat, 11); // IMaterial::FindVar using GetTexFn = void *(*)(void *); using ShaderFn = const char *(*)(void *); static const char *tex_vars[] = {"$basetexture", "$hdrbasetexture", "$hdrcompressedtexture", "$hdrcompressedtexture0", "$hdrcompressedtexture1", "$hdrcompressedtexture2", nullptr}; for (int vi = 0; tex_vars[vi]; vi++) { bool found = false; void *var = find_var(mat, tex_vars[vi], &found, false); if (!found || !var) continue; auto vget = vfunc(var, 6); // GetStringValue auto vgettex = vfunc(var, 0); // GetTextureValue auto vset = vfunc(var, 14); // SetTextureValue record_orig(name, tex_vars[vi], vget(var), vgettex(var)); vset(var, g_sky_tex); if (vi == 0) hits++; } static bool logged_shader = false; if (!logged_shader) { logged_shader = true; const char *shader = vfunc(mat, 49)(mat); // GetShaderName logf("skybox: first sky material \"%s\" shader \"%s\"", name, shader ? shader : "?"); } } return hits; } void restore_sky_texture() { if (g_sky_orig.empty()) { g_sky_applied = false; return; } void *ms = material_system(); if (!ms) { g_sky_orig.clear(); g_sky_applied = false; return; } 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) { g_sky_orig.clear(); g_sky_applied = false; return; } using FindVarFn = void *(*)(void *, const char *, bool *, bool); using SetStrFn = void (*)(void *, const char *); using SetTexFn = void (*)(void *, void *); unsigned short inv = invalid(ms); int seen = 0; for (unsigned short h = first(ms); h != inv; h = next(ms, h)) { if (++seen > total + 16) break; void *mat = get(ms, h); if (!mat) continue; const char *name = vfunc(mat, MAT_GetName)(mat); const char *group = vfunc(mat, MAT_GetTextureGroupName)(mat); if (!is_sky_material(name, group)) continue; for (auto &r : g_sky_orig) { if (r.material != name) continue; bool found = false; void *var = vfunc(mat, 11)(mat, r.var.c_str(), &found, false); if (!found || !var) continue; if (r.texture) vfunc(var, 14)(var, r.texture); // back to the map texture else if (!r.value.empty()) vfunc(var, 5)(var, r.value.c_str()); } } g_sky_orig.clear(); g_sky_applied = false; } bool sky_uses_hdr_shader() { 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) return false; using ShaderFn = const char *(*)(void *); unsigned short inv = invalid(ms); int seen = 0; for (unsigned short h = first(ms); h != inv; h = next(ms, h)) { if (++seen > total + 16) break; void *mat = get(ms, h); if (!mat) continue; const char *name = vfunc(mat, MAT_GetName)(mat); const char *group = vfunc(mat, MAT_GetTextureGroupName)(mat); if (!is_sky_material(name, group)) continue; const char *shader = vfunc(mat, 49)(mat); // The shipped shader is registered as "Sky_HDR_DX9" (older SDK name // "sky"); match both and stay case-insensitive about the prefix. if (shader && (strstr(shader, "Sky_HDR") != nullptr || strstr(shader, "sky_hdr") != nullptr || strcmp(shader, "sky") == 0)) return true; } return false; } void load_sky_texture() { g_sky_tex_tried = true; void *ms = material_system(); if (!ms) { logf("skybox: material system unavailable"); return; } std::vector pixels; int w = 0, h = 0; const char *path = config_file_cstr("skybox.png"); if (!load_png_rgba(path, pixels, w, h)) { if (!g_sky_fail_logged) { g_sky_fail_logged = true; toastf(TOAST_ERROR, "Skybox image missing or unreadable: %s", path); } return; } g_sky_pixels.swap(pixels); g_sky_px_w = w; g_sky_px_h = h; // The procedural texture keeps its own copy through the regenerator, which // is called on Download() and again on device restore. using CreateProcFn = void *(*)(void *, const char *, const char *, int, int, int, int); g_sky_tex = vfunc(ms, 81)( ms, "hl2hake/skybox", GROUP_SKYBOX, w, h, 0 /* RGBA8888 */, 0x10C /* CLAMPS|CLAMPT|NOMIP */); if (!g_sky_tex) { logf("skybox: CreateProceduralTexture failed for %dx%d", w, h); return; } using SetRegenFn = void (*)(void *, void *); using DownloadFn = void (*)(void *, void *, int); vfunc(g_sky_tex, 12)(g_sky_tex, &g_sky_regenerator); vfunc(g_sky_tex, 13)(g_sky_tex, nullptr, 0); g_sky_compensated = false; g_sky_fail_logged = false; logf("skybox: image %s (%dx%d) uploaded", path, w, h); } } // namespace void skybox_invalidate() { // Level change: re-apply on the new map. Keep the captured originals: with // the same skybox the same material objects (still showing our texture) are // reused, and the saved texture pointers are what lets us restore them. g_sky_applied = false; g_sky_retry_at = 0.0; } void skybox_reload_image() { restore_sky_texture(); g_sky_tex = nullptr; g_sky_tex_tried = false; g_sky_reload = true; g_sky_retry_at = 0.0; g_sky_fail_logged = false; } void skybox_update() { bool on = feature_active(FEAT_SKYBOX); if (!on) { if (g_sky_applied || !g_sky_orig.empty()) restore_sky_texture(); return; } if (g_sky_reload) { g_sky_reload = false; restore_sky_texture(); } if (g_sky_applied) return; double now = now_sec(); if (now < g_sky_retry_at) return; if (!g_sky_tex && !g_sky_tex_tried) load_sky_texture(); if (!g_sky_tex) { // The sky materials may not be loaded yet (menu, level transition). Try // again in a moment; the PNG itself is only re-read on Reload. g_sky_retry_at = now + 1.0; return; } int hits = apply_sky_texture(); if (hits > 0) { g_sky_applied = true; logf("skybox: image applied to %d sky materials", hits); } else { 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); const char *candidates[] = {name, "models/debug/debugwhite2", "models/debug/debugwhite3", "vgui/white", "debug/debugtranslucentsinglecolor", "debug/debugwireframevertexcolorignorez", nullptr}; 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) { for (int c = 0; candidates[c]; c++) if (strcmp(n, candidates[c]) == 0) { logf("chams: material %s = %p", n, mat); return mat; } } } h = next(ms, h); } } // Not resident yet: let the material system load it. using FindFn = void *(*)(void *, const char *, const char *, bool, const char *); static const char *groups[] = {"Model textures", "Other textures", nullptr}; for (int c = 0; candidates[c]; c++) { for (int g = 0; groups[g]; g++) { void *mat = vfunc(ms, 71)(ms, candidates[c], groups[g], false, nullptr); if (!mat) { logf("chams: FindMaterial(%s, %s) -> null", candidates[c], groups[g]); continue; } NameFn mat_name = (NameFn)vfunc(mat, 0); const char *n = mat_name(mat); logf("chams: FindMaterial(%s, %s) -> %p name=%s", candidates[c], groups[g], mat, n ? n : "?"); if (n && strstr(n, "debugwhite")) return mat; } } return nullptr; }