From 9c4455af43da34127864b09ad2a3d3e996c9dce1 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 3 Nov 2026 23:58:59 +0100 Subject: World: replace skybox changer with a PNG skybox image sv_skyname did not take effect at runtime in this build, so the Skybox card now replaces the skybox materials' textures with /skybox.png. The PNG is decoded through libpng16's simplified API (dlopen'ed, so no new link dependency) and uploaded with IMaterialSystem::CreateTextureFromBits as an RGBA8888 texture. IMaterialVar::SetTextureValue assigns it to $basetexture and the HDR variants for every material in the SkyBox textures group (all six faces get the same image). Original textures are recorded per material/var (texture pointer plus string fallback) and restored when the feature is off. Records survive level changes so a reused sky material is still restorable; the override is re-applied to a new map automatically. The card shows the image path and has a Reload image button; a missing/unreadable PNG raises an error toast. --- src/world.cpp | 342 +++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 243 insertions(+), 99 deletions(-) (limited to 'src/world.cpp') diff --git a/src/world.cpp b/src/world.cpp index 73738db..cf85795 100644 --- a/src/world.cpp +++ b/src/world.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include namespace { @@ -342,66 +343,116 @@ void fog_update() { } // --------------------------------------------------------------------------- -// Skybox changer (sv_skyname; same mechanism as the skybox_swapper entity) +// Skybox image (skybox.png from the config directory) // --------------------------------------------------------------------------- namespace { -// HL2's skybox set. Index 0 means "do not force a sky"; the rest are the -// base names the engine expands to skybox/{rt,bk,lf,ft,up,dn}. -const char *const kSkyboxLabels[] = { - "Map default", "sky_day01_01", "sky_day01_04", "sky_day01_05", - "sky_day01_06", "sky_day01_07", "sky_day01_08", "sky_day01_09", - "sky_day02_01", "sky_day02_02", "sky_day02_03", "sky_day02_04", - "sky_day02_05", "sky_day02_06", "sky_day02_07", "sky_day02_09", - "sky_day02_10", "sky_day03_01", "sky_day03_02", "sky_day03_03", - "sky_day03_04", "sky_day03_05", "sky_day03_06", "sky_day03_06b", - "sky_day03_06c", "sky_wasteland02", "sky_borealis01", "sky_fog_002", - "sky_fake_white"}; - -std::string g_sky_original; // map's own sky, captured before overriding -bool g_sky_original_valid = false; -std::string g_sky_active; // value we forced, empty when not forcing - -// "skybox/sky_day01_01_hdrrt" -> "sky_day01_01" -bool parse_sky_material(const char *mat, char *out, size_t outn) { - if (!mat) +struct PngImage { + void *opaque; + uint32_t version, width, height, format, flags, colormap_entries, + warning_or_error; + char message[64]; +}; + +bool load_png_rgba(const char *path, std::vector &pixels, + int &w, int &h) { + static void *lib = nullptr; + static bool tried = false; + if (!tried) { + tried = true; + lib = dlopen("libpng16.so.16", RTLD_LAZY | RTLD_GLOBAL); + if (!lib) + lib = dlopen("libpng16.so", RTLD_LAZY | RTLD_GLOBAL); + } + if (!lib) { + logf("skybox: libpng16 not available"); return false; - const char *n = mat; - if (strncmp(n, "skybox/", 7) == 0) - n += 7; - size_t len = strlen(n); - if (len < 4) + } + using BeginFn = int (*)(PngImage *, const char *); + using FinishFn = int (*)(PngImage *, void *, void *, int32_t, void *); + using FreeFn = void (*)(PngImage *); + static BeginFn begin = + (BeginFn)dlsym(lib, "png_image_begin_read_from_file"); + static FinishFn finish = (FinishFn)dlsym(lib, "png_image_finish_read"); + static FreeFn free_img = (FreeFn)dlsym(lib, "png_image_free"); + if (!begin || !finish || !free_img) { + logf("skybox: libpng16 simplified API missing"); return false; - static const char *faces[] = {"rt", "bk", "lf", "ft", "up", "dn", nullptr}; - const char *suf = n + len - 2; - bool face = false; - for (int i = 0; faces[i]; i++) { - if (strcmp(suf, faces[i]) == 0) { - face = true; - break; - } } - if (!face) + + PngImage img{}; + img.version = 1; // PNG_IMAGE_VERSION + img.format = 0x03; // PNG_FORMAT_RGBA + if (!begin(&img, path)) { + logf("skybox: cannot open %s (%s)", path, + img.message[0] ? img.message : "read error"); return false; - size_t blen = len - 2; - if (blen > 4 && (strncmp(n + blen - 4, "_hdr", 4) == 0 || - strncmp(n + blen - 4, "_ldr", 4) == 0)) - blen -= 4; - if (blen == 0 || blen >= outn) + } + if (img.width == 0 || img.height == 0 || img.width > 8192 || + img.height > 8192) { + logf("skybox: %s has unsupported size %ux%u", path, img.width, img.height); + free_img(&img); + return false; + } + pixels.assign((size_t)img.width * img.height * 4, 255); + if (!finish(&img, nullptr, pixels.data(), 0, nullptr)) { + logf("skybox: cannot decode %s (%s)", path, + img.message[0] ? img.message : "decode error"); + pixels.clear(); + free_img(&img); return false; - memcpy(out, n, blen); - out[blen] = 0; + } + w = (int)img.width; + h = (int)img.height; + free_img(&img); return true; } -void capture_map_sky() { - if (g_sky_original_valid) +void *g_sky_tex = nullptr; +bool g_sky_tex_tried = false; +bool g_sky_applied = 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; - g_sky_original_valid = true; // one attempt per map, even on failure + for (auto &r : g_sky_orig) { + if (r.material == material && r.var == var) + return; + } + g_sky_orig.push_back( + {material, var, value ? value : "", texture}); +} + +int apply_sky_texture() { void *ms = material_system(); - if (!ms) - return; + 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); @@ -409,10 +460,74 @@ void capture_map_sky() { auto count = vfunc(ms, MATSYS_GetNumMaterials); int total = count(ms); if (total <= 0 || total > 200000) + return 0; + 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 + + static const char *vars[] = {"$basetexture", "$hdrbasetexture", + "$hdrcompressedtexture", nullptr}; + for (int vi = 0; vars[vi]; vi++) { + bool found = false; + void *var = find_var(mat, vars[vi], &found, false); + if (!found || !var) + continue; + using GetTexFn = void *(*)(void *); + auto vget = vfunc(var, 6); // GetStringValue + auto vgettex = vfunc(var, 0); // GetTextureValue + auto vset = vfunc(var, 14); // SetTextureValue + const char *orig = vget(var); + void *orig_tex = vgettex(var); + record_orig(name, vars[vi], orig, orig_tex); + vset(var, g_sky_tex); + if (vi == 0) + hits++; + } + } + 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; - std::string last; for (unsigned short h = first(ms); h != inv; h = next(ms, h)) { if (++seen > total + 16) break; @@ -422,73 +537,102 @@ void capture_map_sky() { const char *name = vfunc(mat, MAT_GetName)(mat); const char *group = vfunc(mat, MAT_GetTextureGroupName)(mat); - if (!plausible_name(name) || !plausible_name(group)) + if (!is_sky_material(name, group)) continue; - if (strncmp(group, GROUP_SKYBOX, strlen(GROUP_SKYBOX)) != 0) - continue; - char base[128]; - if (parse_sky_material(name, base, sizeof(base))) - last = base; // newest loaded sky materials win after a level change + 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; +} + +void load_sky_texture() { + g_sky_tex_tried = true; + void *ms = material_system(); + if (!ms) { + logf("skybox: material system unavailable"); + return; } - if (last.empty()) { - logd("skybox: could not capture the map sky"); + 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; + } + using CreateTexFn = void *(*)(void *, int, int, int, int, int, + unsigned char *); + g_sky_tex = vfunc(ms, 134)(ms, w, h, 1, 0 /* RGBA8888 */, + (int)pixels.size(), pixels.data()); + if (!g_sky_tex) { + logf("skybox: CreateTextureFromBits failed for %dx%d", w, h); return; } - g_sky_original = last; - logf("skybox: map sky is %s", g_sky_original.c_str()); + g_sky_fail_logged = false; + logf("skybox: image %s (%dx%d) loaded", path, w, h); } } // namespace -int skybox_option_count() { - return (int)(sizeof(kSkyboxLabels) / sizeof(kSkyboxLabels[0])); -} - -const char *skybox_option_name(int i) { - int n = skybox_option_count(); - return (i >= 0 && i < n) ? kSkyboxLabels[i] : kSkyboxLabels[0]; +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; } -const char *const *skybox_options() { return kSkyboxLabels; } - -void skybox_invalidate() { - // Level change: the engine sets sv_skyname from worldspawn again, so there is - // nothing to restore. Forget the captured base and let it recapture. - g_sky_original.clear(); - g_sky_original_valid = false; - g_sky_active.clear(); +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); - int choice = g_skybox_choice; - int n = skybox_option_count(); - if (choice < 0 || choice >= n) - choice = 0; - - if (!on || choice == 0) { - if (g_sky_active.empty()) - return; - if (g_sky_original_valid && !g_sky_original.empty() && - g_sky_original != g_sky_active) { - char cmd[160]; - snprintf(cmd, sizeof(cmd), "sv_skyname %s", g_sky_original.c_str()); - console_cmd(cmd); - logf("skybox: restored %s", g_sky_original.c_str()); - world_tint_invalidate(); // the new sky materials need the sky tint - } - g_sky_active.clear(); + if (!on) { + if (g_sky_applied || !g_sky_orig.empty()) + restore_sky_texture(); return; } - - capture_map_sky(); // before the first override of this map - const char *want = skybox_option_name(choice); - if (g_sky_active == want) + if (g_sky_reload) { + g_sky_reload = false; + restore_sky_texture(); + } + if (g_sky_applied) return; - char cmd[160]; - snprintf(cmd, sizeof(cmd), "sv_skyname %s", want); - console_cmd(cmd); - logf("skybox: %s", want); - g_sky_active = want; - world_tint_invalidate(); // tint materials for the freshly loaded sky + 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; + } } -- cgit v1.2.3