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/gui.cpp | 66 +++++++++++++++++++++++-- src/hl2h.cpp | 1 + src/hl2h.h | 11 ++++- src/menu.cpp | 21 +++++++- src/world.cpp | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 244 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/gui.cpp b/src/gui.cpp index aa47868..5fb8fa7 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -183,7 +183,8 @@ enum CtrlKind { CK_SECTION, CK_INFO, CK_BUTTON, - CK_COLOR + CK_COLOR, + CK_CHOICE }; enum CtrlAction { ACT_NONE = 0, ACT_RELOAD, ACT_RESET }; @@ -195,6 +196,8 @@ 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" @@ -253,6 +256,8 @@ 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; @@ -282,11 +287,19 @@ 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.ival) + if ((c.kind == CK_SLIDER || c.kind == CK_CHOICE) && c.ival) return *c.ival; return 0; } @@ -294,7 +307,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.ival) { + } else if ((c.kind == CK_SLIDER || c.kind == CK_CHOICE) && c.ival) { if (v < c.min) v = c.min; if (v > c.max) v = c.max; if (*c.ival != v) { @@ -420,6 +433,13 @@ static void build_cards() { add_toggle(fog, "Remove fog", "Disable fog entirely instead of recolouring it.", &g_fog_disable); + 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.", + true); + add_choice(sky, "Sky", "Pick a skybox from the HL2 set.", skybox_options(), + skybox_option_count(), &g_skybox_choice); + int spread = add_card(3, &g_spread, "Spread control", "Choose a server shot seed that tightens the cone.", true); @@ -835,6 +855,8 @@ 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); } @@ -1409,6 +1431,25 @@ 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, @@ -1799,6 +1840,25 @@ 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.cpp b/src/hl2h.cpp index 3f1acb4..4ba1af3 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -592,6 +592,7 @@ static void do_move(int seq, float frametime) { fullbright_invalidate(); world_tint_invalidate(); fog_invalidate(); + skybox_invalidate(); } if (feature_active(FEAT_SPREAD)) { diff --git a/src/hl2h.h b/src/hl2h.h index 877afac..3cb131d 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -180,7 +180,8 @@ enum FeatureId { FEAT_FULLBRIGHT = 10, FEAT_WORLD_TINT = 11, FEAT_FOG = 12, - FEAT_COUNT = 13 + FEAT_SKYBOX = 13, + FEAT_COUNT = 14 }; enum EspOption { ESP_OPT_SHOW_NEUTRAL = 0, @@ -213,11 +214,15 @@ struct FeatureState { extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp, g_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright, - g_world_tint, g_fog; + g_world_tint, g_fog, g_skybox; 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(); extern bool g_bhop_autostrafe; extern bool g_jumpbug_assist; extern bool g_spread_no_spread; @@ -260,6 +265,8 @@ void world_tint_update(); void world_tint_invalidate(); void fog_update(); void fog_invalidate(); +void skybox_update(); +void skybox_invalidate(); 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 237502b..dfd973d 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -59,6 +59,8 @@ 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 bool g_item_health = true; bool g_item_ammo = true; bool g_item_armor = true; @@ -212,6 +214,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, "[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, @@ -246,7 +250,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_tint = false, in_fog = false, in_skybox = false; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { @@ -270,6 +274,8 @@ static bool config_load() { cur = &g_world_tint; else if (strstr(line, "[fog]")) cur = &g_fog; + else if (strstr(line, "[skybox]")) + cur = &g_skybox; else if (strstr(line, "[aimbot]")) cur = &g_aimbot; else if (strstr(line, "[triggerbot]")) @@ -287,6 +293,7 @@ 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) @@ -407,6 +414,12 @@ 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; @@ -475,6 +488,8 @@ void config_reset_defaults() { g_fog_rgb[2] = 190; 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; @@ -592,7 +607,7 @@ static FeatureState *feature_state(int feature) { static FeatureState *feats[FEAT_COUNT] = { &g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot, &g_silentaim, &g_jumpbug, &g_melee, &g_spread, &g_item_esp, - &g_fullbright, &g_world_tint, &g_fog}; + &g_fullbright, &g_world_tint, &g_fog, &g_skybox}; if (feature < 0 || feature >= FEAT_COUNT) return nullptr; return feats[feature]; @@ -800,6 +815,7 @@ void outer_draw() { fullbright_update(); world_tint_update(); fog_update(); + skybox_update(); gui_handle_input(); // HL2H_DRAW_TEST=1 draws a probe rect without needing the GUI open static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr; @@ -822,6 +838,7 @@ void outer_draw() { font_reset = true; world_tint_invalidate(); fog_invalidate(); + skybox_invalidate(); } } if (font_reset && g_font) { 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