From 95b5be2299b332917ae63c45252cb9744a3460cd Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 3 Nov 2026 23:51:02 +0100 Subject: World: add skybox changer New Visuals -> Skybox card with a runtime sv_skyname override using the same mechanism as the stock skybox_swapper entity. The choice list is the HL2 skybox set extracted from the game VPKs. The map's own sky is captured from loaded materials (SkyBox group, parsing skybox/... face names) before the first override, so turning the feature off or picking "Map default" restores it; a level change restores it implicitly. Sky changes invalidate the world tint so new sky materials are tinted as well. GUI: add a CK_CHOICE control (value box with < / > zones). --- src/world.cpp | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) (limited to 'src/world.cpp') diff --git a/src/world.cpp b/src/world.cpp index 2333829..73738db 100644 --- a/src/world.cpp +++ b/src/world.cpp @@ -340,3 +340,155 @@ void fog_update() { logf("fog: %s colour=(%d,%d,%d) start=%d end=%d skybox=%d", disable ? "off" : "on", r, g, b, start, end, (int)skybox); } + +// --------------------------------------------------------------------------- +// Skybox changer (sv_skyname; same mechanism as the skybox_swapper entity) +// --------------------------------------------------------------------------- + +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) + return false; + const char *n = mat; + if (strncmp(n, "skybox/", 7) == 0) + n += 7; + size_t len = strlen(n); + if (len < 4) + 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) + 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) + return false; + memcpy(out, n, blen); + out[blen] = 0; + return true; +} + +void capture_map_sky() { + if (g_sky_original_valid) + return; + g_sky_original_valid = true; // one attempt per map, even on failure + void *ms = material_system(); + if (!ms) + 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) + return; + 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; + 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 (!plausible_name(name) || !plausible_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 + } + if (last.empty()) { + logd("skybox: could not capture the map sky"); + return; + } + g_sky_original = last; + logf("skybox: map sky is %s", g_sky_original.c_str()); +} + +} // 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]; +} + +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_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(); + return; + } + + capture_map_sky(); // before the first override of this map + const char *want = skybox_option_name(choice); + if (g_sky_active == want) + 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 +} -- cgit v1.2.3