summaryrefslogtreecommitdiffstats
path: root/src/world.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/world.cpp')
-rw-r--r--src/world.cpp342
1 files changed, 243 insertions, 99 deletions
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}