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> &current_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