dxx-rebirth/similar/main/menu.cpp
Kp f491059ed7 Enable building with SDL2
This commit enables Rebirth to build with SDL2, but the result is not
perfect.

- SDL2 removed some sticky key support.  Rebirth may behave differently
  now in this area.
- SDL2 removed some key-repeat related support.  Rebirth may behave
  differently now in this area.
- SDL2 gained the ability to make a window fullscreen by sizing it to
  the desktop instead of by changing the desktop resolution.  Rebirth
  uses this, and it mostly works.
  - Resizing while in the automap does not notify the automap code, so
    the view is wrong until the player switches out of automap mode and
    back in.
- SDL2 changed how to enumerate available resolutions.  Since
  fitting the window to the desktop is generally more useful than
  fitting the desktop to the window, I chose to drop support for
  enumerating resolutions instead of porting to the new API.  Users can
  now enter an arbitrary window dimension and Rebirth will make an
  attempt to use it.
  - It might be useful to cap the window dimension at the desktop
    dimension, but that is not done yet.
  - Entering fullscreen mode through the Controls->Graphics submenu
    failed to notify the relevant subsystems, causing the rendered
    content not to rescale.  For now, compile out the option to toggle
    full screen through that menu.  Toggling through Alt+Enter works
    properly.

Despite these quirks, this is a substantial improvement over the prior
commit, where SDL2 cannot be used at all.  The remaining issues can be
resolved in future work.

References: <https://github.com/dxx-rebirth/dxx-rebirth/issues/82>
2018-07-28 23:22:58 +00:00

2621 lines
75 KiB
C++

/*
* 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 <stdio.h>
#include <string.h>
#include <SDL.h>
#include "menu.h"
#include "inferno.h"
#include "game.h"
#include "gr.h"
#include "key.h"
#include "mouse.h"
#include "iff.h"
#include "u_mem.h"
#include "dxxerror.h"
#include "bm.h"
#include "screens.h"
#include "joy.h"
#include "player.h"
#include "vecmat.h"
#include "effects.h"
#include "game.h"
#include "slew.h"
#include "gamemine.h"
#include "gamesave.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 "titles.h"
#include "credits.h"
#include "texmap.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 defined(DXX_BUILD_DESCENT_II)
#include "movie.h"
#endif
#include "gamepal.h"
#include "gauges.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"
#endif
#include "physfs_list.h"
#include "dxxsconf.h"
#include "dsx-ns.h"
#include "compiler-exchange.h"
#include "compiler-make_unique.h"
#include "compiler-range_for.h"
#include "partial_range.h"
// Menu IDs...
enum MENUS
{
MENU_NEW_GAME = 0,
MENU_GAME,
MENU_EDITOR,
MENU_VIEW_SCORES,
MENU_QUIT,
MENU_LOAD_GAME,
MENU_SAVE_GAME,
MENU_DEMO_PLAY,
MENU_CONFIG,
MENU_REJOIN_NETGAME,
MENU_DIFFICULTY,
MENU_HELP,
MENU_NEW_PLAYER,
#if DXX_USE_UDP
MENU_MULTIPLAYER,
#endif
MENU_SHOW_CREDITS,
MENU_ORDER_INFO,
#if DXX_USE_UDP
MENU_START_UDP_NETGAME,
MENU_JOIN_MANUAL_UDP_NETGAME,
MENU_JOIN_LIST_UDP_NETGAME,
#endif
#ifndef RELEASE
MENU_SANDBOX
#endif
};
//ADD_ITEM("Start netgame...", MENU_START_NETGAME, -1 );
//ADD_ITEM("Send net message...", MENU_SEND_NET_MESSAGE, -1 );
#define ADD_ITEM(t,value,key) do { nm_set_item_menu(m[num_options], t); menu_choice[num_options]=value;num_options++; } while (0)
static array<window *, 16> menus;
// Function Prototypes added after LINTING
static int do_option(int select);
static window_event_result do_new_game_menu(void);
#if DXX_USE_UDP
static void do_multi_player_menu();
#endif
#ifndef RELEASE
static void do_sandbox_menu();
#endif
namespace {
template <typename T>
using select_file_subfunction = window_event_result (*)(T *, const char *);
}
__attribute_nonnull()
static int select_file_recursive2(const char *title, const char *orig_path, const partial_range_t<const file_extension_t *> &ext_list, int select_dir, select_file_subfunction<void> when_selected, void *userdata);
template <typename T>
__attribute_nonnull()
static int select_file_recursive(const char *title, const char *orig_path, const partial_range_t<const file_extension_t *> &ext_list, int select_dir, select_file_subfunction<T> when_selected, T *userdata)
{
return select_file_recursive2(title, orig_path, ext_list, select_dir, reinterpret_cast<select_file_subfunction<void>>(when_selected), reinterpret_cast<void *>(userdata));
}
// 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 = window_set_visible(*wind, 0);
}
Assert(window_get_front() == NULL);
return 1;
}
// Show all menus, with the front one shown first
// This makes sure EVENT_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))
window_set_visible(exchange(i, nullptr), 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__--";
}
static int MakeNewPlayerFile(int allow_abort)
{
int x;
char filename[PATH_MAX];
callsign_t text = get_local_player().callsign;
try_again:
{
array<newmenu_item, 1> m{{
nm_item_input(text.buffer()),
}};
Newmenu_allowed_chars = playername_allowed_chars;
x = newmenu_do( NULL, TXT_ENTER_PILOT_NAME, m, unused_newmenu_subfunction, unused_newmenu_userdata );
}
Newmenu_allowed_chars = NULL;
if ( x < 0 ) {
if ( allow_abort ) return 0;
goto try_again;
}
if (!*static_cast<const char *>(text)) //null string
goto try_again;
text.lower();
snprintf(filename, sizeof(filename), PLAYER_DIRECTORY_STRING("%s.plr"), static_cast<const char *>(text) );
if (PHYSFSX_exists(filename,0))
{
nm_messagebox(NULL, 1, TXT_OK, "%s '%s' %s", TXT_PLAYER, static_cast<const char *>(text), TXT_ALREADY_EXISTS );
goto try_again;
}
if ( !new_player_config() )
goto try_again; // They hit Esc during New player config
get_local_player().callsign = text;
write_player_file();
return 1;
}
static void delete_player_saved_games(const char * name);
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( NULL, 2, 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 (PHYSFSX_exists(plxfile,0))
PHYSFS_delete(plxfile);
// delete EFF file
snprintf(efffile, sizeof(efffile), PLAYER_DIRECTORY_STRING("%.8s.eff"), items[citem]);
if (PHYSFSX_exists(efffile,0))
PHYSFS_delete(efffile);
// delete NGP file
snprintf(ngpfile, sizeof(ngpfile), PLAYER_DIRECTORY_STRING("%.8s.ngp"), items[citem]);
if (PHYSFSX_exists(ngpfile,0))
PHYSFS_delete(ngpfile);
}
if (ret)
nm_messagebox( NULL, 1, 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;
}
static window_event_result player_menu_handler( listbox *lb,const d_event &event, char **list )
{
const char **items = listbox_get_items(lb);
switch (event.type)
{
case EVENT_KEY_COMMAND:
return player_menu_keycommand(lb, event);
case EVENT_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
{
get_local_player().callsign.copy_lower(items[citem], strlen(items[citem]));
}
return window_event_result::close;
break;
}
case EVENT_WINDOW_CLOSE:
if (read_player_file() != EZERO)
return window_event_result::handled; // abort close!
WriteConfigFile(); // Update lastplr
PHYSFS_freeList(list);
d_free(items);
break;
default:
break;
}
return window_event_result::ignored;
}
//Inputs the player's name, without putting up the background screen
int RegisterPlayer()
{
static const array<file_extension_t, 1> types{{"plr"}};
int i = 0, NumItems;
int citem = 0;
int allow_abort_flag = 1;
auto &plr = get_local_player();
if (!*static_cast<const char *>(plr.callsign))
{
if (!*static_cast<const char *>(GameCfg.LastPlayer))
{
plr.callsign = "player";
allow_abort_flag = 0;
}
else
{
// Read the last player's name from config file, not lastplr.txt
plr.callsign = GameCfg.LastPlayer;
}
}
auto list = PHYSFSX_findFiles(PLAYER_DIRECTORY_STRING(""), types);
if (!list)
return 0; // memory error
if (!list[0])
{
MakeNewPlayerFile(0); // make a new player without showing listbox
return 0;
}
for (NumItems = 0; list[NumItems] != NULL; NumItems++) {}
NumItems++; // for TXT_CREATE_NEW
RAIIdmem<const char *[]> m;
MALLOC(m, const char *[], NumItems);
if (m == NULL)
{
return 0;
}
m[i++] = TXT_CREATE_NEW;
range_for (const auto f, list)
{
char *p;
size_t lenf = strlen(f);
if (lenf > FILENAME_LEN-1 || lenf < 5) // sorry guys, can only have up to eight chars for the player name
{
NumItems--;
continue;
}
m[i++] = f;
p = strchr(f, '.');
if (p)
*p = '\0'; // chop the .plr
}
if (NumItems <= 1) // so it seems all plr files we found were too long. funny. let's make a real player
{
MakeNewPlayerFile(0); // make a new player without showing listbox
return 0;
}
// Sort by name, except the <Create New Player> string
qsort(&m[1], NumItems - 1, sizeof(char *), string_array_sort_func);
for ( i=0; i<NumItems; i++ )
if (!d_stricmp(static_cast<const char *>(get_local_player().callsign), m[i]) )
citem = i;
newmenu_listbox1(TXT_SELECT_PILOT, NumItems, m.release(), allow_abort_flag, citem, player_menu_handler, list.release());
return 1;
}
// Draw Copyright and Version strings
static void draw_copyright()
{
gr_set_default_canvas();
auto &canvas = *grd_curcanv;
auto &game_font = *GAME_FONT;
gr_set_fontcolor(canvas, BM_XRGB(6, 6, 6), -1);
const auto &&line_spacing = LINE_SPACING(game_font, game_font);
gr_string(canvas, game_font, 0x8000, SHEIGHT - line_spacing, TXT_COPYRIGHT);
gr_set_fontcolor(canvas, BM_XRGB(25, 0, 0), -1);
gr_string(canvas, game_font, 0x8000, SHEIGHT - (line_spacing * 2), DESCENT_VERSION);
}
namespace dsx {
// ------------------------------------------------------------------------
static int main_menu_handler(newmenu *menu,const d_event &event, int *menu_choice )
{
newmenu_item *items = newmenu_get_items(menu);
switch (event.type)
{
case EVENT_WINDOW_ACTIVATED:
load_palette(MENU_PALETTE,0,1); //get correct palette
if (!*static_cast<const char *>(get_local_player().callsign))
RegisterPlayer();
else
keyd_time_when_last_pressed = timer_query(); // .. 20 seconds from now!
break;
case EVENT_KEY_COMMAND:
// Don't allow them to hit ESC in the main menu.
if (event_key_get(event)==KEY_ESC)
return 1;
break;
case EVENT_MOUSE_BUTTON_DOWN:
case EVENT_MOUSE_BUTTON_UP:
// Don't allow mousebutton-closing in main menu.
if (event_mouse_get_button(event) == MBTN_RIGHT)
return 1;
break;
case EVENT_IDLE:
#if defined(DXX_BUILD_DESCENT_I)
#define DXX_DEMO_KEY_DELAY 45
#elif defined(DXX_BUILD_DESCENT_II)
#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 defined(DXX_BUILD_DESCENT_II)
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",0);
songs_play_song(SONG_TITLE,1);
set_screen_mode(SCREEN_MENU);
}
else
#endif
{
newdemo_start_playback(NULL); // Randomly pick a file, assume native endian (crashes if not)
#if defined(DXX_BUILD_DESCENT_II)
if (Newdemo_state == ND_STATE_PLAYBACK)
return 0;
#endif
}
}
break;
case EVENT_NEWMENU_DRAW:
draw_copyright();
break;
case EVENT_NEWMENU_SELECTED:
{
auto &citem = static_cast<const d_select_event &>(event).citem;
return do_option(menu_choice[citem]);
}
case EVENT_WINDOW_CLOSE:
d_free(menu_choice);
d_free(items);
break;
default:
break;
}
return 0;
}
// -----------------------------------------------------------------------------
// Create the main menu.
static void create_main_menu(newmenu_item *m, int *menu_choice, int *callers_num_options)
{
int num_options = 0;
#ifndef DEMO_ONLY
ADD_ITEM(TXT_NEW_GAME,MENU_NEW_GAME,KEY_N);
ADD_ITEM(TXT_LOAD_GAME,MENU_LOAD_GAME,KEY_L);
#if DXX_USE_UDP
ADD_ITEM(TXT_MULTIPLAYER_,MENU_MULTIPLAYER,-1);
#endif
ADD_ITEM(TXT_OPTIONS_, MENU_CONFIG, -1 );
ADD_ITEM(TXT_CHANGE_PILOTS,MENU_NEW_PLAYER,unused);
ADD_ITEM(TXT_VIEW_DEMO,MENU_DEMO_PLAY,0);
ADD_ITEM(TXT_VIEW_SCORES,MENU_VIEW_SCORES,KEY_V);
#if defined(DXX_BUILD_DESCENT_I)
if (!PHYSFSX_exists("warning.pcx",1)) /* SHAREWARE */
#elif defined(DXX_BUILD_DESCENT_II)
if (PHYSFSX_exists("orderd2.pcx",1)) /* SHAREWARE */
#endif
ADD_ITEM(TXT_ORDERING_INFO,MENU_ORDER_INFO,-1);
ADD_ITEM(TXT_CREDITS,MENU_SHOW_CREDITS,-1);
#endif
ADD_ITEM(TXT_QUIT,MENU_QUIT,KEY_Q);
#ifndef RELEASE
if (!(Game_mode & GM_MULTI )) {
#if DXX_USE_EDITOR
ADD_ITEM(" Editor", MENU_EDITOR, KEY_E);
#endif
}
ADD_ITEM(" SANDBOX", MENU_SANDBOX, -1);
#endif
*callers_num_options = num_options;
}
//returns number of item chosen
int DoMenu()
{
int *menu_choice;
newmenu_item *m;
int num_options = 0;
CALLOC(menu_choice, int, 25);
if (!menu_choice)
return -1;
CALLOC(m, newmenu_item, 25);
if (!m)
{
d_free(menu_choice);
return -1;
}
create_main_menu(m, menu_choice, &num_options); // may have to change, eg, maybe selected pilot and no save games.
newmenu_do3( "", NULL, num_options, m, main_menu_handler, menu_choice, 0, Menu_pcx_name);
return 0;
}
}
//returns flag, true means quit menu
int do_option ( int select)
{
switch (select) {
case MENU_NEW_GAME:
select_mission(0, "New Game\n\nSelect mission", do_new_game_menu);
break;
case MENU_GAME:
break;
case MENU_DEMO_PLAY:
select_demo();
break;
case MENU_LOAD_GAME:
state_restore_all(0, secret_restore::none, nullptr, blind_save::no);
break;
#if DXX_USE_EDITOR
case MENU_EDITOR:
if (!Current_mission)
{
create_new_mine();
SetPlayerFromCurseg();
}
hide_menus();
init_editor();
break;
#endif
case MENU_VIEW_SCORES:
scores_view(NULL, -1);
break;
#if 1 //def SHAREWARE
case MENU_ORDER_INFO:
show_order_form();
break;
#endif
case MENU_QUIT:
#if DXX_USE_EDITOR
if (! SafetyCheck()) break;
#endif
return 0;
case MENU_NEW_PLAYER:
RegisterPlayer();
break;
#if DXX_USE_UDP
case MENU_START_UDP_NETGAME:
multi_protocol = MULTI_PROTO_UDP;
select_mission(1, TXT_MULTI_MISSION, net_udp_setup_game);
break;
case MENU_JOIN_MANUAL_UDP_NETGAME:
multi_protocol = MULTI_PROTO_UDP;
net_udp_manual_join_game();
break;
case MENU_JOIN_LIST_UDP_NETGAME:
multi_protocol = MULTI_PROTO_UDP;
net_udp_list_join_game();
break;
#endif
#if DXX_USE_UDP
case MENU_MULTIPLAYER:
do_multi_player_menu();
break;
#endif
case MENU_CONFIG:
do_options_menu();
break;
case MENU_SHOW_CREDITS:
credits_show();
break;
#ifndef RELEASE
case MENU_SANDBOX:
do_sandbox_menu();
break;
#endif
default:
Error("Unknown option %d in do_option",select);
break;
}
return 1; // stay in main menu unless quitting
}
static void delete_player_saved_games(const char * name)
{
int i;
char filename[PATH_MAX];
for (i=0;i<10; i++)
{
snprintf( filename, sizeof(filename), PLAYER_DIRECTORY_STRING("%s.sg%x"), name, i );
PHYSFS_delete(filename);
snprintf( filename, sizeof(filename), PLAYER_DIRECTORY_STRING("%s.mg%x"), name, i );
PHYSFS_delete(filename);
}
}
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( NULL, 2, 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( NULL, 1, 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:
{
int x = 1;
char bakname[PATH_MAX];
// Get backup name
change_filename_extension(bakname, items[citem]+((items[citem][0]=='$')?1:0), DEMO_BACKUP_EXT);
x = nm_messagebox( NULL, 2, 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", items[citem]+((items[citem][0]=='$')?1:0), bakname );
if (!x)
newdemo_swap_endian(items[citem]);
return window_event_result::handled;
}
break;
}
return window_event_result::ignored;
}
static window_event_result demo_menu_handler(listbox *lb, const d_event &event, char **items)
{
switch (event.type)
{
case EVENT_KEY_COMMAND:
return demo_menu_keycommand(lb, event);
case EVENT_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(items[citem]);
return window_event_result::handled; // stay in demo selector
}
case EVENT_WINDOW_CLOSE:
PHYSFS_freeList(items);
break;
default:
break;
}
return window_event_result::ignored;
}
int select_demo(void)
{
int NumItems;
auto list = PHYSFSX_findFiles(DEMO_DIR, demo_file_extensions);
if (!list)
return 0; // memory error
if (!list[0])
{
nm_messagebox( NULL, 1, 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++) {}
// Sort by name
qsort(list.get(), NumItems, sizeof(char *), string_array_sort_func);
auto clist = const_cast<const char **>(list.get());
newmenu_listbox1(TXT_SELECT_DEMO, NumItems, clist, 1, 0, demo_menu_handler, list.release());
return 1;
}
static int do_difficulty_menu()
{
array<newmenu_item, NDL> m{{
nm_item_menu(MENU_DIFFICULTY_TEXT(0)),
nm_item_menu(MENU_DIFFICULTY_TEXT(1)),
nm_item_menu(MENU_DIFFICULTY_TEXT(2)),
nm_item_menu(MENU_DIFFICULTY_TEXT(3)),
nm_item_menu(MENU_DIFFICULTY_TEXT(4)),
}};
const unsigned s = newmenu_do1(nullptr, TXT_DIFFICULTY_LEVEL, m.size(), &m.front(), unused_newmenu_subfunction, unused_newmenu_userdata, Difficulty_level);
if (s <= Difficulty_4)
{
const auto d = static_cast<Difficulty_level_type>(s);
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,player_highest_level;
new_level_num = 1;
#ifdef NDEBUG
player_highest_level = get_highest_level();
if (player_highest_level > Last_level)
#endif
player_highest_level = Last_level;
if (player_highest_level > 1) {
char info_text[80];
int choice;
int valid = 0;
snprintf(info_text,sizeof(info_text),"%s %d",TXT_START_ANY_LEVEL, player_highest_level);
while (!valid)
{
array<char, 10> num_text{"1"};
array<newmenu_item, 2> m{{
nm_item_text(info_text),
nm_item_input(num_text),
}};
choice = newmenu_do( NULL, TXT_SELECT_START_LEV, m, unused_newmenu_subfunction, unused_newmenu_userdata );
if (choice==-1 || m[1].text[0]==0)
return window_event_result::handled;
new_level_num = atoi(m[1].text);
if (!(new_level_num>0 && new_level_num<=player_highest_level)) {
nm_messagebox( NULL, 1, TXT_OK, TXT_INVALID_LEVEL);
valid = 0;
}
else
valid = 1;
}
}
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 do_sound_menu();
static void input_config();
static void change_res();
namespace dsx {
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)
};
array<newmenu_item, DXX_OPTIONS_MENU(COUNT)> m;
options_menu_items()
{
DXX_OPTIONS_MENU(ADD);
}
};
}
static int options_menuset(newmenu *, const d_event &event, options_menu_items *items)
{
switch (event.type)
{
case EVENT_NEWMENU_CHANGED:
break;
case EVENT_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 1; // stay in menu until escape
}
case EVENT_WINDOW_CLOSE:
{
std::default_delete<options_menu_items>()(items);
write_player_file();
break;
}
default:
break;
}
return 0;
}
static int gcd(int a, int b)
{
if (!b)
return a;
return gcd(b, a%b);
}
void change_res()
{
newmenu_item m[50+8];
array<char, 12> crestext, casptext;
int mc = 0, citem = -1;
#if SDL_MAJOR_VERSION == 1
array<screen_mode, 50> modes;
const auto num_presets = gr_list_modes(modes);
array<array<char, 12>, 50> restext;
range_for (auto &i, partial_const_range(modes, num_presets))
{
const auto &&sm_w = SM_W(i);
const auto &&sm_h = SM_H(i);
snprintf(restext[mc].data(), restext[mc].size(), "%ix%i", sm_w, sm_h);
const auto checked = (citem == -1 && Game_screen_mode == i && GameCfg.AspectY == sm_w / gcd(sm_w, sm_h) && GameCfg.AspectX == sm_h / gcd(sm_w, sm_h));
if (checked)
citem = mc;
nm_set_item_radio(m[mc], restext[mc].data(), checked, 0);
mc++;
}
nm_set_item_text(m[mc], ""); mc++; // little space for overview
// the fields for custom resolution and aspect
const auto opt_cval = mc;
#endif
nm_set_item_radio(m[mc], "use custom values", (citem == -1), 0); mc++;
nm_set_item_text(m[mc], "resolution:"); mc++;
snprintf(crestext.data(), crestext.size(), "%ix%i", SM_W(Game_screen_mode), SM_H(Game_screen_mode));
nm_set_item_input(m[mc], crestext);
mc++;
nm_set_item_text(m[mc], "aspect:"); mc++;
snprintf(casptext.data(), casptext.size(), "%ix%i", GameCfg.AspectY, GameCfg.AspectX);
nm_set_item_input(m[mc], casptext);
mc++;
nm_set_item_text(m[mc], ""); mc++; // little space for overview
// fullscreen
#if SDL_MAJOR_VERSION == 1
const auto opt_fullscr = mc;
nm_set_item_checkbox(m[mc], "Fullscreen", gr_check_fullscreen());
mc++;
#endif
// create the menu
newmenu_do1(NULL, "Screen Resolution", mc, m, unused_newmenu_subfunction, unused_newmenu_userdata, 0);
// menu is done, now do what we need to do
// check which resolution field was selected
#if SDL_MAJOR_VERSION == 1
unsigned i;
for (i = 0; i <= mc; i++)
if (m[i].type == NM_TYPE_RADIO && m[i].radio().group == 0 && m[i].value == 1)
break;
// now check for fullscreen toggle and apply if necessary
if (m[opt_fullscr].value != gr_check_fullscreen())
gr_toggle_fullscreen();
#endif
screen_mode new_mode;
#if SDL_MAJOR_VERSION == 1
if (i == opt_cval) // set custom resolution and aspect
#endif
{
char revert[32];
char *x;
const char *errstr;
unsigned long w = strtoul(crestext.data(), &x, 10), h;
screen_mode cmode;
if (
((x == crestext.data() || *x != 'x' || !x[1] || ((h = strtoul(x + 1, &x, 10)), *x)) && (errstr = "Entered resolution must\nbe formatted as\n<number>x<number>", true)) ||
((w < 320 || h < 200) && (errstr = "Entered resolution must\nbe at least 320x200", true))
)
{
cmode = Game_screen_mode;
w = SM_W(cmode);
h = SM_H(cmode);
snprintf(revert, sizeof(revert), "Revert to %lux%lu", w, h);
nm_messagebox_str(TXT_WARNING, revert, errstr);
}
else
{
cmode.width = w;
cmode.height = h;
}
auto casp = cmode;
w = strtoul(casptext.data(), &x, 10);
if (
((x == casptext.data() || *x != 'x' || !x[1] || ((h = strtoul(x + 1, &x, 10)), *x)) && (errstr = "Entered aspect ratio must\nbe formatted as\n<number>x<number>", true)) ||
((!w || !h) && (errstr = "Entered aspect ratio must\nnot use 0 term", true))
)
{
nm_messagebox_str(TXT_WARNING, "IGNORE ASPECT RATIO", errstr);
}
else
{
// we even have a custom aspect set up
casp.width = w;
casp.height = h;
}
const auto g = gcd(SM_W(casp), SM_H(casp));
GameCfg.AspectY = SM_W(casp) / g;
GameCfg.AspectX = SM_H(casp) / g;
new_mode = cmode;
}
#if SDL_MAJOR_VERSION == 1
else if (i < num_presets) // set preset resolution
{
new_mode = modes[i];
const auto g = gcd(SM_W(new_mode), SM_H(new_mode));
GameCfg.AspectY = SM_W(new_mode) / g;
GameCfg.AspectX = SM_H(new_mode) / g;
}
#endif
// 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 (Game_wind) // shortly activate Game_wind so it's canvas will align to new resolution. really minor glitch but whatever
{
{
const d_event event{EVENT_WINDOW_ACTIVATED};
WINDOW_SEND_EVENT(Game_wind);
}
{
const d_event event{EVENT_WINDOW_DEACTIVATED};
WINDOW_SEND_EVENT(Game_wind);
}
}
}
game_init_render_buffers(SM_W(Game_screen_mode), SM_H(Game_screen_mode));
}
static void input_config_keyboard()
{
#define DXX_INPUT_SENSITIVITY(VERB,OPT,VAL) \
DXX_MENUITEM(VERB, SLIDER, TXT_TURN_LR, opt_##OPT##_turn_lr, VAL[0], 0, 16) \
DXX_MENUITEM(VERB, SLIDER, TXT_PITCH_UD, opt_##OPT##_pitch_ud, VAL[1], 0, 16) \
DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_LR, opt_##OPT##_slide_lr, VAL[2], 0, 16) \
DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_UD, opt_##OPT##_slide_ud, VAL[3], 0, 16) \
DXX_MENUITEM(VERB, SLIDER, TXT_BANK_LR, opt_##OPT##_bank_lr, VAL[4], 0, 16) \
#define DXX_INPUT_CONFIG_MENU(VERB) \
DXX_MENUITEM(VERB, TEXT, "Keyboard Sensitivity:", opt_label_kb) \
DXX_INPUT_SENSITIVITY(VERB,kb,PlayerCfg.KeyboardSens) \
class menu_items
{
public:
enum
{
DXX_INPUT_CONFIG_MENU(ENUM)
};
array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
menu_items()
{
DXX_INPUT_CONFIG_MENU(ADD);
}
};
#undef DXX_INPUT_CONFIG_MENU
#undef DXX_INPUT_SENSITIVITY
menu_items items;
newmenu_do1(nullptr, "Keyboard Calibration", items.m.size(), items.m.data(), unused_newmenu_subfunction, unused_newmenu_userdata, 1);
constexpr uint_fast32_t keysens = items.opt_label_kb + 1;
const auto &m = items.m;
for (unsigned i = 0; i < 5; i++)
{
PlayerCfg.KeyboardSens[i] = m[keysens+i].value;
}
}
static void input_config_mouse()
{
#define DXX_INPUT_SENSITIVITY(VERB,OPT,VAL) \
DXX_MENUITEM(VERB, SLIDER, TXT_TURN_LR, opt_##OPT##_turn_lr, VAL[0], 0, 16) \
DXX_MENUITEM(VERB, SLIDER, TXT_PITCH_UD, opt_##OPT##_pitch_ud, VAL[1], 0, 16) \
DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_LR, opt_##OPT##_slide_lr, VAL[2], 0, 16) \
DXX_MENUITEM(VERB, SLIDER, TXT_SLIDE_UD, opt_##OPT##_slide_ud, VAL[3], 0, 16) \
DXX_MENUITEM(VERB, SLIDER, TXT_BANK_LR, opt_##OPT##_bank_lr, VAL[4], 0, 16) \
#define DXX_INPUT_THROTTLE_SENSITIVITY(VERB,OPT,VAL) \
DXX_INPUT_SENSITIVITY(VERB,OPT,VAL) \
DXX_MENUITEM(VERB, SLIDER, TXT_THROTTLE, opt_##OPT##_throttle, VAL[5], 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) \
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) \
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) \
class menu_items
{
public:
enum
{
DXX_INPUT_CONFIG_MENU(ENUM)
};
array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
menu_items()
{
DXX_INPUT_CONFIG_MENU(ADD);
}
};
#undef DXX_INPUT_CONFIG_MENU
menu_items items;
newmenu_do1(nullptr, "Mouse Calibration", items.m.size(), items.m.data(), unused_newmenu_subfunction, unused_newmenu_userdata, 1);
constexpr uint_fast32_t mousesens = items.opt_label_ms + 1;
constexpr uint_fast32_t mouseoverrun = items.opt_label_mo + 1;
const auto &m = items.m;
for (unsigned i = 0; i <= 5; i++)
{
PlayerCfg.MouseSens[i] = m[mousesens+i].value;
PlayerCfg.MouseOverrun[i] = m[mouseoverrun+i].value;
}
constexpr uint_fast32_t mousefsdead = items.opt_mfsd_deadzone;
PlayerCfg.MouseFSDead = m[mousefsdead].value;
}
#if DXX_MAX_AXES_PER_JOYSTICK
static void input_config_joystick()
{
#define DXX_INPUT_CONFIG_MENU(VERB) \
DXX_MENUITEM(VERB, TEXT, "Joystick Sensitivity:", opt_label_js) \
DXX_INPUT_THROTTLE_SENSITIVITY(VERB,js,PlayerCfg.JoystickSens) \
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) \
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) \
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) \
class menu_items
{
public:
enum
{
DXX_INPUT_CONFIG_MENU(ENUM)
};
array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
menu_items()
{
DXX_INPUT_CONFIG_MENU(ADD);
}
};
#undef DXX_INPUT_CONFIG_MENU
menu_items items;
newmenu_do1(nullptr, "Joystick Calibration", items.m.size(), items.m.data(), unused_newmenu_subfunction, unused_newmenu_userdata, 1);
constexpr uint_fast32_t joysens = items.opt_label_js + 1;
constexpr uint_fast32_t joylin = items.opt_label_jl + 1;
constexpr uint_fast32_t joyspd = items.opt_label_jp + 1;
constexpr uint_fast32_t joydead = items.opt_label_jd + 1;
const auto &m = items.m;
for (unsigned i = 0; i <= 5; i++)
{
PlayerCfg.JoystickLinear[i] = m[joylin+i].value;
PlayerCfg.JoystickSpeed[i] = m[joyspd+i].value;
PlayerCfg.JoystickSens[i] = m[joysens+i].value;
PlayerCfg.JoystickDead[i] = m[joydead+i].value;
}
}
#endif
#undef DXX_INPUT_THROTTLE_SENSITIVITY
#undef DXX_INPUT_SENSITIVITY
namespace {
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, 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)
};
array<newmenu_item, DXX_INPUT_CONFIG_MENU(COUNT)> m;
input_config_menu_items()
{
DXX_INPUT_CONFIG_MENU(ADD);
}
static int menuset(newmenu *, const d_event &event, input_config_menu_items *pitems);
#undef DXX_INPUT_CONFIG_MENU
#undef DXX_INPUT_CONFIG_JOYSTICK_AXIS_ITEM
#undef DXX_INPUT_CONFIG_JOYSTICK_ITEM
};
}
int input_config_menu_items::menuset(newmenu *, const d_event &event, input_config_menu_items *pitems)
{
const auto &items = pitems->m;
switch (event.type)
{
case EVENT_NEWMENU_CHANGED:
{
const auto citem = static_cast<const d_change_event &>(event).citem;
MouselookMode mousemode;
#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;
}
break;
}
case EVENT_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 1; // stay in menu
}
default:
break;
}
return 0;
}
void input_config()
{
input_config_menu_items menu_items;
newmenu_do1(nullptr, TXT_CONTROLS, menu_items.m.size(), menu_items.m.data(), &input_config_menu_items::menuset, &menu_items, menu_items.opt_ic_confkey);
}
static void reticle_config()
{
#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) \
class menu_items
{
public:
enum
{
optgrp_reticle,
};
enum
{
DXX_RETICLE_CONFIG_MENU(ENUM)
};
array<newmenu_item, DXX_RETICLE_CONFIG_MENU(COUNT)> m;
menu_items()
{
DXX_RETICLE_CONFIG_MENU(ADD);
}
void read()
{
DXX_RETICLE_CONFIG_MENU(READ);
}
};
#undef DXX_RETICLE_CONFIG_MENU
#undef DXX_RETICLE_TYPE_OGL
menu_items items;
{
auto i = PlayerCfg.ReticleType;
#if !DXX_USE_OGL
if (i > 1) i--;
#endif
items.m[items.opt_reticle_classic + i].value = 1;
}
newmenu_do1(nullptr, "Reticle Customization", items.m.size(), items.m.data(), unused_newmenu_subfunction, unused_newmenu_userdata, 1);
for (uint_fast32_t i = items.opt_reticle_classic; i != items.opt_label_blank_reticle_type; ++i)
if (items.m[i].value)
{
#if !DXX_USE_OGL
if (i != items.opt_reticle_classic)
++i;
#endif
PlayerCfg.ReticleType = i - items.opt_reticle_classic;
break;
}
items.read();
}
#if defined(DXX_BUILD_DESCENT_I)
#define DXX_GAME_SPECIFIC_HUDOPTIONS(VERB) \
DXX_MENUITEM(VERB, CHECK, "Always-on Bomb Counter",opt_d2bomb,PlayerCfg.BombGauge) \
#elif defined(DXX_BUILD_DESCENT_II)
enum {
optgrp_missileview,
};
#define DXX_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 DXX_HUD_MENU_OPTIONS(VERB) \
DXX_MENUITEM(VERB, MENU, "Reticle Customization...", opt_hud_reticlemenu) \
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) \
DXX_GAME_SPECIFIC_HUDOPTIONS(VERB) \
enum {
DXX_HUD_MENU_OPTIONS(ENUM)
};
static int hud_config_menuset(newmenu *, const d_event &event, const unused_newmenu_userdata_t *)
{
switch (event.type)
{
case EVENT_NEWMENU_SELECTED:
{
auto &citem = static_cast<const d_select_event &>(event).citem;
if (citem == opt_hud_reticlemenu)
reticle_config();
return 1; // stay in menu
}
default:
break;
}
return 0;
}
namespace dsx {
void hud_config()
{
int i = 0;
do {
newmenu_item m[DXX_HUD_MENU_OPTIONS(COUNT)];
DXX_HUD_MENU_OPTIONS(ADD);
i = newmenu_do1( NULL, "Hud Options", sizeof(m)/sizeof(*m), m, hud_config_menuset, unused_newmenu_userdata, 0 );
DXX_HUD_MENU_OPTIONS(READ);
#if defined(DXX_BUILD_DESCENT_II)
PlayerCfg.MissileViewEnabled = m[opt_missileview_selfandallies].value
? MissileViewMode::EnabledSelfAndAllies
: (m[opt_missileview_selfonly].value
? MissileViewMode::EnabledSelfOnly
: MissileViewMode::None);
#endif
} while( i>-1 );
}
}
#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) \
#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 defined(DXX_BUILD_DESCENT_I)
#define D2X_OGL_GRAPHICS_MENU(VERB)
#elif defined(DXX_BUILD_DESCENT_II)
#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)
};
static int graphics_config_menuset(newmenu *, const d_event &event, newmenu_item *const items)
{
switch (event.type)
{
case EVENT_NEWMENU_CHANGED:
{
auto &citem = static_cast<const d_change_event &>(event).citem;
if (citem == opt_gr_brightness)
gr_palette_set_gamma(items[citem].value);
#if DXX_USE_OGL
else
if (citem == opt_filter_anisotropy && ogl_maxanisotropy <= 1.0)
{
nm_messagebox( TXT_ERROR, 1, TXT_OK, "Anisotropic Filtering not\nsupported by your hardware/driver.");
items[opt_filter_anisotropy].value = 0;
}
#endif
break;
}
case EVENT_NEWMENU_SELECTED:
{
auto &citem = static_cast<const d_select_event &>(event).citem;
if (citem == opt_gr_screenres)
change_res();
if (citem == opt_gr_hudmenu)
hud_config();
return 1; // stay in menu
}
default:
break;
}
return 0;
}
namespace dsx {
void graphics_config()
{
array<newmenu_item, DXX_GRAPHICS_MENU(COUNT)> m;
DXX_GRAPHICS_MENU(ADD);
#if DXX_USE_OGL
m[opt_filter_none+CGameCfg.TexFilt].value=1;
#endif
newmenu_do1(nullptr, "Graphics Options", m.size(), m.data(), graphics_config_menuset, m.data(), 0);
#if DXX_USE_OGL
if (CGameCfg.VSync != m[opt_gr_vsync].value || CGameCfg.Multisample != m[opt_gr_multisample].value)
nm_messagebox( NULL, 1, TXT_OK, "Setting VSync or 4x Multisample\nrequires restart on some systems.");
for (uint_fast32_t i = 0; i != 3; ++i)
if (m[i+opt_filter_none].value)
{
CGameCfg.TexFilt = i;
break;
}
CGameCfg.TexAnisotropy = m[opt_filter_anisotropy].value;
#if defined(DXX_BUILD_DESCENT_II)
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
GameCfg.GammaLevel = m[opt_gr_brightness].value;
CGameCfg.FPSIndicator = m[opt_gr_fpsindi].value;
#if DXX_USE_OGL
gr_set_attributes();
gr_set_mode(Game_screen_mode);
#endif
}
}
#if PHYSFS_VER_MAJOR >= 2
namespace {
struct browser
{
browser(const partial_range_t<const file_extension_t *> &r) :
ext_range(r)
{
}
const char *title; // The title - needed for making another listbox when changing directory
window_event_result (*when_selected)(void *userdata, const char *filename); // What to do when something chosen
void *userdata; // Whatever you want passed to when_selected
string_array_t list;
// List of file extensions we're looking for (if looking for a music file many types are possible)
const partial_range_t<const file_extension_t *> ext_range;
int select_dir; // Allow selecting the current directory (e.g. for Jukebox level song directory)
int new_path; // Whether the view_path is a new searchpath, if so, remove it when finished
char view_path[PATH_MAX]; // The absolute path we're currently looking at
};
}
static void list_dir_el(void *vb, const char *, const char *fname)
{
browser *b = reinterpret_cast<browser *>(vb);
const char *r = PHYSFS_getRealDir(fname);
if (!r)
r = "";
if (!strcmp(r, b->view_path) && (PHYSFS_isDirectory(fname) || PHYSFSX_checkMatchingExtension(fname, b->ext_range))
#if defined(__APPLE__) && defined(__MACH__)
&& d_stricmp(fname, "Volumes") // this messes things up, use '..' instead
#endif
)
b->list.add(fname);
}
static int list_directory(browser *b)
{
b->list.clear();
b->list.add(".."); // go to parent directory
if (b->select_dir)
{
b->list.add("<this directory>"); // choose the directory being viewed
}
PHYSFS_enumerateFilesCallback("", list_dir_el, b);
b->list.tidy(1 + (b->select_dir ? 1 : 0),
#ifdef __linux__
strcmp
#else
d_stricmp
#endif
);
return 1;
}
static window_event_result select_file_handler(listbox *menu,const d_event &event, browser *b)
{
char newpath[PATH_MAX];
const char **list = listbox_get_items(menu);
const char *sep = PHYSFS_getDirSeparator();
memset(newpath, 0, sizeof(char)*PATH_MAX);
switch (event.type)
{
#ifdef _WIN32
case EVENT_KEY_COMMAND:
{
if (event_key_get(event) == KEY_CTRLED + KEY_D)
{
char text[4] = "c";
int rval = 0;
array<newmenu_item, 1> m{{
nm_item_input(text),
}};
rval = newmenu_do( NULL, "Enter drive letter", m, unused_newmenu_subfunction, unused_newmenu_userdata );
text[1] = '\0';
snprintf(newpath, sizeof(char)*PATH_MAX, "%s:%s", text, sep);
if (!rval && text[0])
{
select_file_recursive(b->title, newpath, b->ext_range, b->select_dir, b->when_selected, b->userdata);
// close old box.
return window_event_result::close;
}
return window_event_result::handled;
}
break;
}
#endif
case EVENT_NEWMENU_SELECTED:
{
auto &citem = static_cast<const d_select_event &>(event).citem;
strcpy(newpath, b->view_path);
if (citem == 0) // go to parent dir
{
char *p;
size_t len_newpath = strlen(newpath);
size_t len_sep = strlen(sep);
if ((p = strstr(&newpath[len_newpath - len_sep], sep)))
if (p != strstr(newpath, sep)) // if this isn't the only separator (i.e. it's not about to look at the root)
*p = 0;
p = newpath + len_newpath - 1;
while ((p > newpath) && strncmp(p, sep, len_sep)) // make sure full separator string is matched (typically is)
p--;
if (p == strstr(newpath, 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, "/Volumes");
#else
return window_event_result::handled;
#endif
}
}
else
*p = '\0';
}
else if (citem == 1 && b->select_dir)
return (*b->when_selected)(b->userdata, "");
else
{
size_t len_newpath = strlen(newpath);
size_t len_sep = strlen(sep);
if (strncmp(&newpath[len_newpath - len_sep], sep, len_sep))
{
strncat(newpath, sep, PATH_MAX - 1 - len_newpath);
newpath[PATH_MAX - 1] = '\0';
}
strncat(newpath, list[citem], PATH_MAX - 1 - len_newpath);
newpath[PATH_MAX - 1] = '\0';
}
if ((citem == 0) || PHYSFS_isDirectory(list[citem]))
{
// If it fails, stay in this one
return select_file_recursive(b->title, newpath, b->ext_range, b->select_dir, b->when_selected, b->userdata) ? window_event_result::close : window_event_result::handled;
}
return (*b->when_selected)(b->userdata, list[citem]);
}
case EVENT_WINDOW_CLOSE:
if (b->new_path)
PHYSFS_removeFromSearchPath(b->view_path);
std::default_delete<browser>()(b);
break;
default:
break;
}
return window_event_result::ignored;
}
static int select_file_recursive2(const char *title, const char *orig_path, const partial_range_t<const file_extension_t *> &ext_range, int select_dir, select_file_subfunction<void> when_selected, void *userdata)
{
const char *sep = PHYSFS_getDirSeparator();
char *p;
array<char, PATH_MAX> new_path;
auto b = make_unique<browser>(ext_range);
b->title = title;
b->when_selected = when_selected;
b->userdata = userdata;
b->select_dir = select_dir;
b->view_path[0] = '\0';
b->new_path = 1;
// Check for a PhysicsFS path first, saves complication!
if (strncmp(orig_path, sep, strlen(sep)) && PHYSFSX_exists(orig_path,0))
{
PHYSFSX_getRealPath(orig_path, new_path);
orig_path = new_path.data();
}
// Set the viewing directory to orig_path, or some parent of it
if (orig_path)
{
// Must make this an absolute path for directory browsing to work properly
#ifdef _WIN32
if (!(isalpha(orig_path[0]) && (orig_path[1] == ':'))) // drive letter prompt (e.g. "C:"
#elif defined(macintosh)
if (orig_path[0] == ':')
#else
if (orig_path[0] != '/')
#endif
{
strncpy(b->view_path, PHYSFS_getBaseDir(), PATH_MAX - 1); // current write directory must be set to base directory
b->view_path[PATH_MAX - 1] = '\0';
#ifdef macintosh
orig_path++; // go past ':'
#endif
strncat(b->view_path, orig_path, PATH_MAX - 1 - strlen(b->view_path));
b->view_path[PATH_MAX - 1] = '\0';
}
else
{
strncpy(b->view_path, orig_path, PATH_MAX - 1);
b->view_path[PATH_MAX - 1] = '\0';
}
p = b->view_path + strlen(b->view_path) - 1;
const size_t len_sep = strlen(sep);
while (b->new_path = PHYSFSX_isNewPath(b->view_path), !PHYSFS_addToSearchPath(b->view_path, 0))
{
while ((p > b->view_path) && strncmp(p, sep, len_sep))
p--;
*p = '\0';
if (p == b->view_path)
break;
}
}
// Set to user directory if we couldn't find a searchpath
if (!b->view_path[0])
{
strncpy(b->view_path, PHYSFS_getUserDir(), PATH_MAX - 1);
b->view_path[PATH_MAX - 1] = '\0';
b->new_path = PHYSFSX_isNewPath(b->view_path);
if (!PHYSFS_addToSearchPath(b->view_path, 0))
{
return 0;
}
}
if (!list_directory(b.get()))
{
return 0;
}
auto pb = b.get();
return newmenu_listbox1(title, pb->list.pointer().size(), &pb->list.pointer().front(), 1, 0, select_file_handler, std::move(b)) != NULL;
}
#define DXX_MENU_ITEM_BROWSE(VERB, TXT, OPT) \
DXX_MENUITEM(VERB, MENU, TXT " (browse...)", OPT)
#else
int select_file_recursive2(const char *title, const char *orig_path, const partial_range_t<const file_extension_t *> &ext_range, int select_dir, int (*when_selected)(void *userdata, const char *filename), void *userdata)
{
return 0;
}
/* Include blank string to force a compile error if TXT cannot be
* string-pasted
*/
#define DXX_MENU_ITEM_BROWSE(VERB, TXT, OPT) \
DXX_MENUITEM(VERB, TEXT, TXT "", OPT)
#endif
#if DXX_USE_SDLMIXER
static window_event_result get_absolute_path(char *full_path, const char *rel_path)
{
PHYSFSX_getRealPath(rel_path, full_path, PATH_MAX);
return window_event_result::close;
}
#define SELECT_SONG(t, s) select_file_recursive(t, CGameCfg.CMMiscMusic[s].data(), jukebox_exts, 0, get_absolute_path, CGameCfg.CMMiscMusic[s].data())
#endif
namespace {
#if defined(DXX_BUILD_DESCENT_I)
#define REDBOOK_PLAYORDER_TEXT "force mac cd track order"
#elif defined(DXX_BUILD_DESCENT_II)
#define 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, GameCfg.MusicType == MUSIC_TYPE_BUILTIN, optgrp_music_type) \
#else
#define DXX_SOUND_ADDON_MUSIC_MENU_ITEM(VERB)
#endif
#if SDL_MAJOR_VERSION == 1
#define DXX_SOUND_CD_MUSIC_MENU_ITEM(VERB) \
DXX_MENUITEM(VERB, RADIO, "CD music", opt_sm_mtype2, GameCfg.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, GameCfg.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, GameCfg.CMLevelMusicPlayOrder == MUSIC_CM_PLAYORDER_CONT, optgrp_music_order) \
DXX_MENUITEM(VERB, RADIO, "one track per level", opt_sm_mtype3_lmplayorder2, GameCfg.CMLevelMusicPlayOrder == MUSIC_CM_PLAYORDER_LEVEL, optgrp_music_order) \
DXX_MENUITEM(VERB, RADIO, "random", opt_sm_mtype3_lmplayorder3, GameCfg.CMLevelMusicPlayOrder == MUSIC_CM_PLAYORDER_RAND, 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_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_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_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_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_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 DXX_SOUND_MENU(VERB) \
DXX_MENUITEM(VERB, SLIDER, TXT_FX_VOLUME, opt_sm_digivol, GameCfg.DigiVolume, 0, 8) \
DXX_MENUITEM(VERB, SLIDER, "Music volume", opt_sm_musicvol, GameCfg.MusicVolume, 0, 8) \
DXX_MENUITEM(VERB, CHECK, TXT_REVERSE_STEREO, opt_sm_revstereo, GameCfg.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, GameCfg.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, REDBOOK_PLAYORDER_TEXT, opt_sm_redbook_playorder, GameCfg.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
{
DXX_SOUND_MENU(ENUM)
};
array<newmenu_item, DXX_SOUND_MENU(COUNT)> m;
sound_menu_items()
{
DXX_SOUND_MENU(ADD);
}
void read()
{
DXX_SOUND_MENU(READ);
}
static int menuset(newmenu *, const d_event &event, sound_menu_items *pitems);
};
#undef DXX_SOUND_MENU
}
int sound_menu_items::menuset(newmenu *, const d_event &event, sound_menu_items *pitems)
{
const auto &items = pitems->m;
int replay = 0;
int rval = 0;
switch (event.type)
{
case EVENT_NEWMENU_CHANGED:
{
auto &citem = static_cast<const d_change_event &>(event).citem;
if (citem == opt_sm_digivol)
{
GameCfg.DigiVolume = items[citem].value;
digi_set_digi_volume( (GameCfg.DigiVolume*32768)/8 );
digi_play_sample_once( SOUND_DROP_BOMB, F1_0 );
}
else if (citem == opt_sm_musicvol)
{
GameCfg.MusicVolume = items[citem].value;
songs_set_volume(GameCfg.MusicVolume);
}
else if (citem == opt_sm_revstereo)
{
GameCfg.ReverseStereo = items[citem].value;
}
else if (citem == opt_sm_mtype0)
{
GameCfg.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)
{
GameCfg.MusicType = MUSIC_TYPE_BUILTIN;
replay = 1;
}
#endif
#if SDL_MAJOR_VERSION == 1
else if (citem == opt_sm_mtype2)
{
GameCfg.MusicType = MUSIC_TYPE_REDBOOK;
replay = 1;
}
#endif
#if DXX_USE_SDLMIXER
else if (citem == opt_sm_mtype3)
{
GameCfg.MusicType = MUSIC_TYPE_CUSTOM;
replay = 1;
}
#endif
else if (citem == opt_sm_redbook_playorder)
{
GameCfg.OrigTrackOrder = items[citem].value;
replay = (Game_wind != NULL);
}
#if DXX_USE_SDLMIXER
else if (citem == opt_sm_mtype3_lmplayorder1)
{
GameCfg.CMLevelMusicPlayOrder = MUSIC_CM_PLAYORDER_CONT;
replay = (Game_wind != NULL);
}
else if (citem == opt_sm_mtype3_lmplayorder2)
{
GameCfg.CMLevelMusicPlayOrder = MUSIC_CM_PLAYORDER_LEVEL;
replay = (Game_wind != NULL);
}
else if (citem == opt_sm_mtype3_lmplayorder3)
{
GameCfg.CMLevelMusicPlayOrder = MUSIC_CM_PLAYORDER_RAND;
replay = (Game_wind != NULL);
}
#endif
break;
}
case EVENT_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 array<file_extension_t, 1> ext_list{{"m3u"}}; // select a directory or M3U playlist
const auto cfgpath = CGameCfg.CMLevelMusicPath.data();
select_file_recursive(
"Select directory or\nM3U playlist to\n play level music from" WINDOWS_DRIVE_CHANGE_TEXT,
cfgpath, ext_list, 1, // look in current music path for ext_list files and allow directory selection
get_absolute_path, cfgpath); // just copy the absolute path
}
else if (citem == opt_sm_cm_mtype3_file1_b)
SELECT_SONG("Select main menu music" WINDOWS_DRIVE_CHANGE_TEXT, SONG_TITLE);
else if (citem == opt_sm_cm_mtype3_file2_b)
SELECT_SONG("Select briefing music" WINDOWS_DRIVE_CHANGE_TEXT, SONG_BRIEFING);
else if (citem == opt_sm_cm_mtype3_file3_b)
SELECT_SONG("Select credits music" WINDOWS_DRIVE_CHANGE_TEXT, SONG_CREDITS);
else if (citem == opt_sm_cm_mtype3_file4_b)
SELECT_SONG("Select escape sequence music" WINDOWS_DRIVE_CHANGE_TEXT, SONG_ENDLEVEL);
else if (citem == opt_sm_cm_mtype3_file5_b)
SELECT_SONG("Select game ending music" WINDOWS_DRIVE_CHANGE_TEXT, SONG_ENDGAME);
#endif
rval = 1; // stay in menu
break;
}
case EVENT_WINDOW_CLOSE:
break;
default:
break;
}
if (replay)
{
songs_uninit();
if (Game_wind)
songs_play_level_song( Current_level_num, 0 );
else
songs_play_song(SONG_TITLE, 1);
}
return rval;
}
void do_sound_menu()
{
#if DXX_USE_SDLMIXER
const auto old_CMLevelMusicPath = CGameCfg.CMLevelMusicPath;
const auto old_CMMiscMusic0 = CGameCfg.CMMiscMusic[SONG_TITLE];
#endif
sound_menu_items items;
newmenu_do1(nullptr, "Sound Effects & Music", items.m.size(), items.m.data(), &sound_menu_items::menuset, &items, 0);
#if DXX_USE_SDLMIXER
if ((Game_wind != NULL && strcmp(old_CMLevelMusicPath.data(), CGameCfg.CMLevelMusicPath.data())) || (Game_wind == NULL && strcmp(old_CMMiscMusic0.data(), CGameCfg.CMMiscMusic[SONG_TITLE].data())))
{
songs_uninit();
if (Game_wind)
songs_play_level_song( Current_level_num, 0 );
else
songs_play_song(SONG_TITLE, 1);
}
#endif
}
#if defined(DXX_BUILD_DESCENT_I)
#define DXX_GAME_SPECIFIC_OPTIONS(VERB) \
#elif defined(DXX_BUILD_DESCENT_II)
#define DXX_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
enum {
optgrp_autoselect_firing,
};
#define DXX_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) \
DXX_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) \
enum {
DXX_GAMEPLAY_MENU_OPTIONS(ENUM)
};
static int gameplay_config_menuset(newmenu *, const d_event &event, const unused_newmenu_userdata_t *)
{
switch (event.type)
{
case EVENT_NEWMENU_SELECTED:
{
auto &citem = static_cast<const d_select_event &>(event).citem;
if (citem == opt_gameplay_reorderprimary_menu)
ReorderPrimary();
if (citem == opt_gameplay_reordersecondary_menu)
ReorderSecondary();
return 1; // stay in menu
}
default:
break;
}
return 0;
}
void gameplay_config()
{
int i = 0;
do {
newmenu_item m[DXX_GAMEPLAY_MENU_OPTIONS(COUNT)];
#if defined(DXX_BUILD_DESCENT_II)
auto thief_absent = PlayerCfg.ThiefModifierFlags & ThiefModifier::Absent;
auto thief_cannot_steal_energy_weapons = PlayerCfg.ThiefModifierFlags & ThiefModifier::NoEnergyWeapons;
#endif
DXX_GAMEPLAY_MENU_OPTIONS(ADD);
i = newmenu_do1( NULL, "Gameplay Options", sizeof(m)/sizeof(*m), m, gameplay_config_menuset, unused_newmenu_userdata, 0 );
DXX_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 defined(DXX_BUILD_DESCENT_II)
PlayerCfg.ThiefModifierFlags =
(thief_absent ? ThiefModifier::Absent : 0) |
(thief_cannot_steal_energy_weapons ? ThiefModifier::NoEnergyWeapons : 0);
#endif
} while( i>-1 );
}
#if DXX_USE_UDP
static int multi_player_menu_handler(newmenu *menu,const d_event &event, int *menu_choice)
{
newmenu_item *items = newmenu_get_items(menu);
switch (event.type)
{
case EVENT_NEWMENU_SELECTED:
{
auto &citem = static_cast<const d_select_event &>(event).citem;
// stay in multiplayer menu, even after having played a game
return do_option(menu_choice[citem]);
}
case EVENT_WINDOW_CLOSE:
d_free(menu_choice);
d_free(items);
break;
default:
break;
}
return 0;
}
void do_multi_player_menu()
{
int *menu_choice;
newmenu_item *m;
int num_options = 0;
MALLOC(menu_choice, int, 3);
if (!menu_choice)
return;
MALLOC(m, newmenu_item, 3);
if (!m)
{
d_free(menu_choice);
return;
}
#if DXX_USE_UDP
ADD_ITEM("HOST GAME", MENU_START_UDP_NETGAME, -1);
#if DXX_USE_TRACKER
ADD_ITEM("FIND LAN/ONLINE GAMES", MENU_JOIN_LIST_UDP_NETGAME, -1);
#else
ADD_ITEM("FIND LAN GAMES", MENU_JOIN_LIST_UDP_NETGAME, -1);
#endif
ADD_ITEM("JOIN GAME MANUALLY", MENU_JOIN_MANUAL_UDP_NETGAME, -1);
#endif
newmenu_do3( NULL, TXT_MULTIPLAYER, num_options, m, multi_player_menu_handler, menu_choice, 0, NULL );
}
#endif
void do_options_menu()
{
auto items = new options_menu_items;
// Fall back to main event loop
// Allows clean closing and re-opening when resolution changes
newmenu_do3(nullptr, TXT_OPTIONS, items->m.size(), items->m.data(), options_menuset, items, 0, nullptr);
}
#ifndef RELEASE
namespace dsx {
static window_event_result polygon_models_viewer_handler(window *, const d_event &event, const unused_window_userdata_t *)
{
static unsigned view_idx;
int key = 0;
static vms_angvec ang;
switch (event.type)
{
case EVENT_WINDOW_ACTIVATED:
#if defined(DXX_BUILD_DESCENT_II)
gr_use_palette_table("groupa.256");
#endif
key_toggle_repeat(1);
view_idx = 0;
ang.p = ang.b = 0;
ang.h = F0_5-1;
break;
case EVENT_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 >= N_polygon_models) view_idx = 0;
break;
case KEY_BACKSP:
if (!view_idx)
view_idx = N_polygon_models - 1;
else
view_idx --;
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_WINDOW_DRAW:
timer_delay(F1_0/60);
{
auto &canvas = *grd_curcanv;
draw_model_picture(canvas, 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", view_idx, N_polygon_models - 1);
}
break;
case EVENT_WINDOW_CLOSE:
load_palette(MENU_PALETTE,0,1);
key_toggle_repeat(0);
break;
default:
break;
}
return window_event_result::ignored;
}
}
static void polygon_models_viewer()
{
const auto wind = window_create(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT, polygon_models_viewer_handler, unused_window_userdata);
if (!wind)
{
d_event event = { EVENT_WINDOW_CLOSE };
polygon_models_viewer_handler(NULL, event, NULL);
return;
}
event_process_all();
}
namespace dsx {
static window_event_result gamebitmaps_viewer_handler(window *, const d_event &event, const unused_window_userdata_t *)
{
static int view_idx = 0;
int key = 0;
#if DXX_USE_OGL
float scale = 1.0;
#endif
bitmap_index bi;
grs_bitmap *bm;
switch (event.type)
{
case EVENT_WINDOW_ACTIVATED:
#if defined(DXX_BUILD_DESCENT_II)
gr_use_palette_table("groupa.256");
#endif
key_toggle_repeat(1);
view_idx = 0;
break;
case EVENT_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:
view_idx --;
if (view_idx < 0 ) view_idx = Num_bitmap_files - 1;
break;
default:
break;
}
return window_event_result::handled;
case EVENT_WINDOW_DRAW:
bi.index = view_idx;
bm = &GameBitmaps[view_idx];
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, F1_0);
#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_WINDOW_CLOSE:
load_palette(MENU_PALETTE,0,1);
key_toggle_repeat(0);
break;
default:
break;
}
return window_event_result::ignored;
}
}
static void gamebitmaps_viewer()
{
const auto wind = window_create(grd_curscreen->sc_canvas, 0, 0, SWIDTH, SHEIGHT, gamebitmaps_viewer_handler, unused_window_userdata);
if (!wind)
{
d_event event = { EVENT_WINDOW_CLOSE };
gamebitmaps_viewer_handler(NULL, event, NULL);
return;
}
event_process_all();
}
#define DXX_SANDBOX_MENU(VERB) \
DXX_MENUITEM(VERB, MENU, "Polygon_models viewer", polygon_models) \
DXX_MENUITEM(VERB, MENU, "GameBitmaps viewer", bitmaps) \
namespace {
class sandbox_menu_items
{
public:
enum
{
DXX_SANDBOX_MENU(ENUM)
};
array<newmenu_item, DXX_SANDBOX_MENU(COUNT)> m;
sandbox_menu_items()
{
DXX_SANDBOX_MENU(ADD);
}
};
}
static int sandbox_menuset(newmenu *, const d_event &event, sandbox_menu_items *items)
{
switch (event.type)
{
case EVENT_NEWMENU_CHANGED:
break;
case EVENT_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 1; // stay in menu until escape
}
case EVENT_WINDOW_CLOSE:
{
std::default_delete<sandbox_menu_items>()(items);
break;
}
default:
break;
}
return 0;
}
void do_sandbox_menu()
{
auto items = new sandbox_menu_items;
newmenu_do3(nullptr, "Coder's sandbox", items->m.size(), items->m.data(), sandbox_menuset, items, 0, nullptr);
}
#endif