diff options
Diffstat (limited to 'src/world.cpp')
| -rw-r--r-- | src/world.cpp | 342 |
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 | ||
| 24 | namespace { | 25 | namespace { |
| @@ -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 | ||
| 348 | namespace { | 349 | namespace { |
| 349 | 350 | ||
| 350 | // HL2's skybox set. Index 0 means "do not force a sky"; the rest are the | 351 | struct PngImage { |
| 351 | // base names the engine expands to skybox/<name>{rt,bk,lf,ft,up,dn}. | 352 | void *opaque; |
| 352 | const 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", | 358 | bool 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) { | |
| 362 | std::string g_sky_original; // map's own sky, captured before overriding | 363 | tried = true; |
| 363 | bool g_sky_original_valid = false; | 364 | lib = dlopen("libpng16.so.16", RTLD_LAZY | RTLD_GLOBAL); |
| 364 | std::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 | } |
| 367 | bool 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 | ||
| 398 | void capture_map_sky() { | 412 | void *g_sky_tex = nullptr; |
| 399 | if (g_sky_original_valid) | 413 | bool g_sky_tex_tried = false; |
| 414 | bool g_sky_applied = false; | ||
| 415 | bool g_sky_reload = false; | ||
| 416 | double g_sky_retry_at = 0.0; | ||
| 417 | bool g_sky_fail_logged = false; | ||
| 418 | |||
| 419 | struct 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 | }; | ||
| 425 | std::vector<SkyOrig> g_sky_orig; | ||
| 426 | |||
| 427 | double 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 | |||
| 433 | bool 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 | |||
| 440 | void 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 | |||
| 452 | int 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 | |||
| 504 | void 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 | |||
| 559 | void 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 | ||
| 443 | int skybox_option_count() { | 590 | void 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. | |
| 447 | const 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 | ||
| 452 | const char *const *skybox_options() { return kSkyboxLabels; } | 598 | void skybox_reload_image() { |
| 453 | 599 | restore_sky_texture(); | |
| 454 | void 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 | ||
| 462 | void skybox_update() { | 607 | void 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 | } |
