1 /*
2 * Portions of this file are copyright Rebirth contributors and licensed as
3 * described in COPYING.txt.
4 * Portions of this file are copyright Parallax Software and licensed
5 * according to the Parallax license below.
6 * See COPYING.txt for license details.
7
8 THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
9 SOFTWARE CORPORATION ("PARALLAX"). PARALLAX, IN DISTRIBUTING THE CODE TO
10 END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
11 ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
12 IN USING, DISPLAYING, AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
13 SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
14 FREE PURPOSES. IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
15 CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES. THE END-USER UNDERSTANDS
16 AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
17 COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION. ALL RIGHTS RESERVED.
18 */
19
20 /*
21 *
22 * Inferno main menu.
23 *
24 */
25
26 #include <stdio.h>
27 #include <string.h>
28 #include <SDL.h>
29
30 #include "menu.h"
31 #include "inferno.h"
32 #include "game.h"
33 #include "gr.h"
34 #include "key.h"
35 #include "mouse.h"
36 #include "u_mem.h"
37 #include "dxxerror.h"
38 #include "bm.h"
39 #include "screens.h"
40 #include "joy.h"
41 #include "player.h"
42 #include "vecmat.h"
43 #include "game.h"
44 #include "palette.h"
45 #include "args.h"
46 #include "newdemo.h"
47 #include "timer.h"
48 #include "sounds.h"
49 #include "gameseq.h"
50 #include "text.h"
51 #include "gamefont.h"
52 #include "newmenu.h"
53 #include "scores.h"
54 #include "playsave.h"
55 #include "kconfig.h"
56 #include "credits.h"
57 #include "polyobj.h"
58 #include "state.h"
59 #include "mission.h"
60 #include "songs.h"
61 #if DXX_USE_SDLMIXER
62 #include "jukebox.h" // for jukebox_exts
63 #endif
64 #include "config.h"
65 #if defined(DXX_BUILD_DESCENT_II)
66 #include "movie.h"
67 #endif
68 #include "gamepal.h"
69 #include "powerup.h"
70 #include "strutil.h"
71 #include "multi.h"
72 #include "vers_id.h"
73 #if DXX_USE_UDP
74 #include "net_udp.h"
75 #endif
76 #if DXX_USE_EDITOR
77 #include "editor/editor.h"
78 #include "editor/kdefs.h"
79 #endif
80 #if DXX_USE_OGL
81 #include "ogl_init.h"
82 #include "ogl_extensions.h"
83 #endif
84 #include "physfs_list.h"
85
86 #include "dxxsconf.h"
87 #include "dsx-ns.h"
88 #include "compiler-range_for.h"
89 #include "d_enumerate.h"
90 #include "d_range.h"
91 #include "d_zip.h"
92 #include "partial_range.h"
93 #include <memory>
94 #include <utility>
95
96 // Menu IDs...
97
98 namespace dcx {
99
100 namespace {
101
102 enum {
103 optgrp_autoselect_firing,
104 };
105
106 enum class main_menu_item_index
107 {
108 start_new_singleplayer_game = 0,
109 load_existing_singleplayer_game,
110 #if DXX_USE_UDP
111 open_multiplayer_submenu,
112 #endif
113 open_options_submenu,
114 create_new_pilot_profile,
115 open_pick_recorded_demo_submenu,
116 open_high_scores_dialog,
117 open_credits_scroll_window,
118 quit_program,
119 #ifndef RELEASE
120 #if DXX_USE_EDITOR
121 open_mine_editor_window,
122 #endif
123 open_coder_sandbox_submenu,
124 #endif
125 end,
126 };
127
128 struct main_menu_items
129 {
130 enumerated_array<newmenu_item, static_cast<std::size_t>(main_menu_item_index::end), main_menu_item_index> m;
131 main_menu_items();
132 };
133
134 #if DXX_USE_UDP
135 enum class netgame_menu_item_index
136 {
137 start_new_multiplayer_game,
138 list_multiplayer_games,
139 join_multiplayer_game,
140 };
141
142 struct netgame_menu_items
143 {
144 enumerated_array<newmenu_item, 3, netgame_menu_item_index> m;
145 netgame_menu_items();
146 };
147
netgame_menu_items()148 netgame_menu_items::netgame_menu_items()
149 {
150 nm_set_item_menu(m[netgame_menu_item_index::start_new_multiplayer_game], "HOST GAME");
151 #if DXX_USE_TRACKER
152 #define DXX_MULTIPLAYER_MENU_FIND_GAME_TYPE_STRING "/ONLINE"
153 #else
154 #define DXX_MULTIPLAYER_MENU_FIND_GAME_TYPE_STRING ""
155 #endif
156 nm_set_item_menu(m[netgame_menu_item_index::list_multiplayer_games], "FIND LAN" DXX_MULTIPLAYER_MENU_FIND_GAME_TYPE_STRING " GAMES");
157 #undef DXX_MULTIPLAYER_MENU_FIND_GAME_TYPE_STRING
158 nm_set_item_menu(m[netgame_menu_item_index::join_multiplayer_game], "JOIN GAME MANUALLY");
159 }
160 #endif
161
162 static std::array<window *, 16> menus;
163
main_menu_items()164 main_menu_items::main_menu_items()
165 {
166 nm_set_item_menu(m[main_menu_item_index::start_new_singleplayer_game], TXT_NEW_GAME);
167 nm_set_item_menu(m[main_menu_item_index::load_existing_singleplayer_game], TXT_LOAD_GAME);
168 #if DXX_USE_UDP
169 nm_set_item_menu(m[main_menu_item_index::open_multiplayer_submenu], TXT_MULTIPLAYER_);
170 #endif
171
172 nm_set_item_menu(m[main_menu_item_index::open_options_submenu], TXT_OPTIONS_);
173 nm_set_item_menu(m[main_menu_item_index::create_new_pilot_profile], TXT_CHANGE_PILOTS);
174 nm_set_item_menu(m[main_menu_item_index::open_pick_recorded_demo_submenu], TXT_VIEW_DEMO);
175 nm_set_item_menu(m[main_menu_item_index::open_high_scores_dialog], TXT_VIEW_SCORES);
176 nm_set_item_menu(m[main_menu_item_index::open_credits_scroll_window], TXT_CREDITS);
177 nm_set_item_menu(m[main_menu_item_index::quit_program], TXT_QUIT);
178
179 #ifndef RELEASE
180 #if DXX_USE_EDITOR
181 nm_set_item_menu(m[main_menu_item_index::open_mine_editor_window], " Editor");
182 #endif
183 nm_set_item_menu(m[main_menu_item_index::open_coder_sandbox_submenu], " SANDBOX");
184 #endif
185 }
186
delete_player_single_player_saved_game(const char * const name,const unsigned i)187 static void delete_player_single_player_saved_game(const char *const name, const unsigned i)
188 {
189 char filename[PATH_MAX];
190 snprintf(filename, sizeof(filename), PLAYER_DIRECTORY_STRING("%s.sg%x"), name, i);
191 PHYSFS_delete(filename);
192 }
193
delete_player_multi_player_saved_game(const char * const name,const unsigned i)194 static void delete_player_multi_player_saved_game(const char *const name, const unsigned i)
195 {
196 char filename[PATH_MAX];
197 snprintf(filename, sizeof(filename), PLAYER_DIRECTORY_STRING("%s.mg%x"), name, i);
198 PHYSFS_delete(filename);
199 }
200
delete_player_saved_games(const char * const name)201 static void delete_player_saved_games(const char *const name)
202 {
203 for (const auto i : xrange(11u))
204 {
205 delete_player_single_player_saved_game(name, i);
206 delete_player_multi_player_saved_game(name, i);
207 }
208 }
209
210 template <typename T>
211 using select_file_subfunction = window_event_result (*)(T *, const char *);
212
format_human_readable_time(char * const data,std::size_t size,const int duration_seconds)213 void format_human_readable_time(char *const data, std::size_t size, const int duration_seconds)
214 {
215 const auto &&split_interval = std::div(duration_seconds, static_cast<int>(std::chrono::minutes::period::num));
216 snprintf(data, size, "%im%is", split_interval.quot, split_interval.rem);
217 }
218
parse_human_readable_time(const char * const buf)219 std::pair<std::chrono::seconds, bool> parse_human_readable_time(const char *const buf)
220 {
221 char *p{};
222 const std::chrono::minutes m(strtoul(buf, &p, 10));
223 if (*p == 0)
224 /* Assume that a pure-integer string is a count of minutes. */
225 return {m, true};
226 const auto c0 = *p;
227 if (c0 == 'm')
228 {
229 const std::chrono::seconds s(strtoul(p + 1, &p, 10));
230 if (*p == 's')
231 /* The trailing 's' is optional, but no character other than
232 * the optional 's' can follow the number.
233 */
234 ++p;
235 if (*p == 0)
236 return {m + s, true};
237 }
238 else if (c0 == 's' && p[1] == 0)
239 /* Input is only seconds. Use `.count()` to extract the raw
240 * value without scaling.
241 */
242 return {std::chrono::seconds(m.count()), true};
243 return {{}, false};
244 }
245
246 #if DXX_USE_SDLMIXER
247 enum class select_dir_flag : uint8_t
248 {
249 files_only,
250 directories_or_files,
251 };
252
253 __attribute_nonnull()
254 static int select_file_recursive(const menu_title title, const std::array<char, PATH_MAX> &orig_path, const partial_range_t<const file_extension_t *> &ext_list, select_dir_flag select_dir, ntstring<PATH_MAX - 1> &userdata);
255
get_absolute_path(ntstring<PATH_MAX-1> & full_path,const char * rel_path)256 static window_event_result get_absolute_path(ntstring<PATH_MAX - 1> &full_path, const char *rel_path)
257 {
258 PHYSFSX_getRealPath(rel_path, full_path);
259 return window_event_result::close;
260 }
261
262 #define SELECT_SONG(t, s) select_file_recursive(t, CGameCfg.CMMiscMusic[s], jukebox_exts, select_dir_flag::files_only, CGameCfg.CMMiscMusic[s])
263 #endif
264
265 }
266
267 template <typename Rep, std::size_t S>
format_human_readable_time(std::array<char,S> & buf,const std::chrono::duration<Rep,std::chrono::seconds::period> duration)268 void format_human_readable_time(std::array<char, S> &buf, const std::chrono::duration<Rep, std::chrono::seconds::period> duration)
269 {
270 static_assert(S >= std::tuple_size<human_readable_mmss_time<Rep>>::value, "array is too small");
271 static_assert(std::numeric_limits<Rep>::max() <= std::numeric_limits<int>::max(), "Rep allows too large a value");
272 format_human_readable_time(buf.data(), buf.size(), duration.count());
273 }
274
275 template <typename Rep, std::size_t S>
parse_human_readable_time(std::chrono::duration<Rep,std::chrono::seconds::period> & duration,const std::array<char,S> & buf)276 void parse_human_readable_time(std::chrono::duration<Rep, std::chrono::seconds::period> &duration, const std::array<char, S> &buf)
277 {
278 const auto &&r = parse_human_readable_time(buf.data());
279 if (r.second)
280 duration = r.first;
281 }
282
283 template void format_human_readable_time(human_readable_mmss_time<autosave_interval_type::rep> &buf, autosave_interval_type);
284 template void parse_human_readable_time(autosave_interval_type &, const human_readable_mmss_time<autosave_interval_type::rep> &buf);
285
286 }
287
288 namespace dsx {
289
290 namespace {
291
292 struct main_menu : main_menu_items, newmenu
293 {
main_menudsx::__anon301c837e0311::main_menu294 main_menu(grs_canvas &src) :
295 newmenu(menu_title{""}, menu_subtitle{nullptr}, menu_filename{Menu_pcx_name}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 0), src, draw_box_flag::none)
296 {
297 }
298 virtual window_event_result event_handler(const d_event &event) override;
299 };
300
301 static window_event_result do_new_game_menu();
302 #ifndef RELEASE
303 void do_sandbox_menu();
304 #endif
305 int select_demo();
306 #if DXX_USE_UDP
307 static void do_multi_player_menu();
308 #endif
309
310 #if DXX_USE_UDP
311 struct netgame_menu : netgame_menu_items, newmenu
312 {
netgame_menudsx::__anon301c837e0311::netgame_menu313 netgame_menu(grs_canvas &src) :
314 newmenu(menu_title{nullptr}, menu_subtitle{TXT_MULTIPLAYER}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 0), src)
315 {
316 }
317 virtual window_event_result event_handler(const d_event &event) override;
318 };
319 #endif
320
321 }
322
323 }
324
325 // Hide all menus
hide_menus(void)326 int hide_menus(void)
327 {
328 window *wind;
329 if (menus[0])
330 return 0; // there are already hidden menus
331
332 wind = window_get_front();
333 range_for (auto &i, menus)
334 {
335 i = wind;
336 if (!wind)
337 break;
338 wind = wind->set_visible(0);
339 }
340 Assert(window_get_front() == NULL);
341 return 1;
342 }
343
344 // Show all menus, with the front one shown first
345 // This makes sure EVENT_WINDOW_ACTIVATED is only sent to that window
show_menus(void)346 void show_menus(void)
347 {
348 range_for (auto &i, menus)
349 {
350 if (!i)
351 break;
352
353 // Hidden windows don't receive events, so the only way to close is outside its handler
354 // Which there should be no cases of here
355 // window_exists could return a false positive if a new window was created
356 // with the same pointer value as the deleted one, so killing window_exists (call and function)
357 // if (window_exists(i))
358 std::exchange(i, nullptr)->set_visible(1);
359 }
360 }
361
362 namespace dcx {
363
364 /* This is a hack to prevent writing to freed memory. Various points in
365 * the game code call `hide_menus()`, then later use `show_menus()` to
366 * reverse the effect. If the forcibly hidden window is deleted before
367 * `show_menus()` is called, the attempt to show it would write to freed
368 * memory. This hook is called when a window is deleted, so that the
369 * deleted window can be removed from menus[]. Removing it from menus[]
370 * prevents `show_menus()` trying to make it visible later.
371 *
372 * It would be cleaner, but more invasive, to restructure the code so
373 * that the menus[] array does not need to exist and window pointers are
374 * not stored outside the control of their owner.
375 */
menu_destroy_hook(window * w)376 void menu_destroy_hook(window *w)
377 {
378 const auto &&e = menus.end();
379 const auto &&i = std::find(menus.begin(), e, w);
380 if (i == e)
381 /* Not a hidden menu */
382 return;
383 /* This is not run often enough to merit a clever loop that stops
384 * when it reaches an unused element.
385 */
386 std::move(std::next(i), e, i);
387 menus.back() = nullptr;
388 }
389
390 //pairs of chars describing ranges
391 constexpr char playername_allowed_chars[] = "azAZ09__--";
392
393 }
394
395 namespace dsx {
396
397 namespace {
398
MakeNewPlayerFile(int allow_abort)399 static int MakeNewPlayerFile(int allow_abort)
400 {
401 char filename[PATH_MAX];
402 auto text = InterfaceUniqueState.PilotName;
403
404 for (;;)
405 {
406 using items_type = std::array<newmenu_item, 1>;
407 struct pilot_name_menu : items_type, passive_newmenu
408 {
409 pilot_name_menu(grs_canvas &canvas, callsign_t &text) :
410 items_type{{
411 newmenu_item::nm_item_input(text.a, playername_allowed_chars),
412 }},
413 passive_newmenu(menu_title{nullptr}, menu_subtitle{TXT_ENTER_PILOT_NAME}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(*static_cast<items_type *>(this), 0), canvas)
414 {
415 }
416 };
417 const auto x = run_blocking_newmenu<pilot_name_menu>(*grd_curcanv, text);
418 const char *const name = text;
419 if (x < 0 || !*name)
420 {
421 if (allow_abort)
422 return 0;
423 /* If the entered name is empty, reject it and prompt again.
424 */
425 continue;
426 }
427 text.lower();
428 snprintf(filename, sizeof(filename), PLAYER_DIRECTORY_STRING("%s.plr"), name);
429 if (PHYSFSX_exists(filename, 0))
430 {
431 nm_messagebox(menu_title{nullptr}, 1, TXT_OK, "%s '%s' %s", TXT_PLAYER, name, TXT_ALREADY_EXISTS);
432 continue;
433 }
434 break;
435 }
436
437 new_player_config();
438 InterfaceUniqueState.PilotName = text;
439 InterfaceUniqueState.update_window_title();
440 write_player_file();
441
442 return 1;
443 }
444
445 }
446
447 }
448
449 namespace {
450
451 struct pilot_selection_listbox : listbox
452 {
pilot_selection_listbox__anon301c837e0511::pilot_selection_listbox453 pilot_selection_listbox(int citem, unsigned nitems, std::unique_ptr<const char *[]> name_pointer_strings, PHYSFSX_uncounted_list &&physfs_list_strings, grs_canvas &canvas, uint8_t allow_abort_flag) :
454 listbox(citem, nitems, name_pointer_strings.get(), menu_title{TXT_SELECT_PILOT}, canvas, allow_abort_flag),
455 name_pointer_storage(std::move(name_pointer_strings)), physfs_list_storage(std::move(physfs_list_strings))
456 {
457 }
458 std::unique_ptr<const char *[]> name_pointer_storage;
459 PHYSFSX_uncounted_list physfs_list_storage;
460 virtual window_event_result callback_handler(const d_event &, window_event_result default_return_value) override;
461 };
462
player_menu_keycommand(listbox * lb,const d_event & event)463 static window_event_result player_menu_keycommand( listbox *lb,const d_event &event )
464 {
465 const char **items = listbox_get_items(*lb);
466 int citem = listbox_get_citem(*lb);
467
468 switch (event_key_get(event))
469 {
470 case KEY_CTRLED+KEY_D:
471 if (citem > 0)
472 {
473 int x = 1;
474 x = nm_messagebox(menu_title{nullptr}, 2, TXT_YES, TXT_NO, "%s %s?", TXT_DELETE_PILOT, items[citem]+((items[citem][0]=='$')?1:0) );
475 if (x==0) {
476 char plxfile[PATH_MAX], efffile[PATH_MAX], ngpfile[PATH_MAX];
477 int ret;
478 char name[PATH_MAX];
479
480 snprintf(name, sizeof(name), PLAYER_DIRECTORY_STRING("%.8s.plr"), items[citem]);
481
482 ret = !PHYSFS_delete(name);
483
484 if (!ret)
485 {
486 delete_player_saved_games( items[citem] );
487 // delete PLX file
488 snprintf(plxfile, sizeof(plxfile), PLAYER_DIRECTORY_STRING("%.8s.plx"), items[citem]);
489 if (PHYSFSX_exists(plxfile,0))
490 PHYSFS_delete(plxfile);
491 // delete EFF file
492 snprintf(efffile, sizeof(efffile), PLAYER_DIRECTORY_STRING("%.8s.eff"), items[citem]);
493 if (PHYSFSX_exists(efffile,0))
494 PHYSFS_delete(efffile);
495 // delete NGP file
496 snprintf(ngpfile, sizeof(ngpfile), PLAYER_DIRECTORY_STRING("%.8s.ngp"), items[citem]);
497 if (PHYSFSX_exists(ngpfile,0))
498 PHYSFS_delete(ngpfile);
499 }
500
501 if (ret)
502 nm_messagebox(menu_title{nullptr}, 1, TXT_OK, "%s %s %s", TXT_COULDNT, TXT_DELETE_PILOT, items[citem]+((items[citem][0]=='$')?1:0) );
503 else
504 listbox_delete_item(*lb, citem);
505 }
506
507 return window_event_result::handled;
508 }
509 break;
510 }
511
512 return window_event_result::ignored;
513 }
514
callback_handler(const d_event & event,window_event_result)515 window_event_result pilot_selection_listbox::callback_handler(const d_event &event, window_event_result)
516 {
517 switch (event.type)
518 {
519 case EVENT_KEY_COMMAND:
520 return player_menu_keycommand(this, event);
521 case EVENT_NEWMENU_SELECTED:
522 {
523 auto &citem = static_cast<const d_select_event &>(event).citem;
524 if (citem < 0)
525 return window_event_result::ignored; // shouldn't happen
526 else if (citem == 0)
527 {
528 // They selected 'create new pilot'
529 return MakeNewPlayerFile(1) ? window_event_result::close : window_event_result::handled;
530 }
531 else
532 {
533 const auto p = item[citem];
534 InterfaceUniqueState.PilotName.copy_lower(p, strlen(p));
535 InterfaceUniqueState.update_window_title();
536 }
537 return window_event_result::close;
538 }
539
540 case EVENT_WINDOW_CLOSE:
541 if (read_player_file() != EZERO)
542 return window_event_result::handled; // abort close!
543
544 WriteConfigFile(); // Update lastplr
545 break;
546
547 default:
548 break;
549 }
550
551 return window_event_result::ignored;
552 }
553
554 }
555
556 namespace dsx {
557
558 namespace {
559
560 //Inputs the player's name, without putting up the background screen
RegisterPlayer()561 static void RegisterPlayer()
562 {
563 static const std::array<file_extension_t, 1> types{{"plr"}};
564 int NumItems;
565 int citem = 0;
566 uint8_t allow_abort_flag = 1;
567
568 auto &callsign = InterfaceUniqueState.PilotName;
569 if (!callsign[0u])
570 {
571 if (!*static_cast<const char *>(GameCfg.LastPlayer))
572 {
573 callsign = "player";
574 allow_abort_flag = 0;
575 }
576 else
577 {
578 // Read the last player's name from config file, not lastplr.txt
579 callsign = GameCfg.LastPlayer;
580 }
581 InterfaceUniqueState.update_window_title();
582 }
583
584 auto list = PHYSFSX_findFiles(PLAYER_DIRECTORY_STRING(""), types);
585 if (!list)
586 return; // memory error
587 if (!list[0])
588 {
589 MakeNewPlayerFile(0); // make a new player without showing listbox
590 return;
591 }
592
593
594 for (NumItems = 0; list[NumItems] != NULL; NumItems++) {}
595 NumItems++; // for TXT_CREATE_NEW
596
597 auto m = std::make_unique<const char *[]>(NumItems);
598
599 /* Index of the first undefined element */
600 auto idx_next_string = 0u;
601 m[idx_next_string++] = TXT_CREATE_NEW;
602 const auto idx_first_player_string = idx_next_string;
603
604 range_for (const auto f, list)
605 {
606 const auto p = strchr(f, '.');
607 if (!p)
608 /* This should not happen. */
609 continue;
610 if (f == p)
611 /* First character is '.', so there is no name. */
612 continue;
613 if (std::distance(f, p) > CALLSIGN_LEN)
614 /* Filename is too long to be a valid callsign. */
615 continue;
616 *p = 0;
617 m[idx_next_string++] = f;
618 }
619
620 if (idx_first_player_string == idx_next_string)
621 {
622 /* Every returned file was unacceptable. */
623 MakeNewPlayerFile(0); // make a new player without showing listbox
624 return;
625 }
626
627 for (auto &&[i, mi] : enumerate(unchecked_partial_range(m.get(), idx_next_string)))
628 if (!d_stricmp(static_cast<const char *>(callsign), mi))
629 {
630 citem = i;
631 break;
632 }
633
634 auto lb = window_create<pilot_selection_listbox>(citem, idx_next_string, std::move(m), std::move(list), grd_curscreen->sc_canvas, allow_abort_flag);
635 (void)lb;
636 }
637
638 static void input_config();
639
640 // Draw Copyright and Version strings
draw_copyright(grs_canvas & canvas,grs_font & game_font)641 static void draw_copyright(grs_canvas &canvas, grs_font &game_font)
642 {
643 gr_set_fontcolor(canvas, BM_XRGB(6, 6, 6), -1);
644 const auto &&line_spacing = LINE_SPACING(game_font, game_font);
645 const auto bm_h = canvas.cv_bitmap.bm_h;
646 gr_string(canvas, game_font, 0x8000, bm_h - line_spacing, TXT_COPYRIGHT);
647 gr_set_fontcolor(canvas, BM_XRGB(25, 0, 0), -1);
648 gr_string(canvas, game_font, 0x8000, bm_h - (line_spacing * 2), DESCENT_VERSION);
649 }
650
651 //returns flag, true means quit menu
dispatch_menu_option(const main_menu_item_index select)652 window_event_result dispatch_menu_option(const main_menu_item_index select)
653 {
654 switch (select)
655 {
656 case main_menu_item_index::start_new_singleplayer_game:
657 select_mission(mission_filter_mode::exclude_anarchy, menu_title{"New Game\n\nSelect mission"}, do_new_game_menu);
658 break;
659 case main_menu_item_index::open_pick_recorded_demo_submenu:
660 select_demo();
661 break;
662 case main_menu_item_index::load_existing_singleplayer_game:
663 state_restore_all(0, secret_restore::none, nullptr, blind_save::no);
664 break;
665 #ifndef RELEASE
666 #if DXX_USE_EDITOR
667 case main_menu_item_index::open_mine_editor_window:
668 if (!Current_mission)
669 {
670 create_new_mine();
671 SetPlayerFromCurseg();
672 }
673
674 hide_menus();
675 init_editor();
676 break;
677 #endif
678 #endif
679 case main_menu_item_index::open_high_scores_dialog:
680 scores_view_menu(grd_curscreen->sc_canvas);
681 break;
682 case main_menu_item_index::quit_program:
683 #if DXX_USE_EDITOR
684 if (!SafetyCheck())
685 break;
686 #endif
687 return window_event_result::close;
688
689 case main_menu_item_index::create_new_pilot_profile:
690 RegisterPlayer();
691 break;
692
693 #if DXX_USE_UDP
694 case main_menu_item_index::open_multiplayer_submenu:
695 do_multi_player_menu();
696 break;
697 #endif
698 case main_menu_item_index::open_options_submenu:
699 do_options_menu();
700 break;
701 case main_menu_item_index::open_credits_scroll_window:
702 credits_show();
703 break;
704 #ifndef RELEASE
705 case main_menu_item_index::open_coder_sandbox_submenu:
706 do_sandbox_menu();
707 break;
708 #endif
709 default:
710 break;
711 }
712 return window_event_result::handled; // stay in main menu unless quitting
713 }
714
715 #if DXX_USE_UDP
dispatch_menu_option(const netgame_menu_item_index select)716 window_event_result dispatch_menu_option(const netgame_menu_item_index select)
717 {
718 switch (select)
719 {
720 case netgame_menu_item_index::start_new_multiplayer_game:
721 multi_protocol = MULTI_PROTO_UDP;
722 select_mission(mission_filter_mode::include_anarchy, menu_title{TXT_MULTI_MISSION}, net_udp_setup_game);
723 break;
724 case netgame_menu_item_index::join_multiplayer_game:
725 multi_protocol = MULTI_PROTO_UDP;
726 net_udp_manual_join_game();
727 break;
728 case netgame_menu_item_index::list_multiplayer_games:
729 multi_protocol = MULTI_PROTO_UDP;
730 net_udp_list_join_game(*grd_curcanv);
731 break;
732 default:
733 break;
734 }
735 return window_event_result::handled;
736 }
737 #endif
738
739 // ------------------------------------------------------------------------
event_handler(const d_event & event)740 window_event_result main_menu::event_handler(const d_event &event)
741 {
742 switch (event.type)
743 {
744 case EVENT_WINDOW_CREATED:
745 if (InterfaceUniqueState.PilotName[0u])
746 break;
747 RegisterPlayer();
748 break;
749 case EVENT_WINDOW_ACTIVATED:
750 load_palette(MENU_PALETTE,0,1); //get correct palette
751 keyd_time_when_last_pressed = timer_query(); // .. 20 seconds from now!
752 break;
753
754 case EVENT_KEY_COMMAND:
755 // Don't allow them to hit ESC in the main menu.
756 if (event_key_get(event)==KEY_ESC)
757 return window_event_result::ignored;
758 break;
759
760 case EVENT_MOUSE_BUTTON_DOWN:
761 case EVENT_MOUSE_BUTTON_UP:
762 // Don't allow mousebutton-closing in main menu.
763 if (event_mouse_get_button(event) == MBTN_RIGHT)
764 return window_event_result::ignored;
765 break;
766
767 case EVENT_IDLE:
768 #if defined(DXX_BUILD_DESCENT_I)
769 #define DXX_DEMO_KEY_DELAY 45
770 #elif defined(DXX_BUILD_DESCENT_II)
771 #define DXX_DEMO_KEY_DELAY 25
772 #endif
773 if (keyd_time_when_last_pressed + i2f(DXX_DEMO_KEY_DELAY) < timer_query() || CGameArg.SysAutoDemo)
774 {
775 keyd_time_when_last_pressed = timer_query(); // Reset timer so that disk won't thrash if no demos.
776
777 #if defined(DXX_BUILD_DESCENT_II)
778 int n_demos = newdemo_count_demos();
779 if ((d_rand() % (n_demos+1)) == 0 && !CGameArg.SysAutoDemo)
780 {
781 #if DXX_USE_OGL
782 Screen_mode = -1;
783 #endif
784 PlayMovie("intro.tex", "intro.mve",0);
785 songs_play_song(SONG_TITLE,1);
786 set_screen_mode(SCREEN_MENU);
787 }
788 else
789 #endif
790 {
791 newdemo_start_playback(NULL); // Randomly pick a file, assume native endian (crashes if not)
792 }
793 }
794 break;
795
796 case EVENT_NEWMENU_DRAW:
797 draw_copyright(parent_canvas, *GAME_FONT);
798 break;
799
800 case EVENT_NEWMENU_SELECTED:
801 {
802 auto &citem = static_cast<const d_select_event &>(event).citem;
803 return dispatch_menu_option(static_cast<main_menu_item_index>(citem));
804 }
805
806 default:
807 break;
808 }
809 return newmenu::event_handler(event);
810 }
811
812 }
813
814 // -----------------------------------------------------------------------------
815 // Create the main menu.
816 //returns number of item chosen
DoMenu()817 int DoMenu()
818 {
819 auto menu = window_create<main_menu>(grd_curscreen->sc_canvas);
820 (void)menu;
821 return 0;
822 }
823
824 namespace {
825
826 struct demo_selection_listbox : listbox
827 {
demo_selection_listboxdsx::__anon301c837e0711::demo_selection_listbox828 demo_selection_listbox(unsigned nitems, PHYSFSX_uncounted_list &&physfs_list_strings, grs_canvas &canvas) :
829 listbox(0, nitems, const_cast<const char **>(physfs_list_strings.get()), menu_title{TXT_SELECT_DEMO}, canvas, 1),
830 physfs_list_storage(std::move(physfs_list_strings))
831 {
832 }
833 PHYSFSX_uncounted_list physfs_list_storage;
834 virtual window_event_result callback_handler(const d_event &, window_event_result default_return_value) override;
835 };
836
demo_menu_keycommand(listbox * lb,const d_event & event)837 static window_event_result demo_menu_keycommand( listbox *lb,const d_event &event )
838 {
839 const char **items = listbox_get_items(*lb);
840 int citem = listbox_get_citem(*lb);
841
842 switch (event_key_get(event))
843 {
844 case KEY_CTRLED+KEY_D:
845 if (citem >= 0)
846 {
847 int x = 1;
848 x = nm_messagebox(menu_title{nullptr}, 2, TXT_YES, TXT_NO, "%s %s?", TXT_DELETE_DEMO, items[citem]+((items[citem][0]=='$')?1:0) );
849 if (x==0)
850 {
851 int ret;
852 char name[PATH_MAX];
853
854 strcpy(name, DEMO_DIR);
855 strcat(name,items[citem]);
856
857 ret = !PHYSFS_delete(name);
858
859 if (ret)
860 nm_messagebox(menu_title{nullptr}, 1, TXT_OK, "%s %s %s", TXT_COULDNT, TXT_DELETE_DEMO, items[citem]+((items[citem][0]=='$')?1:0) );
861 else
862 listbox_delete_item(*lb, citem);
863 }
864
865 return window_event_result::handled;
866 }
867 break;
868
869 case KEY_CTRLED+KEY_C:
870 {
871 int x = 1;
872 char bakname[PATH_MAX];
873
874 // Get backup name
875 change_filename_extension(bakname, items[citem]+((items[citem][0]=='$')?1:0), DEMO_BACKUP_EXT);
876 x = nm_messagebox(menu_title{nullptr}, 2, TXT_YES, TXT_NO, "Are you sure you want to\n"
877 "swap the endianness of\n"
878 "%s? If the file is\n"
879 "already endian native, D1X\n"
880 "will likely crash. A backup\n"
881 "%s will be created", items[citem]+((items[citem][0]=='$')?1:0), bakname );
882 if (!x)
883 newdemo_swap_endian(items[citem]);
884
885 return window_event_result::handled;
886 }
887 break;
888 }
889 return window_event_result::ignored;
890 }
891
callback_handler(const d_event & event,window_event_result)892 window_event_result demo_selection_listbox::callback_handler(const d_event &event, window_event_result)
893 {
894 switch (event.type)
895 {
896 case EVENT_KEY_COMMAND:
897 return demo_menu_keycommand(this, event);
898 case EVENT_NEWMENU_SELECTED:
899 {
900 auto &citem = static_cast<const d_select_event &>(event).citem;
901 if (citem < 0)
902 return window_event_result::ignored; // shouldn't happen
903 newdemo_start_playback(item[citem]);
904 return window_event_result::handled; // stay in demo selector
905 }
906 case EVENT_WINDOW_CLOSE:
907 break;
908 default:
909 break;
910 }
911 return window_event_result::ignored;
912 }
913
select_demo()914 int select_demo()
915 {
916 int NumItems;
917
918 auto list = PHYSFSX_findFiles(DEMO_DIR, demo_file_extensions);
919 if (!list)
920 return 0; // memory error
921 if (!list[0])
922 {
923 nm_messagebox(menu_title{nullptr}, 1, TXT_OK, "%s %s\n%s", TXT_NO_DEMO_FILES, TXT_USE_F5, TXT_TO_CREATE_ONE);
924 return 0;
925 }
926
927 for (NumItems = 0; list[NumItems] != NULL; NumItems++) {}
928
929 auto lb = window_create<demo_selection_listbox>(NumItems, std::move(list), grd_curscreen->sc_canvas);
930 (void)lb;
931 return 1;
932 }
933
do_difficulty_menu()934 static int do_difficulty_menu()
935 {
936 using items_type = enumerated_array<newmenu_item, NDL, Difficulty_level_type>;
937 struct difficulty_prompt_menu : items_type, passive_newmenu
938 {
939 difficulty_prompt_menu(const unsigned Difficulty_level) :
940 items_type{{{
941 newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_0)},
942 newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_1)},
943 newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_2)},
944 newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_3)},
945 newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_4)},
946 }}},
947 passive_newmenu(menu_title{nullptr}, menu_subtitle{TXT_DIFFICULTY_LEVEL}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(*static_cast<items_type *>(this), Difficulty_level), grd_curscreen->sc_canvas)
948 {
949 }
950 };
951 auto &Difficulty_level = GameUniqueState.Difficulty_level;
952 const unsigned s = run_blocking_newmenu<difficulty_prompt_menu>(Difficulty_level);
953
954 if (s <= Difficulty_4)
955 {
956 const auto d = static_cast<Difficulty_level_type>(s);
957 if (d != Difficulty_level)
958 {
959 PlayerCfg.DefaultDifficulty = d;
960 write_player_file();
961 }
962 Difficulty_level = d;
963 return 1;
964 }
965 return 0;
966 }
967
do_new_game_menu()968 window_event_result do_new_game_menu()
969 {
970 int new_level_num;
971
972 new_level_num = 1;
973 const auto recorded_player_highest_level = get_highest_level();
974 const auto last_level = Current_mission->last_level;
975 const auto clamped_player_highest_level = std::min<decltype(recorded_player_highest_level)>(recorded_player_highest_level, last_level);
976 if (last_level > 1)
977 {
978 struct items_type
979 {
980 std::array<char, 8> num_text{"1"};
981 std::array<char, 64> subtitle_text;
982 std::array<char, 68> info_text;
983 std::array<newmenu_item, 2> m;
984 items_type(const char *const mission_name, const unsigned last_level, const int clamped_player_highest_level) :
985 m{{
986 newmenu_item::nm_item_text{info_text.data()},
987 newmenu_item::nm_item_input(num_text),
988 }}
989 {
990 char buf[28];
991 std::snprintf(std::data(subtitle_text), std::size(subtitle_text), "%s\n\n%s", TXT_SELECT_START_LEV, mission_name);
992 const auto trailer = clamped_player_highest_level
993 ? (std::snprintf(buf, std::size(buf), "finished level %d", clamped_player_highest_level), buf)
994 : "not finished any level";
995 std::snprintf(std::data(info_text), std::size(info_text), "This mission has %u levels.\n\nYou have %s.", last_level, trailer);
996 }
997 };
998 items_type menu_items{Current_mission->mission_name, last_level, clamped_player_highest_level};
999 for (;;)
1000 {
1001 struct select_start_level_menu : passive_newmenu
1002 {
1003 select_start_level_menu(items_type &i) :
1004 passive_newmenu(menu_title{nullptr}, menu_subtitle{i.subtitle_text.data()}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(i.m, 1), grd_curscreen->sc_canvas)
1005 {
1006 }
1007 };
1008 const int choice = run_blocking_newmenu<select_start_level_menu>(menu_items);
1009
1010 if (choice == -1 || !menu_items.num_text[0])
1011 return window_event_result::handled;
1012
1013 char *p = nullptr;
1014 new_level_num = strtol(menu_items.num_text.data(), &p, 10);
1015
1016 if (*p || new_level_num <= 0 || new_level_num > last_level)
1017 {
1018 nm_messagebox(menu_title{TXT_INVALID_LEVEL}, 1, TXT_OK, "You must enter a\npositive level number\nless than or\nequal to %u.\n", static_cast<unsigned>(Current_mission->last_level));
1019 }
1020 else
1021 break;
1022 }
1023 }
1024
1025 GameUniqueState.Difficulty_level = PlayerCfg.DefaultDifficulty;
1026
1027 if (!do_difficulty_menu())
1028 return window_event_result::handled;
1029
1030 StartNewGame(new_level_num);
1031
1032 return window_event_result::close; // exit mission listbox
1033 }
1034
1035 }
1036
1037 }
1038
1039 static void do_sound_menu();
1040 namespace dsx {
1041 namespace {
1042 static void hud_config();
1043 static void graphics_config();
1044 static void gameplay_config();
1045 }
1046 }
1047
1048 #define DXX_OPTIONS_MENU(VERB) \
1049 DXX_MENUITEM(VERB, MENU, "Sound & music...", sfx) \
1050 DXX_MENUITEM(VERB, MENU, TXT_CONTROLS_, controls) \
1051 DXX_MENUITEM(VERB, MENU, "Graphics...", graphics) \
1052 DXX_MENUITEM(VERB, MENU, "Gameplay...", misc) \
1053
1054 namespace {
1055
1056 class options_menu_items
1057 {
1058 public:
1059 enum
1060 {
1061 DXX_OPTIONS_MENU(ENUM)
1062 };
1063 DXX_OPTIONS_MENU(DECL);
1064 std::array<newmenu_item, DXX_OPTIONS_MENU(COUNT)> m;
options_menu_items()1065 options_menu_items()
1066 {
1067 DXX_OPTIONS_MENU(ADD);
1068 }
1069 };
1070
1071 struct options_menu : options_menu_items, newmenu
1072 {
options_menu__anon301c837e0911::options_menu1073 options_menu(grs_canvas &src) :
1074 newmenu(menu_title{nullptr}, menu_subtitle{TXT_OPTIONS}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 0), src)
1075 {
1076 }
1077 virtual window_event_result event_handler(const d_event &event) override;
1078 };
1079
event_handler(const d_event & event)1080 window_event_result options_menu::event_handler(const d_event &event)
1081 {
1082 switch (event.type)
1083 {
1084 case EVENT_NEWMENU_SELECTED:
1085 {
1086 auto &citem = static_cast<const d_select_event &>(event).citem;
1087 switch (citem)
1088 {
1089 case options_menu_items::sfx:
1090 do_sound_menu();
1091 break;
1092 case options_menu_items::controls:
1093 input_config();
1094 break;
1095 case options_menu_items::graphics:
1096 graphics_config();
1097 break;
1098 case options_menu_items::misc:
1099 gameplay_config();
1100 break;
1101 }
1102 return window_event_result::handled; // stay in menu until escape
1103 }
1104
1105 case EVENT_WINDOW_CLOSE:
1106 write_player_file();
1107 break;
1108
1109 default:
1110 break;
1111 }
1112 return newmenu::event_handler(event);
1113 }
1114
gcd(int a,int b)1115 static int gcd(int a, int b)
1116 {
1117 if (!b)
1118 return a;
1119
1120 return gcd(b, a%b);
1121 }
1122
1123 struct screen_resolution_menu_items
1124 {
1125 enum
1126 {
1127 grp_resolution = 0,
1128 };
1129 enum class ni_index : unsigned;
1130 enum class fixed_field_index : unsigned
1131 {
1132 #if SDL_MAJOR_VERSION == 1
1133 /* SDL1 has a variable number of records before this line, so
1134 * this line exists to separate them from the next lines.
1135 *
1136 * SDL2 has no records before the custom values line, so no
1137 * separator is needed.
1138 */
1139 opt_blank_custom_values,
1140 #endif
1141 opt_radio_custom_values,
1142 opt_label_resolution,
1143 opt_input_resolution,
1144 opt_label_aspect,
1145 opt_input_aspect,
1146 opt_blank_fullscreen,
1147 opt_checkbox_fullscreen,
1148 /* Must be last. This is not a real element, and access to
1149 * array[end] is undefined.
1150 */
1151 end,
1152 };
1153 #if SDL_MAJOR_VERSION == 1
1154 static constexpr std::size_t maximum_preset_modes = 50;
1155 std::array<screen_mode, maximum_preset_modes> modes;
1156 std::array<std::array<char, 12>, maximum_preset_modes> restext;
1157 const unsigned num_presets;
convert_fixed_field_to_ni__anon301c837e0911::screen_resolution_menu_items1158 ni_index convert_fixed_field_to_ni(fixed_field_index i) const
1159 {
1160 return static_cast<ni_index>(static_cast<unsigned>(i) + num_presets);
1161 }
1162 #elif SDL_MAJOR_VERSION == 2
1163 static constexpr std::size_t maximum_preset_modes = 0;
convert_fixed_field_to_ni__anon301c837e0911::screen_resolution_menu_items1164 static constexpr ni_index convert_fixed_field_to_ni(fixed_field_index i)
1165 {
1166 return static_cast<ni_index>(i);
1167 }
1168 #endif
1169 std::array<char, 12> crestext, casptext;
1170 enumerated_array<newmenu_item, maximum_preset_modes + static_cast<unsigned>(fixed_field_index::end), ni_index> m;
1171 screen_resolution_menu_items();
1172 };
1173
screen_resolution_menu_items()1174 screen_resolution_menu_items::screen_resolution_menu_items()
1175 #if SDL_MAJOR_VERSION == 1
1176 : num_presets(gr_list_modes(modes))
1177 #endif
1178 {
1179 int citem = -1;
1180 #if SDL_MAJOR_VERSION == 1
1181 for (auto &&[idx, mode, resolution_text, menuitem] : enumerate(zip(partial_const_range(modes, num_presets), restext, m)))
1182 {
1183 const auto &&sm_w = SM_W(mode);
1184 const auto &&sm_h = SM_H(mode);
1185 snprintf(resolution_text.data(), resolution_text.size(), "%ix%i", sm_w, sm_h);
1186 /* At most one entry can be checked. When the correct entry is
1187 * found, update citem so that no later entries can be checked.
1188 */
1189 const auto checked = (citem == -1 && Game_screen_mode == mode && GameCfg.AspectY == sm_w / gcd(sm_w, sm_h) && GameCfg.AspectX == sm_h / gcd(sm_w, sm_h));
1190 if (checked)
1191 citem = idx;
1192 nm_set_item_radio(menuitem, resolution_text.data(), checked, grp_resolution);
1193 }
1194 /* Leave a blank line for visual separation */
1195 nm_set_item_text(m[convert_fixed_field_to_ni(fixed_field_index::opt_blank_custom_values)], "");
1196 #endif
1197 nm_set_item_radio(m[convert_fixed_field_to_ni(fixed_field_index::opt_radio_custom_values)], "Use custom values", (citem == -1), grp_resolution);
1198 nm_set_item_text(m[convert_fixed_field_to_ni(fixed_field_index::opt_label_resolution)], "resolution:");
1199 snprintf(crestext.data(), crestext.size(), "%ix%i", SM_W(Game_screen_mode), SM_H(Game_screen_mode));
1200 nm_set_item_input(m[convert_fixed_field_to_ni(fixed_field_index::opt_input_resolution)], crestext);
1201 nm_set_item_text(m[convert_fixed_field_to_ni(fixed_field_index::opt_label_aspect)], "aspect:");
1202 snprintf(casptext.data(), casptext.size(), "%ix%i", GameCfg.AspectY, GameCfg.AspectX);
1203 nm_set_item_input(m[convert_fixed_field_to_ni(fixed_field_index::opt_input_aspect)], casptext);
1204 nm_set_item_text(m[convert_fixed_field_to_ni(fixed_field_index::opt_blank_fullscreen)], "");
1205 nm_set_item_checkbox(m[convert_fixed_field_to_ni(fixed_field_index::opt_checkbox_fullscreen)], "Fullscreen", gr_check_fullscreen());
1206 }
1207
1208 struct screen_resolution_menu : screen_resolution_menu_items, passive_newmenu
1209 {
screen_resolution_menu__anon301c837e0911::screen_resolution_menu1210 screen_resolution_menu() :
1211 passive_newmenu(menu_title{nullptr}, menu_subtitle{"Screen Resolution"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(partial_range(m, static_cast<std::size_t>(convert_fixed_field_to_ni(fixed_field_index::end))), 0), grd_curscreen->sc_canvas)
1212 {
1213 }
1214 virtual window_event_result event_handler(const d_event &event) override;
1215 void handle_close_event() const;
1216 #if SDL_MAJOR_VERSION == 1
1217 void check_apply_preset_resolution() const;
1218 #endif
1219 void apply_custom_resolution() const;
1220 void apply_resolution(screen_mode) const;
1221 };
1222
event_handler(const d_event & event)1223 window_event_result screen_resolution_menu::event_handler(const d_event &event)
1224 {
1225 switch (event.type)
1226 {
1227 case EVENT_WINDOW_CLOSE:
1228 handle_close_event();
1229 return window_event_result::ignored;
1230 default:
1231 return newmenu::event_handler(event);
1232 }
1233 }
1234
handle_close_event() const1235 void screen_resolution_menu::handle_close_event() const
1236 {
1237 // check which resolution field was selected
1238 #if SDL_MAJOR_VERSION == 1
1239 if (m[convert_fixed_field_to_ni(fixed_field_index::opt_checkbox_fullscreen)].value != gr_check_fullscreen())
1240 gr_toggle_fullscreen();
1241 if (!m[convert_fixed_field_to_ni(fixed_field_index::opt_radio_custom_values)].value)
1242 {
1243 /* If the radio item for "Use custom resolution" is not set,
1244 * then one of the items for a preset resolution must be set.
1245 * Find the chosen item and apply it.
1246 */
1247 check_apply_preset_resolution();
1248 }
1249 else
1250 #endif
1251 {
1252 apply_custom_resolution();
1253 }
1254 }
1255
1256 #if SDL_MAJOR_VERSION == 1
check_apply_preset_resolution() const1257 void screen_resolution_menu::check_apply_preset_resolution() const
1258 {
1259 const auto r = zip(partial_range(modes, num_presets), m);
1260 const auto predicate = [](const auto &v) {
1261 auto &ni = std::get<1>(v);
1262 if (ni.type != nm_type::radio)
1263 return 0;
1264 auto &radio = ni.radio();
1265 if (radio.group != grp_resolution)
1266 return 0;
1267 return ni.value;
1268 };
1269 const auto i = std::find_if(r.begin(), r.end(), predicate);
1270 if (i == r.end())
1271 return;
1272 const auto requested_mode = std::get<0>(*i);
1273 const auto g = gcd(SM_W(requested_mode), SM_H(requested_mode));
1274 GameCfg.AspectY = SM_W(requested_mode) / g;
1275 GameCfg.AspectX = SM_H(requested_mode) / g;
1276 apply_resolution(requested_mode);
1277 }
1278 #endif
1279
apply_custom_resolution() const1280 void screen_resolution_menu::apply_custom_resolution() const
1281 {
1282 char *x;
1283 const char *errstr;
1284 const auto resolution_width = strtoul(crestext.data(), &x, 10);
1285 unsigned long resolution_height;
1286 screen_mode cmode;
1287 if (
1288 ((x == crestext.data() || *x != 'x' || !x[1] || ((resolution_height = strtoul(x + 1, &x, 10)), *x)) && (errstr = "Entered resolution must\nbe formatted as\n<number>x<number>", true)) ||
1289 ((resolution_width < 320 || resolution_height < 200) && (errstr = "Entered resolution must\nbe at least 320x200", true))
1290 )
1291 {
1292 cmode = Game_screen_mode;
1293 struct error_change_resolution :
1294 std::array<char, 32>,
1295 passive_messagebox
1296 {
1297 error_change_resolution(const char *errstr, screen_mode cmode) :
1298 passive_messagebox(menu_title{TXT_WARNING}, menu_subtitle{errstr}, prepare_choice_text(*this, cmode), grd_curscreen->sc_canvas)
1299 {
1300 }
1301 static const char *prepare_choice_text(std::array<char, 32> &b, screen_mode cmode)
1302 {
1303 auto r = b.data();
1304 std::snprintf(r, b.size(), "Revert to %ix%i", SM_W(cmode), SM_H(cmode));
1305 return r;
1306 }
1307 };
1308 run_blocking_newmenu<error_change_resolution>(errstr, cmode);
1309 }
1310 else
1311 {
1312 cmode.width = resolution_width;
1313 cmode.height = resolution_height;
1314 }
1315 screen_mode casp;
1316 const auto aspect_width = strtoul(casptext.data(), &x, 10);
1317 unsigned long aspect_height;
1318 if (
1319 ((x == casptext.data() || *x != 'x' || !x[1] || ((aspect_height = strtoul(x + 1, &x, 10)), *x)) && (errstr = "Entered aspect ratio must\nbe formatted as\n<number>x<number>", true)) ||
1320 ((!aspect_width || !aspect_height) && (errstr = "Entered aspect ratio must\nnot use 0 term", true))
1321 )
1322 {
1323 casp = cmode;
1324 struct error_invalid_aspect_ratio : passive_messagebox
1325 {
1326 error_invalid_aspect_ratio(const char *errstr) :
1327 passive_messagebox(menu_title{TXT_WARNING}, menu_subtitle{errstr}, "IGNORE ASPECT RATIO", grd_curscreen->sc_canvas)
1328 {
1329 }
1330 };
1331 run_blocking_newmenu<error_invalid_aspect_ratio>(errstr);
1332 }
1333 else
1334 {
1335 casp.width = aspect_width;
1336 casp.height = aspect_height;
1337 }
1338 const auto g = gcd(SM_W(casp), SM_H(casp));
1339 GameCfg.AspectY = SM_W(casp) / g;
1340 GameCfg.AspectX = SM_H(casp) / g;
1341 apply_resolution(cmode);
1342 }
1343
apply_resolution(const screen_mode new_mode) const1344 void screen_resolution_menu::apply_resolution(const screen_mode new_mode) const
1345 {
1346 // clean up and apply everything
1347 newmenu_free_background();
1348 set_screen_mode(SCREEN_MENU);
1349 if (new_mode != Game_screen_mode)
1350 {
1351 gr_set_mode(new_mode);
1352 Game_screen_mode = new_mode;
1353 if (Game_wind) // shortly activate Game_wind so it's canvas will align to new resolution. really minor glitch but whatever
1354 {
1355 {
1356 const d_event event{EVENT_WINDOW_ACTIVATED};
1357 WINDOW_SEND_EVENT(Game_wind);
1358 }
1359 {
1360 const d_event event{EVENT_WINDOW_DEACTIVATED};
1361 WINDOW_SEND_EVENT(Game_wind);
1362 }
1363 }
1364 }
1365 game_init_render_sub_buffers(*grd_curcanv, 0, 0, SM_W(Game_screen_mode), SM_H(Game_screen_mode));
1366 }
1367
1368 template <typename PMF>
1369 struct copy_sensitivity
1370 {
1371 const std::size_t offset;
1372 const PMF pmf;
copy_sensitivity__anon301c837e0911::copy_sensitivity1373 copy_sensitivity(std::size_t offset, const PMF pmf) :
1374 offset(offset), pmf(pmf)
1375 {
1376 }
1377 };
1378
1379 template <typename XRange, typename MenuItems, typename... CopyParameters>
copy_sensitivity_from_menu_to_cfg2(XRange && r,const MenuItems & menuitems,const CopyParameters...cn)1380 void copy_sensitivity_from_menu_to_cfg2(XRange &&r, const MenuItems &menuitems, const CopyParameters ... cn)
1381 {
1382 for (const auto i : r)
1383 (((PlayerCfg.*(cn.pmf))[i] = menuitems[1 + i + cn.offset].value), ...);
1384 }
1385
1386 template <typename MenuItems, typename CopyParameter0, typename... CopyParameterN>
copy_sensitivity_from_menu_to_cfg(const MenuItems & menuitems,const CopyParameter0 c0,const CopyParameterN...cn)1387 void copy_sensitivity_from_menu_to_cfg(const MenuItems &menuitems, const CopyParameter0 c0, const CopyParameterN ... cn)
1388 {
1389 copy_sensitivity_from_menu_to_cfg2(xrange(std::size(PlayerCfg.*(c0.pmf))), menuitems, c0, cn...);
1390 }
1391
1392 #define DXX_INPUT_SENSITIVITY(VERB,OPT,VAL) \
1393 DXX_MENUITEM(VERB, SLIDER, TXT_TURN_LR, opt_##OPT##_turn_lr, VAL[0], 0, 16) \
1394 DXX_MENUITEM(VERB, SLIDER, TXT_PITCH_UD, opt_##OPT##_pitch_ud, VAL[1], 0, 16) \
1395 DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_LR, opt_##OPT##_slide_lr, VAL[2], 0, 16) \
1396 DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_UD, opt_##OPT##_slide_ud, VAL[3], 0, 16) \
1397 DXX_MENUITEM(VERB, SLIDER, TXT_BANK_LR, opt_##OPT##_bank_lr, VAL[4], 0, 16) \
1398
1399 #define DXX_INPUT_CONFIG_MENU(VERB) \
1400 DXX_MENUITEM(VERB, TEXT, "Keyboard Sensitivity:", opt_label_kb) \
1401 DXX_INPUT_SENSITIVITY(VERB,kb,PlayerCfg.KeyboardSens) \
1402
1403 namespace keyboard_sensitivity {
1404
1405 struct menu_items
1406 {
1407 enum
1408 {
1409 DXX_INPUT_CONFIG_MENU(ENUM)
1410 };
1411 DXX_INPUT_CONFIG_MENU(DECL);
1412 std::array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
menu_items__anon301c837e0911::keyboard_sensitivity::menu_items1413 menu_items()
1414 {
1415 DXX_INPUT_CONFIG_MENU(ADD);
1416 }
1417 };
1418
1419 struct menu : menu_items, newmenu
1420 {
menu__anon301c837e0911::keyboard_sensitivity::menu1421 menu(grs_canvas &src) :
1422 newmenu(menu_title{nullptr}, menu_subtitle{"Keyboard Calibration"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 1), src)
1423 {
1424 }
1425 virtual window_event_result event_handler(const d_event &event) override;
1426 };
1427
event_handler(const d_event & event)1428 window_event_result menu::event_handler(const d_event &event)
1429 {
1430 switch (event.type)
1431 {
1432 case EVENT_WINDOW_CLOSE:
1433 copy_sensitivity_from_menu_to_cfg(m, copy_sensitivity(opt_label_kb, &player_config::KeyboardSens));
1434 break;
1435 default:
1436 break;
1437 }
1438 return newmenu::event_handler(event);
1439 }
1440
1441 }
1442
1443 #undef DXX_INPUT_CONFIG_MENU
1444 #undef DXX_INPUT_SENSITIVITY
1445
input_config_keyboard()1446 static void input_config_keyboard()
1447 {
1448 auto menu = window_create<keyboard_sensitivity::menu>(grd_curscreen->sc_canvas);
1449 (void)menu;
1450 }
1451
1452 #define DXX_INPUT_SENSITIVITY(VERB,OPT,VAL) \
1453 DXX_MENUITEM(VERB, SLIDER, TXT_TURN_LR, opt_##OPT##_turn_lr, VAL[0], 0, 16) \
1454 DXX_MENUITEM(VERB, SLIDER, TXT_PITCH_UD, opt_##OPT##_pitch_ud, VAL[1], 0, 16) \
1455 DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_LR, opt_##OPT##_slide_lr, VAL[2], 0, 16) \
1456 DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_UD, opt_##OPT##_slide_ud, VAL[3], 0, 16) \
1457 DXX_MENUITEM(VERB, SLIDER, TXT_BANK_LR, opt_##OPT##_bank_lr, VAL[4], 0, 16) \
1458
1459 #define DXX_INPUT_THROTTLE_SENSITIVITY(VERB,OPT,VAL) \
1460 DXX_INPUT_SENSITIVITY(VERB,OPT,VAL) \
1461 DXX_MENUITEM(VERB, SLIDER, TXT_THROTTLE, opt_##OPT##_throttle, VAL[5], 0, 16) \
1462
1463 #define DXX_INPUT_CONFIG_MENU(VERB) \
1464 DXX_MENUITEM(VERB, TEXT, "Mouse Sensitivity:", opt_label_ms) \
1465 DXX_INPUT_THROTTLE_SENSITIVITY(VERB,ms,PlayerCfg.MouseSens) \
1466 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_ms) \
1467 DXX_MENUITEM(VERB, TEXT, "Mouse Overrun Buffer:", opt_label_mo) \
1468 DXX_INPUT_THROTTLE_SENSITIVITY(VERB,mo,PlayerCfg.MouseOverrun) \
1469 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_mo) \
1470 DXX_MENUITEM(VERB, TEXT, "Mouse FlightSim Deadzone:", opt_label_mfsd) \
1471 DXX_MENUITEM(VERB, SLIDER, "X/Y", opt_mfsd_deadzone, PlayerCfg.MouseFSDead, 0, 16) \
1472
1473 namespace mouse_sensitivity {
1474
1475 struct menu_items
1476 {
1477 public:
1478 enum
1479 {
1480 DXX_INPUT_CONFIG_MENU(ENUM)
1481 };
1482 DXX_INPUT_CONFIG_MENU(DECL);
1483 std::array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
menu_items__anon301c837e0911::mouse_sensitivity::menu_items1484 menu_items()
1485 {
1486 DXX_INPUT_CONFIG_MENU(ADD);
1487 }
1488 };
1489
1490 struct menu : menu_items, newmenu
1491 {
menu__anon301c837e0911::mouse_sensitivity::menu1492 menu(grs_canvas &src) :
1493 newmenu(menu_title{nullptr}, menu_subtitle{"Mouse Calibration"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 1), src)
1494 {
1495 }
1496 virtual window_event_result event_handler(const d_event &event) override;
1497 };
1498
event_handler(const d_event & event)1499 window_event_result menu::event_handler(const d_event &event)
1500 {
1501 switch (event.type)
1502 {
1503 case EVENT_WINDOW_CLOSE:
1504 PlayerCfg.MouseFSDead = m[opt_mfsd_deadzone].value;
1505 copy_sensitivity_from_menu_to_cfg(m,
1506 copy_sensitivity(opt_label_ms, &player_config::MouseSens),
1507 copy_sensitivity(opt_label_mo, &player_config::MouseOverrun)
1508 );
1509 break;
1510 default:
1511 break;
1512 }
1513 return newmenu::event_handler(event);
1514 }
1515
1516 }
1517 #undef DXX_INPUT_CONFIG_MENU
1518
input_config_mouse()1519 static void input_config_mouse()
1520 {
1521 auto menu = window_create<mouse_sensitivity::menu>(grd_curscreen->sc_canvas);
1522 (void)menu;
1523 }
1524
1525 #if DXX_MAX_AXES_PER_JOYSTICK
1526 namespace joystick_sensitivity {
1527
1528 #define DXX_INPUT_CONFIG_MENU(VERB) \
1529 DXX_MENUITEM(VERB, TEXT, "Joystick Sensitivity:", opt_label_js) \
1530 DXX_INPUT_THROTTLE_SENSITIVITY(VERB,js,PlayerCfg.JoystickSens) \
1531 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_js) \
1532 DXX_MENUITEM(VERB, TEXT, "Joystick Linearity:", opt_label_jl) \
1533 DXX_INPUT_THROTTLE_SENSITIVITY(VERB,jl,PlayerCfg.JoystickLinear) \
1534 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_jl) \
1535 DXX_MENUITEM(VERB, TEXT, "Joystick Linear Speed:", opt_label_jp) \
1536 DXX_INPUT_THROTTLE_SENSITIVITY(VERB,jp,PlayerCfg.JoystickSpeed) \
1537 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_jp) \
1538 DXX_MENUITEM(VERB, TEXT, "Joystick Deadzone:", opt_label_jd) \
1539 DXX_INPUT_THROTTLE_SENSITIVITY(VERB,jd,PlayerCfg.JoystickDead) \
1540
1541 class menu_items
1542 {
1543 public:
1544 enum
1545 {
1546 DXX_INPUT_CONFIG_MENU(ENUM)
1547 };
1548 DXX_INPUT_CONFIG_MENU(DECL);
1549 std::array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
menu_items()1550 menu_items()
1551 {
1552 DXX_INPUT_CONFIG_MENU(ADD);
1553 }
1554 };
1555 #undef DXX_INPUT_CONFIG_MENU
1556
1557 struct menu : menu_items, newmenu
1558 {
menu__anon301c837e0911::joystick_sensitivity::menu1559 menu(grs_canvas &src) :
1560 newmenu(menu_title{nullptr}, menu_subtitle{"Joystick Calibration"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 1), src)
1561 {
1562 }
1563 virtual window_event_result event_handler(const d_event &event) override;
1564 };
1565
event_handler(const d_event & event)1566 window_event_result menu::event_handler(const d_event &event)
1567 {
1568 switch (event.type)
1569 {
1570 case EVENT_WINDOW_CLOSE:
1571 copy_sensitivity_from_menu_to_cfg(m,
1572 copy_sensitivity(opt_label_js, &player_config::JoystickSens),
1573 copy_sensitivity(opt_label_jl, &player_config::JoystickLinear),
1574 copy_sensitivity(opt_label_jp, &player_config::JoystickSpeed),
1575 copy_sensitivity(opt_label_jd, &player_config::JoystickDead)
1576 );
1577 break;
1578 default:
1579 break;
1580 }
1581 return newmenu::event_handler(event);
1582 }
1583
1584 }
1585
input_config_joystick()1586 static void input_config_joystick()
1587 {
1588 auto menu = window_create<joystick_sensitivity::menu>(grd_curscreen->sc_canvas);
1589 (void)menu;
1590 }
1591 #endif
1592
1593 #undef DXX_INPUT_THROTTLE_SENSITIVITY
1594 #undef DXX_INPUT_SENSITIVITY
1595
1596 class input_config_menu_items
1597 {
1598 #if DXX_MAX_JOYSTICKS
1599 #define DXX_INPUT_CONFIG_JOYSTICK_ITEM(I) I
1600 #else
1601 #define DXX_INPUT_CONFIG_JOYSTICK_ITEM(I)
1602 #endif
1603
1604 #if DXX_MAX_AXES_PER_JOYSTICK
1605 #define DXX_INPUT_CONFIG_JOYSTICK_AXIS_ITEM(I) I
1606 #else
1607 #define DXX_INPUT_CONFIG_JOYSTICK_AXIS_ITEM(I)
1608 #endif
1609
1610 #define DXX_INPUT_CONFIG_MENU(VERB) \
1611 DXX_INPUT_CONFIG_JOYSTICK_ITEM(DXX_MENUITEM(VERB, CHECK, "Use joystick", opt_ic_usejoy, PlayerCfg.ControlType & CONTROL_USING_JOYSTICK)) \
1612 DXX_MENUITEM(VERB, CHECK, "Use mouse", opt_ic_usemouse, PlayerCfg.ControlType & CONTROL_USING_MOUSE) \
1613 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_use) \
1614 DXX_MENUITEM(VERB, MENU, TXT_CUST_KEYBOARD, opt_ic_confkey) \
1615 DXX_INPUT_CONFIG_JOYSTICK_ITEM(DXX_MENUITEM(VERB, MENU, "Customize Joystick", opt_ic_confjoy)) \
1616 DXX_MENUITEM(VERB, MENU, "Customize Mouse", opt_ic_confmouse) \
1617 DXX_MENUITEM(VERB, MENU, "Customize Weapon Keys", opt_ic_confweap) \
1618 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_customize) \
1619 DXX_MENUITEM(VERB, TEXT, "Mouse Control Type:", opt_label_mouse_control_type) \
1620 DXX_MENUITEM(VERB, RADIO, "Normal", opt_mouse_control_normal, PlayerCfg.MouseFlightSim == 0, optgrp_mouse_control_type) \
1621 DXX_MENUITEM(VERB, RADIO, "FlightSim", opt_mouse_control_flightsim, PlayerCfg.MouseFlightSim == 1, optgrp_mouse_control_type) \
1622 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_mouse_control_type) \
1623 DXX_MENUITEM(VERB, MENU, "Keyboard Calibration", opt_ic_keyboard) \
1624 DXX_MENUITEM(VERB, MENU, "Mouse Calibration", opt_ic_mouse) \
1625 DXX_INPUT_CONFIG_JOYSTICK_AXIS_ITEM(DXX_MENUITEM(VERB, MENU, "Joystick Calibration", opt_ic_joystick)) \
1626 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_sensitivity_deadzone) \
1627 DXX_MENUITEM(VERB, CHECK, "Keep Keyboard/Mouse focus", opt_ic_grabinput, CGameCfg.Grabinput) \
1628 DXX_MENUITEM(VERB, CHECK, "Mouse FlightSim Indicator", opt_ic_mousefsgauge, PlayerCfg.MouseFSIndicator) \
1629 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_focus) \
1630 DXX_MENUITEM(VERB, TEXT, "When dead, respawn by pressing:", opt_label_respawn_mode) \
1631 DXX_MENUITEM(VERB, RADIO, "Any key", opt_respawn_any_key, PlayerCfg.RespawnMode == RespawnPress::Any, optgrp_respawn_mode) \
1632 DXX_MENUITEM(VERB, RADIO, "Fire keys (pri., sec., flare)", opt_respawn_fire_key, PlayerCfg.RespawnMode == RespawnPress::Fire, optgrp_respawn_mode) \
1633 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_respawn) \
1634 DXX_MENUITEM(VERB, TEXT, "Uncapped turning in:", opt_label_mouselook_mode) \
1635 DXX_MENUITEM(VERB, CHECK, "Single player", opt_ic_mouselook_sp, PlayerCfg.MouselookFlags & MouselookMode::Singleplayer) \
1636 DXX_MENUITEM(VERB, CHECK, "Multi Coop (if host allows)", opt_ic_mouselook_mp_cooperative, PlayerCfg.MouselookFlags & MouselookMode::MPCoop) \
1637 DXX_MENUITEM(VERB, CHECK, "Multi Anarchy (if host allows)", opt_ic_mouselook_mp_anarchy, PlayerCfg.MouselookFlags & MouselookMode::MPAnarchy) \
1638 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_mouselook) \
1639 DXX_MENUITEM(VERB, MENU, "GAME SYSTEM KEYS", opt_ic_help0) \
1640 DXX_MENUITEM(VERB, MENU, "NETGAME SYSTEM KEYS", opt_ic_help1) \
1641 DXX_MENUITEM(VERB, MENU, "DEMO SYSTEM KEYS", opt_ic_help2) \
1642
1643 public:
1644 enum
1645 {
1646 optgrp_mouse_control_type,
1647 optgrp_respawn_mode,
1648 };
1649 enum
1650 {
1651 DXX_INPUT_CONFIG_MENU(ENUM)
1652 };
1653 std::array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
input_config_menu_items()1654 input_config_menu_items()
1655 {
1656 DXX_INPUT_CONFIG_MENU(ADD);
1657 }
1658 #undef DXX_INPUT_CONFIG_MENU
1659 #undef DXX_INPUT_CONFIG_JOYSTICK_AXIS_ITEM
1660 #undef DXX_INPUT_CONFIG_JOYSTICK_ITEM
1661 };
1662
1663 }
1664
1665 namespace dsx {
1666 namespace {
1667
1668 struct input_config_menu : input_config_menu_items, newmenu
1669 {
input_config_menudsx::__anon301c837e1211::input_config_menu1670 input_config_menu(grs_canvas &src) :
1671 newmenu(menu_title{nullptr}, menu_subtitle{TXT_CONTROLS}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, opt_ic_confkey), src)
1672 {
1673 }
1674 virtual window_event_result event_handler(const d_event &event) override;
1675 };
1676
event_handler(const d_event & event)1677 window_event_result input_config_menu::event_handler(const d_event &event)
1678 {
1679 const auto &items = m;
1680 switch (event.type)
1681 {
1682 case EVENT_NEWMENU_CHANGED:
1683 {
1684 const auto citem = static_cast<const d_change_event &>(event).citem;
1685 MouselookMode mousemode;
1686 #if DXX_MAX_JOYSTICKS
1687 if (citem == opt_ic_usejoy)
1688 {
1689 constexpr auto flag = CONTROL_USING_JOYSTICK;
1690 if (items[citem].value)
1691 PlayerCfg.ControlType |= flag;
1692 else
1693 PlayerCfg.ControlType &= ~flag;
1694 }
1695 #endif
1696 if (citem == opt_ic_usemouse)
1697 {
1698 constexpr auto flag = CONTROL_USING_MOUSE;
1699 if (items[citem].value)
1700 PlayerCfg.ControlType |= flag;
1701 else
1702 PlayerCfg.ControlType &= ~flag;
1703 }
1704 if (citem == opt_mouse_control_normal)
1705 PlayerCfg.MouseFlightSim = 0;
1706 if (citem == opt_mouse_control_flightsim)
1707 PlayerCfg.MouseFlightSim = 1;
1708 if (citem == opt_ic_grabinput)
1709 CGameCfg.Grabinput = items[citem].value;
1710 if (citem == opt_ic_mousefsgauge)
1711 PlayerCfg.MouseFSIndicator = items[citem].value;
1712 else if (citem == opt_respawn_any_key)
1713 PlayerCfg.RespawnMode = RespawnPress::Any;
1714 else if (citem == opt_respawn_fire_key)
1715 PlayerCfg.RespawnMode = RespawnPress::Fire;
1716 else if ((citem == opt_ic_mouselook_sp && (mousemode = MouselookMode::Singleplayer, true)) ||
1717 (citem == opt_ic_mouselook_mp_cooperative && (mousemode = MouselookMode::MPCoop, true)) ||
1718 (citem == opt_ic_mouselook_mp_anarchy && (mousemode = MouselookMode::MPAnarchy, true)))
1719 {
1720 if (items[citem].value)
1721 PlayerCfg.MouselookFlags |= mousemode;
1722 else
1723 PlayerCfg.MouselookFlags &= ~mousemode;
1724 }
1725 break;
1726 }
1727 case EVENT_NEWMENU_SELECTED:
1728 {
1729 const auto citem = static_cast<const d_select_event &>(event).citem;
1730 if (citem == opt_ic_confkey)
1731 kconfig(kconfig_type::keyboard);
1732 #if DXX_MAX_JOYSTICKS
1733 if (citem == opt_ic_confjoy)
1734 kconfig(kconfig_type::joystick);
1735 #endif
1736 if (citem == opt_ic_confmouse)
1737 kconfig(kconfig_type::mouse);
1738 if (citem == opt_ic_confweap)
1739 kconfig(kconfig_type::rebirth);
1740 if (citem == opt_ic_keyboard)
1741 input_config_keyboard();
1742 if (citem == opt_ic_mouse)
1743 input_config_mouse();
1744 #if DXX_MAX_AXES_PER_JOYSTICK
1745 if (citem == opt_ic_joystick)
1746 input_config_joystick();
1747 #endif
1748 if (citem == opt_ic_help0)
1749 show_help();
1750 if (citem == opt_ic_help1)
1751 show_netgame_help();
1752 if (citem == opt_ic_help2)
1753 show_newdemo_help();
1754 return window_event_result::handled; // stay in menu
1755 }
1756
1757 default:
1758 break;
1759 }
1760 return newmenu::event_handler(event);
1761 }
1762
input_config()1763 void input_config()
1764 {
1765 auto menu = window_create<input_config_menu>(grd_curscreen->sc_canvas);
1766 (void)menu;
1767 }
1768
1769 struct reticle_config_menu_items
1770 {
1771 #if DXX_USE_OGL
1772 #define DXX_RETICLE_TYPE_OGL(VERB) \
1773 DXX_MENUITEM(VERB, RADIO, "Classic Reboot", opt_reticle_classic_reboot, 0, optgrp_reticle)
1774 #else
1775 #define DXX_RETICLE_TYPE_OGL(VERB)
1776 #endif
1777 #define DXX_RETICLE_CONFIG_MENU(VERB) \
1778 DXX_MENUITEM(VERB, TEXT, "Reticle Type:", opt_label_reticle_type) \
1779 DXX_MENUITEM(VERB, RADIO, "Classic", opt_reticle_classic, 0, optgrp_reticle) \
1780 DXX_RETICLE_TYPE_OGL(VERB) \
1781 DXX_MENUITEM(VERB, RADIO, "None", opt_reticle_none, 0, optgrp_reticle) \
1782 DXX_MENUITEM(VERB, RADIO, "X", opt_reticle_x, 0, optgrp_reticle) \
1783 DXX_MENUITEM(VERB, RADIO, "Dot", opt_reticle_dot, 0, optgrp_reticle) \
1784 DXX_MENUITEM(VERB, RADIO, "Circle", opt_reticle_circle, 0, optgrp_reticle) \
1785 DXX_MENUITEM(VERB, RADIO, "Cross V1", opt_reticle_cross1, 0, optgrp_reticle) \
1786 DXX_MENUITEM(VERB, RADIO, "Cross V2", opt_reticle_cross2, 0, optgrp_reticle) \
1787 DXX_MENUITEM(VERB, RADIO, "Angle", opt_reticle_angle, 0, optgrp_reticle) \
1788 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_reticle_type) \
1789 DXX_MENUITEM(VERB, TEXT, "Reticle Color:", opt_label_reticle_color) \
1790 DXX_MENUITEM(VERB, SCALE_SLIDER, "Red", opt_reticle_color_red, PlayerCfg.ReticleRGBA[0], 0, 16, 2) \
1791 DXX_MENUITEM(VERB, SCALE_SLIDER, "Green", opt_reticle_color_green, PlayerCfg.ReticleRGBA[1], 0, 16, 2) \
1792 DXX_MENUITEM(VERB, SCALE_SLIDER, "Blue", opt_reticle_color_blue, PlayerCfg.ReticleRGBA[2], 0, 16, 2) \
1793 DXX_MENUITEM(VERB, SCALE_SLIDER, "Alpha", opt_reticle_color_alpha, PlayerCfg.ReticleRGBA[3], 0, 16, 2) \
1794 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_reticle_color) \
1795 DXX_MENUITEM(VERB, SLIDER, "Reticle Size:", opt_label_reticle_size, PlayerCfg.ReticleSize, 0, 4) \
1796
1797 enum
1798 {
1799 optgrp_reticle,
1800 };
1801 enum
1802 {
1803 DXX_RETICLE_CONFIG_MENU(ENUM)
1804 };
1805 DXX_RETICLE_CONFIG_MENU(DECL);
1806 std::array<newmenu_item, DXX_RETICLE_CONFIG_MENU(COUNT)> m;
reticle_config_menu_itemsdsx::__anon301c837e1211::reticle_config_menu_items1807 reticle_config_menu_items()
1808 {
1809 DXX_RETICLE_CONFIG_MENU(ADD);
1810 auto i = PlayerCfg.ReticleType;
1811 #if !DXX_USE_OGL
1812 if (i > 1)
1813 --i;
1814 #endif
1815 m[opt_reticle_classic + i].value = 1;
1816 }
1817 };
1818
1819 struct reticle_config_menu : reticle_config_menu_items, newmenu
1820 {
reticle_config_menudsx::__anon301c837e1211::reticle_config_menu1821 reticle_config_menu(grs_canvas &src) :
1822 newmenu(menu_title{nullptr}, menu_subtitle{"Reticle Customization"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 1), src)
1823 {
1824 }
1825 virtual window_event_result event_handler(const d_event &event) override;
1826 };
1827
event_handler(const d_event & event)1828 window_event_result reticle_config_menu::event_handler(const d_event &event)
1829 {
1830 switch (event.type)
1831 {
1832 case EVENT_WINDOW_CLOSE:
1833 for (uint_fast32_t i = opt_reticle_classic; i != opt_label_blank_reticle_type; ++i)
1834 if (m[i].value)
1835 {
1836 #if !DXX_USE_OGL
1837 if (i != opt_reticle_classic)
1838 ++i;
1839 #endif
1840 PlayerCfg.ReticleType = i - opt_reticle_classic;
1841 break;
1842 }
1843 DXX_RETICLE_CONFIG_MENU(READ);
1844 break;
1845 default:
1846 break;
1847 }
1848 return newmenu::event_handler(event);
1849 }
1850 #undef DXX_RETICLE_CONFIG_MENU
1851 #undef DXX_RETICLE_TYPE_OGL
1852
reticle_config()1853 static void reticle_config()
1854 {
1855 auto menu = window_create<reticle_config_menu>(grd_curscreen->sc_canvas);
1856 (void)menu;
1857 }
1858
1859 struct hud_style_config_menu_items
1860 {
1861 #define DXX_HUD_STYLE_MENU(VERB) \
1862 DXX_MENUITEM(VERB, TEXT, "View style:", opt_viewstyle_label) \
1863 DXX_MENUITEM(VERB, RADIO, "Cockpit", opt_viewstyle_cockpit, PlayerCfg.CockpitMode[1] == CM_FULL_COCKPIT, optgrp_viewstyle) \
1864 DXX_MENUITEM(VERB, RADIO, "Status bar", opt_viewstyle_bar, PlayerCfg.CockpitMode[1] == CM_STATUS_BAR, optgrp_viewstyle) \
1865 DXX_MENUITEM(VERB, RADIO, "Full screen", opt_viewstyle_fullscreen, PlayerCfg.CockpitMode[1] == CM_FULL_SCREEN, optgrp_viewstyle) \
1866 DXX_MENUITEM(VERB, TEXT, "HUD style:", opt_hudstyle_label) \
1867 DXX_MENUITEM(VERB, RADIO, "Standard", opt_hudstyle_standard, PlayerCfg.HudMode == HudType::Standard, optgrp_hudstyle) \
1868 DXX_MENUITEM(VERB, RADIO, "Alternate #1", opt_hudstyle_alt1, PlayerCfg.HudMode == HudType::Alternate1, optgrp_hudstyle) \
1869 DXX_MENUITEM(VERB, RADIO, "Alternate #2", opt_hudstyle_alt2, PlayerCfg.HudMode == HudType::Alternate2, optgrp_hudstyle) \
1870 DXX_MENUITEM(VERB, RADIO, "Hidden", opt_hudstyle_hidden, PlayerCfg.HudMode == HudType::Hidden, optgrp_hudstyle) \
1871
1872 enum {
1873 optgrp_viewstyle,
1874 optgrp_hudstyle,
1875 };
1876 enum {
1877 DXX_HUD_STYLE_MENU(ENUM)
1878 };
1879 DXX_HUD_STYLE_MENU(DECL);
1880 std::array<newmenu_item, DXX_HUD_STYLE_MENU(COUNT)> m;
hud_style_config_menu_itemsdsx::__anon301c837e1211::hud_style_config_menu_items1881 hud_style_config_menu_items()
1882 {
1883 DXX_HUD_STYLE_MENU(ADD);
1884 }
1885 };
1886
1887 struct hud_style_config_menu : hud_style_config_menu_items, newmenu
1888 {
hud_style_config_menudsx::__anon301c837e1211::hud_style_config_menu1889 hud_style_config_menu(grs_canvas &src) :
1890 newmenu(menu_title{nullptr}, menu_subtitle{"View / HUD Style..."}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 1), src)
1891 {
1892 }
1893 virtual window_event_result event_handler(const d_event &event) override;
1894 };
1895
event_handler(const d_event & event)1896 window_event_result hud_style_config_menu::event_handler(const d_event &event)
1897 {
1898 switch (event.type)
1899 {
1900 case EVENT_WINDOW_CLOSE:
1901 {
1902 enum cockpit_mode_t new_mode = m[opt_viewstyle_cockpit].value
1903 ? CM_FULL_COCKPIT
1904 : m[opt_viewstyle_bar].value
1905 ? CM_STATUS_BAR
1906 : CM_FULL_SCREEN;
1907 select_cockpit(new_mode);
1908 PlayerCfg.CockpitMode[0] = new_mode;
1909 PlayerCfg.HudMode = m[opt_hudstyle_standard].value
1910 ? HudType::Standard
1911 : m[opt_hudstyle_alt1].value
1912 ? HudType::Alternate1
1913 : m[opt_hudstyle_alt2].value
1914 ? HudType::Alternate2
1915 : HudType::Hidden;
1916 break;
1917 }
1918 default:
1919 break;
1920 }
1921 return newmenu::event_handler(event);
1922 }
1923 #undef DXX_HUD_STYLE_MENU
1924
hud_style_config()1925 static void hud_style_config()
1926 {
1927 auto menu = window_create<hud_style_config_menu>(grd_curscreen->sc_canvas);
1928 (void)menu;
1929 }
1930
1931 #if defined(DXX_BUILD_DESCENT_I)
1932 #define DSX_GAME_SPECIFIC_HUDOPTIONS(VERB) \
1933 DXX_MENUITEM(VERB, CHECK, "Always-on Bomb Counter",opt_d2bomb,PlayerCfg.BombGauge) \
1934
1935 #elif defined(DXX_BUILD_DESCENT_II)
1936 enum {
1937 optgrp_missileview,
1938 };
1939 #define DSX_GAME_SPECIFIC_HUDOPTIONS(VERB) \
1940 DXX_MENUITEM(VERB, TEXT, "Missile view:", opt_missileview_label) \
1941 DXX_MENUITEM(VERB, RADIO, "Disabled", opt_missileview_none, PlayerCfg.MissileViewEnabled == MissileViewMode::None, optgrp_missileview) \
1942 DXX_MENUITEM(VERB, RADIO, "Only own missiles", opt_missileview_selfonly, PlayerCfg.MissileViewEnabled == MissileViewMode::EnabledSelfOnly, optgrp_missileview) \
1943 DXX_MENUITEM(VERB, RADIO, "Friendly missiles, preferring self", opt_missileview_selfandallies, PlayerCfg.MissileViewEnabled == MissileViewMode::EnabledSelfAndAllies, optgrp_missileview) \
1944 DXX_MENUITEM(VERB, CHECK, "Show guided missile in main display", opt_guidedbigview,PlayerCfg.GuidedInBigWindow ) \
1945
1946 #endif
1947 #define DSX_HUD_MENU_OPTIONS(VERB) \
1948 DXX_MENUITEM(VERB, MENU, "Reticle Customization...", opt_hud_reticlemenu) \
1949 DXX_MENUITEM(VERB, MENU, "View / HUD Style...", opt_hud_stylemenu) \
1950 DXX_MENUITEM(VERB, CHECK, "Screenshots without HUD",opt_screenshot,PlayerCfg.PRShot) \
1951 DXX_MENUITEM(VERB, CHECK, "No redundant pickup messages",opt_redundant,PlayerCfg.NoRedundancy) \
1952 DXX_MENUITEM(VERB, CHECK, "Show Player chat only (Multi)",opt_playerchat,PlayerCfg.MultiMessages) \
1953 DXX_MENUITEM(VERB, CHECK, "Show Player ping (Multi)",opt_playerping,PlayerCfg.MultiPingHud) \
1954 DXX_MENUITEM(VERB, CHECK, "Cloak/Invulnerability Timers",opt_cloakinvultimer,PlayerCfg.CloakInvulTimer) \
1955 DSX_GAME_SPECIFIC_HUDOPTIONS(VERB) \
1956
1957 struct hud_config_menu_items
1958 {
1959 enum {
1960 DSX_HUD_MENU_OPTIONS(ENUM)
1961 };
1962 DSX_HUD_MENU_OPTIONS(DECL);
1963 std::array<newmenu_item, DSX_HUD_MENU_OPTIONS(COUNT)> m;
hud_config_menu_itemsdsx::__anon301c837e1211::hud_config_menu_items1964 hud_config_menu_items()
1965 {
1966 DSX_HUD_MENU_OPTIONS(ADD);
1967 }
1968 };
1969
1970 struct hud_config_menu : hud_config_menu_items, newmenu
1971 {
hud_config_menudsx::__anon301c837e1211::hud_config_menu1972 hud_config_menu(grs_canvas &src) :
1973 newmenu(menu_title{nullptr}, menu_subtitle{"HUD Options"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 0), src)
1974 {
1975 }
1976 virtual window_event_result event_handler(const d_event &event) override;
1977 };
1978
event_handler(const d_event & event)1979 window_event_result hud_config_menu::event_handler(const d_event &event)
1980 {
1981 switch (event.type)
1982 {
1983 case EVENT_NEWMENU_SELECTED:
1984 {
1985 auto &citem = static_cast<const d_select_event &>(event).citem;
1986 if (citem == opt_hud_reticlemenu)
1987 reticle_config();
1988 if (citem == opt_hud_stylemenu)
1989 hud_style_config();
1990 return window_event_result::handled; // stay in menu
1991 }
1992 case EVENT_WINDOW_CLOSE:
1993 DSX_HUD_MENU_OPTIONS(READ);
1994 #if defined(DXX_BUILD_DESCENT_II)
1995 PlayerCfg.MissileViewEnabled = m[opt_missileview_selfandallies].value
1996 ? MissileViewMode::EnabledSelfAndAllies
1997 : (m[opt_missileview_selfonly].value
1998 ? MissileViewMode::EnabledSelfOnly
1999 : MissileViewMode::None);
2000 #endif
2001 break;
2002
2003 default:
2004 break;
2005 }
2006 return newmenu::event_handler(event);
2007 }
2008
hud_config()2009 void hud_config()
2010 {
2011 auto menu = window_create<hud_config_menu>(grd_curscreen->sc_canvas);
2012 (void)menu;
2013 }
2014
2015 #define DXX_GRAPHICS_MENU(VERB) \
2016 DXX_MENUITEM(VERB, MENU, "Screen resolution...", opt_gr_screenres) \
2017 DXX_MENUITEM(VERB, MENU, "HUD Options...", opt_gr_hudmenu) \
2018 DXX_MENUITEM(VERB, SLIDER, TXT_BRIGHTNESS, opt_gr_brightness, gr_palette_get_gamma(), 0, 16) \
2019 DXX_MENUITEM(VERB, TEXT, "", blank1) \
2020 DXX_OGL0_GRAPHICS_MENU(VERB) \
2021 DXX_OGL1_GRAPHICS_MENU(VERB) \
2022 DXX_MENUITEM(VERB, CHECK, "FPS Counter", opt_gr_fpsindi, CGameCfg.FPSIndicator) \
2023
2024 struct graphics_config_menu_items
2025 {
2026 #if DXX_USE_OGL
2027 enum {
2028 optgrp_texfilt,
2029 };
2030 #define DXX_OGL0_GRAPHICS_MENU(VERB) \
2031 DXX_MENUITEM(VERB, TEXT, "Texture Filtering:", opt_gr_texfilt) \
2032 DXX_MENUITEM(VERB, RADIO, "Classic", opt_filter_none, 0, optgrp_texfilt) \
2033 DXX_MENUITEM(VERB, RADIO, "Blocky Filtered", opt_filter_upscale, 0, optgrp_texfilt) \
2034 DXX_MENUITEM(VERB, RADIO, "Smooth", opt_filter_trilinear, 0, optgrp_texfilt) \
2035 DXX_MENUITEM(VERB, CHECK, "Anisotropic Filtering", opt_filter_anisotropy, CGameCfg.TexAnisotropy) \
2036 D2X_OGL_GRAPHICS_MENU(VERB) \
2037 DXX_MENUITEM(VERB, TEXT, "", blank2) \
2038
2039 #define DXX_OGL1_GRAPHICS_MENU(VERB) \
2040 DXX_MENUITEM(VERB, CHECK, "Transparency Effects", opt_gr_alphafx, PlayerCfg.AlphaEffects) \
2041 DXX_MENUITEM(VERB, CHECK, "Colored Dynamic Light", opt_gr_dynlightcolor, PlayerCfg.DynLightColor) \
2042 DXX_MENUITEM(VERB, CHECK, "VSync", opt_gr_vsync, CGameCfg.VSync) \
2043 DXX_MENUITEM(VERB, CHECK, "4x multisampling", opt_gr_multisample, CGameCfg.Multisample) \
2044
2045 #if defined(DXX_BUILD_DESCENT_I)
2046 #define D2X_OGL_GRAPHICS_MENU(VERB)
2047 #elif defined(DXX_BUILD_DESCENT_II)
2048 #define D2X_OGL_GRAPHICS_MENU(VERB) \
2049 DXX_MENUITEM(VERB, CHECK, "Cutscene Smoothing", opt_gr_movietexfilt, GameCfg.MovieTexFilt)
2050 #endif
2051
2052 #else
2053 #define DXX_OGL0_GRAPHICS_MENU(VERB)
2054 #define DXX_OGL1_GRAPHICS_MENU(VERB)
2055 #endif
2056 enum {
2057 DXX_GRAPHICS_MENU(ENUM)
2058 };
2059 DXX_GRAPHICS_MENU(DECL);
2060 std::array<newmenu_item, DXX_GRAPHICS_MENU(COUNT)> m;
graphics_config_menu_itemsdsx::__anon301c837e1211::graphics_config_menu_items2061 graphics_config_menu_items()
2062 {
2063 DXX_GRAPHICS_MENU(ADD);
2064 #if DXX_USE_OGL
2065 m[opt_filter_none + static_cast<unsigned>(CGameCfg.TexFilt)].value = 1;
2066 #endif
2067 }
2068 };
2069
2070 struct graphics_config_menu : graphics_config_menu_items, newmenu
2071 {
graphics_config_menudsx::__anon301c837e1211::graphics_config_menu2072 graphics_config_menu(grs_canvas &src) :
2073 newmenu(menu_title{nullptr}, menu_subtitle{"Graphics Options"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 0), src)
2074 {
2075 }
2076 virtual window_event_result event_handler(const d_event &event) override;
2077 };
2078
event_handler(const d_event & event)2079 window_event_result graphics_config_menu::event_handler(const d_event &event)
2080 {
2081 switch (event.type)
2082 {
2083 case EVENT_NEWMENU_CHANGED:
2084 {
2085 auto &citem = static_cast<const d_change_event &>(event).citem;
2086 if (citem == opt_gr_brightness)
2087 gr_palette_set_gamma(m[citem].value);
2088 #if DXX_USE_OGL
2089 else if (citem == opt_filter_anisotropy && ogl_maxanisotropy <= 1.0 && m[opt_filter_anisotropy].value)
2090 {
2091 m[opt_filter_anisotropy].value = 0;
2092 window_create<passive_messagebox>(menu_title{TXT_ERROR}, menu_subtitle{"Anisotropic Filtering not\nsupported by your hardware/driver."}, TXT_OK, grd_curscreen->sc_canvas);
2093 }
2094 #endif
2095 break;
2096 }
2097 case EVENT_NEWMENU_SELECTED:
2098 {
2099 auto &citem = static_cast<const d_select_event &>(event).citem;
2100 if (citem == opt_gr_screenres)
2101 window_create<screen_resolution_menu>();
2102 else if (citem == opt_gr_hudmenu)
2103 hud_config();
2104 return window_event_result::handled; // stay in menu
2105 }
2106 case EVENT_WINDOW_CLOSE:
2107 #if DXX_USE_OGL
2108 if (CGameCfg.VSync != m[opt_gr_vsync].value || CGameCfg.Multisample != m[opt_gr_multisample].value)
2109 {
2110 struct warn_might_need_restart : passive_messagebox
2111 {
2112 warn_might_need_restart() :
2113 passive_messagebox(menu_title{nullptr}, menu_subtitle{"On some systems, changing VSync or 4x Multisample\nrequires a restart."}, TXT_OK, grd_curscreen->sc_canvas)
2114 {
2115 }
2116 };
2117 run_blocking_newmenu<warn_might_need_restart>();
2118 }
2119
2120 for (const uint8_t i : xrange(3u))
2121 if (m[i + opt_filter_none].value)
2122 {
2123 CGameCfg.TexFilt = opengl_texture_filter{i};
2124 break;
2125 }
2126 CGameCfg.TexAnisotropy = m[opt_filter_anisotropy].value;
2127 #if defined(DXX_BUILD_DESCENT_II)
2128 GameCfg.MovieTexFilt = m[opt_gr_movietexfilt].value;
2129 #endif
2130 PlayerCfg.AlphaEffects = m[opt_gr_alphafx].value;
2131 PlayerCfg.DynLightColor = m[opt_gr_dynlightcolor].value;
2132 CGameCfg.VSync = m[opt_gr_vsync].value;
2133 CGameCfg.Multisample = m[opt_gr_multisample].value;
2134 #endif
2135 CGameCfg.GammaLevel = m[opt_gr_brightness].value;
2136 CGameCfg.FPSIndicator = m[opt_gr_fpsindi].value;
2137 reset_cockpit();
2138 #if DXX_USE_OGL
2139 gr_set_attributes();
2140 gr_set_mode(Game_screen_mode);
2141 #endif
2142 break;
2143
2144 default:
2145 break;
2146 }
2147 return newmenu::event_handler(event);
2148 }
2149
graphics_config()2150 void graphics_config()
2151 {
2152 auto menu = window_create<graphics_config_menu>(grd_curscreen->sc_canvas);
2153 (void)menu;
2154 }
2155
2156 }
2157 }
2158
2159 namespace dcx {
2160 namespace {
2161
2162 #if DXX_USE_SDLMIXER
2163 struct physfsx_mounted_path
2164 {
2165 /* PhysFS does not count how many times a path is mounted, and all
2166 * mount requests after the first succeed without changing state.
2167 * This flag tracks whether the path in `path` was mounted by this
2168 * instance of `physfsx_mounted_path` (=1) or if the path was
2169 * already mounted by an earlier call to PhysFS (=0).
2170 *
2171 * If the path was already mounted, destruction of this instance
2172 * must not unmount it.
2173 */
2174 uint8_t must_unmount = 0;
2175 std::array<char, PATH_MAX> path;
2176 uint8_t mount();
~physfsx_mounted_pathdcx::__anon301c837e1b11::physfsx_mounted_path2177 ~physfsx_mounted_path()
2178 {
2179 if (must_unmount)
2180 PHYSFS_unmount(path.data());
2181 }
2182 };
2183
mount()2184 uint8_t physfsx_mounted_path::mount()
2185 {
2186 assert(!must_unmount);
2187 const auto current_mount_point = PHYSFS_getMountPoint(path.data());
2188 if (current_mount_point == nullptr)
2189 {
2190 /* Not currently mounted; try to mount it */
2191 must_unmount = PHYSFS_mount(path.data(), nullptr, 0);
2192 return must_unmount;
2193 }
2194 else
2195 {
2196 /* Already mounted */
2197 must_unmount = 0;
2198 return 1;
2199 }
2200 }
2201
2202 struct browser_storage
2203 {
2204 struct target_path_not_mounted {};
2205 // List of file extensions we're looking for (if looking for a music file many types are possible)
2206 const partial_range_t<const file_extension_t *> ext_range;
2207 const select_dir_flag select_dir; // Allow selecting the current directory (e.g. for Jukebox level song directory)
2208 physfsx_mounted_path view_path; // The absolute path we're currently looking at
2209 string_array_t list;
browser_storagedcx::__anon301c837e1b11::browser_storage2210 browser_storage(const char *orig_path, const partial_range_t<const file_extension_t *> &ext_range, const select_dir_flag select_dir, const char *const sep) :
2211 ext_range(ext_range), select_dir(select_dir),
2212 /* view_path is trivially constructed, then properly initialized as
2213 * a side effect of preparing the string list */
2214 list(construct_string_list(orig_path, view_path, ext_range, select_dir, sep))
2215 {
2216 }
2217 static string_array_t construct_string_list(const char *orig_path, physfsx_mounted_path &view_path, const partial_range_t<const file_extension_t *> &r, const select_dir_flag select_dir, const char *const sep);
2218 };
2219
2220 struct browser : browser_storage, listbox
2221 {
browserdcx::__anon301c837e1b11::browser2222 browser(const char *orig_path, menu_title title, const partial_range_t<const file_extension_t *> &r, const select_dir_flag select_dir, const char *const sep, ntstring<PATH_MAX - 1> &userdata) :
2223 browser_storage(orig_path, r, select_dir, sep),
2224 listbox(0, list.pointer().size(), &list.pointer().front(), title, grd_curscreen->sc_canvas, 1),
2225 userdata(userdata)
2226 {
2227 }
2228 ntstring<PATH_MAX - 1> &userdata; // Whatever you want passed to get_absolute_path
2229 virtual window_event_result callback_handler(const d_event &, window_event_result) override;
2230 };
2231
2232 struct list_directory_context
2233 {
2234 string_array_t &string_list;
2235 const partial_range_t<const file_extension_t *> ext_range;
2236 const std::array<char, PATH_MAX> &path;
2237 };
2238
list_dir_el(void * context,const char *,const char * fname)2239 static void list_dir_el(void *context, const char *, const char *fname)
2240 {
2241 const auto c = reinterpret_cast<list_directory_context *>(context);
2242 const char *r = PHYSFS_getRealDir(fname);
2243 if (!r)
2244 r = "";
2245 if (!strcmp(r, c->path.data()) && (PHYSFS_isDirectory(fname) || PHYSFSX_checkMatchingExtension(fname, c->ext_range))
2246 #if defined(__APPLE__) && defined(__MACH__)
2247 && d_stricmp(fname, "Volumes") // this messes things up, use '..' instead
2248 #endif
2249 )
2250 c->string_list.add(fname);
2251 }
2252
callback_handler(const d_event & event,window_event_result)2253 window_event_result browser::callback_handler(const d_event &event, window_event_result)
2254 {
2255 const char **list = listbox_get_items(*this);
2256 const char *sep = PHYSFS_getDirSeparator();
2257 switch (event.type)
2258 {
2259 #ifdef _WIN32
2260 case EVENT_KEY_COMMAND:
2261 {
2262 if (event_key_get(event) == KEY_CTRLED + KEY_D)
2263 {
2264 std::array<char, 4> text{{"c"}};
2265 std::array<newmenu_item, 1> m{{
2266 newmenu_item::nm_item_input(text),
2267 }};
2268 struct drive_letter_menu : passive_newmenu
2269 {
2270 drive_letter_menu(grs_canvas &canvas, partial_range_t<newmenu_item *> items) :
2271 passive_newmenu(menu_title{nullptr}, menu_subtitle{"Enter drive letter"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(items, 0), canvas)
2272 {
2273 }
2274 };
2275 const auto rval = run_blocking_newmenu<drive_letter_menu>(*grd_curcanv, m);
2276 const auto t0 = text[0];
2277 std::array<char, PATH_MAX> newpath;
2278 snprintf(newpath.data(), newpath.size(), "%c:%s", t0, sep);
2279 if (!rval && t0)
2280 {
2281 select_file_recursive(title, newpath, ext_range, select_dir, userdata);
2282 // close old box.
2283 return window_event_result::close;
2284 }
2285 return window_event_result::handled;
2286 }
2287 break;
2288 }
2289 #endif
2290 case EVENT_NEWMENU_SELECTED:
2291 {
2292 auto &citem = static_cast<const d_select_event &>(event).citem;
2293 auto newpath = view_path.path;
2294 if (citem == 0) // go to parent dir
2295 {
2296 const size_t len_newpath = strlen(newpath.data());
2297 const size_t len_sep = strlen(sep);
2298 if (auto p = strstr(&newpath[len_newpath - len_sep], sep))
2299 if (p != strstr(newpath.data(), sep)) // if this isn't the only separator (i.e. it's not about to look at the root)
2300 *p = 0;
2301
2302 auto p = &newpath[len_newpath - 1];
2303 while (p != newpath.begin() && strncmp(p, sep, len_sep)) // make sure full separator string is matched (typically is)
2304 p--;
2305
2306 if (p == strstr(newpath.data(), sep)) // Look at root directory next, if not already
2307 {
2308 #if defined(__APPLE__) && defined(__MACH__)
2309 if (!d_stricmp(p, "/Volumes"))
2310 return window_event_result::handled;
2311 #endif
2312 if (p[len_sep] != '\0')
2313 p[len_sep] = '\0';
2314 else
2315 {
2316 #if defined(__APPLE__) && defined(__MACH__)
2317 // For Mac OS X, list all active volumes if we leave the root
2318 strcpy(newpath.data(), "/Volumes");
2319 #else
2320 return window_event_result::handled;
2321 #endif
2322 }
2323 }
2324 else
2325 *p = '\0';
2326 }
2327 else if (citem == 1 && select_dir != select_dir_flag::files_only)
2328 return get_absolute_path(userdata, "");
2329 else
2330 {
2331 const size_t len_newpath = strlen(newpath.data());
2332 const size_t len_item = strlen(list[citem]);
2333 if (len_newpath + len_item < newpath.size())
2334 {
2335 const size_t len_sep = strlen(sep);
2336 snprintf(&newpath[len_newpath], newpath.size() - len_newpath, "%s%s", strncmp(&newpath[len_newpath - len_sep], sep, len_sep) ? sep : "", list[citem]);
2337 }
2338 }
2339 if ((citem == 0) || PHYSFS_isDirectory(list[citem]))
2340 {
2341 // If it fails, stay in this one
2342 return select_file_recursive(title, newpath, ext_range, select_dir, userdata) ? window_event_result::close : window_event_result::handled;
2343 }
2344 return get_absolute_path(userdata, list[citem]);
2345 }
2346 case EVENT_WINDOW_CLOSE:
2347 break;
2348 default:
2349 break;
2350 }
2351 return window_event_result::ignored;
2352 }
2353
select_file_recursive(const menu_title title,const std::array<char,PATH_MAX> & orig_path_storage,const partial_range_t<const file_extension_t * > & ext_range,const select_dir_flag select_dir,ntstring<PATH_MAX-1> & userdata)2354 static int select_file_recursive(const menu_title title, const std::array<char, PATH_MAX> &orig_path_storage, const partial_range_t<const file_extension_t *> &ext_range, const select_dir_flag select_dir, ntstring<PATH_MAX - 1> &userdata)
2355 {
2356 const auto sep = PHYSFS_getDirSeparator();
2357 auto orig_path = orig_path_storage.data();
2358 std::array<char, PATH_MAX> new_path;
2359
2360 // Check for a PhysicsFS path first, saves complication!
2361 if (strncmp(orig_path, sep, strlen(sep)) && PHYSFSX_exists(orig_path,0))
2362 {
2363 PHYSFSX_getRealPath(orig_path, new_path);
2364 orig_path = new_path.data();
2365 }
2366
2367 try {
2368 auto b = window_create<browser>(orig_path, title, ext_range, select_dir, sep, userdata);
2369 (void)b;
2370 return 1;
2371 } catch (browser::target_path_not_mounted) {
2372 return 0;
2373 }
2374 }
2375
construct_string_list(const char * orig_path,physfsx_mounted_path & view_path,const partial_range_t<const file_extension_t * > & ext_range,const select_dir_flag select_dir,const char * const sep)2376 string_array_t browser_storage::construct_string_list(const char *orig_path, physfsx_mounted_path &view_path, const partial_range_t<const file_extension_t *> &ext_range, const select_dir_flag select_dir, const char *const sep)
2377 {
2378 view_path.path.front() = 0;
2379 // Set the viewing directory to orig_path, or some parent of it
2380 if (orig_path)
2381 {
2382 const auto base =
2383 // Must make this an absolute path for directory browsing to work properly
2384 #ifdef _WIN32
2385 (!(isalpha(orig_path[0]) && (orig_path[1] == ':'))) // drive letter prompt (e.g. "C:"
2386 #else
2387 (orig_path[0] != '/')
2388 #endif
2389 ? PHYSFS_getBaseDir()
2390 : "";
2391 const auto vp_begin = view_path.path.begin();
2392 auto p = std::next(vp_begin, snprintf(view_path.path.data(), view_path.path.size(), "%s%s", base, orig_path) - 1);
2393 const size_t len_sep = strlen(sep);
2394 while (!view_path.mount())
2395 {
2396 /* Search from the end of the string back to the beginning,
2397 * for the first occurrence of a separator.
2398 */
2399 while (p != vp_begin && strncmp(p, sep, len_sep))
2400 p--;
2401 *p = '\0';
2402
2403 if (p == vp_begin)
2404 break;
2405 }
2406 }
2407 // Set to user directory if we couldn't find a searchpath
2408 if (!view_path.path[0])
2409 {
2410 snprintf(view_path.path.data(), view_path.path.size(), "%s", PHYSFS_getUserDir());
2411 if (!view_path.mount())
2412 {
2413 /* If the directory was not mounted, and cannot be mounted,
2414 * prevent showing the dialog.
2415 */
2416 throw target_path_not_mounted();
2417 }
2418 }
2419 string_array_t list;
2420 list.add(".."); // go to parent directory
2421 std::size_t tidy_offset = 1;
2422 if (select_dir != select_dir_flag::files_only)
2423 {
2424 ++tidy_offset;
2425 list.add("<this directory>"); // choose the directory being viewed
2426 }
2427 list_directory_context context{list, ext_range, view_path.path};
2428 PHYSFS_enumerateFilesCallback("", list_dir_el, &context);
2429 list.tidy(tidy_offset);
2430 return list;
2431 }
2432 #endif
2433
2434 #define DXX_MENU_ITEM_BROWSE(VERB, TXT, OPT) \
2435 DXX_MENUITEM(VERB, MENU, TXT " (browse...)", OPT)
2436
2437 }
2438 }
2439
2440 namespace dsx {
2441 namespace {
2442
2443 #if defined(DXX_BUILD_DESCENT_I)
2444 #define DSX_REDBOOK_PLAYORDER_TEXT "force mac cd track order"
2445 #elif defined(DXX_BUILD_DESCENT_II)
2446 #define DSX_REDBOOK_PLAYORDER_TEXT "force descent ][ cd track order"
2447 #endif
2448
2449 #if DXX_USE_SDLMIXER || defined(_WIN32)
2450 #define DXX_SOUND_ADDON_MUSIC_MENU_ITEM(VERB) \
2451 DXX_MENUITEM(VERB, RADIO, "Built-in/Addon music", opt_sm_mtype1, GameCfg.MusicType == MUSIC_TYPE_BUILTIN, optgrp_music_type) \
2452
2453 #else
2454 #define DXX_SOUND_ADDON_MUSIC_MENU_ITEM(VERB)
2455 #endif
2456
2457 #if DXX_USE_SDL_REDBOOK_AUDIO
2458 #define DXX_SOUND_CD_MUSIC_MENU_ITEM(VERB) \
2459 DXX_MENUITEM(VERB, RADIO, "CD music", opt_sm_mtype2, GameCfg.MusicType == MUSIC_TYPE_REDBOOK, optgrp_music_type) \
2460
2461 #define DXX_MUSIC_OPTIONS_CD_LABEL "CD music"
2462 #else
2463 #define DXX_SOUND_CD_MUSIC_MENU_ITEM(VERB)
2464 #define DXX_MUSIC_OPTIONS_CD_LABEL ""
2465 #endif
2466
2467 #if DXX_USE_SDLMIXER
2468 #define DXX_SOUND_JUKEBOX_MENU_ITEM(VERB) \
2469 DXX_MENUITEM(VERB, RADIO, "Jukebox", opt_sm_mtype3, GameCfg.MusicType == MUSIC_TYPE_CUSTOM, optgrp_music_type) \
2470
2471 #define DXX_MUSIC_OPTIONS_JUKEBOX_LABEL "Jukebox"
2472 #define DXX_SOUND_SDLMIXER_MENU_ITEMS(VERB) \
2473 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank2) \
2474 DXX_MENUITEM(VERB, TEXT, "Jukebox options:", opt_label_jukebox_options) \
2475 DXX_MENU_ITEM_BROWSE(VERB, "Path for level music", opt_sm_mtype3_lmpath) \
2476 DXX_MENUITEM(VERB, INPUT, CGameCfg.CMLevelMusicPath, opt_sm_mtype3_lmpath_input) \
2477 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank3) \
2478 DXX_MENUITEM(VERB, TEXT, "Level music play order:", opt_label_lm_order) \
2479 DXX_MENUITEM(VERB, RADIO, "continuous", opt_sm_mtype3_lmplayorder1, CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Continuous, optgrp_music_order) \
2480 DXX_MENUITEM(VERB, RADIO, "one track per level", opt_sm_mtype3_lmplayorder2, CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Level, optgrp_music_order) \
2481 DXX_MENUITEM(VERB, RADIO, "random", opt_sm_mtype3_lmplayorder3, CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Random, optgrp_music_order) \
2482 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank4) \
2483 DXX_MENUITEM(VERB, TEXT, "Non-level music:", opt_label_nonlevel_music) \
2484 DXX_MENU_ITEM_BROWSE(VERB, "Main menu", opt_sm_cm_mtype3_file1_b) \
2485 DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[SONG_TITLE], opt_sm_cm_mtype3_file1) \
2486 DXX_MENU_ITEM_BROWSE(VERB, "Briefing", opt_sm_cm_mtype3_file2_b) \
2487 DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[SONG_BRIEFING], opt_sm_cm_mtype3_file2) \
2488 DXX_MENU_ITEM_BROWSE(VERB, "Credits", opt_sm_cm_mtype3_file3_b) \
2489 DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[SONG_CREDITS], opt_sm_cm_mtype3_file3) \
2490 DXX_MENU_ITEM_BROWSE(VERB, "Escape sequence", opt_sm_cm_mtype3_file4_b) \
2491 DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[SONG_ENDLEVEL], opt_sm_cm_mtype3_file4) \
2492 DXX_MENU_ITEM_BROWSE(VERB, "Game ending", opt_sm_cm_mtype3_file5_b) \
2493 DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[SONG_ENDGAME], opt_sm_cm_mtype3_file5) \
2494
2495 #else
2496 #define DXX_SOUND_JUKEBOX_MENU_ITEM(VERB)
2497 #define DXX_MUSIC_OPTIONS_JUKEBOX_LABEL ""
2498 #define DXX_SOUND_SDLMIXER_MENU_ITEMS(VERB)
2499 #endif
2500
2501 #if SDL_MAJOR_VERSION == 1 && DXX_USE_SDLMIXER
2502 #define DXX_MUSIC_OPTIONS_SEPARATOR_TEXT " / "
2503 #else
2504 #define DXX_MUSIC_OPTIONS_SEPARATOR_TEXT ""
2505 #endif
2506
2507 #define DSX_SOUND_MENU(VERB) \
2508 DXX_MENUITEM(VERB, SLIDER, TXT_FX_VOLUME, opt_sm_digivol, GameCfg.DigiVolume, 0, 8) \
2509 DXX_MENUITEM(VERB, SLIDER, "Music volume", opt_sm_musicvol, GameCfg.MusicVolume, 0, 8) \
2510 DXX_MENUITEM(VERB, CHECK, TXT_REVERSE_STEREO, opt_sm_revstereo, GameCfg.ReverseStereo) \
2511 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank0) \
2512 DXX_MENUITEM(VERB, TEXT, "Music type:", opt_label_music_type) \
2513 DXX_MENUITEM(VERB, RADIO, "No music", opt_sm_mtype0, GameCfg.MusicType == MUSIC_TYPE_NONE, optgrp_music_type) \
2514 DXX_SOUND_ADDON_MUSIC_MENU_ITEM(VERB) \
2515 DXX_SOUND_CD_MUSIC_MENU_ITEM(VERB) \
2516 DXX_SOUND_JUKEBOX_MENU_ITEM(VERB) \
2517 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank1) \
2518 DXX_MENUITEM(VERB, TEXT, DXX_MUSIC_OPTIONS_CD_LABEL DXX_MUSIC_OPTIONS_SEPARATOR_TEXT DXX_MUSIC_OPTIONS_JUKEBOX_LABEL " options:", opt_label_music_options) \
2519 DXX_MENUITEM(VERB, CHECK, DSX_REDBOOK_PLAYORDER_TEXT, opt_sm_redbook_playorder, GameCfg.OrigTrackOrder) \
2520 DXX_SOUND_SDLMIXER_MENU_ITEMS(VERB) \
2521
2522 class sound_menu_items
2523 {
2524 public:
2525 enum
2526 {
2527 optgrp_music_type,
2528 #if DXX_USE_SDLMIXER
2529 optgrp_music_order,
2530 #endif
2531 };
2532 enum
2533 {
2534 DSX_SOUND_MENU(ENUM)
2535 };
2536 DSX_SOUND_MENU(DECL);
2537 std::array<newmenu_item, DSX_SOUND_MENU(COUNT)> m;
sound_menu_items()2538 sound_menu_items()
2539 {
2540 DSX_SOUND_MENU(ADD);
2541 }
read()2542 void read()
2543 {
2544 DSX_SOUND_MENU(READ);
2545 }
2546 };
2547
2548 struct sound_menu : sound_menu_items, newmenu
2549 {
2550 #if DXX_USE_SDLMIXER
2551 ntstring<PATH_MAX - 1> ¤t_music = Game_wind
2552 ? CGameCfg.CMLevelMusicPath
2553 : CGameCfg.CMMiscMusic[SONG_TITLE];
2554 ntstring<PATH_MAX - 1> old_music = current_music;
2555 #endif
sound_menudsx::__anon301c837e1c11::sound_menu2556 sound_menu(grs_canvas &src) :
2557 newmenu(menu_title{nullptr}, menu_subtitle{"Sound Effects & Music"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 0), src)
2558 {
2559 }
2560 virtual window_event_result event_handler(const d_event &event) override;
2561 };
2562
2563 #undef DSX_SOUND_MENU
2564
event_handler(const d_event & event)2565 window_event_result sound_menu::event_handler(const d_event &event)
2566 {
2567 const auto &items = m;
2568 int replay = 0;
2569 switch (event.type)
2570 {
2571 case EVENT_NEWMENU_CHANGED:
2572 {
2573 auto &citem = static_cast<const d_change_event &>(event).citem;
2574 if (citem == opt_sm_digivol)
2575 {
2576 GameCfg.DigiVolume = items[citem].value;
2577 digi_set_digi_volume( (GameCfg.DigiVolume*32768)/8 );
2578 digi_play_sample_once( SOUND_DROP_BOMB, F1_0 );
2579 }
2580 else if (citem == opt_sm_musicvol)
2581 {
2582 GameCfg.MusicVolume = items[citem].value;
2583 songs_set_volume(GameCfg.MusicVolume);
2584 }
2585 else if (citem == opt_sm_revstereo)
2586 {
2587 GameCfg.ReverseStereo = items[citem].value;
2588 }
2589 else if (citem == opt_sm_mtype0)
2590 {
2591 GameCfg.MusicType = MUSIC_TYPE_NONE;
2592 replay = 1;
2593 }
2594 /*
2595 * When builtin music is enabled, the next line expands to
2596 * `#if +1 + 0`; when it is disabled, the line expands to
2597 * `#if + 0`.
2598 */
2599 #if DXX_SOUND_ADDON_MUSIC_MENU_ITEM(COUNT) + 0
2600 else if (citem == opt_sm_mtype1)
2601 {
2602 GameCfg.MusicType = MUSIC_TYPE_BUILTIN;
2603 replay = 1;
2604 }
2605 #endif
2606 #if DXX_USE_SDL_REDBOOK_AUDIO
2607 else if (citem == opt_sm_mtype2)
2608 {
2609 GameCfg.MusicType = MUSIC_TYPE_REDBOOK;
2610 replay = 1;
2611 }
2612 #endif
2613 #if DXX_USE_SDLMIXER
2614 else if (citem == opt_sm_mtype3)
2615 {
2616 GameCfg.MusicType = MUSIC_TYPE_CUSTOM;
2617 replay = 1;
2618 }
2619 #endif
2620 else if (citem == opt_sm_redbook_playorder)
2621 {
2622 GameCfg.OrigTrackOrder = items[citem].value;
2623 replay = static_cast<bool>(Game_wind);
2624 }
2625 #if DXX_USE_SDLMIXER
2626 else if (citem == opt_sm_mtype3_lmplayorder1)
2627 {
2628 CGameCfg.CMLevelMusicPlayOrder = LevelMusicPlayOrder::Continuous;
2629 replay = static_cast<bool>(Game_wind);
2630 }
2631 else if (citem == opt_sm_mtype3_lmplayorder2)
2632 {
2633 CGameCfg.CMLevelMusicPlayOrder = LevelMusicPlayOrder::Level;
2634 replay = static_cast<bool>(Game_wind);
2635 }
2636 else if (citem == opt_sm_mtype3_lmplayorder3)
2637 {
2638 CGameCfg.CMLevelMusicPlayOrder = LevelMusicPlayOrder::Random;
2639 replay = static_cast<bool>(Game_wind);
2640 }
2641 #endif
2642 break;
2643 }
2644 case EVENT_NEWMENU_SELECTED:
2645 {
2646 #if DXX_USE_SDLMIXER
2647 auto &citem = static_cast<const d_select_event &>(event).citem;
2648 #ifdef _WIN32
2649 #define WINDOWS_DRIVE_CHANGE_TEXT ".\nCTRL-D to change drive"
2650 #else
2651 #define WINDOWS_DRIVE_CHANGE_TEXT
2652 #endif
2653 if (citem == opt_sm_mtype3_lmpath)
2654 {
2655 static const std::array<file_extension_t, 1> ext_list{{"m3u"}}; // select a directory or M3U playlist
2656 select_file_recursive(
2657 menu_title{"Select directory or\nM3U playlist to\n play level music from" WINDOWS_DRIVE_CHANGE_TEXT},
2658 CGameCfg.CMLevelMusicPath, ext_list, select_dir_flag::directories_or_files, // look in current music path for ext_list files and allow directory selection
2659 CGameCfg.CMLevelMusicPath); // just copy the absolute path
2660 }
2661 else if (citem == opt_sm_cm_mtype3_file1_b)
2662 SELECT_SONG(menu_title{"Select main menu music" WINDOWS_DRIVE_CHANGE_TEXT}, SONG_TITLE);
2663 else if (citem == opt_sm_cm_mtype3_file2_b)
2664 SELECT_SONG(menu_title{"Select briefing music" WINDOWS_DRIVE_CHANGE_TEXT}, SONG_BRIEFING);
2665 else if (citem == opt_sm_cm_mtype3_file3_b)
2666 SELECT_SONG(menu_title{"Select credits music" WINDOWS_DRIVE_CHANGE_TEXT}, SONG_CREDITS);
2667 else if (citem == opt_sm_cm_mtype3_file4_b)
2668 SELECT_SONG(menu_title{"Select escape sequence music" WINDOWS_DRIVE_CHANGE_TEXT}, SONG_ENDLEVEL);
2669 else if (citem == opt_sm_cm_mtype3_file5_b)
2670 SELECT_SONG(menu_title{"Select game ending music" WINDOWS_DRIVE_CHANGE_TEXT}, SONG_ENDGAME);
2671 #endif
2672 return window_event_result::handled; // stay in menu
2673 }
2674 case EVENT_WINDOW_CLOSE:
2675 #if DXX_USE_SDLMIXER
2676 if (strcmp(old_music.data(), current_music.data()))
2677 {
2678 songs_uninit();
2679 if (Game_wind)
2680 songs_play_level_song(Current_level_num, 0);
2681 else
2682 songs_play_song(SONG_TITLE, 1);
2683 }
2684 #endif
2685 break;
2686
2687 default:
2688 break;
2689 }
2690
2691 if (replay)
2692 {
2693 songs_uninit();
2694
2695 if (Game_wind)
2696 songs_play_level_song( Current_level_num, 0 );
2697 else
2698 songs_play_song(SONG_TITLE, 1);
2699 }
2700 return newmenu::event_handler(event);
2701 }
2702
2703 }
2704 }
2705
do_sound_menu()2706 void do_sound_menu()
2707 {
2708 auto menu = window_create<sound_menu>(grd_curscreen->sc_canvas);
2709 (void)menu;
2710 }
2711
2712 namespace dsx {
2713
2714 namespace {
2715
2716 #if defined(DXX_BUILD_DESCENT_I)
2717 #define DSX_GAME_SPECIFIC_OPTIONS(VERB) \
2718
2719 #elif defined(DXX_BUILD_DESCENT_II)
2720 #define DSX_GAME_SPECIFIC_OPTIONS(VERB) \
2721 DXX_MENUITEM(VERB, CHECK, "Headlight on when picked up", opt_headlighton,PlayerCfg.HeadlightActiveDefault ) \
2722 DXX_MENUITEM(VERB, CHECK, "Escort robot hot keys",opt_escorthotkey,PlayerCfg.EscortHotKeys) \
2723 DXX_MENUITEM(VERB, CHECK, "Movie Subtitles",opt_moviesubtitle,GameCfg.MovieSubtitles) \
2724 DXX_MENUITEM(VERB, CHECK, "Remove Thief at level start", opt_thief_presence, thief_absent) \
2725 DXX_MENUITEM(VERB, CHECK, "Prevent Thief Stealing Energy Weapons", opt_thief_steal_energy, thief_cannot_steal_energy_weapons) \
2726
2727 #endif
2728
2729 #define DSX_GAMEPLAY_MENU_OPTIONS(VERB) \
2730 DXX_MENUITEM(VERB, CHECK, "Ship auto-leveling",opt_autolevel, PlayerCfg.AutoLeveling) \
2731 DXX_MENUITEM(VERB, CHECK, "Persistent Debris",opt_persist_debris,PlayerCfg.PersistentDebris) \
2732 DXX_MENUITEM(VERB, CHECK, "No Rankings (Multi)",opt_noranking,PlayerCfg.NoRankings) \
2733 DXX_MENUITEM(VERB, CHECK, "Free Flight in Automap",opt_freeflight, PlayerCfg.AutomapFreeFlight) \
2734 DSX_GAME_SPECIFIC_OPTIONS(VERB) \
2735 DXX_MENUITEM(VERB, TEXT, "", opt_label_blank) \
2736 DXX_MENUITEM(VERB, TEXT, "Weapon Autoselect options:", opt_label_autoselect) \
2737 DXX_MENUITEM(VERB, MENU, "Primary ordering...", opt_gameplay_reorderprimary_menu) \
2738 DXX_MENUITEM(VERB, MENU, "Secondary ordering...", opt_gameplay_reordersecondary_menu) \
2739 DXX_MENUITEM(VERB, TEXT, "Autoselect while firing:", opt_autoselect_firing_label) \
2740 DXX_MENUITEM(VERB, RADIO, "Immediately", opt_autoselect_firing_immediate, PlayerCfg.NoFireAutoselect == FiringAutoselectMode::Immediate, optgrp_autoselect_firing) \
2741 DXX_MENUITEM(VERB, RADIO, "Never", opt_autoselect_firing_never, PlayerCfg.NoFireAutoselect == FiringAutoselectMode::Never, optgrp_autoselect_firing) \
2742 DXX_MENUITEM(VERB, RADIO, "When firing stops", opt_autoselect_firing_delayed, PlayerCfg.NoFireAutoselect == FiringAutoselectMode::Delayed, optgrp_autoselect_firing) \
2743 DXX_MENUITEM(VERB, CHECK, "Only Cycle Autoselect Weapons",opt_only_autoselect,PlayerCfg.CycleAutoselectOnly) \
2744 DXX_MENUITEM_AUTOSAVE_LABEL_INPUT(VERB) \
2745
2746 struct gameplay_config_menu_items
2747 {
2748 enum {
2749 DSX_GAMEPLAY_MENU_OPTIONS(ENUM)
2750 };
2751 DSX_GAMEPLAY_MENU_OPTIONS(DECL);
2752 std::array<newmenu_item, DSX_GAMEPLAY_MENU_OPTIONS(COUNT)> m;
2753 human_readable_mmss_time<decltype(d_gameplay_options::AutosaveInterval)::rep> AutosaveInterval;
gameplay_config_menu_itemsdsx::__anon301c837e1f11::gameplay_config_menu_items2754 gameplay_config_menu_items()
2755 {
2756 #if defined(DXX_BUILD_DESCENT_II)
2757 auto thief_absent = PlayerCfg.ThiefModifierFlags & ThiefModifier::Absent;
2758 auto thief_cannot_steal_energy_weapons = PlayerCfg.ThiefModifierFlags & ThiefModifier::NoEnergyWeapons;
2759 #endif
2760 format_human_readable_time(AutosaveInterval, PlayerCfg.SPGameplayOptions.AutosaveInterval);
2761 DSX_GAMEPLAY_MENU_OPTIONS(ADD);
2762 }
2763 };
2764
2765 struct gameplay_config_menu : gameplay_config_menu_items, newmenu
2766 {
gameplay_config_menudsx::__anon301c837e1f11::gameplay_config_menu2767 gameplay_config_menu(grs_canvas &src) :
2768 newmenu(menu_title{nullptr}, menu_subtitle{"Gameplay Options"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 0), src)
2769 {
2770 }
2771 virtual window_event_result event_handler(const d_event &event) override;
2772 };
2773
event_handler(const d_event & event)2774 window_event_result gameplay_config_menu::event_handler(const d_event &event)
2775 {
2776 switch (event.type)
2777 {
2778 case EVENT_NEWMENU_SELECTED:
2779 {
2780 auto &citem = static_cast<const d_select_event &>(event).citem;
2781 if (citem == opt_gameplay_reorderprimary_menu)
2782 ReorderPrimary();
2783 else if (citem == opt_gameplay_reordersecondary_menu)
2784 ReorderSecondary();
2785 return window_event_result::handled; // stay in menu
2786 }
2787 case EVENT_WINDOW_CLOSE:
2788 {
2789 #if defined(DXX_BUILD_DESCENT_II)
2790 uint8_t thief_absent;
2791 uint8_t thief_cannot_steal_energy_weapons;
2792 #endif
2793 DSX_GAMEPLAY_MENU_OPTIONS(READ);
2794 PlayerCfg.NoFireAutoselect = m[opt_autoselect_firing_delayed].value
2795 ? FiringAutoselectMode::Delayed
2796 : (m[opt_autoselect_firing_immediate].value
2797 ? FiringAutoselectMode::Immediate
2798 : FiringAutoselectMode::Never);
2799 #if defined(DXX_BUILD_DESCENT_II)
2800 PlayerCfg.ThiefModifierFlags =
2801 (thief_absent ? ThiefModifier::Absent : 0) |
2802 (thief_cannot_steal_energy_weapons ? ThiefModifier::NoEnergyWeapons : 0);
2803 #endif
2804 parse_human_readable_time(PlayerCfg.SPGameplayOptions.AutosaveInterval, AutosaveInterval);
2805 }
2806 break;
2807
2808 default:
2809 break;
2810 }
2811 return newmenu::event_handler(event);
2812 }
2813
gameplay_config()2814 void gameplay_config()
2815 {
2816 auto menu = window_create<gameplay_config_menu>(grd_curscreen->sc_canvas);
2817 (void)menu;
2818 }
2819
2820 #if DXX_USE_UDP
event_handler(const d_event & event)2821 window_event_result netgame_menu::event_handler(const d_event &event)
2822 {
2823 switch (event.type)
2824 {
2825 case EVENT_NEWMENU_SELECTED:
2826 {
2827 auto &citem = static_cast<const d_select_event &>(event).citem;
2828 // stay in multiplayer menu, even after having played a game
2829 return dispatch_menu_option(static_cast<netgame_menu_item_index>(citem));
2830 }
2831 default:
2832 break;
2833 }
2834 return newmenu::event_handler(event);
2835 }
2836
do_multi_player_menu()2837 void do_multi_player_menu()
2838 {
2839 auto menu = window_create<netgame_menu>(grd_curscreen->sc_canvas);
2840 (void)menu;
2841 }
2842 #endif
2843
2844 }
2845
2846 }
2847
do_options_menu()2848 void do_options_menu()
2849 {
2850 // Fall back to main event loop
2851 // Allows clean closing and re-opening when resolution changes
2852 auto menu = window_create<options_menu>(grd_curscreen->sc_canvas);
2853 (void)menu;
2854 }
2855
2856 #ifndef RELEASE
2857 namespace dsx {
2858
2859 namespace {
2860
2861 struct polygon_models_viewer_window : window
2862 {
2863 vms_angvec ang{0, 0, F0_5 - 1};
2864 unsigned view_idx = 0;
2865 using window::window;
2866 virtual window_event_result event_handler(const d_event &) override;
2867 };
2868
2869 struct gamebitmaps_viewer_window : window
2870 {
2871 unsigned view_idx = 0;
2872 using window::window;
2873 virtual window_event_result event_handler(const d_event &) override;
2874 };
2875
event_handler(const d_event & event)2876 window_event_result polygon_models_viewer_window::event_handler(const d_event &event)
2877 {
2878 int key = 0;
2879
2880 switch (event.type)
2881 {
2882 case EVENT_WINDOW_ACTIVATED:
2883 #if defined(DXX_BUILD_DESCENT_II)
2884 gr_use_palette_table("groupa.256");
2885 #endif
2886 key_toggle_repeat(1);
2887 break;
2888 case EVENT_KEY_COMMAND:
2889 key = event_key_get(event);
2890 switch (key)
2891 {
2892 case KEY_ESC:
2893 return window_event_result::close;
2894 case KEY_SPACEBAR:
2895 view_idx ++;
2896 if (view_idx >= LevelSharedPolygonModelState.N_polygon_models)
2897 view_idx = 0;
2898 break;
2899 case KEY_BACKSP:
2900 if (!view_idx)
2901 view_idx = LevelSharedPolygonModelState.N_polygon_models - 1;
2902 else
2903 view_idx --;
2904 break;
2905 case KEY_A:
2906 ang.h -= 100;
2907 break;
2908 case KEY_D:
2909 ang.h += 100;
2910 break;
2911 case KEY_W:
2912 ang.p -= 100;
2913 break;
2914 case KEY_S:
2915 ang.p += 100;
2916 break;
2917 case KEY_Q:
2918 ang.b -= 100;
2919 break;
2920 case KEY_E:
2921 ang.b += 100;
2922 break;
2923 case KEY_R:
2924 ang.p = ang.b = 0;
2925 ang.h = F0_5-1;
2926 break;
2927 default:
2928 break;
2929 }
2930 return window_event_result::handled;
2931 case EVENT_WINDOW_DRAW:
2932 timer_delay(F1_0/60);
2933 {
2934 auto &canvas = *grd_curcanv;
2935 draw_model_picture(canvas, view_idx, ang);
2936 gr_set_fontcolor(canvas, BM_XRGB(255, 255, 255), -1);
2937 auto &game_font = *GAME_FONT;
2938 gr_printf(canvas, game_font, FSPACX(1), FSPACY(1), "ESC: leave\nSPACE/BACKSP: next/prev model (%i/%i)\nA/D: rotate y\nW/S: rotate x\nQ/E: rotate z\nR: reset orientation", view_idx, LevelSharedPolygonModelState.N_polygon_models - 1);
2939 }
2940 break;
2941 case EVENT_WINDOW_CLOSE:
2942 load_palette(MENU_PALETTE,0,1);
2943 key_toggle_repeat(0);
2944 break;
2945 default:
2946 break;
2947 }
2948 return window_event_result::ignored;
2949 }
2950
polygon_models_viewer()2951 static void polygon_models_viewer()
2952 {
2953 auto viewer_window = window_create<polygon_models_viewer_window>(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT);
2954 (void)viewer_window;
2955 }
2956
event_handler(const d_event & event)2957 window_event_result gamebitmaps_viewer_window::event_handler(const d_event &event)
2958 {
2959 int key = 0;
2960 #if DXX_USE_OGL
2961 float scale = 1.0;
2962 #endif
2963 bitmap_index bi;
2964 grs_bitmap *bm;
2965
2966 switch (event.type)
2967 {
2968 case EVENT_WINDOW_ACTIVATED:
2969 #if defined(DXX_BUILD_DESCENT_II)
2970 gr_use_palette_table("groupa.256");
2971 #endif
2972 key_toggle_repeat(1);
2973 break;
2974 case EVENT_KEY_COMMAND:
2975 key = event_key_get(event);
2976 switch (key)
2977 {
2978 case KEY_ESC:
2979 return window_event_result::close;
2980 case KEY_SPACEBAR:
2981 view_idx ++;
2982 if (view_idx >= Num_bitmap_files) view_idx = 0;
2983 break;
2984 case KEY_BACKSP:
2985 if (!view_idx)
2986 view_idx = Num_bitmap_files;
2987 view_idx --;
2988 break;
2989 default:
2990 break;
2991 }
2992 return window_event_result::handled;
2993 case EVENT_WINDOW_DRAW:
2994 bi.index = view_idx;
2995 bm = &GameBitmaps[view_idx];
2996 timer_delay(F1_0/60);
2997 PIGGY_PAGE_IN(bi);
2998 {
2999 auto &canvas = *grd_curcanv;
3000 gr_clear_canvas(canvas, BM_XRGB(0,0,0));
3001 #if DXX_USE_OGL
3002 scale = (bm->bm_w > bm->bm_h)?(SHEIGHT/bm->bm_w)*0.8:(SHEIGHT/bm->bm_h)*0.8;
3003 ogl_ubitmapm_cs(canvas, (SWIDTH / 2) - (bm->bm_w * scale / 2), (SHEIGHT / 2) - (bm->bm_h * scale / 2), bm->bm_w * scale, bm->bm_h * scale, *bm, ogl_colors::white);
3004 #else
3005 gr_bitmap(canvas, (SWIDTH / 2) - (bm->bm_w / 2), (SHEIGHT / 2) - (bm->bm_h / 2), *bm);
3006 #endif
3007 gr_set_fontcolor(canvas, BM_XRGB(255, 255, 255), -1);
3008 auto &game_font = *GAME_FONT;
3009 gr_printf(canvas, game_font, FSPACX(1), FSPACY(1), "ESC: leave\nSPACE/BACKSP: next/prev bitmap (%i/%i)", view_idx, Num_bitmap_files-1);
3010 }
3011 break;
3012 case EVENT_WINDOW_CLOSE:
3013 load_palette(MENU_PALETTE,0,1);
3014 key_toggle_repeat(0);
3015 break;
3016 default:
3017 break;
3018 }
3019 return window_event_result::ignored;
3020 }
3021
gamebitmaps_viewer()3022 static void gamebitmaps_viewer()
3023 {
3024 auto viewer_window = window_create<gamebitmaps_viewer_window>(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT);
3025 (void)viewer_window;
3026 }
3027
3028 #define DXX_SANDBOX_MENU(VERB) \
3029 DXX_MENUITEM(VERB, MENU, "Polygon_models viewer", polygon_models) \
3030 DXX_MENUITEM(VERB, MENU, "GameBitmaps viewer", bitmaps) \
3031
3032 class sandbox_menu_items
3033 {
3034 public:
3035 enum
3036 {
3037 DXX_SANDBOX_MENU(ENUM)
3038 };
3039 DXX_SANDBOX_MENU(DECL);
3040 std::array<newmenu_item, DXX_SANDBOX_MENU(COUNT)> m;
sandbox_menu_items()3041 sandbox_menu_items()
3042 {
3043 DXX_SANDBOX_MENU(ADD);
3044 }
3045 };
3046
3047 struct sandbox_menu : sandbox_menu_items, newmenu
3048 {
sandbox_menudsx::__anon301c837e2111::sandbox_menu3049 sandbox_menu(grs_canvas &src) :
3050 newmenu(menu_title{nullptr}, menu_subtitle{"Coder's sandbox"}, menu_filename{nullptr}, tiny_mode_flag::normal, tab_processing_flag::ignore, adjusted_citem::create(m, 0), src)
3051 {
3052 }
3053 virtual window_event_result event_handler(const d_event &event) override;
3054 };
3055
event_handler(const d_event & event)3056 window_event_result sandbox_menu::event_handler(const d_event &event)
3057 {
3058 switch (event.type)
3059 {
3060 case EVENT_NEWMENU_SELECTED:
3061 {
3062 auto &citem = static_cast<const d_select_event &>(event).citem;
3063 switch (citem)
3064 {
3065 case sandbox_menu_items::polygon_models:
3066 polygon_models_viewer();
3067 break;
3068 case sandbox_menu_items::bitmaps:
3069 gamebitmaps_viewer();
3070 break;
3071 }
3072 return window_event_result::handled; // stay in menu until escape
3073 }
3074 default:
3075 break;
3076 }
3077 return newmenu::event_handler(event);
3078 }
3079
do_sandbox_menu()3080 void do_sandbox_menu()
3081 {
3082 auto menu = window_create<sandbox_menu>(grd_curscreen->sc_canvas);
3083 (void)menu;
3084 }
3085
3086 }
3087
3088 }
3089 #endif
3090