/*
 * Portions of this file are copyright Rebirth contributors and licensed as
 * described in COPYING.txt.
 * Portions of this file are copyright Parallax Software and licensed
 * according to the Parallax license below.
 * See COPYING.txt for license details.

THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
SOFTWARE CORPORATION ("PARALLAX").  PARALLAX, IN DISTRIBUTING THE CODE TO
END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
IN USING, DISPLAYING,  AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
FREE PURPOSES.  IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES.  THE END-USER UNDERSTANDS
AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION.  ALL RIGHTS RESERVED.
*/

/*
 *
 * Inferno main menu.
 *
 */

#include "dxxsconf.h"
#include <stdio.h>
#include <string.h>
#include <ranges>
#include <SDL.h>

#include "menu.h"
#include "inferno.h"
#include "game.h"
#include "gr.h"
#include "key.h"
#include "mouse.h"
#include "u_mem.h"
#include "dxxerror.h"
#include "bm.h"
#include "screens.h"
#include "joy.h"
#include "player.h"
#include "vecmat.h"
#include "game.h"
#include "palette.h"
#include "args.h"
#include "newdemo.h"
#include "timer.h"
#include "sounds.h"
#include "gameseq.h"
#include "text.h"
#include "gamefont.h"
#include "newmenu.h"
#include "scores.h"
#include "playsave.h"
#include "kconfig.h"
#include "credits.h"
#include "polyobj.h"
#include "state.h"
#include "mission.h"
#include "songs.h"
#if DXX_USE_SDLMIXER
#include "jukebox.h" // for jukebox_exts
#endif
#include "config.h"
#if DXX_BUILD_DESCENT == 2
#include "movie.h"
#endif
#include "gamepal.h"
#include "powerup.h"
#include "strutil.h"
#include "multi.h"
#include "vers_id.h"
#if DXX_USE_UDP
#include "net_udp.h"
#endif
#if DXX_USE_EDITOR
#include "editor/editor.h"
#include "editor/kdefs.h"
#endif
#if DXX_USE_OGL
#include "ogl_init.h"
#include "ogl_extensions.h"
#endif
#include "physfs_list.h"

#include "dsx-ns.h"
#include "compiler-range_for.h"
#include "d_enumerate.h"
#include "d_range.h"
#include "d_zip.h"
#include "partial_range.h"
#include <memory>
#include <utility>

// Menu IDs...

namespace dcx {

namespace {

enum {
	optgrp_autoselect_firing,
};

enum class main_menu_item_index : uint8_t
{
	start_new_singleplayer_game = 0,
	load_existing_singleplayer_game,
#if DXX_USE_UDP
	open_multiplayer_submenu,
#endif
	open_options_submenu,
	create_new_pilot_profile,
	open_pick_recorded_demo_submenu,
	open_high_scores_dialog,
	open_credits_scroll_window,
    quit_program,
#ifndef RELEASE
#if DXX_USE_EDITOR
    open_mine_editor_window,
#endif
	open_coder_sandbox_submenu,
#endif
	end,
};

struct main_menu_items
{
	enumerated_array<newmenu_item, static_cast<std::size_t>(main_menu_item_index::end), main_menu_item_index> m;
	main_menu_items();
};

#if DXX_USE_UDP
enum class netgame_menu_item_index : uint8_t
{
	start_new_multiplayer_game,
	list_multiplayer_games,
	join_multiplayer_game,
};

struct netgame_menu_items
{
	enumerated_array<newmenu_item, 3, netgame_menu_item_index> m;
	netgame_menu_items();
};

netgame_menu_items::netgame_menu_items()
{
	nm_set_item_menu(m[netgame_menu_item_index::start_new_multiplayer_game], "HOST GAME");
#if DXX_USE_TRACKER
#define DXX_MULTIPLAYER_MENU_FIND_GAME_TYPE_STRING	"/ONLINE"
#else
#define DXX_MULTIPLAYER_MENU_FIND_GAME_TYPE_STRING	""
#endif
	nm_set_item_menu(m[netgame_menu_item_index::list_multiplayer_games], "FIND LAN" DXX_MULTIPLAYER_MENU_FIND_GAME_TYPE_STRING " GAMES");
#undef DXX_MULTIPLAYER_MENU_FIND_GAME_TYPE_STRING
	nm_set_item_menu(m[netgame_menu_item_index::join_multiplayer_game], "JOIN GAME MANUALLY");
}
#endif

static std::array<window *, 16> menus;

main_menu_items::main_menu_items()
{
	nm_set_item_menu(m[main_menu_item_index::start_new_singleplayer_game], TXT_NEW_GAME);
	nm_set_item_menu(m[main_menu_item_index::load_existing_singleplayer_game], TXT_LOAD_GAME);
#if DXX_USE_UDP
	nm_set_item_menu(m[main_menu_item_index::open_multiplayer_submenu], TXT_MULTIPLAYER_);
#endif

	nm_set_item_menu(m[main_menu_item_index::open_options_submenu], TXT_OPTIONS_);
	nm_set_item_menu(m[main_menu_item_index::create_new_pilot_profile], TXT_CHANGE_PILOTS);
	nm_set_item_menu(m[main_menu_item_index::open_pick_recorded_demo_submenu], TXT_VIEW_DEMO);
	nm_set_item_menu(m[main_menu_item_index::open_high_scores_dialog], TXT_VIEW_SCORES);
	nm_set_item_menu(m[main_menu_item_index::open_credits_scroll_window], TXT_CREDITS);
	nm_set_item_menu(m[main_menu_item_index::quit_program], TXT_QUIT);

#ifndef RELEASE
#if DXX_USE_EDITOR
	nm_set_item_menu(m[main_menu_item_index::open_mine_editor_window], "  Editor");
#endif
	nm_set_item_menu(m[main_menu_item_index::open_coder_sandbox_submenu], "  SANDBOX");
#endif
}

static void delete_player_single_player_saved_game(const char *const name, const unsigned i)
{
	char filename[PATH_MAX];
	snprintf(filename, sizeof(filename), PLAYER_DIRECTORY_STRING("%s.sg%x"), name, i);
	PHYSFS_delete(filename);
}

static void delete_player_multi_player_saved_game(const char *const name, const unsigned i)
{
	char filename[PATH_MAX];
	snprintf(filename, sizeof(filename), PLAYER_DIRECTORY_STRING("%s.mg%x"), name, i);
	PHYSFS_delete(filename);
}

static void delete_player_saved_games(const char *const name)
{
	for (const auto i : xrange(11u))
	{
		delete_player_single_player_saved_game(name, i);
		delete_player_multi_player_saved_game(name, i);
	}
}

template <typename T>
using select_file_subfunction = window_event_result (*)(T *, const char *);

[[nodiscard]]
std::optional<std::chrono::seconds> parse_human_readable_time(const char *const buf)
{
	char *endptr_minutes{};
	const std::chrono::minutes m{strtoul(buf, &endptr_minutes, 10)};
	if (*endptr_minutes == 0)
		/* Assume that a pure-integer string is a count of minutes. */
		return m;
	if (const auto c0{*endptr_minutes}; c0 == 'm')
	{
		char *endptr_seconds{};
		const std::chrono::seconds s{strtoul(endptr_minutes + 1, &endptr_seconds, 10)};
		if (*endptr_seconds == 's')
			/* The trailing 's' is optional, but no character other than
			 * the optional 's' can follow the number.
			 */
			++endptr_seconds;
		if (*endptr_seconds == 0)
			return m + s;
	}
	else if (c0 == 's' && endptr_minutes[1] == 0)
		/* Input is only seconds.  Use `.count()` to extract the raw
		 * value without scaling.
		 */
		return std::chrono::seconds{m.count()};
	return std::nullopt;
}

#if DXX_USE_SDLMIXER
enum class select_dir_flag : bool
{
	files_only,
	directories_or_files,
};

dxx_compiler_attribute_nonnull()
static int select_file_recursive(const menu_title title, const std::array<char, PATH_MAX> &orig_path, const std::ranges::subrange<const file_extension_t *> &ext_list, select_dir_flag select_dir, ntstring<PATH_MAX - 1> &userdata);

static window_event_result get_absolute_path(ntstring<PATH_MAX - 1> &full_path, const char *rel_path)
{
	if (!PHYSFSX_getRealPath(rel_path, full_path))
		full_path.front() = 0;
	return window_event_result::close;
}

#define SELECT_SONG(t, s)	select_file_recursive(t, CGameCfg.CMMiscMusic[s], jukebox_exts, select_dir_flag::files_only, CGameCfg.CMMiscMusic[s])
#endif

}

human_readable_mmss_time<uint16_t> build_human_readable_time(const std::chrono::duration<uint16_t, std::chrono::seconds::period> duration)
{
	const auto &&split_interval = std::div(duration.count(), static_cast<int>(std::chrono::minutes::period::num));
	human_readable_mmss_time<uint16_t> buf{};
	const std::size_t bufsize{std::size(buf)};
	if (unlikely(static_cast<std::size_t>(snprintf(std::data(buf), bufsize, "%im%is", split_interval.quot, split_interval.rem)) >= bufsize))
		buf[0] = 0;
	return buf;
}

template <typename Rep, std::size_t S>
void parse_human_readable_time(std::chrono::duration<Rep, std::chrono::seconds::period> &duration, const std::array<char, S> &buf)
{
	const auto &&r = parse_human_readable_time(buf.data());
	if (r)
		duration = *r;
}

template void parse_human_readable_time(autosave_interval_type &, const human_readable_mmss_time<autosave_interval_type::rep> &buf);

}

namespace dsx {

namespace {

struct main_menu : main_menu_items, newmenu
{
	main_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

static window_event_result do_new_game_menu();
#ifndef RELEASE
void do_sandbox_menu();
#endif
int select_demo();
#if DXX_USE_UDP
static void do_multi_player_menu();
#endif

#if DXX_USE_UDP
struct netgame_menu : netgame_menu_items, newmenu
{
	netgame_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};
#endif

}

}

// Hide all menus
int hide_menus(void)
{
	window *wind;
	if (menus[0])
		return 0;		// there are already hidden menus

	wind = window_get_front();
	range_for (auto &i, menus)
	{
		i = wind;
		if (!wind)
			break;
		wind = wind->set_visible(0);
	}
	Assert(window_get_front() == NULL);
	return 1;
}

// Show all menus, with the front one shown first
// This makes sure event_type::window_activated is only sent to that window
void show_menus(void)
{
	range_for (auto &i, menus)
	{
		if (!i)
			break;

		// Hidden windows don't receive events, so the only way to close is outside its handler
		// Which there should be no cases of here
		// window_exists could return a false positive if a new window was created
		// with the same pointer value as the deleted one, so killing window_exists (call and function)
		// if (window_exists(i))
		std::exchange(i, nullptr)->set_visible(1);
	}
}

namespace dcx {

/* This is a hack to prevent writing to freed memory.  Various points in
 * the game code call `hide_menus()`, then later use `show_menus()` to
 * reverse the effect.  If the forcibly hidden window is deleted before
 * `show_menus()` is called, the attempt to show it would write to freed
 * memory.  This hook is called when a window is deleted, so that the
 * deleted window can be removed from menus[].  Removing it from menus[]
 * prevents `show_menus()` trying to make it visible later.
 *
 * It would be cleaner, but more invasive, to restructure the code so
 * that the menus[] array does not need to exist and window pointers are
 * not stored outside the control of their owner.
 */
void menu_destroy_hook(window *w)
{
	const auto &&e = menus.end();
	const auto &&i = std::find(menus.begin(), e, w);
	if (i == e)
		/* Not a hidden menu */
		return;
	/* This is not run often enough to merit a clever loop that stops
	 * when it reaches an unused element.
	 */
	std::move(std::next(i), e, i);
	menus.back() = nullptr;
}

//pairs of chars describing ranges
constexpr char playername_allowed_chars[] = "azAZ09__--";

}

namespace dsx {

namespace {

static int MakeNewPlayerFile(int allow_abort)
{
	char filename[PATH_MAX];
	auto text = InterfaceUniqueState.PilotName;

	for (;;)
	{
		using items_type = std::array<newmenu_item, 1>;
		struct pilot_name_menu : items_type, passive_newmenu
		{
			pilot_name_menu(grs_canvas &canvas, callsign_t &text) :
				items_type{{
					newmenu_item::nm_item_input(text.a, playername_allowed_chars),
				}},
				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)
			{
			}
		};
		const auto x = run_blocking_newmenu<pilot_name_menu>(*grd_curcanv, text);
		const char *const name = text;
		if (x < 0 || !*name)
		{
			if (allow_abort)
				return 0;
			/* If the entered name is empty, reject it and prompt again.
			 */
			continue;
		}
		text.lower();
		snprintf(filename, sizeof(filename), PLAYER_DIRECTORY_STRING("%s.plr"), name);
		if (PHYSFS_exists(filename))
		{
			nm_messagebox(menu_title{nullptr}, {TXT_OK}, "%s '%s' %s", TXT_PLAYER, name, TXT_ALREADY_EXISTS);
			continue;
		}
		break;
	}

	new_player_config();
	InterfaceUniqueState.PilotName = text;
	InterfaceUniqueState.update_window_title();
	write_player_file();

	return 1;
}

static void do_sound_menu();
}

}

namespace {

struct pilot_selection_listbox : listbox
{
	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) :
		listbox(citem, nitems, name_pointer_strings.get(), menu_title{TXT_SELECT_PILOT}, canvas, allow_abort_flag),
		name_pointer_storage(std::move(name_pointer_strings)), physfs_list_storage(std::move(physfs_list_strings))
	{
	}
	std::unique_ptr<const char *[]> name_pointer_storage;
	PHYSFSX_uncounted_list physfs_list_storage;
	virtual window_event_result callback_handler(const d_event &, window_event_result default_return_value) override;
};

static window_event_result player_menu_keycommand( listbox *lb,const d_event &event )
{
	const char **items = listbox_get_items(*lb);
	int citem = listbox_get_citem(*lb);

	switch (event_key_get(event))
	{
		case KEY_CTRLED+KEY_D:
			if (citem > 0)
			{
				int x{1};
				x = nm_messagebox(menu_title{nullptr}, {TXT_YES, TXT_NO}, "%s %s?", TXT_DELETE_PILOT, items[citem]+((items[citem][0]=='$')?1:0) );
				if (x==0)	{
					char plxfile[PATH_MAX], efffile[PATH_MAX], ngpfile[PATH_MAX];
					int ret;
					char name[PATH_MAX];

					snprintf(name, sizeof(name), PLAYER_DIRECTORY_STRING("%.8s.plr"), items[citem]);

					ret = !PHYSFS_delete(name);

					if (!ret)
					{
						delete_player_saved_games( items[citem] );
						// delete PLX file
						snprintf(plxfile, sizeof(plxfile), PLAYER_DIRECTORY_STRING("%.8s.plx"), items[citem]);
						if (PHYSFS_exists(plxfile))
							PHYSFS_delete(plxfile);
						// delete EFF file
						snprintf(efffile, sizeof(efffile), PLAYER_DIRECTORY_STRING("%.8s.eff"), items[citem]);
						if (PHYSFS_exists(efffile))
							PHYSFS_delete(efffile);
						// delete NGP file
						snprintf(ngpfile, sizeof(ngpfile), PLAYER_DIRECTORY_STRING("%.8s.ngp"), items[citem]);
						if (PHYSFS_exists(ngpfile))
							PHYSFS_delete(ngpfile);
					}

					if (ret)
						nm_messagebox(menu_title{nullptr}, {TXT_OK}, "%s %s %s", TXT_COULDNT, TXT_DELETE_PILOT, items[citem]+((items[citem][0]=='$')?1:0) );
					else
						listbox_delete_item(*lb, citem);
				}

				return window_event_result::handled;
			}
			break;
	}

	return window_event_result::ignored;
}

window_event_result pilot_selection_listbox::callback_handler(const d_event &event, window_event_result)
{
	switch (event.type)
	{
		case event_type::key_command:
			return player_menu_keycommand(this, event);
		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
			if (citem < 0)
				return window_event_result::ignored;		// shouldn't happen
			else if (citem == 0)
			{
				// They selected 'create new pilot'
				return MakeNewPlayerFile(1) ? window_event_result::close : window_event_result::handled;
			}
			else
			{
				const auto p = item[citem];
				InterfaceUniqueState.PilotName.copy_lower(std::span(p, strlen(p)));
				InterfaceUniqueState.update_window_title();
			}
			return window_event_result::close;
		}

		case event_type::window_close:
			if (read_player_file() != EZERO)
				return window_event_result::handled;		// abort close!

			WriteConfigFile(CGameCfg, GameCfg);		// Update lastplr
			break;

		default:
			break;
	}

	return window_event_result::ignored;
}

}

namespace dsx {

namespace {

//Inputs the player's name, without putting up the background screen
static void RegisterPlayer()
{
	static const std::array<file_extension_t, 1> types{{"plr"}};
	int NumItems;
	int citem{0};
	uint8_t allow_abort_flag{1};

	auto &callsign = InterfaceUniqueState.PilotName;
	if (!callsign[0u])
	{
		if (!*static_cast<const char *>(GameCfg.LastPlayer))
		{
			callsign = "player";
			allow_abort_flag = 0;
		}
		else
		{
			// Read the last player's name from config file, not lastplr.txt
			callsign = GameCfg.LastPlayer;
		}
		InterfaceUniqueState.update_window_title();
	}

	auto list = PHYSFSX_findFiles(PLAYER_DIRECTORY_STRING(""), types);
	if (!list)
		return;	// memory error
	if (!list[0])
	{
		MakeNewPlayerFile(0);	// make a new player without showing listbox
		return;
	}


	for (NumItems = 0; list[NumItems] != NULL; NumItems++) {}
	NumItems++;		// for TXT_CREATE_NEW

	auto m = std::make_unique<const char *[]>(NumItems);

	/* Index of the first undefined element */
	auto idx_next_string = 0u;
	m[idx_next_string++] = TXT_CREATE_NEW;
	const auto idx_first_player_string{idx_next_string};

	range_for (const auto f, list)
	{
		const auto p = strchr(f, '.');
		if (!p)
			/* This should not happen. */
			continue;
		if (f == p)
			/* First character is '.', so there is no name. */
			continue;
		if (std::distance(f, p) > CALLSIGN_LEN)
			/* Filename is too long to be a valid callsign. */
			continue;
		*p = 0;
		m[idx_next_string++] = f;
	}

	if (idx_first_player_string == idx_next_string)
	{
		/* Every returned file was unacceptable. */
		MakeNewPlayerFile(0);	// make a new player without showing listbox
		return;
	}

	for (auto &&[i, mi] : enumerate(unchecked_partial_range(m.get(), idx_next_string)))
		if (!d_stricmp(static_cast<const char *>(callsign), mi))
		{
			citem = i;
			break;
		}

	auto lb = window_create<pilot_selection_listbox>(citem, idx_next_string, std::move(m), std::move(list), grd_curscreen->sc_canvas, allow_abort_flag);
	(void)lb;
}

static void input_config();

// Draw Copyright and Version strings
static void draw_copyright(grs_canvas &canvas, grs_font &game_font)
{
	gr_set_fontcolor(canvas, BM_XRGB(6, 6, 6), -1);
	const auto &&line_spacing = LINE_SPACING(game_font, game_font);
	const auto bm_h = canvas.cv_bitmap.bm_h;
	gr_string(canvas, game_font, 0x8000, bm_h - line_spacing, TXT_COPYRIGHT);
	gr_set_fontcolor(canvas, BM_XRGB(25, 0, 0), -1);
	gr_string(canvas, game_font, 0x8000, bm_h - (line_spacing * 2), DESCENT_VERSION);
}

//returns flag, true means quit menu
window_event_result dispatch_menu_option(const main_menu_item_index select)
{
	switch (select)
	{
		case main_menu_item_index::start_new_singleplayer_game:
			select_mission(mission_filter_mode::exclude_anarchy, menu_title{"New Game\n\nSelect mission"}, do_new_game_menu);
			break;
		case main_menu_item_index::open_pick_recorded_demo_submenu:
			select_demo();
			break;
		case main_menu_item_index::load_existing_singleplayer_game:
			state_restore_all(0, secret_restore::none, nullptr, blind_save::no);
			break;
#ifndef RELEASE
#if DXX_USE_EDITOR
		case main_menu_item_index::open_mine_editor_window:
			if (!Current_mission)
			{
				create_new_mine();
				SetPlayerFromCurseg();
			}

			hide_menus();
			init_editor();
			break;
#endif
#endif
		case main_menu_item_index::open_high_scores_dialog:
			scores_view_menu(grd_curscreen->sc_canvas);
			break;
		case main_menu_item_index::quit_program:
#if DXX_USE_EDITOR
			if (!SafetyCheck())
				break;
#endif
			return window_event_result::close;

		case main_menu_item_index::create_new_pilot_profile:
			RegisterPlayer();
			break;

#if DXX_USE_UDP
		case main_menu_item_index::open_multiplayer_submenu:
			do_multi_player_menu();
			break;
#endif
		case main_menu_item_index::open_options_submenu:
			do_options_menu();
			break;
		case main_menu_item_index::open_credits_scroll_window:
			credits_show();
			break;
#ifndef RELEASE
		case main_menu_item_index::open_coder_sandbox_submenu:
			do_sandbox_menu();
			break;
#endif
		default:
			break;
	}
	return window_event_result::handled;		// stay in main menu unless quitting
}

#if DXX_USE_UDP
window_event_result dispatch_menu_option(const netgame_menu_item_index select)
{
	switch (select)
	{
		case netgame_menu_item_index::start_new_multiplayer_game:
			select_mission(mission_filter_mode::include_anarchy, menu_title{TXT_MULTI_MISSION}, net_udp_setup_game);
			break;
		case netgame_menu_item_index::join_multiplayer_game:
			net_udp_manual_join_game();
			break;
		case netgame_menu_item_index::list_multiplayer_games:
			net_udp_list_join_game(*grd_curcanv);
			break;
		default:
			break;
	}
	return window_event_result::handled;
}
#endif

// ------------------------------------------------------------------------
window_event_result main_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::window_created:
			if (InterfaceUniqueState.PilotName[0u])
				break;
			RegisterPlayer();
			break;
		case event_type::window_activated:
#if DXX_BUILD_DESCENT == 2
			load_palette(MENU_PALETTE, load_palette_use::background, load_palette_change_screen::delayed);		//get correct palette
#endif
			keyd_time_when_last_pressed = timer_query();		// .. 20 seconds from now!
			break;

		case event_type::key_command:
			// Don't allow them to hit ESC in the main menu.
			if (event_key_get(event)==KEY_ESC)
				return window_event_result::ignored;
			break;

		case event_type::mouse_button_down:
		case event_type::mouse_button_up:
			// Don't allow mousebutton-closing in main menu.
			if (event_mouse_get_button(event) == mbtn::right)
				return window_event_result::ignored;
			break;

		case event_type::idle:
#if DXX_BUILD_DESCENT == 1
#define DXX_DEMO_KEY_DELAY	45
#elif DXX_BUILD_DESCENT == 2
#define DXX_DEMO_KEY_DELAY	25
#endif
			if (keyd_time_when_last_pressed + i2f(DXX_DEMO_KEY_DELAY) < timer_query() || CGameArg.SysAutoDemo)
			{
				keyd_time_when_last_pressed = timer_query();			// Reset timer so that disk won't thrash if no demos.

#if DXX_BUILD_DESCENT == 2
				int n_demos = newdemo_count_demos();
				if ((d_rand() % (n_demos+1)) == 0 && !CGameArg.SysAutoDemo)
				{
#if DXX_USE_OGL
					Screen_mode = -1;
#endif
					PlayMovie("intro.tex", "intro.mve", play_movie_warn_missing::verbose);
					songs_play_song(song_number::title, 1);
					set_screen_mode(SCREEN_MENU);
				}
				else
#endif
				{
					newdemo_start_playback(NULL);		// Randomly pick a file, assume native endian (crashes if not)
				}
			}
			break;

		case event_type::newmenu_draw:
			draw_copyright(parent_canvas, *GAME_FONT);
			break;

		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
			return dispatch_menu_option(static_cast<main_menu_item_index>(citem));
		}

		default:
			break;
	}
	return newmenu::event_handler(event);
}

}

//	-----------------------------------------------------------------------------
//	Create the main menu.
//returns number of item chosen
int DoMenu()
{
	auto menu = window_create<main_menu>(grd_curscreen->sc_canvas);
	(void)menu;
	return 0;
}

namespace {

struct demo_selection_listbox : listbox
{
	demo_selection_listbox(unsigned nitems, PHYSFSX_uncounted_list &&physfs_list_strings, grs_canvas &canvas) :
		listbox(0, nitems, const_cast<const char **>(physfs_list_strings.get()), menu_title{TXT_SELECT_DEMO}, canvas, 1),
		physfs_list_storage(std::move(physfs_list_strings))
	{
	}
	PHYSFSX_uncounted_list physfs_list_storage;
	virtual window_event_result callback_handler(const d_event &, window_event_result default_return_value) override;
};

static window_event_result demo_menu_keycommand( listbox *lb,const d_event &event )
{
	const char **items = listbox_get_items(*lb);
	int citem = listbox_get_citem(*lb);

	switch (event_key_get(event))
	{
		case KEY_CTRLED+KEY_D:
			if (citem >= 0)
			{
				int x{1};
				x = nm_messagebox(menu_title{nullptr}, {TXT_YES, TXT_NO}, "%s %s?", TXT_DELETE_DEMO, items[citem]+((items[citem][0]=='$')?1:0) );
				if (x==0)
				{
					int ret;
					char name[PATH_MAX];

					strcpy(name, DEMO_DIR);
					strcat(name,items[citem]);

					ret = !PHYSFS_delete(name);

					if (ret)
						nm_messagebox(menu_title{nullptr}, {TXT_OK}, "%s %s %s", TXT_COULDNT, TXT_DELETE_DEMO, items[citem]+((items[citem][0]=='$')?1:0) );
					else
						listbox_delete_item(*lb, citem);
				}

				return window_event_result::handled;
			}
			break;

		case KEY_CTRLED+KEY_C:
			{
				std::array<char, PATH_MAX> bakname;

				// Get backup name
				const auto ic = items[citem];
				if (!change_filename_extension(bakname, ic, DEMO_BACKUP_EXT))
					return window_event_result::handled;
				const auto x = nm_messagebox(menu_title{nullptr}, {TXT_YES, TXT_NO}, "Are you sure you want to\n"
								  "swap the endianness of\n"
								  "%s? If the file is\n"
								  "already endian native, D1X\n"
								  "will likely crash. A backup\n"
								  "%s will be created.", ic, bakname.data());
				if (!x)
					newdemo_swap_endian(ic);

				return window_event_result::handled;
			}
			break;
	}
	return window_event_result::ignored;
}

window_event_result demo_selection_listbox::callback_handler(const d_event &event, window_event_result)
{
	switch (event.type)
	{
		case event_type::key_command:
			return demo_menu_keycommand(this, event);
		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
			if (citem < 0)
				return window_event_result::ignored;		// shouldn't happen
			newdemo_start_playback(item[citem]);
			return window_event_result::handled;		// stay in demo selector
		}
		case event_type::window_close:
			break;
		default:
			break;
	}
	return window_event_result::ignored;
}

int select_demo()
{
	int NumItems;

	auto list = PHYSFSX_findFiles(DEMO_DIR, demo_file_extensions);
	if (!list)
		return 0;	// memory error
	if (!list[0])
	{
		nm_messagebox(menu_title{nullptr}, {TXT_OK}, "%s %s\n%s", TXT_NO_DEMO_FILES, TXT_USE_F5, TXT_TO_CREATE_ONE);
		return 0;
	}

	for (NumItems = 0; list[NumItems] != NULL; NumItems++) {}

	auto lb = window_create<demo_selection_listbox>(NumItems, std::move(list), grd_curscreen->sc_canvas);
	(void)lb;
	return 1;
}

static int do_difficulty_menu()
{
	using items_type = per_difficulty_level_array<newmenu_item>;
	struct difficulty_prompt_menu : items_type, passive_newmenu
	{
		difficulty_prompt_menu(const Difficulty_level_type Difficulty_level) :
			items_type{{{
				newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_level_type::_0)},
				newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_level_type::_1)},
				newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_level_type::_2)},
				newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_level_type::_3)},
				newmenu_item::nm_item_menu{MENU_DIFFICULTY_TEXT(Difficulty_level_type::_4)},
			}}},
			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), underlying_value(Difficulty_level)), grd_curscreen->sc_canvas)
		{
		}
	};
	auto &Difficulty_level = GameUniqueState.Difficulty_level;
	const unsigned s = run_blocking_newmenu<difficulty_prompt_menu>(Difficulty_level);

	if (const auto od = build_difficulty_level_from_untrusted(s))
	{
		const auto d = *od;
		if (d != Difficulty_level)
		{
			PlayerCfg.DefaultDifficulty = d;
			write_player_file();
		}
		Difficulty_level = d;
		return 1;
	}
	return 0;
}

window_event_result do_new_game_menu()
{
	int new_level_num;

	new_level_num = 1;
	const auto recorded_player_highest_level = get_highest_level();
	const auto last_level = Current_mission->last_level;
	const auto clamped_player_highest_level = std::min<decltype(recorded_player_highest_level)>(recorded_player_highest_level, last_level);
	if (last_level > 1)
	{
		struct items_type
		{
			std::array<char, 8> num_text{"1"};
			std::array<char, 64> subtitle_text;
			std::array<char, 68> info_text;
			std::array<newmenu_item, 2> m;
			items_type(const char *const mission_name, const unsigned last_level, const int clamped_player_highest_level) :
				m{{
					newmenu_item::nm_item_text{info_text.data()},
					newmenu_item::nm_item_input(num_text),
				}}
			{
				char buf[28];
				std::snprintf(std::data(subtitle_text), std::size(subtitle_text), "%s\n\n%s", TXT_SELECT_START_LEV, mission_name);
				const auto trailer = clamped_player_highest_level
					? (std::snprintf(buf, std::size(buf), "finished level %d", clamped_player_highest_level), buf)
					: "not finished any level";
				std::snprintf(std::data(info_text), std::size(info_text), "This mission has %u levels.\n\nYou have %s.", last_level, trailer);
			}
		};
		items_type menu_items{Current_mission->mission_name, last_level, clamped_player_highest_level};
		for (;;)
		{
			struct select_start_level_menu : passive_newmenu
			{
				select_start_level_menu(items_type &i) :
					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)
				{
				}
			};
			const int choice = run_blocking_newmenu<select_start_level_menu>(menu_items);

			if (choice == -1 || !menu_items.num_text[0])
				return window_event_result::handled;

			char *p = nullptr;
			new_level_num = strtol(menu_items.num_text.data(), &p, 10);

			if (*p || new_level_num <= 0 || new_level_num > last_level)
			{
				nm_messagebox(menu_title{TXT_INVALID_LEVEL}, {TXT_OK}, "You must enter a\npositive level number\nless than or\nequal to %u.\n", static_cast<unsigned>(Current_mission->last_level));
			}
			else
				break;
		}
	}

	GameUniqueState.Difficulty_level = PlayerCfg.DefaultDifficulty;

	if (!do_difficulty_menu())
		return window_event_result::handled;

	StartNewGame(new_level_num);

	return window_event_result::close;	// exit mission listbox
}

static void hud_config();
static void graphics_config();
static void gameplay_config();
}
}

#define DXX_OPTIONS_MENU(VERB)	\
	DXX_MENUITEM(VERB, MENU, "Sound & music...", sfx)	\
	DXX_MENUITEM(VERB, MENU, TXT_CONTROLS_, controls)	\
	DXX_MENUITEM(VERB, MENU, "Graphics...", graphics)	\
	DXX_MENUITEM(VERB, MENU, "Gameplay...", misc)	\

namespace {

class options_menu_items
{
public:
	enum
	{
		DXX_OPTIONS_MENU(ENUM)
	};
	DXX_OPTIONS_MENU(DECL);
	std::array<newmenu_item, DXX_OPTIONS_MENU(COUNT)> m;
	options_menu_items()
	{
		DXX_OPTIONS_MENU(ADD);
	}
};

struct options_menu : options_menu_items, newmenu
{
	options_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result options_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
			switch (citem)
			{
				case options_menu_items::sfx:
					do_sound_menu();
					break;
				case options_menu_items::controls:
					input_config();
					break;
				case options_menu_items::graphics:
					graphics_config();
					break;
				case options_menu_items::misc:
					gameplay_config();
					break;
			}
			return window_event_result::handled;	// stay in menu until escape
		}

		case event_type::window_close:
			write_player_file();
			break;

		default:
			break;
	}
	return newmenu::event_handler(event);
}

static int gcd(int a, int b)
{
	if (!b)
		return a;

	return gcd(b, a%b);
}

struct screen_resolution_menu_items
{
	enum
	{
		grp_resolution = 0,
	};
	enum class ni_index : unsigned;
	enum class fixed_field_index : unsigned
	{
#if SDL_MAJOR_VERSION == 1
		/* SDL1 has a variable number of records before this line, so
		 * this line exists to separate them from the next lines.
		 *
		 * SDL2 has no records before the custom values line, so no
		 * separator is needed.
		 */
		opt_blank_custom_values,
#endif
		opt_radio_custom_values,
		opt_label_resolution,
		opt_input_resolution,
		opt_label_aspect,
		opt_input_aspect,
		opt_blank_fullscreen,
		opt_checkbox_fullscreen,
		/* Must be last.  This is not a real element, and access to
		 * array[end] is undefined.
		 */
		end,
	};
#if SDL_MAJOR_VERSION == 1
	static constexpr std::size_t maximum_preset_modes = 50;
	std::array<screen_mode, maximum_preset_modes> modes;
	std::array<std::array<char, 12>, maximum_preset_modes> restext;
	const unsigned num_presets;
	ni_index convert_fixed_field_to_ni(fixed_field_index i) const
	{
		return static_cast<ni_index>(static_cast<unsigned>(i) + num_presets);
	}
#elif SDL_MAJOR_VERSION == 2
	static constexpr std::size_t maximum_preset_modes = 0;
	static constexpr ni_index convert_fixed_field_to_ni(fixed_field_index i)
	{
		return static_cast<ni_index>(i);
	}
#endif
	std::array<char, 12> crestext, casptext;
	enumerated_array<newmenu_item, maximum_preset_modes + static_cast<unsigned>(fixed_field_index::end), ni_index> m;
	screen_resolution_menu_items();
};

screen_resolution_menu_items::screen_resolution_menu_items()
#if SDL_MAJOR_VERSION == 1
	: num_presets(gr_list_modes(modes))
#endif
{
	int citem = -1;
#if SDL_MAJOR_VERSION == 1
	for (auto &&[idx, mode, resolution_text, menuitem] : enumerate(zip(partial_const_range(modes, num_presets), restext, m)))
	{
		const auto &&sm_w = SM_W(mode);
		const auto &&sm_h = SM_H(mode);
		snprintf(resolution_text.data(), resolution_text.size(), "%ix%i", sm_w, sm_h);
		/* At most one entry can be checked.  When the correct entry is
		 * found, update citem so that no later entries can be checked.
		 */
		const auto checked = (citem == -1 && Game_screen_mode == mode && CGameCfg.AspectY == sm_w / gcd(sm_w, sm_h) && CGameCfg.AspectX == sm_h / gcd(sm_w, sm_h));
		if (checked)
			citem = idx;
		nm_set_item_radio(menuitem, resolution_text.data(), checked, grp_resolution);
	}
	/* Leave a blank line for visual separation */
	nm_set_item_text(m[convert_fixed_field_to_ni(fixed_field_index::opt_blank_custom_values)], "");
#endif
	nm_set_item_radio(m[convert_fixed_field_to_ni(fixed_field_index::opt_radio_custom_values)], "Use custom values", (citem == -1), grp_resolution);
	nm_set_item_text(m[convert_fixed_field_to_ni(fixed_field_index::opt_label_resolution)], "resolution:");
	snprintf(crestext.data(), crestext.size(), "%ix%i", SM_W(Game_screen_mode), SM_H(Game_screen_mode));
	nm_set_item_input(m[convert_fixed_field_to_ni(fixed_field_index::opt_input_resolution)], crestext);
	nm_set_item_text(m[convert_fixed_field_to_ni(fixed_field_index::opt_label_aspect)], "aspect:");
	snprintf(casptext.data(), casptext.size(), "%ix%i", CGameCfg.AspectY, CGameCfg.AspectX);
	nm_set_item_input(m[convert_fixed_field_to_ni(fixed_field_index::opt_input_aspect)], casptext);
	nm_set_item_text(m[convert_fixed_field_to_ni(fixed_field_index::opt_blank_fullscreen)], "");
	nm_set_item_checkbox(m[convert_fixed_field_to_ni(fixed_field_index::opt_checkbox_fullscreen)], "Fullscreen", gr_check_fullscreen());
}

struct screen_resolution_menu : screen_resolution_menu_items, passive_newmenu
{
	screen_resolution_menu() :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
	void handle_close_event() const;
#if SDL_MAJOR_VERSION == 1
	void check_apply_preset_resolution() const;
#endif
	void apply_custom_resolution() const;
	void apply_resolution(screen_mode) const;
};

window_event_result screen_resolution_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::window_close:
			handle_close_event();
			return window_event_result::ignored;
		default:
			return newmenu::event_handler(event);
	}
}

void screen_resolution_menu::handle_close_event() const
{
	// check which resolution field was selected
#if SDL_MAJOR_VERSION == 1
	if (m[convert_fixed_field_to_ni(fixed_field_index::opt_checkbox_fullscreen)].value != gr_check_fullscreen())
		gr_toggle_fullscreen();
	if (!m[convert_fixed_field_to_ni(fixed_field_index::opt_radio_custom_values)].value)
	{
		/* If the radio item for "Use custom resolution" is not set,
		 * then one of the items for a preset resolution must be set.
		 * Find the chosen item and apply it.
		 */
		check_apply_preset_resolution();
	}
	else
#endif
	{
		apply_custom_resolution();
	}
}

#if SDL_MAJOR_VERSION == 1
void screen_resolution_menu::check_apply_preset_resolution() const
{
	const auto r = zip(partial_range(modes, num_presets), m);
	const auto predicate = [](const auto &v) {
		auto &ni = std::get<1>(v);
		if (ni.type != nm_type::radio)
			return 0;
		auto &radio = ni.radio();
		if (radio.group != grp_resolution)
			return 0;
		return ni.value;
	};
	const auto &&i{std::ranges::find_if(r, predicate)};
	if (i == r.end())
		return;
	const auto requested_mode = std::get<0>(*i);
	const auto g = gcd(SM_W(requested_mode), SM_H(requested_mode));
	CGameCfg.AspectY = SM_W(requested_mode) / g;
	CGameCfg.AspectX = SM_H(requested_mode) / g;
	apply_resolution(requested_mode);
}
#endif

void screen_resolution_menu::apply_custom_resolution() const
{
	char *x;
	const char *errstr;
	const auto resolution_width = strtoul(crestext.data(), &x, 10);
	unsigned long resolution_height;
	screen_mode cmode;
	if (
		((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)) ||
		((resolution_width < 320 || resolution_height < 200) && (errstr = "Entered resolution must\nbe at least 320x200", true))
	)
	{
		cmode = Game_screen_mode;
		struct error_change_resolution :
			std::array<char, 32>,
			passive_messagebox
		{
			error_change_resolution(const char *errstr, screen_mode cmode) :
				passive_messagebox(menu_title{TXT_WARNING}, menu_subtitle{errstr}, prepare_choice_text(*this, cmode), grd_curscreen->sc_canvas)
			{
			}
			static const char *prepare_choice_text(std::array<char, 32> &b, screen_mode cmode)
			{
				auto r = b.data();
				std::snprintf(r, b.size(), "Revert to %ix%i", SM_W(cmode), SM_H(cmode));
				return r;
			}
		};
		run_blocking_newmenu<error_change_resolution>(errstr, cmode);
	}
	else
	{
		cmode.width = resolution_width;
		cmode.height = resolution_height;
	}
	screen_mode casp;
	const auto aspect_width = strtoul(casptext.data(), &x, 10);
	unsigned long aspect_height;
	if (
		((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)) ||
		((!aspect_width || !aspect_height) && (errstr = "Entered aspect ratio must\nnot use 0 term", true))
	)
	{
		casp = cmode;
		struct error_invalid_aspect_ratio : passive_messagebox
		{
			error_invalid_aspect_ratio(const char *errstr) :
				passive_messagebox(menu_title{TXT_WARNING}, menu_subtitle{errstr}, "IGNORE ASPECT RATIO", grd_curscreen->sc_canvas)
				{
				}
		};
		run_blocking_newmenu<error_invalid_aspect_ratio>(errstr);
	}
	else
	{
		casp.width = aspect_width;
		casp.height = aspect_height;
	}
	const auto g = gcd(SM_W(casp), SM_H(casp));
	CGameCfg.AspectY = SM_W(casp) / g;
	CGameCfg.AspectX = SM_H(casp) / g;
	apply_resolution(cmode);
}

void screen_resolution_menu::apply_resolution(const screen_mode new_mode) const
{
	// clean up and apply everything
	newmenu_free_background();
	set_screen_mode(SCREEN_MENU);
	if (new_mode != Game_screen_mode)
	{
		gr_set_mode(new_mode);
		Game_screen_mode = new_mode;
		if (const auto g = Game_wind) // shortly activate Game_wind so it's canvas will align to new resolution. really minor glitch but whatever
		{
			g->send_event(d_event{event_type::window_activated});
			g->send_event(d_event{event_type::window_deactivated});
		}
	}
	game_init_render_sub_buffers(*grd_curcanv, 0, 0, SM_W(Game_screen_mode), SM_H(Game_screen_mode));
}

template <std::size_t Offset, auto PMF>
struct copy_sensitivity
{
	static constexpr std::integral_constant<std::size_t, Offset> offset{};
	static constexpr std::integral_constant<decltype(PMF), PMF> pmf{};
};

template <typename XRange, typename MenuItems, typename... CopyParameters>
void copy_sensitivity_from_menu_to_cfg2(XRange &&r, const MenuItems &menuitems, const CopyParameters ... cn)
{
	for (const auto i : r)
		(((PlayerCfg.*(cn.pmf.value))[
			(typename std::remove_reference<
				decltype(PlayerCfg.*(cn.pmf.value))
				>::type::index_type{i})
		] = menuitems[1 + i + cn.offset].value), ...);
}

template <typename MenuItems, typename CopyParameter0, typename... CopyParameterN>
void copy_sensitivity_from_menu_to_cfg(const MenuItems &menuitems, const CopyParameter0 c0, const CopyParameterN ... cn)
{
	copy_sensitivity_from_menu_to_cfg2(xrange(std::size(PlayerCfg.*(c0.pmf.value))), menuitems, c0, cn...);
}

#define DXX_INPUT_SENSITIVITY(VERB,OPT,VAL,VALUE_INDEX_ENUM_SCOPE)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_TURN_LR, opt_##OPT##_turn_lr, VAL[VALUE_INDEX_ENUM_SCOPE::turn_lr], 0, 16)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_PITCH_UD, opt_##OPT##_pitch_ud, VAL[VALUE_INDEX_ENUM_SCOPE::pitch_ud], 0, 16)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_LR, opt_##OPT##_slide_lr, VAL[VALUE_INDEX_ENUM_SCOPE::slide_lr], 0, 16)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_UD, opt_##OPT##_slide_ud, VAL[VALUE_INDEX_ENUM_SCOPE::slide_ud], 0, 16)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_BANK_LR, opt_##OPT##_bank_lr, VAL[VALUE_INDEX_ENUM_SCOPE::bank_lr], 0, 16)	\

#define DXX_INPUT_CONFIG_MENU(VERB)	\
	DXX_MENUITEM(VERB, TEXT, "Keyboard Sensitivity:", opt_label_kb)	\
	DXX_INPUT_SENSITIVITY(VERB,kb,PlayerCfg.KeyboardSens,player_config_keyboard_index)	             \

namespace keyboard_sensitivity {

struct menu_items
{
		enum
		{
			DXX_INPUT_CONFIG_MENU(ENUM)
		};
		DXX_INPUT_CONFIG_MENU(DECL);
		std::array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
		menu_items()
		{
			DXX_INPUT_CONFIG_MENU(ADD);
		}
};

struct menu : menu_items, newmenu
{
	menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::window_close:
			copy_sensitivity_from_menu_to_cfg(m, copy_sensitivity<opt_label_kb, &player_config::KeyboardSens>());
			break;
		default:
			break;
	}
	return newmenu::event_handler(event);
}

}

#undef DXX_INPUT_CONFIG_MENU
#undef DXX_INPUT_SENSITIVITY

static void input_config_keyboard()
{
	auto menu = window_create<keyboard_sensitivity::menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

#define DXX_INPUT_SENSITIVITY(VERB,OPT,VAL,VALUE_INDEX_ENUM_SCOPE)	                           \
	DXX_MENUITEM(VERB, SLIDER, TXT_TURN_LR, opt_##OPT##_turn_lr, VAL[VALUE_INDEX_ENUM_SCOPE::turn_lr], 0, 16)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_PITCH_UD, opt_##OPT##_pitch_ud, VAL[VALUE_INDEX_ENUM_SCOPE::pitch_ud], 0, 16)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_LR, opt_##OPT##_slide_lr, VAL[VALUE_INDEX_ENUM_SCOPE::slide_lr], 0, 16)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_UD, opt_##OPT##_slide_ud, VAL[VALUE_INDEX_ENUM_SCOPE::slide_ud], 0, 16)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_BANK_LR, opt_##OPT##_bank_lr, VAL[VALUE_INDEX_ENUM_SCOPE::bank_lr], 0, 16)	\

#define DXX_INPUT_THROTTLE_SENSITIVITY(VERB,OPT,VAL,VALUE_INDEX_ENUM_SCOPE)	\
	DXX_INPUT_SENSITIVITY(VERB,OPT,VAL,VALUE_INDEX_ENUM_SCOPE)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_THROTTLE, opt_##OPT##_throttle, VAL[VALUE_INDEX_ENUM_SCOPE::throttle], 0, 16)	\

#define DXX_INPUT_CONFIG_MENU(VERB)	                                   \
	DXX_MENUITEM(VERB, TEXT, "Mouse Sensitivity:", opt_label_ms)	             \
	DXX_INPUT_THROTTLE_SENSITIVITY(VERB,ms,PlayerCfg.MouseSens,player_config_mouse_index)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_ms)	\
	DXX_MENUITEM(VERB, TEXT, "Mouse Overrun Buffer:", opt_label_mo)	\
	DXX_INPUT_THROTTLE_SENSITIVITY(VERB,mo,PlayerCfg.MouseOverrun,player_config_mouse_index)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_mo)	\
	DXX_MENUITEM(VERB, TEXT, "Mouse FlightSim Deadzone:", opt_label_mfsd)	\
	DXX_MENUITEM(VERB, SLIDER, "X/Y", opt_mfsd_deadzone, PlayerCfg.MouseFSDead, 0, 16)	\

namespace mouse_sensitivity {

	struct menu_items
	{
	public:
		enum
		{
			DXX_INPUT_CONFIG_MENU(ENUM)
		};
		DXX_INPUT_CONFIG_MENU(DECL);
		std::array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
		menu_items()
		{
			DXX_INPUT_CONFIG_MENU(ADD);
		}
	};

struct menu : menu_items, newmenu
{
	menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::window_close:
			PlayerCfg.MouseFSDead = m[opt_mfsd_deadzone].value;
			copy_sensitivity_from_menu_to_cfg(m,
				copy_sensitivity<opt_label_ms, &player_config::MouseSens>(),
				copy_sensitivity<opt_label_mo, &player_config::MouseOverrun>()
			);
			break;
		default:
			break;
	}
	return newmenu::event_handler(event);
}

}
#undef DXX_INPUT_CONFIG_MENU

static void input_config_mouse()
{
	auto menu = window_create<mouse_sensitivity::menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

#if DXX_MAX_AXES_PER_JOYSTICK
namespace joystick_sensitivity {

#define DXX_INPUT_CONFIG_MENU(VERB)	                                   \
	DXX_MENUITEM(VERB, TEXT, "Joystick Sensitivity:", opt_label_js)	          \
	DXX_INPUT_THROTTLE_SENSITIVITY(VERB,js,PlayerCfg.JoystickSens,player_config_joystick_index)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_js)	\
	DXX_MENUITEM(VERB, TEXT, "Joystick Linearity:", opt_label_jl)	\
	DXX_INPUT_THROTTLE_SENSITIVITY(VERB,jl,PlayerCfg.JoystickLinear,player_config_joystick_index)	  \
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_jl)	\
	DXX_MENUITEM(VERB, TEXT, "Joystick Linear Speed:", opt_label_jp)	\
	DXX_INPUT_THROTTLE_SENSITIVITY(VERB,jp,PlayerCfg.JoystickSpeed,player_config_joystick_index)	   \
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_jp)	\
	DXX_MENUITEM(VERB, TEXT, "Joystick Deadzone:", opt_label_jd)	\
	DXX_INPUT_THROTTLE_SENSITIVITY(VERB,jd,PlayerCfg.JoystickDead,player_config_joystick_index)	    \

	class menu_items
	{
	public:
		enum
		{
			DXX_INPUT_CONFIG_MENU(ENUM)
		};
		DXX_INPUT_CONFIG_MENU(DECL);
		std::array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
		menu_items()
		{
			DXX_INPUT_CONFIG_MENU(ADD);
		}
	};
#undef DXX_INPUT_CONFIG_MENU

struct menu : menu_items, newmenu
{
	menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::window_close:
			copy_sensitivity_from_menu_to_cfg(m,
				copy_sensitivity<opt_label_js, &player_config::JoystickSens>(),
				copy_sensitivity<opt_label_jl, &player_config::JoystickLinear>(),
				copy_sensitivity<opt_label_jp, &player_config::JoystickSpeed>(),
				copy_sensitivity<opt_label_jd, &player_config::JoystickDead>()
			);
			break;
		default:
			break;
	}
	return newmenu::event_handler(event);
}

}

static void input_config_joystick()
{
	auto menu = window_create<joystick_sensitivity::menu>(grd_curscreen->sc_canvas);
	(void)menu;
}
#endif

#undef DXX_INPUT_THROTTLE_SENSITIVITY
#undef DXX_INPUT_SENSITIVITY

class input_config_menu_items
{
#if DXX_MAX_JOYSTICKS
#define DXX_INPUT_CONFIG_JOYSTICK_ITEM(I)	I
#else
#define DXX_INPUT_CONFIG_JOYSTICK_ITEM(I)
#endif

#if DXX_MAX_AXES_PER_JOYSTICK
#define DXX_INPUT_CONFIG_JOYSTICK_AXIS_ITEM(I)	I
#else
#define DXX_INPUT_CONFIG_JOYSTICK_AXIS_ITEM(I)
#endif

#define DXX_INPUT_CONFIG_MENU(VERB)	\
	DXX_INPUT_CONFIG_JOYSTICK_ITEM(DXX_MENUITEM(VERB, CHECK, "Use joystick", opt_ic_usejoy, PlayerCfg.ControlType & CONTROL_USING_JOYSTICK))	\
	DXX_MENUITEM(VERB, CHECK, "Use mouse", opt_ic_usemouse, PlayerCfg.ControlType & CONTROL_USING_MOUSE)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_use)	\
	DXX_MENUITEM(VERB, MENU, TXT_CUST_KEYBOARD, opt_ic_confkey)	\
	DXX_INPUT_CONFIG_JOYSTICK_ITEM(DXX_MENUITEM(VERB, MENU, "Customize Joystick", opt_ic_confjoy))	\
	DXX_MENUITEM(VERB, MENU, "Customize Mouse", opt_ic_confmouse)	\
	DXX_MENUITEM(VERB, MENU, "Customize Weapon Keys", opt_ic_confweap)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_customize)	\
	DXX_MENUITEM(VERB, TEXT, "Mouse Control Type:", opt_label_mouse_control_type)	\
	DXX_MENUITEM(VERB, RADIO, "Normal", opt_mouse_control_normal, PlayerCfg.MouseFlightSim == 0, optgrp_mouse_control_type)	\
	DXX_MENUITEM(VERB, RADIO, "FlightSim", opt_mouse_control_flightsim, PlayerCfg.MouseFlightSim == 1, optgrp_mouse_control_type)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_mouse_control_type)	\
	DXX_MENUITEM(VERB, MENU, "Keyboard Calibration", opt_ic_keyboard)	        \
	DXX_MENUITEM(VERB, MENU, "Mouse Calibration", opt_ic_mouse)	              \
	DXX_INPUT_CONFIG_JOYSTICK_AXIS_ITEM(DXX_MENUITEM(VERB, MENU, "Joystick Calibration", opt_ic_joystick))	         \
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_sensitivity_deadzone)	\
	DXX_MENUITEM(VERB, CHECK, "Keep Keyboard/Mouse focus", opt_ic_grabinput, CGameCfg.Grabinput)	\
	DXX_MENUITEM(VERB, CHECK, "Mouse FlightSim Indicator", opt_ic_mousefsgauge, PlayerCfg.MouseFSIndicator)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_focus)	\
	DXX_MENUITEM(VERB, TEXT, "When dead, respawn by pressing:", opt_label_respawn_mode)	\
	DXX_MENUITEM(VERB, RADIO, "Any key", opt_respawn_any_key, PlayerCfg.RespawnMode == RespawnPress::Any, optgrp_respawn_mode)	\
	DXX_MENUITEM(VERB, RADIO, "Fire keys (pri., sec., flare)", opt_respawn_fire_key, PlayerCfg.RespawnMode == RespawnPress::Fire, optgrp_respawn_mode)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_respawn)	\
	DXX_MENUITEM(VERB, TEXT, "Uncapped turning in:", opt_label_mouselook_mode)	\
	DXX_MENUITEM(VERB, CHECK, "Single player", opt_ic_mouselook_sp, PlayerCfg.MouselookFlags & MouselookMode::Singleplayer)	\
	DXX_MENUITEM(VERB, CHECK, "Multi Coop (if host allows)", opt_ic_mouselook_mp_cooperative, PlayerCfg.MouselookFlags & MouselookMode::MPCoop)	\
	DXX_MENUITEM(VERB, CHECK, "Multi Anarchy (if host allows)", opt_ic_mouselook_mp_anarchy, PlayerCfg.MouselookFlags & MouselookMode::MPAnarchy)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_mouselook)	\
	DXX_MENUITEM(VERB, TEXT, "Release pitch lock:", opt_label_pitch_lock_mode) \
	DXX_MENUITEM(VERB, CHECK, "Single player", opt_ic_pitch_lock_sp, PlayerCfg.PitchLockFlags & MouselookMode::Singleplayer) \
	DXX_MENUITEM(VERB, CHECK, "Multi Coop (if host allows)", opt_ic_pitch_lock_mp_coop, PlayerCfg.PitchLockFlags & MouselookMode::MPCoop) \
	DXX_MENUITEM(VERB, CHECK, "Multi Anarchy (if host allows)", opt_ic_pitch_lock_mp_anarchy, PlayerCfg.PitchLockFlags & MouselookMode::MPAnarchy) \
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_rotational_setting)	\
	DXX_MENUITEM(VERB, MENU, "GAME SYSTEM KEYS", opt_ic_help0)	\
	DXX_MENUITEM(VERB, MENU, "NETGAME SYSTEM KEYS", opt_ic_help1)	\
	DXX_MENUITEM(VERB, MENU, "DEMO SYSTEM KEYS", opt_ic_help2)	\

public:
	enum
	{
		optgrp_mouse_control_type,
		optgrp_respawn_mode,
	};
	enum
	{
		DXX_INPUT_CONFIG_MENU(ENUM)
	};
	std::array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
	input_config_menu_items()
	{
		DXX_INPUT_CONFIG_MENU(ADD);
	}
#undef DXX_INPUT_CONFIG_MENU
#undef DXX_INPUT_CONFIG_JOYSTICK_AXIS_ITEM
#undef DXX_INPUT_CONFIG_JOYSTICK_ITEM
};

}

namespace dsx {
namespace {

struct input_config_menu : input_config_menu_items, newmenu
{
	input_config_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result input_config_menu::event_handler(const d_event &event)
{
	const auto &items = m;
	switch (event.type)
	{
		case event_type::newmenu_changed:
		{
			const auto citem = static_cast<const d_change_event &>(event).citem;
			MouselookMode mousemode;
			MouselookMode pitchlock;
#if DXX_MAX_JOYSTICKS
			if (citem == opt_ic_usejoy)
			{
				constexpr auto flag = CONTROL_USING_JOYSTICK;
				if (items[citem].value)
					PlayerCfg.ControlType |= flag;
				else
					PlayerCfg.ControlType &= ~flag;
			}
#endif
			if (citem == opt_ic_usemouse)
			{
				constexpr auto flag = CONTROL_USING_MOUSE;
				if (items[citem].value)
					PlayerCfg.ControlType |= flag;
				else
					PlayerCfg.ControlType &= ~flag;
			}
			if (citem == opt_mouse_control_normal)
				PlayerCfg.MouseFlightSim = 0;
			if (citem == opt_mouse_control_flightsim)
				PlayerCfg.MouseFlightSim = 1;
			if (citem == opt_ic_grabinput)
				CGameCfg.Grabinput = items[citem].value;
			if (citem == opt_ic_mousefsgauge)
				PlayerCfg.MouseFSIndicator = items[citem].value;
			else if (citem == opt_respawn_any_key)
				PlayerCfg.RespawnMode = RespawnPress::Any;
			else if (citem == opt_respawn_fire_key)
				PlayerCfg.RespawnMode = RespawnPress::Fire;
			else if ((citem == opt_ic_mouselook_sp && (mousemode = MouselookMode::Singleplayer, true)) ||
				(citem == opt_ic_mouselook_mp_cooperative && (mousemode = MouselookMode::MPCoop, true)) ||
				(citem == opt_ic_mouselook_mp_anarchy && (mousemode = MouselookMode::MPAnarchy, true)))
			{
				if (items[citem].value)
					PlayerCfg.MouselookFlags |= mousemode;
				else
					PlayerCfg.MouselookFlags &= ~mousemode;
			}
			else if ((citem == opt_ic_pitch_lock_sp && (pitchlock = MouselookMode::Singleplayer, true)) ||
					(citem == opt_ic_pitch_lock_mp_coop && (pitchlock = MouselookMode::MPCoop, true)) ||
					(citem == opt_ic_pitch_lock_mp_anarchy && (pitchlock = MouselookMode::MPAnarchy, true))) {
				if (items[citem].value)
					PlayerCfg.PitchLockFlags |= pitchlock;
				else
					PlayerCfg.PitchLockFlags &= ~pitchlock;
			}

			break;
		}
		case event_type::newmenu_selected:
		{
			const auto citem = static_cast<const d_select_event &>(event).citem;
			if (citem == opt_ic_confkey)
				kconfig(kconfig_type::keyboard);
#if DXX_MAX_JOYSTICKS
			if (citem == opt_ic_confjoy)
				kconfig(kconfig_type::joystick);
#endif
			if (citem == opt_ic_confmouse)
				kconfig(kconfig_type::mouse);
			if (citem == opt_ic_confweap)
				kconfig(kconfig_type::rebirth);
			if (citem == opt_ic_keyboard)
				input_config_keyboard();
			if (citem == opt_ic_mouse)
				input_config_mouse();
#if DXX_MAX_AXES_PER_JOYSTICK
			if (citem == opt_ic_joystick)
				input_config_joystick();
#endif
			if (citem == opt_ic_help0)
				show_help();
			if (citem == opt_ic_help1)
				show_netgame_help();
			if (citem == opt_ic_help2)
				show_newdemo_help();
			return window_event_result::handled;		// stay in menu
		}

		default:
			break;
	}
	return newmenu::event_handler(event);
}

void input_config()
{
	auto menu = window_create<input_config_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

struct reticle_config_menu_items
{
#if DXX_USE_OGL
#define DXX_RETICLE_TYPE_OGL(VERB)	\
	DXX_MENUITEM(VERB, RADIO, "Classic Reboot", opt_reticle_classic_reboot, 0, optgrp_reticle)
#else
#define DXX_RETICLE_TYPE_OGL(VERB)
#endif
#define DXX_RETICLE_CONFIG_MENU(VERB)	\
	DXX_MENUITEM(VERB, TEXT, "Reticle Type:", opt_label_reticle_type)	\
	DXX_MENUITEM(VERB, RADIO, "Classic", opt_reticle_classic, 0, optgrp_reticle)	\
	DXX_RETICLE_TYPE_OGL(VERB)	\
	DXX_MENUITEM(VERB, RADIO, "None", opt_reticle_none, 0, optgrp_reticle)	\
	DXX_MENUITEM(VERB, RADIO, "X", opt_reticle_x, 0, optgrp_reticle)	\
	DXX_MENUITEM(VERB, RADIO, "Dot", opt_reticle_dot, 0, optgrp_reticle)	\
	DXX_MENUITEM(VERB, RADIO, "Circle", opt_reticle_circle, 0, optgrp_reticle)	\
	DXX_MENUITEM(VERB, RADIO, "Cross V1", opt_reticle_cross1, 0, optgrp_reticle)	\
	DXX_MENUITEM(VERB, RADIO, "Cross V2", opt_reticle_cross2, 0, optgrp_reticle)	\
	DXX_MENUITEM(VERB, RADIO, "Angle", opt_reticle_angle, 0, optgrp_reticle)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_reticle_type)	\
	DXX_MENUITEM(VERB, TEXT, "Reticle Color:", opt_label_reticle_color)	\
	DXX_MENUITEM(VERB, SCALE_SLIDER, "Red", opt_reticle_color_red, PlayerCfg.ReticleRGBA[0], 0, 16, 2)	\
	DXX_MENUITEM(VERB, SCALE_SLIDER, "Green", opt_reticle_color_green, PlayerCfg.ReticleRGBA[1], 0, 16, 2)	\
	DXX_MENUITEM(VERB, SCALE_SLIDER, "Blue", opt_reticle_color_blue, PlayerCfg.ReticleRGBA[2], 0, 16, 2)	\
	DXX_MENUITEM(VERB, SCALE_SLIDER, "Alpha", opt_reticle_color_alpha, PlayerCfg.ReticleRGBA[3], 0, 16, 2)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank_reticle_color)	\
	DXX_MENUITEM(VERB, SLIDER, "Reticle Size:", opt_label_reticle_size, PlayerCfg.ReticleSize, 0, 4)	\

		enum
		{
			optgrp_reticle,
		};
		enum
		{
			DXX_RETICLE_CONFIG_MENU(ENUM)
		};
		DXX_RETICLE_CONFIG_MENU(DECL);
		std::array<newmenu_item, DXX_RETICLE_CONFIG_MENU(COUNT)> m;
		reticle_config_menu_items()
		{
			DXX_RETICLE_CONFIG_MENU(ADD);
			auto i{underlying_value(PlayerCfg.ReticleType)};
			if constexpr (!DXX_USE_OGL)
			{
				if (i > 1)
					--i;
			}
			m[opt_reticle_classic + i].value = 1;
		}
	};

struct reticle_config_menu : reticle_config_menu_items, newmenu
{
	reticle_config_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result reticle_config_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::window_close:
			for (uint_fast32_t i = opt_reticle_classic; i != opt_label_blank_reticle_type; ++i)
				if (m[i].value)
				{
#if !DXX_USE_OGL
					if (i != opt_reticle_classic)
						++i;
#endif
					PlayerCfg.ReticleType = static_cast<reticle_type>(i - opt_reticle_classic);
					break;
				}
			DXX_RETICLE_CONFIG_MENU(READ);
			break;
		default:
			break;
	}
	return newmenu::event_handler(event);
}
#undef DXX_RETICLE_CONFIG_MENU
#undef DXX_RETICLE_TYPE_OGL

static void reticle_config()
{
	auto menu = window_create<reticle_config_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

struct hud_style_config_menu_items
{
#define DXX_HUD_STYLE_MENU(VERB)	\
	DXX_MENUITEM(VERB, TEXT, "View style:", opt_viewstyle_label)	\
	DXX_MENUITEM(VERB, RADIO, "Cockpit", opt_viewstyle_cockpit, PlayerCfg.CockpitMode[1] == cockpit_mode_t::full_cockpit, optgrp_viewstyle)	\
	DXX_MENUITEM(VERB, RADIO, "Status bar", opt_viewstyle_bar, PlayerCfg.CockpitMode[1] == cockpit_mode_t::status_bar, optgrp_viewstyle)	\
	DXX_MENUITEM(VERB, RADIO, "Full screen", opt_viewstyle_fullscreen, PlayerCfg.CockpitMode[1] == cockpit_mode_t::full_screen, optgrp_viewstyle)	\
	DXX_MENUITEM(VERB, TEXT, "HUD style:", opt_hudstyle_label)	\
	DXX_MENUITEM(VERB, RADIO, "Standard", opt_hudstyle_standard, PlayerCfg.HudMode == HudType::Standard, optgrp_hudstyle)	\
	DXX_MENUITEM(VERB, RADIO, "Alternate #1", opt_hudstyle_alt1, PlayerCfg.HudMode == HudType::Alternate1, optgrp_hudstyle)	\
	DXX_MENUITEM(VERB, RADIO, "Alternate #2", opt_hudstyle_alt2, PlayerCfg.HudMode == HudType::Alternate2, optgrp_hudstyle)	\
	DXX_MENUITEM(VERB, RADIO, "Hidden", opt_hudstyle_hidden, PlayerCfg.HudMode == HudType::Hidden, optgrp_hudstyle)	\

		enum {
			optgrp_viewstyle,
			optgrp_hudstyle,
		};
		enum {
			DXX_HUD_STYLE_MENU(ENUM)
		};
		DXX_HUD_STYLE_MENU(DECL);
		std::array<newmenu_item, DXX_HUD_STYLE_MENU(COUNT)> m;
		hud_style_config_menu_items()
		{
			DXX_HUD_STYLE_MENU(ADD);
		}
	};

struct hud_style_config_menu : hud_style_config_menu_items, newmenu
{
	hud_style_config_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result hud_style_config_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::window_close:
		{
			const auto new_mode = m[opt_viewstyle_cockpit].value
				? cockpit_mode_t::full_cockpit
				: m[opt_viewstyle_bar].value
					? cockpit_mode_t::status_bar
					: cockpit_mode_t::full_screen;
			select_cockpit(new_mode);
			PlayerCfg.CockpitMode[0] = new_mode;
			PlayerCfg.HudMode = m[opt_hudstyle_standard].value
				? HudType::Standard
				: m[opt_hudstyle_alt1].value
					? HudType::Alternate1
					: m[opt_hudstyle_alt2].value
						? HudType::Alternate2
						: HudType::Hidden;
			break;
		}
		default:
			break;
	}
	return newmenu::event_handler(event);
}
#undef DXX_HUD_STYLE_MENU

static void hud_style_config()
{
	auto menu = window_create<hud_style_config_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

#if DXX_BUILD_DESCENT == 1
#define DSX_GAME_SPECIFIC_HUDOPTIONS(VERB)	\
	DXX_MENUITEM(VERB, CHECK, "Always-on Bomb Counter",opt_d2bomb,PlayerCfg.BombGauge)	\

#elif DXX_BUILD_DESCENT == 2
enum {
	optgrp_missileview,
};
#define DSX_GAME_SPECIFIC_HUDOPTIONS(VERB)	\
	DXX_MENUITEM(VERB, TEXT, "Missile view:", opt_missileview_label)	\
	DXX_MENUITEM(VERB, RADIO, "Disabled", opt_missileview_none, PlayerCfg.MissileViewEnabled == MissileViewMode::None, optgrp_missileview)	\
	DXX_MENUITEM(VERB, RADIO, "Only own missiles", opt_missileview_selfonly, PlayerCfg.MissileViewEnabled == MissileViewMode::EnabledSelfOnly, optgrp_missileview)	\
	DXX_MENUITEM(VERB, RADIO, "Friendly missiles, preferring self", opt_missileview_selfandallies, PlayerCfg.MissileViewEnabled == MissileViewMode::EnabledSelfAndAllies, optgrp_missileview)	\
	DXX_MENUITEM(VERB, CHECK, "Show guided missile in main display", opt_guidedbigview,PlayerCfg.GuidedInBigWindow )	\

#endif
#define DSX_HUD_MENU_OPTIONS(VERB)	\
        DXX_MENUITEM(VERB, MENU, "Reticle Customization...", opt_hud_reticlemenu)	\
        DXX_MENUITEM(VERB, MENU, "View / HUD Style...", opt_hud_stylemenu)	\
	DXX_MENUITEM(VERB, CHECK, "Screenshots without HUD",opt_screenshot,PlayerCfg.PRShot)	\
	DXX_MENUITEM(VERB, CHECK, "No redundant pickup messages",opt_redundant,PlayerCfg.NoRedundancy)	\
	DXX_MENUITEM(VERB, CHECK, "Show Player chat only (Multi)",opt_playerchat,PlayerCfg.MultiMessages)	\
	DXX_MENUITEM(VERB, CHECK, "Show Player ping (Multi)",opt_playerping,PlayerCfg.MultiPingHud)	\
	DXX_MENUITEM(VERB, CHECK, "Cloak/Invulnerability Timers",opt_cloakinvultimer,PlayerCfg.CloakInvulTimer)	\
	DSX_GAME_SPECIFIC_HUDOPTIONS(VERB)	\

struct hud_config_menu_items
{
	enum {
		DSX_HUD_MENU_OPTIONS(ENUM)
	};
	DSX_HUD_MENU_OPTIONS(DECL);
	std::array<newmenu_item, DSX_HUD_MENU_OPTIONS(COUNT)> m;
	hud_config_menu_items()
	{
		DSX_HUD_MENU_OPTIONS(ADD);
	}
};

struct hud_config_menu : hud_config_menu_items, newmenu
{
	hud_config_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result hud_config_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
                        if (citem == opt_hud_reticlemenu)
                                reticle_config();
                        if (citem == opt_hud_stylemenu)
                                hud_style_config();
			return window_event_result::handled;		// stay in menu
		}
		case event_type::window_close:
			DSX_HUD_MENU_OPTIONS(READ);
#if DXX_BUILD_DESCENT == 2
			PlayerCfg.MissileViewEnabled = m[opt_missileview_selfandallies].value
				? MissileViewMode::EnabledSelfAndAllies
				: (m[opt_missileview_selfonly].value
					? MissileViewMode::EnabledSelfOnly
					: MissileViewMode::None);
#endif
			break;

		default:
			break;
	}
	return newmenu::event_handler(event);
}

void hud_config()
{
	auto menu = window_create<hud_config_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

#define DXX_GRAPHICS_MENU(VERB)	\
	DXX_MENUITEM(VERB, MENU, "Screen resolution...", opt_gr_screenres)	\
	DXX_MENUITEM(VERB, MENU, "HUD Options...", opt_gr_hudmenu)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_BRIGHTNESS, opt_gr_brightness, gr_palette_get_gamma(), 0, 16)	\
	DXX_MENUITEM(VERB, TEXT, "", blank1)	\
	DXX_OGL0_GRAPHICS_MENU(VERB)	\
	DXX_OGL1_GRAPHICS_MENU(VERB)	\
	DXX_MENUITEM(VERB, CHECK, "FPS Counter", opt_gr_fpsindi, CGameCfg.FPSIndicator)	\

struct graphics_config_menu_items
{
#if DXX_USE_OGL
	enum {
		optgrp_texfilt,
	};
#define DXX_OGL0_GRAPHICS_MENU(VERB)	\
	DXX_MENUITEM(VERB, TEXT, "Texture Filtering:", opt_gr_texfilt)	\
	DXX_MENUITEM(VERB, RADIO, "Classic", opt_filter_none, 0, optgrp_texfilt)	\
	DXX_MENUITEM(VERB, RADIO, "Blocky Filtered", opt_filter_upscale, 0, optgrp_texfilt)	\
	DXX_MENUITEM(VERB, RADIO, "Smooth", opt_filter_trilinear, 0, optgrp_texfilt)	\
	DXX_MENUITEM(VERB, CHECK, "Anisotropic Filtering", opt_filter_anisotropy, CGameCfg.TexAnisotropy)	\
	D2X_OGL_GRAPHICS_MENU(VERB)	\
	DXX_MENUITEM(VERB, TEXT, "", blank2)	\

#define DXX_OGL1_GRAPHICS_MENU(VERB)	\
	DXX_MENUITEM(VERB, CHECK, "Transparency Effects", opt_gr_alphafx, PlayerCfg.AlphaEffects)	\
	DXX_MENUITEM(VERB, CHECK, "Colored Dynamic Light", opt_gr_dynlightcolor, PlayerCfg.DynLightColor)	\
	DXX_MENUITEM(VERB, CHECK, "VSync", opt_gr_vsync, CGameCfg.VSync)	\
	DXX_MENUITEM(VERB, CHECK, "4x multisampling", opt_gr_multisample, CGameCfg.Multisample)	\

#if DXX_BUILD_DESCENT == 1
#define D2X_OGL_GRAPHICS_MENU(VERB)
#elif DXX_BUILD_DESCENT == 2
#define D2X_OGL_GRAPHICS_MENU(VERB)	\
	DXX_MENUITEM(VERB, CHECK, "Cutscene Smoothing", opt_gr_movietexfilt, GameCfg.MovieTexFilt)
#endif

#else
#define DXX_OGL0_GRAPHICS_MENU(VERB)
#define DXX_OGL1_GRAPHICS_MENU(VERB)
#endif
	enum {
		DXX_GRAPHICS_MENU(ENUM)
	};
	DXX_GRAPHICS_MENU(DECL);
	std::array<newmenu_item, DXX_GRAPHICS_MENU(COUNT)> m;
	graphics_config_menu_items()
	{
		DXX_GRAPHICS_MENU(ADD);
#if DXX_USE_OGL
		m[opt_filter_none + static_cast<unsigned>(CGameCfg.TexFilt)].value = 1;
#endif
	}
};

struct graphics_config_menu : graphics_config_menu_items, newmenu
{
	graphics_config_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result graphics_config_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::newmenu_changed:
		{
			auto &citem = static_cast<const d_change_event &>(event).citem;
			if (citem == opt_gr_brightness)
			{
				const auto GammaLevel{m[citem].value};
				CGameCfg.GammaLevel = GammaLevel;
				gr_palette_set_gamma(GammaLevel);
			}
#if DXX_USE_OGL
			else if (citem == opt_filter_anisotropy && ogl_maxanisotropy <= 1.0 && m[opt_filter_anisotropy].value)
			{
				m[opt_filter_anisotropy].value = 0;
				window_create<passive_messagebox>(menu_title{TXT_ERROR}, menu_subtitle{"Anisotropic Filtering not\nsupported by your hardware/driver."}, TXT_OK, grd_curscreen->sc_canvas);
			}
#endif
			break;
		}
		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
			if (citem == opt_gr_screenres)
				window_create<screen_resolution_menu>();
			else if (citem == opt_gr_hudmenu)
				hud_config();
			return window_event_result::handled;		// stay in menu
		}
		case event_type::window_close:
#if DXX_USE_OGL
			if (CGameCfg.VSync != m[opt_gr_vsync].value || CGameCfg.Multisample != m[opt_gr_multisample].value)
			{
				struct warn_might_need_restart : passive_messagebox
				{
					warn_might_need_restart() :
						passive_messagebox(menu_title{nullptr}, menu_subtitle{"On some systems, changing VSync or 4x Multisample\nrequires a restart."}, TXT_OK, grd_curscreen->sc_canvas)
						{
						}
				};
				run_blocking_newmenu<warn_might_need_restart>();
			}

			for (const uint8_t i : xrange(3u))
				if (m[i + opt_filter_none].value)
				{
					CGameCfg.TexFilt = opengl_texture_filter{i};
					break;
				}
			CGameCfg.TexAnisotropy = m[opt_filter_anisotropy].value;
#if DXX_BUILD_DESCENT == 2
			GameCfg.MovieTexFilt = m[opt_gr_movietexfilt].value;
#endif
			PlayerCfg.AlphaEffects = m[opt_gr_alphafx].value;
			PlayerCfg.DynLightColor = m[opt_gr_dynlightcolor].value;
			CGameCfg.VSync = m[opt_gr_vsync].value;
			CGameCfg.Multisample = m[opt_gr_multisample].value;
#endif
			CGameCfg.GammaLevel = m[opt_gr_brightness].value;
			CGameCfg.FPSIndicator = m[opt_gr_fpsindi].value;
			reset_cockpit();
#if DXX_USE_OGL
			gr_set_attributes();
			gr_set_mode(Game_screen_mode);
#endif
			break;

		default:
			break;
	}
	return newmenu::event_handler(event);
}

void graphics_config()
{
	auto menu = window_create<graphics_config_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

}
}

namespace dcx {
namespace {

#if DXX_USE_SDLMIXER
struct physfsx_mounted_path
{
	/* PhysFS does not count how many times a path is mounted, and all
	 * mount requests after the first succeed without changing state.
	 * This flag tracks whether the path in `path` was mounted by this
	 * instance of `physfsx_mounted_path` (=1) or if the path was
	 * already mounted by an earlier call to PhysFS (=0).
	 *
	 * If the path was already mounted, destruction of this instance
	 * must not unmount it.
	 */
	uint8_t must_unmount{0};
	std::array<char, PATH_MAX> path;
	uint8_t mount();
	~physfsx_mounted_path()
	{
		if (must_unmount)
			PHYSFS_unmount(path.data());
	}
};

uint8_t physfsx_mounted_path::mount()
{
	assert(!must_unmount);
	const auto current_mount_point = PHYSFS_getMountPoint(path.data());
	if (current_mount_point == nullptr)
	{
		/* Not currently mounted; try to mount it */
		must_unmount = PHYSFS_mount(path.data(), nullptr, 0);
		return must_unmount;
	}
	else
	{
		/* Already mounted */
		must_unmount = 0;
		return 1;
	}
}

struct browser_storage
{
	struct target_path_not_mounted {};
	// List of file extensions we're looking for (if looking for a music file many types are possible)
	const std::ranges::subrange<const file_extension_t *> ext_range;
	const select_dir_flag select_dir;		// Allow selecting the current directory (e.g. for Jukebox level song directory)
	physfsx_mounted_path view_path;	// The absolute path we're currently looking at
	string_array_t list;
	browser_storage(const char *orig_path, const std::ranges::subrange<const file_extension_t *> &ext_range, const select_dir_flag select_dir, const char *const sep) :
		ext_range(ext_range), select_dir(select_dir),
		/* view_path is trivially constructed, then properly initialized as
		 * a side effect of preparing the string list */
		list(construct_string_list(orig_path, view_path, ext_range, select_dir, sep))
	{
	}
	static string_array_t construct_string_list(const char *orig_path, physfsx_mounted_path &view_path, const std::ranges::subrange<const file_extension_t *> &r, const select_dir_flag select_dir, const char *const sep);
};

struct browser : browser_storage, listbox
{
	browser(const char *orig_path, menu_title title, const std::ranges::subrange<const file_extension_t *> &r, const select_dir_flag select_dir, const char *const sep, ntstring<PATH_MAX - 1> &userdata) :
		browser_storage(orig_path, r, select_dir, sep),
		listbox(0, list.pointer().size(), &list.pointer().front(), title, grd_curscreen->sc_canvas, 1),
		userdata(userdata)
	{
	}
	ntstring<PATH_MAX - 1> &userdata;		// Whatever you want passed to get_absolute_path
	virtual window_event_result callback_handler(const d_event &, window_event_result) override;
};

struct list_directory_context
{
	string_array_t &string_list;
	const std::ranges::subrange<const file_extension_t *> ext_range;
	const std::array<char, PATH_MAX> &path;
};

static void list_dir_el(void *context, const char *, const char *fname)
{
	const auto c = reinterpret_cast<list_directory_context *>(context);
	const char *r = PHYSFS_getRealDir(fname);
	if (!r)
		r = "";
	if (!strcmp(r, c->path.data()) && (PHYSFS_isDirectory(fname) || PHYSFSX_checkMatchingExtension(fname, c->ext_range))
#if defined(__APPLE__) && defined(__MACH__)
		&& d_stricmp(fname, "Volumes")	// this messes things up, use '..' instead
#endif
		)
		c->string_list.add(fname);
}

window_event_result browser::callback_handler(const d_event &event, window_event_result)
{
	const char **list = listbox_get_items(*this);
	const char *sep = PHYSFS_getDirSeparator();
	switch (event.type)
	{
#ifdef _WIN32
		case event_type::key_command:
		{
			if (event_key_get(event) == KEY_CTRLED + KEY_D)
			{
				std::array<char, 4> text{{"c"}};
				std::array<newmenu_item, 1> m{{
					newmenu_item::nm_item_input(text),
				}};
				struct drive_letter_menu : passive_newmenu
				{
					drive_letter_menu(grs_canvas &canvas, const std::ranges::subrange<newmenu_item *> items) :
						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)
						{
						}
				};
				const auto rval = run_blocking_newmenu<drive_letter_menu>(*grd_curcanv, m);
				const auto t0 = text[0];
				if (!rval && t0)
				{
					std::array<char, PATH_MAX> newpath;
					snprintf(newpath.data(), newpath.size(), "%c:%s", t0, sep);
					select_file_recursive(title, newpath, ext_range, select_dir, userdata);
					// close old box.
					return window_event_result::close;
				}
				return window_event_result::handled;
			}
			break;
		}
#endif
		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
			auto newpath = view_path.path;
			if (citem == 0)		// go to parent dir
			{
				const size_t len_newpath = strlen(newpath.data());
				const size_t len_sep = strlen(sep);
				if (auto p = strstr(&newpath[len_newpath - len_sep], sep))
					if (p != strstr(newpath.data(), sep))	// if this isn't the only separator (i.e. it's not about to look at the root)
						*p = 0;

				auto p = &newpath[len_newpath - 1];
				while (p != newpath.begin() && strncmp(p, sep, len_sep))	// make sure full separator string is matched (typically is)
					p--;

				if (p == strstr(newpath.data(), sep))	// Look at root directory next, if not already
				{
#if defined(__APPLE__) && defined(__MACH__)
					if (!d_stricmp(p, "/Volumes"))
						return window_event_result::handled;
#endif
					if (p[len_sep] != '\0')
						p[len_sep] = '\0';
					else
					{
#if defined(__APPLE__) && defined(__MACH__)
						// For Mac OS X, list all active volumes if we leave the root
						strcpy(newpath.data(), "/Volumes");
#else
						return window_event_result::handled;
#endif
					}
				}
				else
					*p = '\0';
			}
			else if (citem == 1 && select_dir != select_dir_flag::files_only)
				return get_absolute_path(userdata, "");
			else
			{
				const std::size_t len_newpath{strlen(newpath.data())};
				const std::size_t len_item{1 + strlen(list[citem])};	// also copy the trailing null
				const std::size_t len_sep{strlen(sep)};
				/* If `newpath` is long enough to contain a separator, test
				 * whether it ends with a separator.  If it is not long enough
				 * to contain one, or is tested and does not end with one, then
				 * set `len_copy_sep` to copy from `sep`.  Otherwise, set
				 * `len_copy_sep` to copy nothing.
				 */
				const std::size_t len_copy_sep{
					(len_newpath >= len_sep && !memcmp(&newpath[len_newpath - len_sep], sep, len_sep))
						? std::size_t{0}
						: len_sep
				};
				const std::size_t capacity_newpath{newpath.size()};
				/* In practice, users should never have a path that approaches
				 * this limit, so the `if` test should succeed in all
				 * reasonable cases.
				 */
				if (const std::size_t total_used_after_insertion{len_newpath + len_copy_sep + len_item}; total_used_after_insertion < capacity_newpath)
				{
					const auto begin_insert_sep{std::next(newpath.begin(), len_newpath)};
					const auto begin_insert_item{std::ranges::copy_n(sep, len_copy_sep, begin_insert_sep).out};
					const auto end_insert_item{std::ranges::copy_n(list[citem], len_item, begin_insert_item).out};
					(void)end_insert_item;
					assert(end_insert_item == std::next(newpath.begin(), total_used_after_insertion));
				}
			}
			if ((citem == 0) || PHYSFS_isDirectory(list[citem]))
			{
				// If it fails, stay in this one
				return select_file_recursive(title, newpath, ext_range, select_dir, userdata) ? window_event_result::close : window_event_result::handled;
			}
			return get_absolute_path(userdata, list[citem]);
		}
		case event_type::window_close:
			break;
		default:
			break;
	}
	return window_event_result::ignored;
}

static int select_file_recursive(const menu_title title, const std::array<char, PATH_MAX> &orig_path_storage, const std::ranges::subrange<const file_extension_t *> &ext_range, const select_dir_flag select_dir, ntstring<PATH_MAX - 1> &userdata)
{
	const auto sep = PHYSFS_getDirSeparator();
	auto orig_path = orig_path_storage.data();
	std::array<char, PATH_MAX> new_path;

	// Check for a PhysicsFS path first, saves complication!
	if (strncmp(orig_path, sep, strlen(sep)) && PHYSFS_exists(orig_path) && PHYSFSX_getRealPath(orig_path, new_path))
		orig_path = new_path.data();

	try {
		auto b = window_create<browser>(orig_path, title, ext_range, select_dir, sep, userdata);
		(void)b;
		return 1;
	} catch (browser::target_path_not_mounted) {
		return 0;
	}
}

string_array_t browser_storage::construct_string_list(const char *orig_path, physfsx_mounted_path &view_path, const std::ranges::subrange<const file_extension_t *> &ext_range, const select_dir_flag select_dir, const char *const sep)
{
	view_path.path.front() = 0;
	// Set the viewing directory to orig_path, or some parent of it
	if (orig_path)
	{
		const auto base =
		// Must make this an absolute path for directory browsing to work properly
#ifdef _WIN32
		(!(isalpha(orig_path[0]) && (orig_path[1] == ':')))	// drive letter prompt (e.g. "C:"
#else
		(orig_path[0] != '/')
#endif
		? PHYSFS_getBaseDir()
		: "";
		const auto vp_begin = view_path.path.begin();
		auto p = std::next(vp_begin, snprintf(view_path.path.data(), view_path.path.size(), "%s%s", base, orig_path) - 1);
		const size_t len_sep = strlen(sep);
		while (!view_path.mount())
		{
			/* Search from the end of the string back to the beginning,
			 * for the first occurrence of a separator.
			 */
			while (p != vp_begin && strncmp(p, sep, len_sep))
				p--;
			*p = '\0';

			if (p == vp_begin)
				break;
		}
	}
	// Set to user directory if we couldn't find a searchpath
	if (!view_path.path[0])
	{
		snprintf(view_path.path.data(), view_path.path.size(), "%s", PHYSFS_getUserDir());
		if (!view_path.mount())
		{
			/* If the directory was not mounted, and cannot be mounted,
			 * prevent showing the dialog.
			 */
			throw target_path_not_mounted();
		}
	}
	string_array_t list;
	list.add("..");		// go to parent directory
	std::size_t tidy_offset = 1;
	if (select_dir != select_dir_flag::files_only)
	{
		++tidy_offset;
		list.add("<this directory>");	// choose the directory being viewed
	}
	list_directory_context context{list, ext_range, view_path.path};
	PHYSFS_enumerateFilesCallback("", list_dir_el, &context);
	list.tidy(tidy_offset);
	return list;
}
#endif

#define DXX_MENU_ITEM_BROWSE(VERB, TXT, OPT)	\
	DXX_MENUITEM(VERB, MENU, TXT " (browse...)", OPT)

}
}

namespace dsx {
namespace {

#if DXX_BUILD_DESCENT == 1
#define DSX_REDBOOK_PLAYORDER_TEXT	"force mac cd track order"
#elif DXX_BUILD_DESCENT == 2
#define DSX_REDBOOK_PLAYORDER_TEXT	"force descent ][ cd track order"
#endif

#if DXX_USE_SDLMIXER || defined(_WIN32)
#define DXX_SOUND_ADDON_MUSIC_MENU_ITEM(VERB)	\
	DXX_MENUITEM(VERB, RADIO, "Built-in/Addon music", opt_sm_mtype1, CGameCfg.MusicType == music_type::Builtin, optgrp_music_type)	\

#else
#define DXX_SOUND_ADDON_MUSIC_MENU_ITEM(VERB)
#endif

#if DXX_USE_SDL_REDBOOK_AUDIO
#define DXX_SOUND_CD_MUSIC_MENU_ITEM(VERB)	\
	DXX_MENUITEM(VERB, RADIO, "CD music", opt_sm_mtype2, CGameCfg.MusicType == music_type::Redbook, optgrp_music_type)	\

#define DXX_MUSIC_OPTIONS_CD_LABEL "CD music"
#else
#define DXX_SOUND_CD_MUSIC_MENU_ITEM(VERB)
#define DXX_MUSIC_OPTIONS_CD_LABEL ""
#endif

#if DXX_USE_SDLMIXER
#define DXX_SOUND_JUKEBOX_MENU_ITEM(VERB)	\
	DXX_MENUITEM(VERB, RADIO, "Jukebox", opt_sm_mtype3, CGameCfg.MusicType == music_type::Custom, optgrp_music_type)	\

#define DXX_MUSIC_OPTIONS_JUKEBOX_LABEL "Jukebox"
#define DXX_SOUND_SDLMIXER_MENU_ITEMS(VERB)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank2)	\
	DXX_MENUITEM(VERB, TEXT, "Jukebox options:", opt_label_jukebox_options)	\
	DXX_MENU_ITEM_BROWSE(VERB, "Path for level music", opt_sm_mtype3_lmpath)	\
	DXX_MENUITEM(VERB, INPUT, CGameCfg.CMLevelMusicPath, opt_sm_mtype3_lmpath_input)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank3)	\
	DXX_MENUITEM(VERB, TEXT, "Level music play order:", opt_label_lm_order)	\
	DXX_MENUITEM(VERB, RADIO, "continuous", opt_sm_mtype3_lmplayorder1, CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Continuous, optgrp_music_order)	\
	DXX_MENUITEM(VERB, RADIO, "one track per level", opt_sm_mtype3_lmplayorder2, CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Level, optgrp_music_order)	\
	DXX_MENUITEM(VERB, RADIO, "random", opt_sm_mtype3_lmplayorder3, CGameCfg.CMLevelMusicPlayOrder == LevelMusicPlayOrder::Random, optgrp_music_order)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank4)	\
	DXX_MENUITEM(VERB, TEXT, "Non-level music:", opt_label_nonlevel_music)	\
	DXX_MENU_ITEM_BROWSE(VERB, "Main menu", opt_sm_cm_mtype3_file1_b)	\
	DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[song_number::title], opt_sm_cm_mtype3_file1)	\
	DXX_MENU_ITEM_BROWSE(VERB, "Briefing", opt_sm_cm_mtype3_file2_b)	\
	DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[song_number::briefing], opt_sm_cm_mtype3_file2)	\
	DXX_MENU_ITEM_BROWSE(VERB, "Credits", opt_sm_cm_mtype3_file3_b)	\
	DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[song_number::credits], opt_sm_cm_mtype3_file3)	\
	DXX_MENU_ITEM_BROWSE(VERB, "Escape sequence", opt_sm_cm_mtype3_file4_b)	\
	DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[song_number::endlevel], opt_sm_cm_mtype3_file4)	\
	DXX_MENU_ITEM_BROWSE(VERB, "Game ending", opt_sm_cm_mtype3_file5_b)	\
	DXX_MENUITEM(VERB, INPUT, CGameCfg.CMMiscMusic[song_number::endgame], opt_sm_cm_mtype3_file5)	\

#else
#define DXX_SOUND_JUKEBOX_MENU_ITEM(VERB)
#define DXX_MUSIC_OPTIONS_JUKEBOX_LABEL ""
#define DXX_SOUND_SDLMIXER_MENU_ITEMS(VERB)
#endif

#if SDL_MAJOR_VERSION == 1 && DXX_USE_SDLMIXER
#define DXX_MUSIC_OPTIONS_SEPARATOR_TEXT " / "
#else
#define DXX_MUSIC_OPTIONS_SEPARATOR_TEXT ""
#endif

#define DSX_SOUND_MENU(VERB)	\
	DXX_MENUITEM(VERB, SLIDER, TXT_FX_VOLUME, opt_sm_digivol, CGameCfg.DigiVolume, 0, 8)	\
	DXX_MENUITEM(VERB, SLIDER, "Music volume", opt_sm_musicvol, CGameCfg.MusicVolume, 0, 8)	\
	DXX_MENUITEM(VERB, CHECK, TXT_REVERSE_STEREO, opt_sm_revstereo, CGameCfg.ReverseStereo)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank0)	\
	DXX_MENUITEM(VERB, TEXT, "Music type:", opt_label_music_type)	\
	DXX_MENUITEM(VERB, RADIO, "No music", opt_sm_mtype0, CGameCfg.MusicType == music_type::None, optgrp_music_type)	\
	DXX_SOUND_ADDON_MUSIC_MENU_ITEM(VERB)	\
	DXX_SOUND_CD_MUSIC_MENU_ITEM(VERB)	\
	DXX_SOUND_JUKEBOX_MENU_ITEM(VERB)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank1)	\
	DXX_MENUITEM(VERB, TEXT, DXX_MUSIC_OPTIONS_CD_LABEL DXX_MUSIC_OPTIONS_SEPARATOR_TEXT DXX_MUSIC_OPTIONS_JUKEBOX_LABEL " options:", opt_label_music_options)	\
	DXX_MENUITEM(VERB, CHECK, DSX_REDBOOK_PLAYORDER_TEXT, opt_sm_redbook_playorder, CGameCfg.OrigTrackOrder)	\
	DXX_SOUND_SDLMIXER_MENU_ITEMS(VERB)	\

class sound_menu_items
{
public:
	enum
	{
		optgrp_music_type,
#if DXX_USE_SDLMIXER
		optgrp_music_order,
#endif
	};
	enum
	{
		DSX_SOUND_MENU(ENUM)
	};
	DSX_SOUND_MENU(DECL);
	std::array<newmenu_item, DSX_SOUND_MENU(COUNT)> m;
	sound_menu_items()
	{
		DSX_SOUND_MENU(ADD);
	}
	void read()
	{
		DSX_SOUND_MENU(READ);
	}
};

struct sound_menu : sound_menu_items, newmenu
{
#if DXX_USE_SDLMIXER
	ntstring<PATH_MAX - 1> &current_music = Game_wind
		? CGameCfg.CMLevelMusicPath
		: CGameCfg.CMMiscMusic[song_number::title];
	ntstring<PATH_MAX - 1> old_music = current_music;
#endif
	sound_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

#undef DSX_SOUND_MENU

window_event_result sound_menu::event_handler(const d_event &event)
{
	const auto &items = m;
	int replay{0};
	switch (event.type)
	{
		case event_type::newmenu_changed:
		{
			auto &citem = static_cast<const d_change_event &>(event).citem;
			if (citem == opt_sm_digivol)
			{
				const auto v = items[citem].value;
				CGameCfg.DigiVolume = v;
				digi_set_digi_volume((v * 32768) / 8);
				digi_play_sample_once( sound_effect::SOUND_DROP_BOMB, F1_0 );
			}
			else if (citem == opt_sm_musicvol)
			{
				const auto v = items[citem].value;
				CGameCfg.MusicVolume = v;
				songs_set_volume(v);
			}
			else if (citem == opt_sm_revstereo)
			{
				CGameCfg.ReverseStereo = items[citem].value;
			}
			else if (citem == opt_sm_mtype0)
			{
				CGameCfg.MusicType = music_type::None;
				replay = 1;
			}
			/*
			 * When builtin music is enabled, the next line expands to
			 * `#if +1 + 0`; when it is disabled, the line expands to
			 * `#if + 0`.
			 */
#if DXX_SOUND_ADDON_MUSIC_MENU_ITEM(COUNT) + 0
			else if (citem == opt_sm_mtype1)
			{
				CGameCfg.MusicType = music_type::Builtin;
				replay = 1;
			}
#endif
#if DXX_USE_SDL_REDBOOK_AUDIO
			else if (citem == opt_sm_mtype2)
			{
				CGameCfg.MusicType = music_type::Redbook;
				replay = 1;
			}
#endif
#if DXX_USE_SDLMIXER
			else if (citem == opt_sm_mtype3)
			{
				CGameCfg.MusicType = music_type::Custom;
				replay = 1;
			}
#endif
			else if (citem == opt_sm_redbook_playorder)
			{
				CGameCfg.OrigTrackOrder = items[citem].value;
				replay = static_cast<bool>(Game_wind);
			}
#if DXX_USE_SDLMIXER
			else if (citem == opt_sm_mtype3_lmplayorder1)
			{
				CGameCfg.CMLevelMusicPlayOrder = LevelMusicPlayOrder::Continuous;
				replay = static_cast<bool>(Game_wind);
			}
			else if (citem == opt_sm_mtype3_lmplayorder2)
			{
				CGameCfg.CMLevelMusicPlayOrder = LevelMusicPlayOrder::Level;
				replay = static_cast<bool>(Game_wind);
			}
			else if (citem == opt_sm_mtype3_lmplayorder3)
			{
				CGameCfg.CMLevelMusicPlayOrder = LevelMusicPlayOrder::Random;
				replay = static_cast<bool>(Game_wind);
			}
#endif
			break;
		}
		case event_type::newmenu_selected:
		{
#if DXX_USE_SDLMIXER
			auto &citem = static_cast<const d_select_event &>(event).citem;
#ifdef _WIN32
#define WINDOWS_DRIVE_CHANGE_TEXT	".\nCTRL-D to change drive"
#else
#define WINDOWS_DRIVE_CHANGE_TEXT
#endif
			if (citem == opt_sm_mtype3_lmpath)
			{
				static const std::array<file_extension_t, 1> ext_list{{"m3u"}};		// select a directory or M3U playlist
				select_file_recursive(
					menu_title{"Select directory or\nM3U playlist to\n play level music from" WINDOWS_DRIVE_CHANGE_TEXT},
									  CGameCfg.CMLevelMusicPath, ext_list, select_dir_flag::directories_or_files,	// look in current music path for ext_list files and allow directory selection
									  CGameCfg.CMLevelMusicPath);	// just copy the absolute path
			}
			else if (citem == opt_sm_cm_mtype3_file1_b)
				SELECT_SONG(menu_title{"Select main menu music" WINDOWS_DRIVE_CHANGE_TEXT}, song_number::title);
			else if (citem == opt_sm_cm_mtype3_file2_b)
				SELECT_SONG(menu_title{"Select briefing music" WINDOWS_DRIVE_CHANGE_TEXT}, song_number::briefing);
			else if (citem == opt_sm_cm_mtype3_file3_b)
				SELECT_SONG(menu_title{"Select credits music" WINDOWS_DRIVE_CHANGE_TEXT}, song_number::credits);
			else if (citem == opt_sm_cm_mtype3_file4_b)
				SELECT_SONG(menu_title{"Select escape sequence music" WINDOWS_DRIVE_CHANGE_TEXT}, song_number::endlevel);
			else if (citem == opt_sm_cm_mtype3_file5_b)
				SELECT_SONG(menu_title{"Select game ending music" WINDOWS_DRIVE_CHANGE_TEXT}, song_number::endgame);
#endif
			return window_event_result::handled;	// stay in menu
		}
		case event_type::window_close:
#if DXX_USE_SDLMIXER
			if (strcmp(old_music.data(), current_music.data()))
			{
				songs_uninit();
				if (Game_wind)
					songs_play_level_song(Current_level_num, 0);
				else
					songs_play_song(song_number::title, 1);
			}
#endif
			break;

		default:
			break;
	}

	if (replay)
	{
		songs_uninit();

		if (Game_wind)
			songs_play_level_song( Current_level_num, 0 );
		else
			songs_play_song(song_number::title, 1);
	}
	return newmenu::event_handler(event);
}

void do_sound_menu()
{
	auto menu = window_create<sound_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

#if DXX_BUILD_DESCENT == 1
#define DSX_GAME_SPECIFIC_OPTIONS(VERB)	\

#elif DXX_BUILD_DESCENT == 2
#define DSX_GAME_SPECIFIC_OPTIONS(VERB)	\
	DXX_MENUITEM(VERB, CHECK, "Headlight on when picked up", opt_headlighton,PlayerCfg.HeadlightActiveDefault )	\
	DXX_MENUITEM(VERB, CHECK, "Escort robot hot keys",opt_escorthotkey,PlayerCfg.EscortHotKeys)	\
	DXX_MENUITEM(VERB, CHECK, "Movie Subtitles",opt_moviesubtitle,GameCfg.MovieSubtitles)	\
	DXX_MENUITEM(VERB, CHECK, "Remove Thief at level start", opt_thief_presence, thief_absent)	\
	DXX_MENUITEM(VERB, CHECK, "Prevent Thief Stealing Energy Weapons", opt_thief_steal_energy, thief_cannot_steal_energy_weapons)	\

#endif

#define DSX_GAMEPLAY_MENU_OPTIONS(VERB)	\
	DXX_MENUITEM(VERB, CHECK, "Ship auto-leveling",opt_autolevel, PlayerCfg.AutoLeveling)	\
	DXX_MENUITEM(VERB, CHECK, "Persistent Debris",opt_persist_debris,PlayerCfg.PersistentDebris)	\
	DXX_MENUITEM(VERB, CHECK, "No Rankings (Multi)",opt_noranking,PlayerCfg.NoRankings)	\
	DXX_MENUITEM(VERB, CHECK, "Free Flight in Automap",opt_freeflight, PlayerCfg.AutomapFreeFlight)	\
	DSX_GAME_SPECIFIC_OPTIONS(VERB)	\
	DXX_MENUITEM(VERB, TEXT, "", opt_label_blank)	\
        DXX_MENUITEM(VERB, TEXT, "Weapon Autoselect options:", opt_label_autoselect)	\
	DXX_MENUITEM(VERB, MENU, "Primary ordering...", opt_gameplay_reorderprimary_menu)	\
	DXX_MENUITEM(VERB, MENU, "Secondary ordering...", opt_gameplay_reordersecondary_menu)	\
	DXX_MENUITEM(VERB, TEXT, "Autoselect while firing:", opt_autoselect_firing_label)	\
	DXX_MENUITEM(VERB, RADIO, "Immediately", opt_autoselect_firing_immediate, PlayerCfg.NoFireAutoselect == FiringAutoselectMode::Immediate, optgrp_autoselect_firing)	\
	DXX_MENUITEM(VERB, RADIO, "Never", opt_autoselect_firing_never, PlayerCfg.NoFireAutoselect == FiringAutoselectMode::Never, optgrp_autoselect_firing)	\
	DXX_MENUITEM(VERB, RADIO, "When firing stops", opt_autoselect_firing_delayed, PlayerCfg.NoFireAutoselect == FiringAutoselectMode::Delayed, optgrp_autoselect_firing)	\
	DXX_MENUITEM(VERB, CHECK, "Only Cycle Autoselect Weapons",opt_only_autoselect,PlayerCfg.CycleAutoselectOnly)	\
	DXX_MENUITEM_AUTOSAVE_LABEL_INPUT(VERB)	\

struct gameplay_config_menu_items
{
	enum {
		DSX_GAMEPLAY_MENU_OPTIONS(ENUM)
	};
	DSX_GAMEPLAY_MENU_OPTIONS(DECL);
	std::array<newmenu_item, DSX_GAMEPLAY_MENU_OPTIONS(COUNT)> m;
	human_readable_mmss_time<decltype(d_gameplay_options::AutosaveInterval)::rep> AutosaveInterval;
	gameplay_config_menu_items() :
		AutosaveInterval{build_human_readable_time(PlayerCfg.SPGameplayOptions.AutosaveInterval)}
	{
#if DXX_BUILD_DESCENT == 2
		auto thief_absent = PlayerCfg.ThiefModifierFlags & ThiefModifier::Absent;
		auto thief_cannot_steal_energy_weapons = PlayerCfg.ThiefModifierFlags & ThiefModifier::NoEnergyWeapons;
#endif
		DSX_GAMEPLAY_MENU_OPTIONS(ADD);
	}
};

struct gameplay_config_menu : gameplay_config_menu_items, newmenu
{
	gameplay_config_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result gameplay_config_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
                        if (citem == opt_gameplay_reorderprimary_menu)
                                ReorderPrimary();
			else if (citem == opt_gameplay_reordersecondary_menu)
                                ReorderSecondary();
			return window_event_result::handled;		// stay in menu
		}
		case event_type::window_close:
			{
#if DXX_BUILD_DESCENT == 2
				uint8_t thief_absent;
				uint8_t thief_cannot_steal_energy_weapons;
#endif
				DSX_GAMEPLAY_MENU_OPTIONS(READ);
				PlayerCfg.NoFireAutoselect = m[opt_autoselect_firing_delayed].value
					? FiringAutoselectMode::Delayed
					: (m[opt_autoselect_firing_immediate].value
					   ? FiringAutoselectMode::Immediate
					   : FiringAutoselectMode::Never);
#if DXX_BUILD_DESCENT == 2
				PlayerCfg.ThiefModifierFlags =
					(thief_absent ? ThiefModifier::Absent : 0) |
					(thief_cannot_steal_energy_weapons ? ThiefModifier::NoEnergyWeapons : 0);
#endif
				parse_human_readable_time(PlayerCfg.SPGameplayOptions.AutosaveInterval, AutosaveInterval);
			}
			break;

		default:
			break;
	}
	return newmenu::event_handler(event);
}

void gameplay_config()
{
	auto menu = window_create<gameplay_config_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

#if DXX_USE_UDP
window_event_result netgame_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
			// stay in multiplayer menu, even after having played a game
			return dispatch_menu_option(static_cast<netgame_menu_item_index>(citem));
		}
		default:
			break;
	}
	return newmenu::event_handler(event);
}

void do_multi_player_menu()
{
	auto menu = window_create<netgame_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}
#endif

}

}

void do_options_menu()
{
	// Fall back to main event loop
	// Allows clean closing and re-opening when resolution changes
	auto menu = window_create<options_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

#ifndef RELEASE
namespace dsx {

namespace {

struct polygon_models_viewer_window : window
{
	vms_angvec ang{0, 0, F0_5 - 1};
	polygon_model_index view_idx{};
	using window::window;
	virtual window_event_result event_handler(const d_event &) override;
};

struct gamebitmaps_viewer_window : window
{
	uint16_t view_idx{0};
	using window::window;
	virtual window_event_result event_handler(const d_event &) override;
};

window_event_result polygon_models_viewer_window::event_handler(const d_event &event)
{
	int key{0};

	switch (event.type)
	{
		case event_type::window_activated:
#if DXX_BUILD_DESCENT == 2
			gr_use_palette_table("groupa.256");
#endif
			key_toggle_repeat(1);
			break;
		case event_type::key_command:
			key = event_key_get(event);
			switch (key)
			{
				case KEY_ESC:
					return window_event_result::close;
				case KEY_SPACEBAR:
					{
						auto v = underlying_value(view_idx);
						if (++ v >= LevelSharedPolygonModelState.N_polygon_models)
							v = 0;
						view_idx = polygon_model_index{v};
					}
					break;
				case KEY_BACKSP:
					{
						auto v = underlying_value(view_idx);
						const auto n = LevelSharedPolygonModelState.N_polygon_models;
						if (-- v >= n)
							v = n - 1;
						view_idx = polygon_model_index{v};
					}
					break;
				case KEY_A:
					ang.h -= 100;
					break;
				case KEY_D:
					ang.h += 100;
					break;
				case KEY_W:
					ang.p -= 100;
					break;
				case KEY_S:
					ang.p += 100;
					break;
				case KEY_Q:
					ang.b -= 100;
					break;
				case KEY_E:
					ang.b += 100;
					break;
				case KEY_R:
					ang.p = ang.b = 0;
					ang.h = F0_5-1;
					break;
				default:
					break;
			}
			return window_event_result::handled;
		case event_type::window_draw:
			timer_delay(F1_0/60);
			{
				auto &canvas = *grd_curcanv;
				auto &Polygon_models = LevelSharedPolygonModelState.Polygon_models;
				draw_model_picture(canvas, Polygon_models[view_idx], ang);
				gr_set_fontcolor(canvas, BM_XRGB(255, 255, 255), -1);
				auto &game_font = *GAME_FONT;
				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", underlying_value(view_idx), LevelSharedPolygonModelState.N_polygon_models - 1);
			}
			break;
		case event_type::window_close:
#if DXX_BUILD_DESCENT == 2
			load_palette(MENU_PALETTE, load_palette_use::background, load_palette_change_screen::delayed);
#endif
			key_toggle_repeat(0);
			break;
		default:
			break;
	}
	return window_event_result::ignored;
}

static void polygon_models_viewer()
{
	auto viewer_window = window_create<polygon_models_viewer_window>(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT);
	(void)viewer_window;
}

window_event_result gamebitmaps_viewer_window::event_handler(const d_event &event)
{
	int key{0};
#if DXX_USE_OGL
	float scale = 1.0;
#endif

	switch (event.type)
	{
		case event_type::window_activated:
#if DXX_BUILD_DESCENT == 2
			gr_use_palette_table("groupa.256");
#endif
			key_toggle_repeat(1);
			break;
		case event_type::key_command:
			key = event_key_get(event);
			switch (key)
			{
				case KEY_ESC:
					return window_event_result::close;
				case KEY_SPACEBAR:
					view_idx ++;
					if (view_idx >= Num_bitmap_files) view_idx = 0;
					break;
				case KEY_BACKSP:
					if (!view_idx)
						view_idx = Num_bitmap_files;
					view_idx --;
					break;
				default:
					break;
			}
			return window_event_result::handled;
		case event_type::window_draw:
			{
				const bitmap_index bi{view_idx};
				grs_bitmap *const bm = &GameBitmaps[bi];
			timer_delay(F1_0/60);
			PIGGY_PAGE_IN(bi);
				auto &canvas = *grd_curcanv;
				gr_clear_canvas(canvas, BM_XRGB(0,0,0));
#if DXX_USE_OGL
				scale = (bm->bm_w > bm->bm_h)?(SHEIGHT/bm->bm_w)*0.8:(SHEIGHT/bm->bm_h)*0.8;
				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);
#else
				gr_bitmap(canvas, (SWIDTH / 2) - (bm->bm_w / 2), (SHEIGHT / 2) - (bm->bm_h / 2), *bm);
#endif
				gr_set_fontcolor(canvas, BM_XRGB(255, 255, 255), -1);
				auto &game_font = *GAME_FONT;
				gr_printf(canvas, game_font, FSPACX(1), FSPACY(1), "ESC: leave\nSPACE/BACKSP: next/prev bitmap (%i/%i)", view_idx, Num_bitmap_files-1);
			}
			break;
		case event_type::window_close:
#if DXX_BUILD_DESCENT == 2
			load_palette(MENU_PALETTE, load_palette_use::background, load_palette_change_screen::delayed);
#endif
			key_toggle_repeat(0);
			break;
		default:
			break;
	}
	return window_event_result::ignored;
}

static void gamebitmaps_viewer()
{
	auto viewer_window = window_create<gamebitmaps_viewer_window>(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT);
	(void)viewer_window;
}

#define DXX_SANDBOX_MENU(VERB)	\
	DXX_MENUITEM(VERB, MENU, "Polygon_models viewer", polygon_models)	\
	DXX_MENUITEM(VERB, MENU, "GameBitmaps viewer", bitmaps)	\

class sandbox_menu_items
{
public:
	enum
	{
		DXX_SANDBOX_MENU(ENUM)
	};
	DXX_SANDBOX_MENU(DECL);
	std::array<newmenu_item, DXX_SANDBOX_MENU(COUNT)> m;
	sandbox_menu_items()
	{
		DXX_SANDBOX_MENU(ADD);
	}
};

struct sandbox_menu : sandbox_menu_items, newmenu
{
	sandbox_menu(grs_canvas &src) :
		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)
	{
	}
	virtual window_event_result event_handler(const d_event &event) override;
};

window_event_result sandbox_menu::event_handler(const d_event &event)
{
	switch (event.type)
	{
		case event_type::newmenu_selected:
		{
			auto &citem = static_cast<const d_select_event &>(event).citem;
			switch (citem)
			{
				case sandbox_menu_items::polygon_models:
					polygon_models_viewer();
					break;
				case sandbox_menu_items::bitmaps:
					gamebitmaps_viewer();
					break;
			}
			return window_event_result::handled; // stay in menu until escape
		}
		default:
			break;
	}
	return newmenu::event_handler(event);
}

void do_sandbox_menu()
{
	auto menu = window_create<sandbox_menu>(grd_curscreen->sc_canvas);
	(void)menu;
}

}

}
#endif
