summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-03 23:58:59 +0100
committersalmonik <salmonik@tuta.io>2026-11-03 23:58:59 +0100
commit9c4455af43da34127864b09ad2a3d3e996c9dce1 (patch)
treeeb3754763db7c00fe664b900f9a560057bcb9955 /src
parentWorld: add skybox changer (diff)
downloadhl2h-9c4455af43da34127864b09ad2a3d3e996c9dce1.tar.gz
hl2h-9c4455af43da34127864b09ad2a3d3e996c9dce1.zip
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 <config dir>/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.
Diffstat (limited to '')
-rw-r--r--src/gui.cpp77
-rw-r--r--src/hl2h.h6
-rw-r--r--src/menu.cpp27
-rw-r--r--src/world.cpp342
4 files changed, 275 insertions, 177 deletions
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 {
183 CK_SECTION, 183 CK_SECTION,
184 CK_INFO, 184 CK_INFO,
185 CK_BUTTON, 185 CK_BUTTON,
186 CK_COLOR, 186 CK_COLOR
187 CK_CHOICE
188}; 187};
189 188
190enum CtrlAction { ACT_NONE = 0, ACT_RELOAD, ACT_RESET }; 189enum CtrlAction { ACT_NONE = 0, ACT_RELOAD, ACT_RESET, ACT_SKY_RELOAD };
191 190
192struct Ctrl { 191struct Ctrl {
193 int kind; 192 int kind;
@@ -196,8 +195,6 @@ struct Ctrl {
196 bool *bval; // CK_TOGGLE 195 bool *bval; // CK_TOGGLE
197 int *ival; // CK_SLIDER 196 int *ival; // CK_SLIDER
198 int *rgb; // CK_COLOR: three ints, 0..255 197 int *rgb; // CK_COLOR: three ints, 0..255
199 const char *const *opts; // CK_CHOICE: option labels
200 int nopts;
201 int min, max; 198 int min, max;
202 int action; // CK_BUTTON 199 int action; // CK_BUTTON
203 bool zero_unlimited; // slider: value 0 displays as "All" 200 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,
256 c.action = action; 253 c.action = action;
257 c.zero_unlimited = false; 254 c.zero_unlimited = false;
258 c.rgb = nullptr; 255 c.rgb = nullptr;
259 c.opts = nullptr;
260 c.nopts = 0;
261 c.hue = 0.0f; 256 c.hue = 0.0f;
262 c.sat = 0.0f; 257 c.sat = 0.0f;
263 c.val = 1.0f; 258 c.val = 1.0f;
@@ -287,19 +282,11 @@ static void add_color(int card, const char *label, const char *desc,
287 add_ctrl(card, CK_COLOR, label, desc, 0, 255, nullptr, nullptr, ACT_NONE); 282 add_ctrl(card, CK_COLOR, label, desc, 0, 255, nullptr, nullptr, ACT_NONE);
288 g_ctrls[i].rgb = rgb; 283 g_ctrls[i].rgb = rgb;
289} 284}
290static void add_choice(int card, const char *label, const char *desc,
291 const char *const *opts, int nopts, int *value) {
292 int i = g_nctrl;
293 add_ctrl(card, CK_CHOICE, label, desc, 0, nopts > 0 ? nopts - 1 : 0,
294 nullptr, value, ACT_NONE);
295 g_ctrls[i].opts = opts;
296 g_ctrls[i].nopts = nopts;
297}
298 285
299static int ctrl_val(const Ctrl &c) { 286static int ctrl_val(const Ctrl &c) {
300 if (c.kind == CK_TOGGLE && c.bval) 287 if (c.kind == CK_TOGGLE && c.bval)
301 return *c.bval ? 1 : 0; 288 return *c.bval ? 1 : 0;
302 if ((c.kind == CK_SLIDER || c.kind == CK_CHOICE) && c.ival) 289 if (c.kind == CK_SLIDER && c.ival)
303 return *c.ival; 290 return *c.ival;
304 return 0; 291 return 0;
305} 292}
@@ -307,7 +294,7 @@ static void ctrl_set(Ctrl &c, int v) {
307 if (c.kind == CK_TOGGLE && c.bval) { 294 if (c.kind == CK_TOGGLE && c.bval) {
308 *c.bval = v != 0; 295 *c.bval = v != 0;
309 config_save(); 296 config_save();
310 } else if ((c.kind == CK_SLIDER || c.kind == CK_CHOICE) && c.ival) { 297 } else if (c.kind == CK_SLIDER && c.ival) {
311 if (v < c.min) v = c.min; 298 if (v < c.min) v = c.min;
312 if (v > c.max) v = c.max; 299 if (v > c.max) v = c.max;
313 if (*c.ival != v) { 300 if (*c.ival != v) {
@@ -433,12 +420,17 @@ static void build_cards() {
433 add_toggle(fog, "Remove fog", 420 add_toggle(fog, "Remove fog",
434 "Disable fog entirely instead of recolouring it.", &g_fog_disable); 421 "Disable fog entirely instead of recolouring it.", &g_fog_disable);
435 422
423 static char sky_label[512];
424 snprintf(sky_label, sizeof(sky_label), "Image: %s",
425 config_file_cstr("skybox.png"));
436 int sky = add_card(2, &g_skybox, "Skybox", 426 int sky = add_card(2, &g_skybox, "Skybox",
437 "Swap the skybox at runtime through sv_skyname. The " 427 "Replace the map skybox with a PNG image from the "
438 "map's own sky comes back on level change.", 428 "config directory (all six faces).",
439 true); 429 true);
440 add_choice(sky, "Sky", "Pick a skybox from the HL2 set.", skybox_options(), 430 add_info(sky, sky_label);
441 skybox_option_count(), &g_skybox_choice); 431 add_info(sky, "Edit skybox.png, then Reload image. Off restores the map sky.");
432 add_button(sky, "Reload image", "Re-read the PNG and apply it again.",
433 ACT_SKY_RELOAD);
442 434
443 int spread = add_card(3, &g_spread, "Spread control", 435 int spread = add_card(3, &g_spread, "Spread control",
444 "Choose a server shot seed that tightens the cone.", 436 "Choose a server shot seed that tightens the cone.",
@@ -855,8 +847,6 @@ static int header_h(const Card &c) {
855static int label_max_w(const Ctrl &c) { 847static int label_max_w(const Ctrl &c) {
856 if (c.kind == CK_SLIDER) 848 if (c.kind == CK_SLIDER)
857 return S(214.0f); 849 return S(214.0f);
858 if (c.kind == CK_CHOICE)
859 return card_content_w() - S(16.0f) - S(250.0f);
860 return card_content_w() - S(16.0f) - S(110.0f); 850 return card_content_w() - S(16.0f) - S(110.0f);
861} 851}
862 852
@@ -1431,25 +1421,6 @@ static void handle_mouse_click() {
1431 } 1421 }
1432 return; 1422 return;
1433 } 1423 }
1434 if (ct.kind == CK_CHOICE) {
1435 int base = label_row_h(ct);
1436 Rect box{cr.x + cr.w - S(238.0f),
1437 by + (base - S(24.0f)) / 2, S(222.0f), S(24.0f)};
1438 if (box.has(mx, my)) {
1439 int v = ctrl_val(ct);
1440 if (mx < box.x + S(36.0f))
1441 v--;
1442 else if (mx >= box.x + box.w - S(36.0f))
1443 v++;
1444 if (v < 0) v = 0;
1445 if (ct.nopts > 0 && v >= ct.nopts) v = ct.nopts - 1;
1446 if (v != ctrl_val(ct)) {
1447 *ct.ival = v;
1448 config_save();
1449 }
1450 }
1451 return;
1452 }
1453 if (ct.kind == CK_BUTTON) { 1424 if (ct.kind == CK_BUTTON) {
1454 int bw = button_w(ct.label); 1425 int bw = button_w(ct.label);
1455 Rect b{cr.x + cr.w - S(16.0f) - bw, 1426 Rect b{cr.x + cr.w - S(16.0f) - bw,
@@ -1469,6 +1440,9 @@ static void handle_mouse_click() {
1469 g_reset_armed = true; 1440 g_reset_armed = true;
1470 g_reset_armed_at = now; 1441 g_reset_armed_at = now;
1471 } 1442 }
1443 } else if (ct.action == ACT_SKY_RELOAD) {
1444 skybox_reload_image();
1445 toastf(TOAST_INFO, "Reloading skybox.png");
1472 } 1446 }
1473 return; 1447 return;
1474 } 1448 }
@@ -1840,25 +1814,6 @@ static void draw_card(const Rect &clip, int index, int y) {
1840 outline(sw, picker_open_for(ct) ? C_ACCENT : C_BORDER, &clip); 1814 outline(sw, picker_open_for(ct) ? C_ACCENT : C_BORDER, &clip);
1841 if (picker_open_for(ct)) 1815 if (picker_open_for(ct))
1842 draw_picker(clip, cr, ct, by); 1816 draw_picker(clip, cr, ct, by);
1843 } else if (ct.kind == CK_CHOICE) {
1844 int v = ctrl_val(ct);
1845 const char *name = (ct.opts && v >= 0 && v < ct.nopts)
1846 ? ct.opts[v]
1847 : "?";
1848 Rect box{cr.x + cr.w - S(238.0f),
1849 by + (base_h - S(24.0f)) / 2, S(222.0f), S(24.0f)};
1850 fill(box, Col{45, 50, 60, 255}, &clip);
1851 outline(box, C_BORDER, &clip);
1852 int ty = box.y + (box.h - g_body_h) / 2;
1853 text_in(clip, box.x + S(8.0f), ty, "<", g_font_body, g_body_h, 0,
1854 C_DIM);
1855 text_in(clip, box.x + box.w - S(16.0f), ty, ">", g_font_body, g_body_h,
1856 0, C_DIM);
1857 int w = text_w(name, g_body_h);
1858 int tx = box.x + (box.w - w) / 2;
1859 if (tx < box.x + S(24.0f))
1860 tx = box.x + S(24.0f);
1861 text_in(clip, tx, ty, name, g_font_body, g_body_h, 0, C_TEXT);
1862 } 1817 }
1863 } 1818 }
1864 by += rh; 1819 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;
219extern int g_tint_world_rgb[3], g_tint_sky_rgb[3], g_tint_prop_rgb[3]; 219extern int g_tint_world_rgb[3], g_tint_sky_rgb[3], g_tint_prop_rgb[3];
220extern bool g_fog_skybox, g_fog_disable; 220extern bool g_fog_skybox, g_fog_disable;
221extern int g_fog_rgb[3], g_fog_start, g_fog_end; 221extern int g_fog_rgb[3], g_fog_start, g_fog_end;
222extern int g_skybox_choice; 222const char *config_file_cstr(const char *name);
223int skybox_option_count();
224const char *skybox_option_name(int i);
225const char *const *skybox_options();
226extern bool g_bhop_autostrafe; 223extern bool g_bhop_autostrafe;
227extern bool g_jumpbug_assist; 224extern bool g_jumpbug_assist;
228extern bool g_spread_no_spread; 225extern bool g_spread_no_spread;
@@ -267,6 +264,7 @@ void fog_update();
267void fog_invalidate(); 264void fog_invalidate();
268void skybox_update(); 265void skybox_update();
269void skybox_invalidate(); 266void skybox_invalidate();
267void skybox_reload_image();
270bool resolve_server_netvars(); 268bool resolve_server_netvars();
271int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); 269int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out);
272bool server_set_velocity_z(int idx, float vz); 270bool 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;
59int g_fog_rgb[3] = {160, 170, 190}; 59int g_fog_rgb[3] = {160, 170, 190};
60int g_fog_start = 200; 60int g_fog_start = 200;
61int g_fog_end = 3000; 61int g_fog_end = 3000;
62FeatureState g_skybox{false, 0}; // sv_skyname override 62FeatureState g_skybox{false, 0}; // skybox.png image override
63int g_skybox_choice = 0; // 0 = map default
64bool g_item_health = true; 63bool g_item_health = true;
65bool g_item_ammo = true; 64bool g_item_ammo = true;
66bool g_item_armor = true; 65bool g_item_armor = true;
@@ -214,8 +213,8 @@ void config_save() {
214 "start=%d\nend=%d\nskybox=%d\ndisable=%d\n", 213 "start=%d\nend=%d\nskybox=%d\ndisable=%d\n",
215 g_fog.active, g_fog.bind, g_fog_rgb[0], g_fog_rgb[1], g_fog_rgb[2], 214 g_fog.active, g_fog.bind, g_fog_rgb[0], g_fog_rgb[1], g_fog_rgb[2],
216 g_fog_start, g_fog_end, (int)g_fog_skybox, (int)g_fog_disable); 215 g_fog_start, g_fog_end, (int)g_fog_skybox, (int)g_fog_disable);
217 fprintf(f, "[skybox]\nactive=%d\nbind=%d\nchoice=%d\n", g_skybox.active, 216 fprintf(f, "[skybox]\nactive=%d\nbind=%d\n", g_skybox.active,
218 g_skybox.bind, g_skybox_choice); 217 g_skybox.bind);
219 fprintf(f, "[items]\nactive=%d\nbind=%d\nhealth=%d\nammo=%d\n" 218 fprintf(f, "[items]\nactive=%d\nbind=%d\nhealth=%d\nammo=%d\n"
220 "armor=%d\nweapons=%d\ncrates=%d\ndistance=%d\nmax_dist=%d\n", 219 "armor=%d\nweapons=%d\ncrates=%d\ndistance=%d\nmax_dist=%d\n",
221 g_item_esp.active, g_item_esp.bind, (int)g_item_health, 220 g_item_esp.active, g_item_esp.bind, (int)g_item_health,
@@ -250,7 +249,7 @@ static bool config_load() {
250 FeatureState *cur = nullptr; 249 FeatureState *cur = nullptr;
251 bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false, 250 bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false,
252 in_jumpbug = false, in_spread = false, in_items = false, 251 in_jumpbug = false, in_spread = false, in_items = false,
253 in_tint = false, in_fog = false, in_skybox = false; 252 in_tint = false, in_fog = false;
254 while (fgets(line, sizeof(line), f)) { 253 while (fgets(line, sizeof(line), f)) {
255 line[strcspn(line, "\r\n")] = 0; 254 line[strcspn(line, "\r\n")] = 0;
256 if (line[0] == '[') { 255 if (line[0] == '[') {
@@ -293,7 +292,6 @@ static bool config_load() {
293 in_items = (cur == &g_item_esp); 292 in_items = (cur == &g_item_esp);
294 in_tint = (cur == &g_world_tint); 293 in_tint = (cur == &g_world_tint);
295 in_fog = (cur == &g_fog); 294 in_fog = (cur == &g_fog);
296 in_skybox = (cur == &g_skybox);
297 } else if (cur) { 295 } else if (cur) {
298 char *eq = strchr(line, '='); 296 char *eq = strchr(line, '=');
299 if (!eq) 297 if (!eq)
@@ -414,12 +412,6 @@ static bool config_load() {
414 g_fog_disable = atoi(val) != 0; 412 g_fog_disable = atoi(val) != 0;
415 } 413 }
416 } 414 }
417 if (in_skybox && strcmp(key, "choice") == 0) {
418 int v = atoi(val);
419 if (v < 0) v = 0;
420 if (v >= skybox_option_count()) v = 0;
421 g_skybox_choice = v;
422 }
423 if (in_esp) { 415 if (in_esp) {
424 if (strcmp(key, "neutral") == 0) 416 if (strcmp(key, "neutral") == 0)
425 g_esp_opt[ESP_OPT_SHOW_NEUTRAL] = atoi(val) != 0; 417 g_esp_opt[ESP_OPT_SHOW_NEUTRAL] = atoi(val) != 0;
@@ -459,6 +451,16 @@ const char *config_path_cstr() {
459 return path.c_str(); 451 return path.c_str();
460} 452}
461 453
454const char *config_file_cstr(const char *name) {
455 static std::string path;
456 std::string p = config_path();
457 size_t slash = p.find_last_of('/');
458 std::string dir =
459 slash == std::string::npos ? std::string(".") : p.substr(0, slash);
460 path = dir + "/" + (name ? name : "");
461 return path.c_str();
462}
463
462void config_reset_defaults() { 464void config_reset_defaults() {
463 g_bhop = {true, 0}; 465 g_bhop = {true, 0};
464 g_bhop_autostrafe = true; 466 g_bhop_autostrafe = true;
@@ -489,7 +491,6 @@ void config_reset_defaults() {
489 g_fog_start = 200; 491 g_fog_start = 200;
490 g_fog_end = 3000; 492 g_fog_end = 3000;
491 g_skybox = {false, 0}; 493 g_skybox = {false, 0};
492 g_skybox_choice = 0;
493 g_item_health = true; 494 g_item_health = true;
494 g_item_ammo = true; 495 g_item_ammo = true;
495 g_item_armor = true; 496 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 @@
19#include <cstdio> 19#include <cstdio>
20#include <cstdlib> 20#include <cstdlib>
21#include <cstring> 21#include <cstring>
22#include <ctime>
22#include <dlfcn.h> 23#include <dlfcn.h>
23 24
24namespace { 25namespace {
@@ -342,66 +343,116 @@ void fog_update() {
342} 343}
343 344
344// --------------------------------------------------------------------------- 345// ---------------------------------------------------------------------------
345// Skybox changer (sv_skyname; same mechanism as the skybox_swapper entity) 346// Skybox image (skybox.png from the config directory)
346// --------------------------------------------------------------------------- 347// ---------------------------------------------------------------------------
347 348
348namespace { 349namespace {
349 350
350// HL2's skybox set. Index 0 means "do not force a sky"; the rest are the 351struct PngImage {
351// base names the engine expands to skybox/<name>{rt,bk,lf,ft,up,dn}. 352 void *opaque;
352const char *const kSkyboxLabels[] = { 353 uint32_t version, width, height, format, flags, colormap_entries,
353 "Map default", "sky_day01_01", "sky_day01_04", "sky_day01_05", 354 warning_or_error;
354 "sky_day01_06", "sky_day01_07", "sky_day01_08", "sky_day01_09", 355 char message[64];
355 "sky_day02_01", "sky_day02_02", "sky_day02_03", "sky_day02_04", 356};
356 "sky_day02_05", "sky_day02_06", "sky_day02_07", "sky_day02_09", 357
357 "sky_day02_10", "sky_day03_01", "sky_day03_02", "sky_day03_03", 358bool load_png_rgba(const char *path, std::vector<unsigned char> &pixels,
358 "sky_day03_04", "sky_day03_05", "sky_day03_06", "sky_day03_06b", 359 int &w, int &h) {
359 "sky_day03_06c", "sky_wasteland02", "sky_borealis01", "sky_fog_002", 360 static void *lib = nullptr;
360 "sky_fake_white"}; 361 static bool tried = false;
361 362 if (!tried) {
362std::string g_sky_original; // map's own sky, captured before overriding 363 tried = true;
363bool g_sky_original_valid = false; 364 lib = dlopen("libpng16.so.16", RTLD_LAZY | RTLD_GLOBAL);
364std::string g_sky_active; // value we forced, empty when not forcing 365 if (!lib)
365 366 lib = dlopen("libpng16.so", RTLD_LAZY | RTLD_GLOBAL);
366// "skybox/sky_day01_01_hdrrt" -> "sky_day01_01" 367 }
367bool parse_sky_material(const char *mat, char *out, size_t outn) { 368 if (!lib) {
368 if (!mat) 369 logf("skybox: libpng16 not available");
369 return false; 370 return false;
370 const char *n = mat; 371 }
371 if (strncmp(n, "skybox/", 7) == 0) 372 using BeginFn = int (*)(PngImage *, const char *);
372 n += 7; 373 using FinishFn = int (*)(PngImage *, void *, void *, int32_t, void *);
373 size_t len = strlen(n); 374 using FreeFn = void (*)(PngImage *);
374 if (len < 4) 375 static BeginFn begin =
376 (BeginFn)dlsym(lib, "png_image_begin_read_from_file");
377 static FinishFn finish = (FinishFn)dlsym(lib, "png_image_finish_read");
378 static FreeFn free_img = (FreeFn)dlsym(lib, "png_image_free");
379 if (!begin || !finish || !free_img) {
380 logf("skybox: libpng16 simplified API missing");
375 return false; 381 return false;
376 static const char *faces[] = {"rt", "bk", "lf", "ft", "up", "dn", nullptr};
377 const char *suf = n + len - 2;
378 bool face = false;
379 for (int i = 0; faces[i]; i++) {
380 if (strcmp(suf, faces[i]) == 0) {
381 face = true;
382 break;
383 }
384 } 382 }
385 if (!face) 383
384 PngImage img{};
385 img.version = 1; // PNG_IMAGE_VERSION
386 img.format = 0x03; // PNG_FORMAT_RGBA
387 if (!begin(&img, path)) {
388 logf("skybox: cannot open %s (%s)", path,
389 img.message[0] ? img.message : "read error");
386 return false; 390 return false;
387 size_t blen = len - 2; 391 }
388 if (blen > 4 && (strncmp(n + blen - 4, "_hdr", 4) == 0 || 392 if (img.width == 0 || img.height == 0 || img.width > 8192 ||
389 strncmp(n + blen - 4, "_ldr", 4) == 0)) 393 img.height > 8192) {
390 blen -= 4; 394 logf("skybox: %s has unsupported size %ux%u", path, img.width, img.height);
391 if (blen == 0 || blen >= outn) 395 free_img(&img);
396 return false;
397 }
398 pixels.assign((size_t)img.width * img.height * 4, 255);
399 if (!finish(&img, nullptr, pixels.data(), 0, nullptr)) {
400 logf("skybox: cannot decode %s (%s)", path,
401 img.message[0] ? img.message : "decode error");
402 pixels.clear();
403 free_img(&img);
392 return false; 404 return false;
393 memcpy(out, n, blen); 405 }
394 out[blen] = 0; 406 w = (int)img.width;
407 h = (int)img.height;
408 free_img(&img);
395 return true; 409 return true;
396} 410}
397 411
398void capture_map_sky() { 412void *g_sky_tex = nullptr;
399 if (g_sky_original_valid) 413bool g_sky_tex_tried = false;
414bool g_sky_applied = false;
415bool g_sky_reload = false;
416double g_sky_retry_at = 0.0;
417bool g_sky_fail_logged = false;
418
419struct SkyOrig {
420 std::string material;
421 std::string var;
422 std::string value; // fallback if the original texture pointer is gone
423 void *texture;
424};
425std::vector<SkyOrig> g_sky_orig;
426
427double now_sec() {
428 struct timespec ts;
429 clock_gettime(CLOCK_MONOTONIC, &ts);
430 return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
431}
432
433bool is_sky_material(const char *name, const char *group) {
434 if (!plausible_name(name) || !plausible_name(group))
435 return false;
436 return strncmp(group, GROUP_SKYBOX, strlen(GROUP_SKYBOX)) == 0 ||
437 strncmp(name, "skybox/", 7) == 0;
438}
439
440void record_orig(const char *material, const char *var, const char *value,
441 void *texture) {
442 if (!material || !var)
400 return; 443 return;
401 g_sky_original_valid = true; // one attempt per map, even on failure 444 for (auto &r : g_sky_orig) {
445 if (r.material == material && r.var == var)
446 return;
447 }
448 g_sky_orig.push_back(
449 {material, var, value ? value : "", texture});
450}
451
452int apply_sky_texture() {
402 void *ms = material_system(); 453 void *ms = material_system();
403 if (!ms) 454 if (!ms || !g_sky_tex)
404 return; 455 return 0;
405 auto invalid = vfunc<MatCalls::InvalidFn>(ms, MATSYS_InvalidMaterial); 456 auto invalid = vfunc<MatCalls::InvalidFn>(ms, MATSYS_InvalidMaterial);
406 auto first = vfunc<MatCalls::FirstFn>(ms, MATSYS_FirstMaterial); 457 auto first = vfunc<MatCalls::FirstFn>(ms, MATSYS_FirstMaterial);
407 auto next = vfunc<MatCalls::NextFn>(ms, MATSYS_NextMaterial); 458 auto next = vfunc<MatCalls::NextFn>(ms, MATSYS_NextMaterial);
@@ -409,10 +460,74 @@ void capture_map_sky() {
409 auto count = vfunc<MatCalls::CountFn>(ms, MATSYS_GetNumMaterials); 460 auto count = vfunc<MatCalls::CountFn>(ms, MATSYS_GetNumMaterials);
410 int total = count(ms); 461 int total = count(ms);
411 if (total <= 0 || total > 200000) 462 if (total <= 0 || total > 200000)
463 return 0;
464 unsigned short inv = invalid(ms);
465 int seen = 0, hits = 0;
466 for (unsigned short h = first(ms); h != inv; h = next(ms, h)) {
467 if (++seen > total + 16)
468 break;
469 void *mat = get(ms, h);
470 if (!mat)
471 continue;
472 const char *name = vfunc<MatCalls::NameFn>(mat, MAT_GetName)(mat);
473 const char *group =
474 vfunc<MatCalls::GroupFn>(mat, MAT_GetTextureGroupName)(mat);
475 if (!is_sky_material(name, group))
476 continue;
477 using FindVarFn = void *(*)(void *, const char *, bool *, bool);
478 using GetStrFn = const char *(*)(void *);
479 using SetTexFn = void (*)(void *, void *);
480 auto find_var = vfunc<FindVarFn>(mat, 11); // IMaterial::FindVar
481
482 static const char *vars[] = {"$basetexture", "$hdrbasetexture",
483 "$hdrcompressedtexture", nullptr};
484 for (int vi = 0; vars[vi]; vi++) {
485 bool found = false;
486 void *var = find_var(mat, vars[vi], &found, false);
487 if (!found || !var)
488 continue;
489 using GetTexFn = void *(*)(void *);
490 auto vget = vfunc<GetStrFn>(var, 6); // GetStringValue
491 auto vgettex = vfunc<GetTexFn>(var, 0); // GetTextureValue
492 auto vset = vfunc<SetTexFn>(var, 14); // SetTextureValue
493 const char *orig = vget(var);
494 void *orig_tex = vgettex(var);
495 record_orig(name, vars[vi], orig, orig_tex);
496 vset(var, g_sky_tex);
497 if (vi == 0)
498 hits++;
499 }
500 }
501 return hits;
502}
503
504void restore_sky_texture() {
505 if (g_sky_orig.empty()) {
506 g_sky_applied = false;
507 return;
508 }
509 void *ms = material_system();
510 if (!ms) {
511 g_sky_orig.clear();
512 g_sky_applied = false;
513 return;
514 }
515 auto invalid = vfunc<MatCalls::InvalidFn>(ms, MATSYS_InvalidMaterial);
516 auto first = vfunc<MatCalls::FirstFn>(ms, MATSYS_FirstMaterial);
517 auto next = vfunc<MatCalls::NextFn>(ms, MATSYS_NextMaterial);
518 auto get = vfunc<MatCalls::GetFn>(ms, MATSYS_GetMaterial);
519 auto count = vfunc<MatCalls::CountFn>(ms, MATSYS_GetNumMaterials);
520 int total = count(ms);
521 if (total <= 0 || total > 200000) {
522 g_sky_orig.clear();
523 g_sky_applied = false;
412 return; 524 return;
525 }
526 using FindVarFn = void *(*)(void *, const char *, bool *, bool);
527 using SetStrFn = void (*)(void *, const char *);
528 using SetTexFn = void (*)(void *, void *);
413 unsigned short inv = invalid(ms); 529 unsigned short inv = invalid(ms);
414 int seen = 0; 530 int seen = 0;
415 std::string last;
416 for (unsigned short h = first(ms); h != inv; h = next(ms, h)) { 531 for (unsigned short h = first(ms); h != inv; h = next(ms, h)) {
417 if (++seen > total + 16) 532 if (++seen > total + 16)
418 break; 533 break;
@@ -422,73 +537,102 @@ void capture_map_sky() {
422 const char *name = vfunc<MatCalls::NameFn>(mat, MAT_GetName)(mat); 537 const char *name = vfunc<MatCalls::NameFn>(mat, MAT_GetName)(mat);
423 const char *group = 538 const char *group =
424 vfunc<MatCalls::GroupFn>(mat, MAT_GetTextureGroupName)(mat); 539 vfunc<MatCalls::GroupFn>(mat, MAT_GetTextureGroupName)(mat);
425 if (!plausible_name(name) || !plausible_name(group)) 540 if (!is_sky_material(name, group))
426 continue; 541 continue;
427 if (strncmp(group, GROUP_SKYBOX, strlen(GROUP_SKYBOX)) != 0) 542 for (auto &r : g_sky_orig) {
428 continue; 543 if (r.material != name)
429 char base[128]; 544 continue;
430 if (parse_sky_material(name, base, sizeof(base))) 545 bool found = false;
431 last = base; // newest loaded sky materials win after a level change 546 void *var = vfunc<FindVarFn>(mat, 11)(mat, r.var.c_str(), &found, false);
547 if (!found || !var)
548 continue;
549 if (r.texture)
550 vfunc<SetTexFn>(var, 14)(var, r.texture); // back to the map texture
551 else if (!r.value.empty())
552 vfunc<SetStrFn>(var, 5)(var, r.value.c_str());
553 }
554 }
555 g_sky_orig.clear();
556 g_sky_applied = false;
557}
558
559void load_sky_texture() {
560 g_sky_tex_tried = true;
561 void *ms = material_system();
562 if (!ms) {
563 logf("skybox: material system unavailable");
564 return;
432 } 565 }
433 if (last.empty()) { 566 std::vector<unsigned char> pixels;
434 logd("skybox: could not capture the map sky"); 567 int w = 0, h = 0;
568 const char *path = config_file_cstr("skybox.png");
569 if (!load_png_rgba(path, pixels, w, h)) {
570 if (!g_sky_fail_logged) {
571 g_sky_fail_logged = true;
572 toastf(TOAST_ERROR, "Skybox image missing or unreadable: %s", path);
573 }
574 return;
575 }
576 using CreateTexFn = void *(*)(void *, int, int, int, int, int,
577 unsigned char *);
578 g_sky_tex = vfunc<CreateTexFn>(ms, 134)(ms, w, h, 1, 0 /* RGBA8888 */,
579 (int)pixels.size(), pixels.data());
580 if (!g_sky_tex) {
581 logf("skybox: CreateTextureFromBits failed for %dx%d", w, h);
435 return; 582 return;
436 } 583 }
437 g_sky_original = last; 584 g_sky_fail_logged = false;
438 logf("skybox: map sky is %s", g_sky_original.c_str()); 585 logf("skybox: image %s (%dx%d) loaded", path, w, h);
439} 586}
440 587
441} // namespace 588} // namespace
442 589
443int skybox_option_count() { 590void skybox_invalidate() {
444 return (int)(sizeof(kSkyboxLabels) / sizeof(kSkyboxLabels[0])); 591 // Level change: re-apply on the new map. Keep the captured originals: with
445} 592 // the same skybox the same material objects (still showing our texture) are
446 593 // reused, and the saved texture pointers are what lets us restore them.
447const char *skybox_option_name(int i) { 594 g_sky_applied = false;
448 int n = skybox_option_count(); 595 g_sky_retry_at = 0.0;
449 return (i >= 0 && i < n) ? kSkyboxLabels[i] : kSkyboxLabels[0];
450} 596}
451 597
452const char *const *skybox_options() { return kSkyboxLabels; } 598void skybox_reload_image() {
453 599 restore_sky_texture();
454void skybox_invalidate() { 600 g_sky_tex = nullptr;
455 // Level change: the engine sets sv_skyname from worldspawn again, so there is 601 g_sky_tex_tried = false;
456 // nothing to restore. Forget the captured base and let it recapture. 602 g_sky_reload = true;
457 g_sky_original.clear(); 603 g_sky_retry_at = 0.0;
458 g_sky_original_valid = false; 604 g_sky_fail_logged = false;
459 g_sky_active.clear();
460} 605}
461 606
462void skybox_update() { 607void skybox_update() {
463 bool on = feature_active(FEAT_SKYBOX); 608 bool on = feature_active(FEAT_SKYBOX);
464 int choice = g_skybox_choice; 609 if (!on) {
465 int n = skybox_option_count(); 610 if (g_sky_applied || !g_sky_orig.empty())
466 if (choice < 0 || choice >= n) 611 restore_sky_texture();
467 choice = 0;
468
469 if (!on || choice == 0) {
470 if (g_sky_active.empty())
471 return;
472 if (g_sky_original_valid && !g_sky_original.empty() &&
473 g_sky_original != g_sky_active) {
474 char cmd[160];
475 snprintf(cmd, sizeof(cmd), "sv_skyname %s", g_sky_original.c_str());
476 console_cmd(cmd);
477 logf("skybox: restored %s", g_sky_original.c_str());
478 world_tint_invalidate(); // the new sky materials need the sky tint
479 }
480 g_sky_active.clear();
481 return; 612 return;
482 } 613 }
483 614 if (g_sky_reload) {
484 capture_map_sky(); // before the first override of this map 615 g_sky_reload = false;
485 const char *want = skybox_option_name(choice); 616 restore_sky_texture();
486 if (g_sky_active == want) 617 }
618 if (g_sky_applied)
487 return; 619 return;
488 char cmd[160]; 620 double now = now_sec();
489 snprintf(cmd, sizeof(cmd), "sv_skyname %s", want); 621 if (now < g_sky_retry_at)
490 console_cmd(cmd); 622 return;
491 logf("skybox: %s", want); 623 if (!g_sky_tex && !g_sky_tex_tried)
492 g_sky_active = want; 624 load_sky_texture();
493 world_tint_invalidate(); // tint materials for the freshly loaded sky 625 if (!g_sky_tex) {
626 // The sky materials may not be loaded yet (menu, level transition). Try
627 // again in a moment; the PNG itself is only re-read on Reload.
628 g_sky_retry_at = now + 1.0;
629 return;
630 }
631 int hits = apply_sky_texture();
632 if (hits > 0) {
633 g_sky_applied = true;
634 logf("skybox: image applied to %d sky materials", hits);
635 } else {
636 g_sky_retry_at = now + 1.0;
637 }
494} 638}