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. --- CONTEXT.md | 33 +++--- src/gui.cpp | 77 +++---------- src/hl2h.h | 6 +- src/menu.cpp | 27 ++--- src/world.cpp | 342 +++++++++++++++++++++++++++++++++++++++++----------------- 5 files changed, 294 insertions(+), 191 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index f2b091a..a490403 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -358,20 +358,25 @@ vocabulary / decisions. - 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`. -### Skybox changer - -- `sv_skyname ` through `ClientCmd`, the same mechanism the stock - `skybox_swapper` entity uses. Choice list is the HL2 skybox set from the - game's VPK (no episode-only skies, so a selected sky always exists). -- Before the first override of a map, the map's own sky is captured by - scanning loaded materials in the `SkyBox textures` group and parsing - `skybox/(_hdr|_ldr)?(rt|bk|lf|ft|up|dn)`. Disabling the feature (or - choosing "Map default") restores that name; a level change also restores it - naturally, so `skybox_invalidate()` just drops the captured state. -- Changing the sky invalidates the world tint so the freshly loaded sky - materials get the sky colour. -- Saved as `[skybox] active/bind/choice`. The GUI uses a `CK_CHOICE` - control (value box with `<`/`>` hit zones). +### Skybox image + +- Replaces the map skybox with `/skybox.png` (same directory as + the INI). Not `sv_skyname`: that did not take effect at runtime in this + build, so the skybox card now works on materials instead. +- PNG is decoded with libpng16's simplified API (`dlopen`ed, no link + dependency); the RGBA pixels become an `ITexture` via + `IMaterialSystem::CreateTextureFromBits` (RGBA8888, one mip). +- The texture is assigned with `IMaterialVar::SetTextureValue` to + `$basetexture`, plus `$hdrbasetexture` / `$hdrcompressedtexture` when those + vars exist, on every material in the `SkyBox textures` group or named + `skybox/*`. The same image is mapped onto all six faces. +- Original textures are recorded per material/var (texture pointer plus the + string as a fallback) and restored when the feature is disabled. Records + survive level changes so a reused sky material can still be restored; the + override is re-applied to the new map automatically. +- The card shows the image path and has a Reload image button. A missing or + unreadable PNG raises an error toast; the image is only re-read on Reload. +- Saved as `[skybox] active/bind`. ## Logging diff --git a/src/gui.cpp b/src/gui.cpp index 5fb8fa7..eb53dff 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -183,11 +183,10 @@ enum CtrlKind { CK_SECTION, CK_INFO, CK_BUTTON, - CK_COLOR, - CK_CHOICE + CK_COLOR }; -enum CtrlAction { ACT_NONE = 0, ACT_RELOAD, ACT_RESET }; +enum CtrlAction { ACT_NONE = 0, ACT_RELOAD, ACT_RESET, ACT_SKY_RELOAD }; struct Ctrl { int kind; @@ -196,8 +195,6 @@ struct Ctrl { bool *bval; // CK_TOGGLE int *ival; // CK_SLIDER int *rgb; // CK_COLOR: three ints, 0..255 - const char *const *opts; // CK_CHOICE: option labels - int nopts; int min, max; int action; // CK_BUTTON bool zero_unlimited; // slider: value 0 displays as "All" @@ -256,8 +253,6 @@ static void add_ctrl(int card, int kind, const char *label, const char *desc, c.action = action; c.zero_unlimited = false; c.rgb = nullptr; - c.opts = nullptr; - c.nopts = 0; c.hue = 0.0f; c.sat = 0.0f; c.val = 1.0f; @@ -287,19 +282,11 @@ static void add_color(int card, const char *label, const char *desc, add_ctrl(card, CK_COLOR, label, desc, 0, 255, nullptr, nullptr, ACT_NONE); g_ctrls[i].rgb = rgb; } -static void add_choice(int card, const char *label, const char *desc, - const char *const *opts, int nopts, int *value) { - int i = g_nctrl; - add_ctrl(card, CK_CHOICE, label, desc, 0, nopts > 0 ? nopts - 1 : 0, - nullptr, value, ACT_NONE); - g_ctrls[i].opts = opts; - g_ctrls[i].nopts = nopts; -} static int ctrl_val(const Ctrl &c) { if (c.kind == CK_TOGGLE && c.bval) return *c.bval ? 1 : 0; - if ((c.kind == CK_SLIDER || c.kind == CK_CHOICE) && c.ival) + if (c.kind == CK_SLIDER && c.ival) return *c.ival; return 0; } @@ -307,7 +294,7 @@ static void ctrl_set(Ctrl &c, int v) { if (c.kind == CK_TOGGLE && c.bval) { *c.bval = v != 0; config_save(); - } else if ((c.kind == CK_SLIDER || c.kind == CK_CHOICE) && c.ival) { + } else if (c.kind == CK_SLIDER && c.ival) { if (v < c.min) v = c.min; if (v > c.max) v = c.max; if (*c.ival != v) { @@ -433,12 +420,17 @@ static void build_cards() { add_toggle(fog, "Remove fog", "Disable fog entirely instead of recolouring it.", &g_fog_disable); + static char sky_label[512]; + snprintf(sky_label, sizeof(sky_label), "Image: %s", + config_file_cstr("skybox.png")); int sky = add_card(2, &g_skybox, "Skybox", - "Swap the skybox at runtime through sv_skyname. The " - "map's own sky comes back on level change.", + "Replace the map skybox with a PNG image from the " + "config directory (all six faces).", true); - add_choice(sky, "Sky", "Pick a skybox from the HL2 set.", skybox_options(), - skybox_option_count(), &g_skybox_choice); + add_info(sky, sky_label); + add_info(sky, "Edit skybox.png, then Reload image. Off restores the map sky."); + add_button(sky, "Reload image", "Re-read the PNG and apply it again.", + ACT_SKY_RELOAD); int spread = add_card(3, &g_spread, "Spread control", "Choose a server shot seed that tightens the cone.", @@ -855,8 +847,6 @@ static int header_h(const Card &c) { static int label_max_w(const Ctrl &c) { if (c.kind == CK_SLIDER) return S(214.0f); - if (c.kind == CK_CHOICE) - return card_content_w() - S(16.0f) - S(250.0f); return card_content_w() - S(16.0f) - S(110.0f); } @@ -1431,25 +1421,6 @@ static void handle_mouse_click() { } return; } - if (ct.kind == CK_CHOICE) { - int base = label_row_h(ct); - Rect box{cr.x + cr.w - S(238.0f), - by + (base - S(24.0f)) / 2, S(222.0f), S(24.0f)}; - if (box.has(mx, my)) { - int v = ctrl_val(ct); - if (mx < box.x + S(36.0f)) - v--; - else if (mx >= box.x + box.w - S(36.0f)) - v++; - if (v < 0) v = 0; - if (ct.nopts > 0 && v >= ct.nopts) v = ct.nopts - 1; - if (v != ctrl_val(ct)) { - *ct.ival = v; - config_save(); - } - } - return; - } if (ct.kind == CK_BUTTON) { int bw = button_w(ct.label); Rect b{cr.x + cr.w - S(16.0f) - bw, @@ -1469,6 +1440,9 @@ static void handle_mouse_click() { g_reset_armed = true; g_reset_armed_at = now; } + } else if (ct.action == ACT_SKY_RELOAD) { + skybox_reload_image(); + toastf(TOAST_INFO, "Reloading skybox.png"); } return; } @@ -1840,25 +1814,6 @@ static void draw_card(const Rect &clip, int index, int y) { outline(sw, picker_open_for(ct) ? C_ACCENT : C_BORDER, &clip); if (picker_open_for(ct)) draw_picker(clip, cr, ct, by); - } else if (ct.kind == CK_CHOICE) { - int v = ctrl_val(ct); - const char *name = (ct.opts && v >= 0 && v < ct.nopts) - ? ct.opts[v] - : "?"; - Rect box{cr.x + cr.w - S(238.0f), - by + (base_h - S(24.0f)) / 2, S(222.0f), S(24.0f)}; - fill(box, Col{45, 50, 60, 255}, &clip); - outline(box, C_BORDER, &clip); - int ty = box.y + (box.h - g_body_h) / 2; - text_in(clip, box.x + S(8.0f), ty, "<", g_font_body, g_body_h, 0, - C_DIM); - text_in(clip, box.x + box.w - S(16.0f), ty, ">", g_font_body, g_body_h, - 0, C_DIM); - int w = text_w(name, g_body_h); - int tx = box.x + (box.w - w) / 2; - if (tx < box.x + S(24.0f)) - tx = box.x + S(24.0f); - text_in(clip, tx, ty, name, g_font_body, g_body_h, 0, C_TEXT); } } by += rh; diff --git a/src/hl2h.h b/src/hl2h.h index 3cb131d..fd7a863 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -219,10 +219,7 @@ 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 int g_skybox_choice; -int skybox_option_count(); -const char *skybox_option_name(int i); -const char *const *skybox_options(); +const char *config_file_cstr(const char *name); extern bool g_bhop_autostrafe; extern bool g_jumpbug_assist; extern bool g_spread_no_spread; @@ -267,6 +264,7 @@ void fog_update(); void fog_invalidate(); void skybox_update(); void skybox_invalidate(); +void skybox_reload_image(); 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 dfd973d..92131d4 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -59,8 +59,7 @@ bool g_fog_disable = false; int g_fog_rgb[3] = {160, 170, 190}; int g_fog_start = 200; int g_fog_end = 3000; -FeatureState g_skybox{false, 0}; // sv_skyname override -int g_skybox_choice = 0; // 0 = map default +FeatureState g_skybox{false, 0}; // skybox.png image override bool g_item_health = true; bool g_item_ammo = true; bool g_item_armor = true; @@ -214,8 +213,8 @@ void config_save() { "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, "[skybox]\nactive=%d\nbind=%d\nchoice=%d\n", g_skybox.active, - g_skybox.bind, g_skybox_choice); + fprintf(f, "[skybox]\nactive=%d\nbind=%d\n", g_skybox.active, + g_skybox.bind); 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, @@ -250,7 +249,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_fog = false, in_skybox = false; + in_tint = false, in_fog = false; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { @@ -293,7 +292,6 @@ static bool config_load() { in_items = (cur == &g_item_esp); in_tint = (cur == &g_world_tint); in_fog = (cur == &g_fog); - in_skybox = (cur == &g_skybox); } else if (cur) { char *eq = strchr(line, '='); if (!eq) @@ -414,12 +412,6 @@ static bool config_load() { g_fog_disable = atoi(val) != 0; } } - if (in_skybox && strcmp(key, "choice") == 0) { - int v = atoi(val); - if (v < 0) v = 0; - if (v >= skybox_option_count()) v = 0; - g_skybox_choice = v; - } if (in_esp) { if (strcmp(key, "neutral") == 0) g_esp_opt[ESP_OPT_SHOW_NEUTRAL] = atoi(val) != 0; @@ -459,6 +451,16 @@ const char *config_path_cstr() { return path.c_str(); } +const char *config_file_cstr(const char *name) { + static std::string path; + std::string p = config_path(); + size_t slash = p.find_last_of('/'); + std::string dir = + slash == std::string::npos ? std::string(".") : p.substr(0, slash); + path = dir + "/" + (name ? name : ""); + return path.c_str(); +} + void config_reset_defaults() { g_bhop = {true, 0}; g_bhop_autostrafe = true; @@ -489,7 +491,6 @@ void config_reset_defaults() { g_fog_start = 200; g_fog_end = 3000; g_skybox = {false, 0}; - g_skybox_choice = 0; g_item_health = true; g_item_ammo = true; g_item_armor = true; 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