Subversion Repositories Games.Descent

Rev

Details | Last modification | View Log | RSS feed

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