summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-03 23:51:02 +0100
committersalmonik <salmonik@tuta.io>2026-11-03 23:51:02 +0100
commit95b5be2299b332917ae63c45252cb9744a3460cd (patch)
tree1da6d89d8ddf9f724368738a4969f8dd308cbcce /src
parentAI: treat scripted NPCs as neutral; Lamarr neutral (diff)
downloadhl2h-95b5be2299b332917ae63c45252cb9744a3460cd.tar.gz
hl2h-95b5be2299b332917ae63c45252cb9744a3460cd.zip
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/<name>... 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).
Diffstat (limited to 'src')
-rw-r--r--src/gui.cpp66
-rw-r--r--src/hl2h.cpp1
-rw-r--r--src/hl2h.h11
-rw-r--r--src/menu.cpp21
-rw-r--r--src/world.cpp152
5 files changed, 244 insertions, 7 deletions
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 {
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
187}; 188};
188 189
189enum CtrlAction { ACT_NONE = 0, ACT_RELOAD, ACT_RESET }; 190enum CtrlAction { ACT_NONE = 0, ACT_RELOAD, ACT_RESET };
@@ -195,6 +196,8 @@ struct Ctrl {
195 bool *bval; // CK_TOGGLE 196 bool *bval; // CK_TOGGLE
196 int *ival; // CK_SLIDER 197 int *ival; // CK_SLIDER
197 int *rgb; // CK_COLOR: three ints, 0..255 198 int *rgb; // CK_COLOR: three ints, 0..255
199 const char *const *opts; // CK_CHOICE: option labels
200 int nopts;
198 int min, max; 201 int min, max;
199 int action; // CK_BUTTON 202 int action; // CK_BUTTON
200 bool zero_unlimited; // slider: value 0 displays as "All" 203 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,
253 c.action = action; 256 c.action = action;
254 c.zero_unlimited = false; 257 c.zero_unlimited = false;
255 c.rgb = nullptr; 258 c.rgb = nullptr;
259 c.opts = nullptr;
260 c.nopts = 0;
256 c.hue = 0.0f; 261 c.hue = 0.0f;
257 c.sat = 0.0f; 262 c.sat = 0.0f;
258 c.val = 1.0f; 263 c.val = 1.0f;
@@ -282,11 +287,19 @@ static void add_color(int card, const char *label, const char *desc,
282 add_ctrl(card, CK_COLOR, label, desc, 0, 255, nullptr, nullptr, ACT_NONE); 287 add_ctrl(card, CK_COLOR, label, desc, 0, 255, nullptr, nullptr, ACT_NONE);
283 g_ctrls[i].rgb = rgb; 288 g_ctrls[i].rgb = rgb;
284} 289}
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}
285 298
286static int ctrl_val(const Ctrl &c) { 299static int ctrl_val(const Ctrl &c) {
287 if (c.kind == CK_TOGGLE && c.bval) 300 if (c.kind == CK_TOGGLE && c.bval)
288 return *c.bval ? 1 : 0; 301 return *c.bval ? 1 : 0;
289 if (c.kind == CK_SLIDER && c.ival) 302 if ((c.kind == CK_SLIDER || c.kind == CK_CHOICE) && c.ival)
290 return *c.ival; 303 return *c.ival;
291 return 0; 304 return 0;
292} 305}
@@ -294,7 +307,7 @@ static void ctrl_set(Ctrl &c, int v) {
294 if (c.kind == CK_TOGGLE && c.bval) { 307 if (c.kind == CK_TOGGLE && c.bval) {
295 *c.bval = v != 0; 308 *c.bval = v != 0;
296 config_save(); 309 config_save();
297 } else if (c.kind == CK_SLIDER && c.ival) { 310 } else if ((c.kind == CK_SLIDER || c.kind == CK_CHOICE) && c.ival) {
298 if (v < c.min) v = c.min; 311 if (v < c.min) v = c.min;
299 if (v > c.max) v = c.max; 312 if (v > c.max) v = c.max;
300 if (*c.ival != v) { 313 if (*c.ival != v) {
@@ -420,6 +433,13 @@ static void build_cards() {
420 add_toggle(fog, "Remove fog", 433 add_toggle(fog, "Remove fog",
421 "Disable fog entirely instead of recolouring it.", &g_fog_disable); 434 "Disable fog entirely instead of recolouring it.", &g_fog_disable);
422 435
436 int sky = add_card(2, &g_skybox, "Skybox",
437 "Swap the skybox at runtime through sv_skyname. The "
438 "map's own sky comes back on level change.",
439 true);
440 add_choice(sky, "Sky", "Pick a skybox from the HL2 set.", skybox_options(),
441 skybox_option_count(), &g_skybox_choice);
442
423 int spread = add_card(3, &g_spread, "Spread control", 443 int spread = add_card(3, &g_spread, "Spread control",
424 "Choose a server shot seed that tightens the cone.", 444 "Choose a server shot seed that tightens the cone.",
425 true); 445 true);
@@ -835,6 +855,8 @@ static int header_h(const Card &c) {
835static int label_max_w(const Ctrl &c) { 855static int label_max_w(const Ctrl &c) {
836 if (c.kind == CK_SLIDER) 856 if (c.kind == CK_SLIDER)
837 return S(214.0f); 857 return S(214.0f);
858 if (c.kind == CK_CHOICE)
859 return card_content_w() - S(16.0f) - S(250.0f);
838 return card_content_w() - S(16.0f) - S(110.0f); 860 return card_content_w() - S(16.0f) - S(110.0f);
839} 861}
840 862
@@ -1409,6 +1431,25 @@ static void handle_mouse_click() {
1409 } 1431 }
1410 return; 1432 return;
1411 } 1433 }
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 }
1412 if (ct.kind == CK_BUTTON) { 1453 if (ct.kind == CK_BUTTON) {
1413 int bw = button_w(ct.label); 1454 int bw = button_w(ct.label);
1414 Rect b{cr.x + cr.w - S(16.0f) - bw, 1455 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) {
1799 outline(sw, picker_open_for(ct) ? C_ACCENT : C_BORDER, &clip); 1840 outline(sw, picker_open_for(ct) ? C_ACCENT : C_BORDER, &clip);
1800 if (picker_open_for(ct)) 1841 if (picker_open_for(ct))
1801 draw_picker(clip, cr, ct, by); 1842 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);
1802 } 1862 }
1803 } 1863 }
1804 by += rh; 1864 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) {
592 fullbright_invalidate(); 592 fullbright_invalidate();
593 world_tint_invalidate(); 593 world_tint_invalidate();
594 fog_invalidate(); 594 fog_invalidate();
595 skybox_invalidate();
595 } 596 }
596 597
597 if (feature_active(FEAT_SPREAD)) { 598 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 {
180 FEAT_FULLBRIGHT = 10, 180 FEAT_FULLBRIGHT = 10,
181 FEAT_WORLD_TINT = 11, 181 FEAT_WORLD_TINT = 11,
182 FEAT_FOG = 12, 182 FEAT_FOG = 12,
183 FEAT_COUNT = 13 183 FEAT_SKYBOX = 13,
184 FEAT_COUNT = 14
184}; 185};
185enum EspOption { 186enum EspOption {
186 ESP_OPT_SHOW_NEUTRAL = 0, 187 ESP_OPT_SHOW_NEUTRAL = 0,
@@ -213,11 +214,15 @@ struct FeatureState {
213 214
214extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp, 215extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp,
215 g_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright, 216 g_aimbot, g_triggerbot, g_silentaim, g_item_esp, g_fullbright,
216 g_world_tint, g_fog; 217 g_world_tint, g_fog, g_skybox;
217extern bool g_tint_world_on, g_tint_sky_on, g_tint_prop_on; 218extern bool g_tint_world_on, g_tint_sky_on, g_tint_prop_on;
218extern 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];
219extern bool g_fog_skybox, g_fog_disable; 220extern bool g_fog_skybox, g_fog_disable;
220extern 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;
223int skybox_option_count();
224const char *skybox_option_name(int i);
225const char *const *skybox_options();
221extern bool g_bhop_autostrafe; 226extern bool g_bhop_autostrafe;
222extern bool g_jumpbug_assist; 227extern bool g_jumpbug_assist;
223extern bool g_spread_no_spread; 228extern bool g_spread_no_spread;
@@ -260,6 +265,8 @@ void world_tint_update();
260void world_tint_invalidate(); 265void world_tint_invalidate();
261void fog_update(); 266void fog_update();
262void fog_invalidate(); 267void fog_invalidate();
268void skybox_update();
269void skybox_invalidate();
263bool resolve_server_netvars(); 270bool resolve_server_netvars();
264int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); 271int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out);
265bool server_set_velocity_z(int idx, float vz); 272bool 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;
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
63int g_skybox_choice = 0; // 0 = map default
62bool g_item_health = true; 64bool g_item_health = true;
63bool g_item_ammo = true; 65bool g_item_ammo = true;
64bool g_item_armor = true; 66bool g_item_armor = true;
@@ -212,6 +214,8 @@ void config_save() {
212 "start=%d\nend=%d\nskybox=%d\ndisable=%d\n", 214 "start=%d\nend=%d\nskybox=%d\ndisable=%d\n",
213 g_fog.active, g_fog.bind, g_fog_rgb[0], g_fog_rgb[1], g_fog_rgb[2], 215 g_fog.active, g_fog.bind, g_fog_rgb[0], g_fog_rgb[1], g_fog_rgb[2],
214 g_fog_start, g_fog_end, (int)g_fog_skybox, (int)g_fog_disable); 216 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,
218 g_skybox.bind, g_skybox_choice);
215 fprintf(f, "[items]\nactive=%d\nbind=%d\nhealth=%d\nammo=%d\n" 219 fprintf(f, "[items]\nactive=%d\nbind=%d\nhealth=%d\nammo=%d\n"
216 "armor=%d\nweapons=%d\ncrates=%d\ndistance=%d\nmax_dist=%d\n", 220 "armor=%d\nweapons=%d\ncrates=%d\ndistance=%d\nmax_dist=%d\n",
217 g_item_esp.active, g_item_esp.bind, (int)g_item_health, 221 g_item_esp.active, g_item_esp.bind, (int)g_item_health,
@@ -246,7 +250,7 @@ static bool config_load() {
246 FeatureState *cur = nullptr; 250 FeatureState *cur = nullptr;
247 bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false, 251 bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false,
248 in_jumpbug = false, in_spread = false, in_items = false, 252 in_jumpbug = false, in_spread = false, in_items = false,
249 in_tint = false, in_fog = false; 253 in_tint = false, in_fog = false, in_skybox = false;
250 while (fgets(line, sizeof(line), f)) { 254 while (fgets(line, sizeof(line), f)) {
251 line[strcspn(line, "\r\n")] = 0; 255 line[strcspn(line, "\r\n")] = 0;
252 if (line[0] == '[') { 256 if (line[0] == '[') {
@@ -270,6 +274,8 @@ static bool config_load() {
270 cur = &g_world_tint; 274 cur = &g_world_tint;
271 else if (strstr(line, "[fog]")) 275 else if (strstr(line, "[fog]"))
272 cur = &g_fog; 276 cur = &g_fog;
277 else if (strstr(line, "[skybox]"))
278 cur = &g_skybox;
273 else if (strstr(line, "[aimbot]")) 279 else if (strstr(line, "[aimbot]"))
274 cur = &g_aimbot; 280 cur = &g_aimbot;
275 else if (strstr(line, "[triggerbot]")) 281 else if (strstr(line, "[triggerbot]"))
@@ -287,6 +293,7 @@ static bool config_load() {
287 in_items = (cur == &g_item_esp); 293 in_items = (cur == &g_item_esp);
288 in_tint = (cur == &g_world_tint); 294 in_tint = (cur == &g_world_tint);
289 in_fog = (cur == &g_fog); 295 in_fog = (cur == &g_fog);
296 in_skybox = (cur == &g_skybox);
290 } else if (cur) { 297 } else if (cur) {
291 char *eq = strchr(line, '='); 298 char *eq = strchr(line, '=');
292 if (!eq) 299 if (!eq)
@@ -407,6 +414,12 @@ static bool config_load() {
407 g_fog_disable = atoi(val) != 0; 414 g_fog_disable = atoi(val) != 0;
408 } 415 }
409 } 416 }
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 }
410 if (in_esp) { 423 if (in_esp) {
411 if (strcmp(key, "neutral") == 0) 424 if (strcmp(key, "neutral") == 0)
412 g_esp_opt[ESP_OPT_SHOW_NEUTRAL] = atoi(val) != 0; 425 g_esp_opt[ESP_OPT_SHOW_NEUTRAL] = atoi(val) != 0;
@@ -475,6 +488,8 @@ void config_reset_defaults() {
475 g_fog_rgb[2] = 190; 488 g_fog_rgb[2] = 190;
476 g_fog_start = 200; 489 g_fog_start = 200;
477 g_fog_end = 3000; 490 g_fog_end = 3000;
491 g_skybox = {false, 0};
492 g_skybox_choice = 0;
478 g_item_health = true; 493 g_item_health = true;
479 g_item_ammo = true; 494 g_item_ammo = true;
480 g_item_armor = true; 495 g_item_armor = true;
@@ -592,7 +607,7 @@ static FeatureState *feature_state(int feature) {
592 static FeatureState *feats[FEAT_COUNT] = { 607 static FeatureState *feats[FEAT_COUNT] = {
593 &g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot, 608 &g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot,
594 &g_silentaim, &g_jumpbug, &g_melee, &g_spread, &g_item_esp, 609 &g_silentaim, &g_jumpbug, &g_melee, &g_spread, &g_item_esp,
595 &g_fullbright, &g_world_tint, &g_fog}; 610 &g_fullbright, &g_world_tint, &g_fog, &g_skybox};
596 if (feature < 0 || feature >= FEAT_COUNT) 611 if (feature < 0 || feature >= FEAT_COUNT)
597 return nullptr; 612 return nullptr;
598 return feats[feature]; 613 return feats[feature];
@@ -800,6 +815,7 @@ void outer_draw() {
800 fullbright_update(); 815 fullbright_update();
801 world_tint_update(); 816 world_tint_update();
802 fog_update(); 817 fog_update();
818 skybox_update();
803 gui_handle_input(); 819 gui_handle_input();
804 // HL2H_DRAW_TEST=1 draws a probe rect without needing the GUI open 820 // HL2H_DRAW_TEST=1 draws a probe rect without needing the GUI open
805 static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr; 821 static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr;
@@ -822,6 +838,7 @@ void outer_draw() {
822 font_reset = true; 838 font_reset = true;
823 world_tint_invalidate(); 839 world_tint_invalidate();
824 fog_invalidate(); 840 fog_invalidate();
841 skybox_invalidate();
825 } 842 }
826 } 843 }
827 if (font_reset && g_font) { 844 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() {
340 logf("fog: %s colour=(%d,%d,%d) start=%d end=%d skybox=%d", disable ? "off" 340 logf("fog: %s colour=(%d,%d,%d) start=%d end=%d skybox=%d", disable ? "off"
341 : "on", r, g, b, start, end, (int)skybox); 341 : "on", r, g, b, start, end, (int)skybox);
342} 342}
343
344// ---------------------------------------------------------------------------
345// Skybox changer (sv_skyname; same mechanism as the skybox_swapper entity)
346// ---------------------------------------------------------------------------
347
348namespace {
349
350// HL2's skybox set. Index 0 means "do not force a sky"; the rest are the
351// base names the engine expands to skybox/<name>{rt,bk,lf,ft,up,dn}.
352const char *const kSkyboxLabels[] = {
353 "Map default", "sky_day01_01", "sky_day01_04", "sky_day01_05",
354 "sky_day01_06", "sky_day01_07", "sky_day01_08", "sky_day01_09",
355 "sky_day02_01", "sky_day02_02", "sky_day02_03", "sky_day02_04",
356 "sky_day02_05", "sky_day02_06", "sky_day02_07", "sky_day02_09",
357 "sky_day02_10", "sky_day03_01", "sky_day03_02", "sky_day03_03",
358 "sky_day03_04", "sky_day03_05", "sky_day03_06", "sky_day03_06b",
359 "sky_day03_06c", "sky_wasteland02", "sky_borealis01", "sky_fog_002",
360 "sky_fake_white"};
361
362std::string g_sky_original; // map's own sky, captured before overriding
363bool g_sky_original_valid = false;
364std::string g_sky_active; // value we forced, empty when not forcing
365
366// "skybox/sky_day01_01_hdrrt" -> "sky_day01_01"
367bool parse_sky_material(const char *mat, char *out, size_t outn) {
368 if (!mat)
369 return false;
370 const char *n = mat;
371 if (strncmp(n, "skybox/", 7) == 0)
372 n += 7;
373 size_t len = strlen(n);
374 if (len < 4)
375 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 }
385 if (!face)
386 return false;
387 size_t blen = len - 2;
388 if (blen > 4 && (strncmp(n + blen - 4, "_hdr", 4) == 0 ||
389 strncmp(n + blen - 4, "_ldr", 4) == 0))
390 blen -= 4;
391 if (blen == 0 || blen >= outn)
392 return false;
393 memcpy(out, n, blen);
394 out[blen] = 0;
395 return true;
396}
397
398void capture_map_sky() {
399 if (g_sky_original_valid)
400 return;
401 g_sky_original_valid = true; // one attempt per map, even on failure
402 void *ms = material_system();
403 if (!ms)
404 return;
405 auto invalid = vfunc<MatCalls::InvalidFn>(ms, MATSYS_InvalidMaterial);
406 auto first = vfunc<MatCalls::FirstFn>(ms, MATSYS_FirstMaterial);
407 auto next = vfunc<MatCalls::NextFn>(ms, MATSYS_NextMaterial);
408 auto get = vfunc<MatCalls::GetFn>(ms, MATSYS_GetMaterial);
409 auto count = vfunc<MatCalls::CountFn>(ms, MATSYS_GetNumMaterials);
410 int total = count(ms);
411 if (total <= 0 || total > 200000)
412 return;
413 unsigned short inv = invalid(ms);
414 int seen = 0;
415 std::string last;
416 for (unsigned short h = first(ms); h != inv; h = next(ms, h)) {
417 if (++seen > total + 16)
418 break;
419 void *mat = get(ms, h);
420 if (!mat)
421 continue;
422 const char *name = vfunc<MatCalls::NameFn>(mat, MAT_GetName)(mat);
423 const char *group =
424 vfunc<MatCalls::GroupFn>(mat, MAT_GetTextureGroupName)(mat);
425 if (!plausible_name(name) || !plausible_name(group))
426 continue;
427 if (strncmp(group, GROUP_SKYBOX, strlen(GROUP_SKYBOX)) != 0)
428 continue;
429 char base[128];
430 if (parse_sky_material(name, base, sizeof(base)))
431 last = base; // newest loaded sky materials win after a level change
432 }
433 if (last.empty()) {
434 logd("skybox: could not capture the map sky");
435 return;
436 }
437 g_sky_original = last;
438 logf("skybox: map sky is %s", g_sky_original.c_str());
439}
440
441} // namespace
442
443int skybox_option_count() {
444 return (int)(sizeof(kSkyboxLabels) / sizeof(kSkyboxLabels[0]));
445}
446
447const char *skybox_option_name(int i) {
448 int n = skybox_option_count();
449 return (i >= 0 && i < n) ? kSkyboxLabels[i] : kSkyboxLabels[0];
450}
451
452const char *const *skybox_options() { return kSkyboxLabels; }
453
454void skybox_invalidate() {
455 // Level change: the engine sets sv_skyname from worldspawn again, so there is
456 // nothing to restore. Forget the captured base and let it recapture.
457 g_sky_original.clear();
458 g_sky_original_valid = false;
459 g_sky_active.clear();
460}
461
462void skybox_update() {
463 bool on = feature_active(FEAT_SKYBOX);
464 int choice = g_skybox_choice;
465 int n = skybox_option_count();
466 if (choice < 0 || choice >= n)
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;
482 }
483
484 capture_map_sky(); // before the first override of this map
485 const char *want = skybox_option_name(choice);
486 if (g_sky_active == want)
487 return;
488 char cmd[160];
489 snprintf(cmd, sizeof(cmd), "sv_skyname %s", want);
490 console_cmd(cmd);
491 logf("skybox: %s", want);
492 g_sky_active = want;
493 world_tint_invalidate(); // tint materials for the freshly loaded sky
494}